passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
507 lines (453 loc) • 16.8 kB
JavaScript
/**
* 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 {withAppContext} from "./AppContext";
import SsoProviders from "../components/Administration/ManageSsoSettings/SsoProviders.data";
import {withDialog} from "./DialogContext";
import NotifyError from "../components/Common/Error/NotifyError/NotifyError";
import {withTranslation} from "react-i18next";
import TestSsoSettingsDialog from "../components/Administration/TestSsoSettingsDialog/TestSsoSettingsDialog";
import ConfirmDeleteSsoSettingsDialog from "../components/Administration/ConfirmDeleteSsoSettingsDialog/ConfirmDeleteSsoSettingsDialog";
import {withActionFeedback} from "./ActionFeedbackContext";
// taken from Validator.isUUID()
const UUID_REGEXP = /^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[0-5][a-fA-F0-9]{3}-[089aAbB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$/;
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
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
shouldFocusOnError: () => {}, // Returns true the first time it is asked after a form validation process detected an error
});
/**
* 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.hasError = false;
}
/**
* Returns the default component state
*/
get defaultState() {
return {
ssoConfig: this.defaultSsoSettings, // The current sso configuration
errors: {}, // The errors detected during the data validation
isLoaded: false, // is the SSO settings data loading from the server finished
hasSettingsChanged: false, // has the current form changed
processing: false, // true when the form is being processed
getErrors: this.getErrors.bind(this), // Returns the errors detected during validation
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
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
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
shouldFocusOnError: this.shouldFocusOnError.bind(this), // Returns true the first time it is asked after a form validation process detected an error
};
}
/**
* Returns a default empty SSO settings.
* @returns {object}
*/
get defaultSsoSettings() {
return {
provider: null,
data: {
url: "",
client_id: "",
tenant_id: "",
client_secret: "",
client_secret_expiry: "",
}
};
}
/**
* 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
}
/**
* Find the sso configuration
* @return {Promise<void>}
*/
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});
}
this.isSsoConfigExisting = Boolean(ssoConfig?.provider);
this.setState({
ssoConfig: ssoConfig,
isLoaded: true,
});
}
/**
* Returns true if an SSO settings exists
* @returns {boolean}
*/
isSsoSettingsExisting() {
return this.state.ssoConfig?.provider;
}
/**
* Get the current sso config from the context's state.
* @returns {Object}
*/
getSsoConfiguration() {
return this.state.ssoConfig;
}
/**
* Get the current SSO configuration with data ready for the background page.
* @return {Object}
* @private
*/
getSsoConfigurationDto() {
const config = this.getSsoConfiguration();
const data = config.data;
return {
provider: config.provider,
data: {
url: data.url,
client_id: data.client_id,
tenant_id: data.tenant_id,
client_secret: data.client_secret,
client_secret_expiry: data.client_secret_expiry,
}
};
}
/**
* Returns true if the sso settings are set to active.
* @returns {boolean}
*/
isSsoConfigActivated() {
return Boolean(this.state.ssoConfig?.provider);
}
/**
* Returns true if the current form changed
* @returns {boolean}
*/
hasFormChanged() {
return this.state.hasSettingsChanged;
}
/**
* Set an SSO settings value to the current config
* @param {string} key
* @param {string} value
*/
setValue(key, value) {
const ssoConfig = this.getSsoConfiguration();
ssoConfig.data[key] = value;
this.setState({ssoConfig, hasSettingsChanged: true});
}
/**
* Disable the Sso configuration.
*/
disableSso() {
const ssoConfig = Object.assign({}, this.state.ssoConfig, {provider: null, data: {}});
this.setState({ssoConfig});
}
/**
* 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;
}
const selectedProvider = SsoProviders.find(p => p.id === provider.id);
this.setState({
ssoConfig: {
provider: selectedProvider.id,
data: Object.assign({}, this.state.ssoConfig.data, selectedProvider?.defaultConfig)
}
});
}
/**
* Returns the errors detected during validation
* @returns {object}
*/
getErrors() {
return this.state.errors;
}
/**
* Validates the current data in the state
* @returns {boolean} true if the data is valid, false otherwise
*/
validateData() {
const settings = this.state.getSsoConfiguration();
const errors = {};
const isProviderValid = this.validate_provider(settings.provider, errors);
if (!isProviderValid) {
this.setState({errors, hasSumittedForm: true});
return false;
}
const validationCallback = `validateDataFromProvider_${settings.provider}`;
const isFormValid = this[validationCallback](settings.data, errors);
this.setState({errors, hasSumittedForm: true});
return isFormValid;
}
/**
* Validates the current data in the state.
* @param {string} provider the provider id to validate
* @param {object} errors a ref object to put the validation onto
*/
validate_provider(provider, errors) {
const isProviderValid = SsoProviders.find(p => p.id === provider);
if (!isProviderValid) {
errors.provider = this.props.t("The Single Sign-On provider must be a supported provider.");
}
return isProviderValid;
}
/**
* Validates the current data in the state assuming the SSO provider is Azure
* @param {string} data the data to validate
* @param {object} errors a ref object to put the validation onto
* @returns {boolean}
*/
validateDataFromProvider_azure(data, errors) {
const {url, client_id, tenant_id, client_secret, client_secret_expiry} = data;
let isDataValid = true;
if (!url || !(url?.length)) { // Validation of url
errors.url = this.props.t("The Login URL is required");
isDataValid = false;
} else if (!this.isValidUrl(url)) {
errors.url = this.props.t("The Login URL must be a valid URL");
isDataValid = false;
}
if (!client_id || !(client_id?.length)) { // Validation of client_id
errors.client_id = this.props.t("The Application (client) ID is required");
isDataValid = false;
} else if (!this.isValidUuid(client_id)) {
errors.client_id = this.props.t("The Application (client) ID must be a valid UUID");
isDataValid = false;
}
if (!tenant_id || !(tenant_id?.length)) { // Validation of tenant_id
errors.tenant_id = this.props.t("The Directory (tenant) ID is required");
isDataValid = false;
} else if (!this.isValidUuid(tenant_id)) {
errors.tenant_id = this.props.t("The Directory (tenant) ID must be a valid UUID");
isDataValid = false;
}
// Validation of client_secret
if (!client_secret || !(client_secret?.length)) {
errors.client_secret = this.props.t("The Secret is required");
isDataValid = false;
}
// Validation of client_secret_expiry
if (!client_secret_expiry) {
errors.client_secret_expiry = this.props.t("The Secret expiry is required");
isDataValid = false;
}
this.hasError = true;
return isDataValid;
}
/**
* Returns true the first time it is asked after a form validation process detected an error
* @returns {boolean}
*/
shouldFocusOnError() {
const hasError = this.hasError;
this.hasError = false;
return hasError;
}
/**
* Returns true if the url is valid;
* @param {string} stringUrl
*/
isValidUrl(stringUrl) {
try {
const url = new URL(stringUrl);
return url.protocol === "http:" || url.protocol === "https:";
} catch (_) {
return false;
}
}
/**
* Returns true if the UUID is valid;
* @param {string} stringUuid
*/
isValidUuid(stringUuid) {
return UUID_REGEXP.test(stringUuid);
}
/**
* 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);
const ssoConfig = Object.assign({}, this.state.ssoConfig, draftConfiguration);
this.setState({ssoConfig});
}
/**
* Returns true if a SSo configuration exist on the API and the user disabled the settings.
* @returns {boolean}
*/
canDeleteSettings() {
const config = this.getSsoConfiguration();
return this.isSsoConfigExisting && config.provider === 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 {
const ssoSettings = this.getSsoConfiguration();
await this.props.context.port.request("passbolt.sso.delete-settings", ssoSettings.id);
this.props.actionFeedbackContext.displaySuccess(this.props.t("The SSO settings has been deleted successfully"));
this.isSsoConfigExisting = false;
this.setState({
ssoConfig: this.defaultSsoSettings,
processing: false,
});
} catch (e) {
this.handleError(e);
this.setState({processing: false});
}
}
/**
* Opens the test SSO settings dialog
*
* @param {SsoConfigurationDto} 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.setState({hasSettingsChanged: false});
}
/**
* 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>
);
}
};
}