passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
632 lines (590 loc) • 22.2 kB
JavaScript
/**
* Passbolt ~ Open source password manager for teams
* Copyright (c) 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) Passbolt SA (https://www.passbolt.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.passbolt.com Passbolt(tm)
* @since 5.5.0
*/
import React, { Component } from "react";
import PropTypes from "prop-types";
import { withAdministrationWorkspace } from "../../../contexts/AdministrationWorkspaceContext";
import { Trans, withTranslation } from "react-i18next";
import memoize from "memoize-one";
import NotifyError from "../../../components/Common/Error/NotifyError/NotifyError";
import ScimSettingsEntity from "../../../../shared/models/entity/scimSettings/scimSettingsEntity";
import ScimSettingsFormEntity from "../../../../shared/models/entity/scimSettings/scimSettingsFormEntity";
import { withActionFeedback } from "../../../contexts/ActionFeedbackContext";
import { withDialog } from "../../../contexts/DialogContext";
import { withAppContext } from "../../../../shared/context/AppContext/AppContext";
import ScimSettingsServiceWorkerService from "../../../../shared/services/serviceWorker/scim/scimSettingsServiceWorkerService";
import CopySVG from "../../../../img/svg/copy.svg";
import RefreshSVG from "../../../../img/svg/refresh.svg";
import { withClipboard } from "../../../contexts/Clipboard/ManagedClipboardServiceProvider";
import Password from "../../../../shared/components/Password/Password";
import CalendarSVG from "../../../../img/svg/calendar.svg";
import { DateTime } from "luxon";
import Select from "../../Common/Select/Select";
import { getUserFormattedName } from "../../../../shared/utils/userUtils";
import { createSafePortal } from "../../../../shared/utils/portals";
import DisplayScimSettingsAdministrationHelp from "./DisplayScimSettingsAdministrationHelp";
import { withRoles } from "../../../contexts/RoleContext";
import RolesCollection from "../../../../shared/models/entity/role/rolesCollection";
/**
* This component allows to display the SCIM settings for the administration
*/
class DisplayScimSettingsAdministration extends Component {
/**
* The original settings used to detect changes
* @type {ScimSettingsFormEntity}
*/
originalSettings = null;
/**
* The form settings.
* @type {ScimSettingsFormEntity}
*/
formSettings = null;
/**
* The SCIM settings service.
* @type {ScimSettingsServiceWorkerService}
*/
scimSettingsService = null;
/**
* Constructor
* @param {Object} props
*/
constructor(props) {
super(props);
this.state = this.defaultState;
this.bindCallbacks();
this.scimSettingsService =
props.scimSettingsServiceWorkerService ?? new ScimSettingsServiceWorkerService(this.props.context.port);
}
/**
* Get default state
* @returns {Object}
*/
get defaultState() {
return {
isProcessing: true, // Is the form processing (loading, submitting).
hasAlreadyBeenValidated: false, // True if the form has already been submitted once.
enabled: false, // True if originalSettings is not null
settings: null,
};
}
/**
* Bind callbacks methods
*/
bindCallbacks() {
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
this.save = this.save.bind(this);
this.handleToggleEnabled = this.handleToggleEnabled.bind(this);
this.handleCopyScimUrl = this.handleCopyScimUrl.bind(this);
this.handleCopySecretToken = this.handleCopySecretToken.bind(this);
this.handleRegenerateSecretToken = this.handleRegenerateSecretToken.bind(this);
}
/**
* ComponentDidMount
* Invoked immediately after component is inserted into the tree
* @return {void}
*/
async componentDidMount() {
if (!this.props.context.users) {
await this.props.context.port.request("passbolt.users.update-local-storage");
}
await this.findScimSettings();
this.setState({ isProcessing: false });
}
/**
* componentWillUnmount
* Use to clear the data from the form in case the user put something that needs to be cleared.
*/
componentWillUnmount() {
this.clearContext();
}
/**
* Get admin users
* @returns {Array} Array of active admin users
*/
get adminUsers() {
const adminRole = this.props.roles.items.filter((r) => r.isAdmin())?.[0] || null;
const users = this.props.context.users;
if (users !== null && adminRole) {
return users.filter((user) => user.active === true && user.role_id === adminRole.id);
}
return [];
}
/**
* Get admin users formatted for select input
* @returns {Array<{value: string, label: string}>} Array of admin users formatted for select input
*/
get adminUsersForSelect() {
return (
this.adminUsers &&
this.adminUsers.map((user) => ({
value: user.id,
label: getUserFormattedName(user, this.props.t, { withUsername: true }),
}))
);
}
/**
* Find the SCIM settings
* @return {Promise<void>}
*/
async findScimSettings() {
this.setState({ isProcessing: true });
try {
const scimSettings = await this.scimSettingsService.findSettings();
if (scimSettings) {
this.originalSettings = new ScimSettingsFormEntity(scimSettings, { validate: false });
this.formSettings = new ScimSettingsFormEntity(scimSettings, { validate: false });
this.setState({
settings: this.formSettings.toDto(),
enabled: true,
});
}
} catch (error) {
await this.handleUnexpectedError(error);
this.setDefaultSettings();
}
this.setState({ isProcessing: false });
}
/**
* Set SCIM form with default settings.
*/
setDefaultSettings() {
this.formSettings = ScimSettingsFormEntity.createFromDefault(this.adminUsers[0].id);
this.setState({ settings: this.formSettings.toDto() });
}
/**
* Enable/Disable the settings
*/
handleToggleEnabled() {
if (!this.state.enabled) {
this.formSettings = ScimSettingsFormEntity.createFromDefault(this.adminUsers[0].id);
this.setState({
settings: this.formSettings.toDto(),
});
}
this.setState({ enabled: !this.state.enabled });
}
/**
* Check if the data have been changed.
* @param {ScimSettingsFormEntity} originalSettings The original settings as provided by the API.
* @param {ScimSettingsFormEntity} formSettings The settings updated by the user.
* @return {boolean}
*/
hasSettingsChanges = memoize(
// eslint-disable-next-line no-unused-vars
(originalSettings, formSettings, settings) => originalSettings?.hasDiffProps(formSettings) || false,
);
/**
* Handle form input changes.
* @param {ReactEvent} event The react event
* @returns {void}
*/
handleInputChange(event) {
if (this.hasAllInputDisabled()) {
return;
}
const { type, checked, name } = event.target;
const parsedValue = type === "checkbox" ? checked : event.target.value;
this.setFormPropertyValue(name, parsedValue);
}
/**
* Set a form property value. Trigger the validation if the form has already been submitted once.
* @param {string} name The property name
* @param {*} parsedValue The parsed value
*/
setFormPropertyValue(name, parsedValue) {
this.formSettings.set(name, parsedValue, { validate: false });
this.setState({ settings: this.formSettings.toDto() });
}
/**
* Should input be disabled? True if state is loading or processing
* @returns {boolean}
*/
hasAllInputDisabled() {
return this.state.isProcessing;
}
/**
* Handle the copy to clipboard button
*/
async handleCopyScimUrl() {
await this.props.clipboardContext.copy(
this.scimUrl,
this.props.t("The SCIM URL has been copied to the clipboard."),
);
}
/**
* Handle the copy to clipboard button
*/
async handleCopySecretToken() {
await this.props.clipboardContext.copy(
this.state.settings.secret_token,
this.props.t("The SCIM secret token has been copied to the clipboard."),
);
}
/**
* Handle the regeneration of the secret token
*/
handleRegenerateSecretToken() {
const secretToken = ScimSettingsEntity.generateScimSecretToken();
this.setFormPropertyValue("secret_token", secretToken);
}
/**
* Handle form submission that can be triggered when hitting `enter`
* @param {Event} event The html event triggering the form submit.
*/
handleFormSubmit(event) {
// Avoid the form to be submitted natively by the browser and avoid a redirect to a broken page.
event.preventDefault();
this.save();
}
/**
* Save the settings.
* @returns {Promise<void>}
*/
async save() {
if (this.state.isProcessing) {
return;
}
this.setState({ isProcessing: true });
const validationError = this.validateForm();
if (validationError?.hasErrors()) {
this.setState({ isProcessing: false, hasAlreadyBeenValidated: true });
return;
}
try {
const result = await this.saveScimSettings();
//Refresh data returned by server
this.formSettings = result ? new ScimSettingsFormEntity(result, { validate: false }) : null;
this.originalSettings = result
? new ScimSettingsFormEntity(this.formSettings.toDto(), { validate: false })
: null;
this.setState({
settings: result ? this.formSettings.toDto() : null,
enabled: result !== null,
});
await this.props.actionFeedbackContext.displaySuccess(this.props.t("The SCIM settings were updated."));
} catch (error) {
await this.handleUnexpectedError(error);
}
this.setState({
isProcessing: false,
});
}
/**
* Validate form.
* @return {EntityValidationError|null}
*/
validateForm() {
if (!this.formSettings) {
return null;
}
return this.formSettings.validate();
}
/**
* Save the SCIM settings.
* @returns {Promise<ScimSettingsEntity|null>}
*/
async saveScimSettings() {
if (this.state.enabled) {
let scimSettingResult;
if (!this.originalSettings) {
scimSettingResult = await this.scimSettingsService.createSettings(this.formSettings);
} else {
scimSettingResult = await this.scimSettingsService.updateSettings(this.formSettings, this.originalSettings.id);
}
return scimSettingResult;
} else if (this.originalSettings) {
await this.scimSettingsService.disableSettings(this.originalSettings.id);
}
return null;
}
/**
* Handle unexpected error
* @param {Error} error The error
* @returns {Promise<string>} Return the dialog key identifier.
*/
handleUnexpectedError(error) {
console.error(error);
if (error.name !== "UserAbortsOperationError") {
return this.props.dialogContext.open(NotifyError, { error });
}
}
/**
* Puts the state to its default in order to avoid keeping the data users didn't want to save.
*/
clearContext() {
this.setState(this.defaultState);
}
/**
* Check if the secret token is expired
* @returns {boolean} true if the expired date is in the past
*/
isSecretTokenExpired() {
const expired = this.state.settings?.expired;
if (!expired) {
return false;
}
return DateTime.fromISO(expired) < DateTime.now();
}
/**
* Return the formatted scim url
* @returns {string} the formated scim url
*/
get scimUrl() {
return `${this.props.context.userSettings.getTrustedDomain()}/scim/v2/${this.state.settings.setting_id}`;
}
/**
* Render the component
* @returns {JSX}
*/
render() {
const errors = this.state.hasAlreadyBeenValidated ? this.validateForm() : null;
const hasSettingsChanges = this.hasSettingsChanges(this.originalSettings, this.formSettings, this.state.settings);
return (
<div className="row">
<div id="scim-settings" className="main-column">
<div className="main-content">
<form onSubmit={this.handleFormSubmit} data-testid="submit-form">
<h3 className="title">
<span className="input toggle-switch form-element">
<input
type="checkbox"
className="toggle-switch-checkbox checkbox"
name="enabled"
onChange={this.handleToggleEnabled}
checked={this.state.enabled}
disabled={this.hasAllInputDisabled()}
id="scimSettingsToggle"
/>
<label htmlFor="scimSettingsToggle">
<Trans>SCIM</Trans>
</label>
</span>
</h3>
<p className="description">
<Trans>
SCIM is a standard protocol that automates user provisioning and deprovisioning with identity
providers.
</Trans>
</p>
{this.state.enabled && this.state.settings && (
<>
<div className={`input text input-wrapper ${this.hasAllInputDisabled() ? "disabled" : ""}`}>
<label>
<Trans>SCIM URL</Trans>
</label>
<div className="button-inline">
<input
id="scim-url-input"
type="text"
className="fluid form-element disabled"
name="scim_url"
value={this.scimUrl}
readOnly
disabled={true}
/>
<button
type="button"
onClick={this.handleCopyScimUrl}
className="copy-to-clipboard button button-icon"
>
<CopySVG />
</button>
</div>
<p>
<Trans>The URL to enter in your provider when configuring user provisioning.</Trans>
</p>
</div>
<div className={`input text input-wrapper ${this.hasAllInputDisabled() ? "disabled" : ""}`}>
<label>
<Trans>Secret Token</Trans>
</label>
<div className="button-inline">
<Password
id="scim-secret-token-input"
className="fluid form-element"
autoComplete="off"
name="scim_secret_token"
value={this.state.settings.secret_token}
preview={true}
disabled={this.state.settings.secret_token === ScimSettingsEntity.EMPTY_SECRET_VALUE}
/>
<button
type="button"
disabled={this.state.settings.secret_token === ScimSettingsEntity.EMPTY_SECRET_VALUE}
onClick={this.handleCopySecretToken}
className="copy-to-clipboard button button-icon"
>
<CopySVG />
</button>
<button
type="button"
onClick={this.handleRegenerateSecretToken}
className="copy-to-clipboard button button-icon"
>
<RefreshSVG />
</button>
</div>
</div>
<div className={`input text date-wrapper disabled`}>
<label>
<Trans>Secret token expiry</Trans>
</label>
<div className="button-inline">
<input
id="scim-secret-token-expiry-input"
type="date"
className={`fluid form-element ${this.state.settings.expired ? "" : "empty"}`}
name="expired"
value={this.state.settings.expired || ""}
disabled
/>
<CalendarSVG className="svg-icon" />
</div>
</div>
<div className={`input text input-wrapper ${this.hasAllInputDisabled() ? "disabled" : ""}`}>
<label>
<Trans>SCIM User</Trans>
</label>
<Select
items={this.adminUsersForSelect}
name="scim_user_id"
className="users"
value={this.state.settings.scim_user_id}
onChange={this.handleInputChange}
disabled={this.state.isProcessing}
search={true}
direction="bottom"
/>
<p>
<Trans>The SCIM user is the user that will perform the operation in the logs.</Trans>
</p>
</div>
<div className="section">
<label>
<Trans>Synchronisation</Trans>
</label>
<span className="input toggle-switch form-element">
<input
type="checkbox"
className="toggle-switch-checkbox checkbox"
name="passwordUpdate"
disabled={true}
checked={true}
id="send-password-update-toggle-button"
/>
<label className="text" htmlFor="send-password-update-toggle-button">
<Trans>Users.</Trans>
</label>
</span>
<span className="input toggle-switch form-element">
<input
type="checkbox"
className="toggle-switch-checkbox checkbox"
name="passwordUpdate"
disabled={true}
checked={false}
id="send-password-update-toggle-button"
/>
<label className="text" htmlFor="send-password-update-toggle-button">
<Trans>Groups (Not supported).</Trans>
</label>
</span>
</div>
</>
)}
</form>
</div>
{
<div className="warning message">
{!this.formSettings?.id && this.state.enabled && (
<div className="form-banner">
<p>
<Trans>Please save the settings to enable the feature.</Trans>
</p>
</div>
)}
{this.formSettings?.id && !this.state.enabled && (
<div className="form-banner">
<p>
<Trans>Please save the settings to disable the feature.</Trans>
</p>
</div>
)}
{hasSettingsChanges && this.state.enabled && this.formSettings.id && (
<div className="form-banner">
<p>
<Trans>Don't forget to save your settings to apply your modification.</Trans>
</p>
</div>
)}
{this.state.enabled && this.isSecretTokenExpired() && (
<div className="form-banner">
<p>
<Trans>The secret token is expired, you are requested to rotate it.</Trans>
</p>
</div>
)}
{errors?.hasErrors() && (
<div className="form-banner">
<p>
<b>
<Trans>Warning:</Trans>
</b>{" "}
<Trans>Please fix the errors in the form before saving.</Trans>
</p>
</div>
)}
</div>
}
</div>
<div className="actions-wrapper">
<button
type="button"
className="button primary"
disabled={this.state.isProcessing || errors?.hasErrors()}
onClick={this.handleFormSubmit}
>
<span>
<Trans>Save</Trans>
</span>
</button>
</div>
{createSafePortal(
<DisplayScimSettingsAdministrationHelp
shouldDisplayWarning={!hasSettingsChanges && this.state.enabled && this.formSettings?.id}
/>,
document.getElementById("administration-help-panel"),
)}
</div>
);
}
}
DisplayScimSettingsAdministration.propTypes = {
context: PropTypes.object, // The application context
actionFeedbackContext: PropTypes.object, // The action feedback context
dialogContext: PropTypes.object, // The dialog context
roleContext: PropTypes.object, // The role context
roles: PropTypes.instanceOf(RolesCollection), // The roles collection
clipboardContext: PropTypes.object, // the clipboard service provider
administrationWorkspaceContext: PropTypes.object,
scimSettingsServiceWorkerService: PropTypes.object,
t: PropTypes.func, // The translation function
};
export default withAdministrationWorkspace(
withDialog(
withActionFeedback(
withClipboard(withAppContext(withRoles(withTranslation("common")(DisplayScimSettingsAdministration)))),
),
),
);