UNPKG

passbolt-styleguide

Version:

Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.

511 lines (464 loc) 18.1 kB
/** * Passbolt ~ Open source password manager for teams * Copyright (c) 2022 Passbolt SA (https://www.passbolt.com) * * Licensed under GNU Affero General Public License version 3 of the or any later version. * For full copyright and license information, please see the LICENSE.txt * Redistributions of files must retain the above copyright notice. * * @copyright Copyright (c) 2022 Passbolt SA (https://www.passbolt.com) * @license https://opensource.org/licenses/AGPL-3.0 AGPL License * @link https://www.passbolt.com Passbolt(tm) * @since 3.9.0 */ import React from "react"; import PropTypes from "prop-types"; import NotifyError from "../components/Common/Error/NotifyError/NotifyError"; import { withAppContext } from "../../shared/context/AppContext/AppContext"; import { withDialog } from "./DialogContext"; import { withTranslation } from "react-i18next"; import { withActionFeedback } from "./ActionFeedbackContext"; import SsoProviders from "../components/Administration/ManageSsoSettings/SsoProviders.data"; import TestSsoSettingsDialog from "../components/Administration/TestSsoSettingsDialog/TestSsoSettingsDialog"; import ConfirmDeleteSsoSettingsDialog from "../components/Administration/ConfirmDeleteSsoSettingsDialog/ConfirmDeleteSsoSettingsDialog"; import AzureSsoSettingsEntity from "../../shared/models/entity/ssoSettings/AzureSsoSettingsEntity"; import AzureSsoSettingsFormEntity from "../../shared/models/entity/ssoSettings/AzureSsoSettingsFormEntity"; import OAuth2SsoSettingsEntity from "../../shared/models/entity/ssoSettings/OAuth2SsoSettingsEntity"; import OAuth2SsoSettingsFormEntity from "../../shared/models/entity/ssoSettings/OAuth2SsoSettingsFormEntity"; import GoogleSsoSettingsEntity from "../../shared/models/entity/ssoSettings/GoogleSsoSettingsEntity"; import GoogleSsoSettingsFormEntity from "../../shared/models/entity/ssoSettings/GoogleSsoSettingsFormEntity"; import AdfsSsoSettingsEntity from "../../shared/models/entity/ssoSettings/AdfsSsoSettingsEntity"; import AdfsSsoSettingsFormEntity from "../../shared/models/entity/ssoSettings/AdfsSsoSettingsFormEntity"; import PingOneSsoSettingsEntity from "../../shared/models/entity/ssoSettings/PingOneSsoSettingsEntity"; import PingOneSsoSettingsFormEntity from "../../shared/models/entity/ssoSettings/PingOneSsoSettingsFormEntity"; import memoize from "memoize-one"; export const AdminSsoContext = React.createContext({ ssoConfig: null, // The current sso configuration canDeleteSettings: () => {}, // Returns true if it is possible to call for a settings deletion isProcessing: () => {}, // true when the form is being processed loadSsoConfiguration: () => {}, // Load the current sso configuration and store it in the state getSsoConfiguration: () => {}, // Return the current sso configuration from the context state getProviderList: () => {}, // Returns a list of the available providers on the API isSsoConfigActivated: () => {}, // Returns true if the sso settings are set to active isDataReady: () => {}, // Returns true if the data has been loaded from the API already save: () => {}, // Save the sso configuration changes disableSso: () => {}, // Disable the SSO configuration hasFormChanged: () => {}, // Returns true if the current form changed validateData: () => {}, // Validates the current data in the state saveAndTestConfiguration: () => {}, // Saves the current settings as a new draft and run the test dialog openTestDialog: () => {}, // Opens the test SSO settings dialog handleError: () => {}, // Handles error by displaying a NotifyError dialog getErrors: () => {}, // Returns the errors detected during validation deleteSettings: () => {}, // Delete the current SSO settings showDeleteConfirmationDialog: () => {}, // Show the delete SSO settings confirmation dialog }); /** * The related context provider */ export class AdminSsoContextProvider extends React.Component { /** * Default constructor * @param props The component props */ constructor(props) { super(props); this.state = this.defaultState; this.bindCallbacks(); this.isSsoConfigExisting = false; this.shouldFocusOnError = false; this.formSettings = null; this.originalSettings = null; this.cachedSsoConfig = {}; } /** * Returns the default component state */ get defaultState() { return { ssoConfig: null, // The current sso configuration DTO providers: [], // The list of the current available providers on the API. isLoaded: false, // is the SSO settings data loading from the server finished processing: false, // true when the form is being processed hasBeenValidated: false, // true when the has been validated once but not submitted hasFormChanged: this.hasFormChanged.bind(this), // Returns true if the current form changed isProcessing: this.isProcessing.bind(this), // returns true if a process is running and the UI must be disabled isDataReady: this.isDataReady.bind(this), // returns true if the data has been loaded from the API already loadSsoConfiguration: this.loadSsoConfiguration.bind(this), // Load the current sso configuration and store it in the state getSsoConfiguration: this.getSsoConfiguration.bind(this), // Return the current sso configuration from the context state getProviderList: this.getProviderList.bind(this), // Returns a list of the available providers on the API isSsoConfigActivated: this.isSsoConfigActivated.bind(this), // Returns true if the sso settings are set to active changeProvider: this.changeProvider.bind(this), // change the provider disableSso: this.disableSso.bind(this), // Disable the SSO configuration setValue: this.setValue.bind(this), // Set an SSO settings value to the current config validateData: this.validateData.bind(this), // Validates the current data in the state saveAndTestConfiguration: this.saveAndTestConfiguration.bind(this), // Saves the current settings as a new draft and run the test dialog handleError: this.handleError.bind(this), // Handles error by displaying a NotifyError dialog getErrors: this.getErrors.bind(this), // Returns the errors detected during validation deleteSettings: this.deleteSettings.bind(this), // Delete the current SSO settings canDeleteSettings: this.canDeleteSettings.bind(this), // Returns true if it is possible to call for a settings deletion showDeleteConfirmationDialog: this.showDeleteConfirmationDialog.bind(this), // Show the delete SSO settings confirmation dialog consumeFocusOnError: this.consumeFocusOnError.bind(this), // Returns true the first time it is asked after a form validation process detected an error }; } /** * Bind callbacks methods */ bindCallbacks() { this.handleTestConfigCloseDialog = this.handleTestConfigCloseDialog.bind(this); // Handles the closing of the SSO test configuration dialog this.handleSettingsActivation = this.handleSettingsActivation.bind(this); // Handles the UI processing after a successful settings activation } /** * Memoized form validation. Re-computes only when the settings DTO changes. * @type {function} */ validateForm = memoize( ( ssoConfigDto, // eslint-disable-line no-unused-vars ) => this.formSettings?.validate(), ); /** * Memoized change detection. Re-computes only when entities or DTO change. * @type {function} */ // eslint-disable-next-line no-unused-vars hasSettingsChanges = memoize((originalDto, formDto) => { // Here we need a try/catch because `hasDiffProps` will throw an error when the entities don't share the same properties. try { return this.originalSettings?.hasDiffProps(this.formSettings) ?? false; } catch { return true; } }); /** * Returns the current sso config DTO for state synchronization. * @returns {object|null} * @private */ getSsoConfigDto() { if (!this.formSettings) { return null; } return { provider: this.formSettings.provider, ...this.formSettings.toFormDto() }; } /** * Find the sso configuration * @return {Promise<Object>} */ async loadSsoConfiguration() { let ssoConfig = null; try { ssoConfig = await this.props.context.port.request("passbolt.sso.get-current"); } catch (error) { this.props.dialogContext.open(NotifyError, { error }); return {}; } this.isSsoConfigExisting = Boolean(ssoConfig.provider); this.formSettings = this.getSsoProviderFormEntity(ssoConfig); this.originalSettings = this.getSsoProviderFormEntity(ssoConfig); this.setState({ ssoConfig: this.getSsoConfigDto(), providers: ssoConfig.providers, isLoaded: true, }); return ssoConfig; } /** * Returns the form entity matching the given SSO settings provider. * @param {object} settings * @returns {EntityV2|null} */ getSsoProviderFormEntity(settings) { if (!settings?.provider) { return null; } switch (settings.provider) { case AzureSsoSettingsEntity.PROVIDER_ID: { return AzureSsoSettingsFormEntity.fromEntityDto(settings); } case GoogleSsoSettingsEntity.PROVIDER_ID: { return GoogleSsoSettingsFormEntity.fromEntityDto(settings); } case OAuth2SsoSettingsEntity.PROVIDER_ID: { return OAuth2SsoSettingsFormEntity.fromEntityDto(settings); } case AdfsSsoSettingsEntity.PROVIDER_ID: { return AdfsSsoSettingsFormEntity.fromEntityDto(settings); } case PingOneSsoSettingsEntity.PROVIDER_ID: { return PingOneSsoSettingsFormEntity.fromEntityDto(settings); } } return null; } /** * Get the current sso config from the context's state. * @returns {object} */ getSsoConfiguration() { return this.state.ssoConfig; } /** * Returns a list of the available providers on the API. * @returns {Array<string>} */ getProviderList() { return this.state.providers; } /** * Get the current SSO configuration with data ready for the background page. * @return {object} * @private */ getSsoConfigurationDto() { return this.formSettings.toEntityDto(); } /** * Returns true if the sso settings are set to active. * @returns {boolean} */ isSsoConfigActivated() { return Boolean(this.formSettings); } /** * Returns true if the current form changed * @returns {boolean} */ hasFormChanged() { if (this.originalSettings && this.formSettings) { return this.hasSettingsChanges(this.originalSettings.toFormDto(), this.formSettings.toFormDto()); } return ( (this.originalSettings !== null && this.formSettings === null) || (this.originalSettings === null && this.formSettings !== null) ); } /** * Set an SSO settings value to the current config * @param {string} key * @param {string} value */ setValue(key, value) { this.formSettings.set(key, value, { validate: false }); this.setState({ ssoConfig: this.getSsoConfigDto() }, () => { if (this.state.hasBeenValidated) { this.validateData(); } }); } /** * Disable the Sso configuration. */ disableSso() { if (this.formSettings) { this.cachedSsoConfig[this.formSettings.provider] = this.formSettings; } this.formSettings = null; this.setState({ ssoConfig: null }); } /** * Returns true if the data has finished to be loaded from the server. * @returns {boolean} */ isDataReady() { return this.state.isLoaded; } /** * Returns true when the data is under processing * @returns {boolean} */ isProcessing() { return this.state.processing; } /** * Change the currently selected provider. */ changeProvider(provider) { if (provider.disabled) { return; } if (this.formSettings?.provider) { this.cachedSsoConfig[this.formSettings.provider] = this.formSettings; } this.formSettings = this.getCachedSsoConfigOrDefault(provider.id); this.setState( { ssoConfig: this.getSsoConfigDto(), }, () => { if (this.state.hasBeenValidated) { this.validateData(); } }, ); } /** * For the given provider returns the SSO config cached or the default configuration if none exists yet. * @param {string} providerId * @returns {EntityV2} */ getCachedSsoConfigOrDefault(providerId) { if (this.cachedSsoConfig[providerId]) { return this.cachedSsoConfig[providerId]; } const defaultProviderConfiguration = SsoProviders.find((provider) => provider.id === providerId); const entityDto = { id: this.formSettings?.id, provider: providerId, data: defaultProviderConfiguration.defaultConfig, }; return this.getSsoProviderFormEntity(entityDto); } /** * Validates the current data in the state * @param {boolean} applyFieldFocusOnError if true a focus on the first erroneous field will be asked * @returns {boolean} true if the data is valid, false otherwise */ validateData(applyFieldFocusOnError = false) { const validationError = this.validateForm(this.state.ssoConfig); const hasErrors = Boolean(validationError?.hasErrors()); this.shouldFocusOnError = applyFieldFocusOnError && hasErrors; this.setState({ hasBeenValidated: true }); return !hasErrors; } /** * Returns true the first time it is asked after a form validation process detected an error * @returns {boolean} */ consumeFocusOnError() { const doFocusOnError = this.shouldFocusOnError; this.shouldFocusOnError = false; return doFocusOnError; } /** * Returns the error set during validation. * @returns {EntityValidationError|null} */ getErrors() { if (!this.state.hasBeenValidated) { return null; } return this.validateForm(this.state.ssoConfig); } /** * Saves the current settings as a new draft and run the test dialog */ async saveAndTestConfiguration() { this.setState({ processing: true }); const ssoSettings = this.getSsoConfigurationDto(); let draftConfiguration; try { draftConfiguration = await this.props.context.port.request("passbolt.sso.save-draft", ssoSettings); } catch (e) { this.handleError(e); this.setState({ processing: false }); return; } await this.runTestConfig(draftConfiguration); this.formSettings = this.getSsoProviderFormEntity(draftConfiguration); this.setState({ ssoConfig: this.getSsoConfigDto() }); } /** * Returns true if a SSo configuration exist on the API and the user disabled the settings. * @returns {boolean} */ canDeleteSettings() { return this.isSsoConfigExisting && this.formSettings === null; } /** * Show the delete settings confirmation dialog. */ showDeleteConfirmationDialog() { this.props.dialogContext.open(ConfirmDeleteSsoSettingsDialog); } /** * Delete the current SSO settings. * @return {Promise<void>} */ async deleteSettings() { this.setState({ processing: true }); try { await this.props.context.port.request("passbolt.sso.delete-settings", this.originalSettings.id); this.props.actionFeedbackContext.displaySuccess(this.props.t("The SSO settings have been deleted successfully")); this.isSsoConfigExisting = false; this.formSettings = null; this.originalSettings = null; this.setState({ ssoConfig: null, processing: false, }); } catch (e) { this.handleError(e); this.setState({ processing: false }); } } /** * Opens the test SSO settings dialog * * @param {object} draftConfiguration */ async runTestConfig(draftConfiguration) { const selectedProvider = SsoProviders.find((provider) => provider.id === draftConfiguration.provider); this.props.dialogContext.open(TestSsoSettingsDialog, { provider: selectedProvider, configurationId: draftConfiguration.id, handleClose: this.handleTestConfigCloseDialog, onSuccessfulSettingsActivation: this.handleSettingsActivation, }); } /** * Handles the closing of the SSO test configuration dialog */ handleTestConfigCloseDialog() { this.setState({ processing: false }); } /** * Handles the UI processing after a successful settings activation */ handleSettingsActivation() { this.isSsoConfigExisting = true; this.originalSettings = this.getSsoProviderFormEntity({ id: this.formSettings.id, provider: this.formSettings.provider, data: this.formSettings.toFormDto(), }); } /** * Handle exception by displaying a pop-up containing the details of the error. * @param {Error} error */ handleError(error) { console.error(error); this.props.dialogContext.open(NotifyError, { error }); } /** * Render the component * @returns {JSX} */ render() { return <AdminSsoContext.Provider value={this.state}>{this.props.children}</AdminSsoContext.Provider>; } } AdminSsoContextProvider.propTypes = { context: PropTypes.any, // The application context children: PropTypes.any, // The children components accountRecoveryContext: PropTypes.object, // The account recovery context dialogContext: PropTypes.object, // The dialog context actionFeedbackContext: PropTypes.object, // The action feedback context t: PropTypes.func, // The translation function }; export default withAppContext(withActionFeedback(withDialog(withTranslation("common")(AdminSsoContextProvider)))); /** * Resource Workspace Context Consumer HOC * @param WrappedComponent */ export function withAdminSso(WrappedComponent) { return class WithAdminSso extends React.Component { render() { return ( <AdminSsoContext.Consumer> {(adminSsoContext) => <WrappedComponent adminSsoContext={adminSsoContext} {...this.props} />} </AdminSsoContext.Consumer> ); } }; }