UNPKG

passbolt-styleguide

Version:

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

584 lines (558 loc) 22.3 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.6.0 */ import React, { Fragment } from "react"; import PropTypes from "prop-types"; import { Trans, withTranslation } from "react-i18next"; import { withAppContext } from "../../../../shared/context/AppContext/AppContext"; import DisplayAdministrationAccountRecoveryActions from "../DisplayAdministrationWorkspaceActions/DisplayAdministrationAccountRecoveryAction/DisplayAdministrationAccountRecoveryActions"; import { withAdministrationWorkspace } from "../../../contexts/AdministrationWorkspaceContext"; import { withDialog } from "../../../contexts/DialogContext"; import SelectAccountRecoveryOrganizationKey from "../SelectAccountRecoveryOrganizationKey/SelectAccountRecoveryOrganizationKey"; import DownloadOrganizationKey from "../SelectAccountRecoveryOrganizationKey/DownloadOrganizationKey"; import { withAdminAccountRecovery } from "../../../contexts/AdminAccountRecoveryContext"; import { formatDateTimeAgo } from "../../../../shared/utils/dateUtils"; import { createSafePortal } from "../../../../shared/utils/portals"; import BuoySVG from "../../../../img/svg/buoy.svg"; class ManageAccountRecoveryAdministrationSettings extends React.Component { /** * Constructor * @param {Object} props */ constructor(props) { super(props); this.state = this.defaultState; this.bindCallbacks(); } /** * Get default state * @returns {Object} */ get defaultState() { return { keyInfoDto: null, // Key info loading: true, // Component is loading or not processing: false, // Component is processing or not }; } /** * On the component did mount, set the workspace action component and get the account recovery policy * */ async componentDidMount() { this.findAccountRecoveryPolicy(); } /** * On the component will unmount. */ componentWillUnmount() { this.props.adminAccountRecoveryContext.resetChanges(); } /** * On the component did updated */ async componentDidUpdate() { const publicArmoredKey = this.props?.adminAccountRecoveryContext?.policyChanges?.publicKey || this.props?.adminAccountRecoveryContext?.currentPolicy?.account_recovery_organization_public_key?.armored_key; const keyInfoDto = await this.props?.adminAccountRecoveryContext?.getKeyInfo(publicArmoredKey); /* * Formerly, we compared the armored key together to check if the key changed. * This yield into a bug sometimes where the react updates in an infinite loop. * The reason was that openpgpjs might produce another amored key string than the original one and the comparison failed everytime. * So comparing fingerprints avoid to have this infinite refresh loop. */ if (Boolean(publicArmoredKey) && this.state.keyInfoDto?.fingerprint !== keyInfoDto?.fingerprint) { this.setState({ keyInfoDto }); } else if (!publicArmoredKey && this.state.keyInfoDto) { this.setState({ keyInfoDto: null }); } } /** * Bind callbacks methods */ bindCallbacks() { this.handlePolicyInputChange = this.handlePolicyInputChange.bind(this); this.HandleUpdatePublicKeyClick = this.HandleUpdatePublicKeyClick.bind(this); this.handleUpdateOrganizationKey = this.handleUpdateOrganizationKey.bind(this); this.downloadPrivateKey = this.downloadPrivateKey.bind(this); } /** * Get the account recovery policy * @returns {null|string} */ get policy() { return ( this.props.adminAccountRecoveryContext.policyChanges.policy || this.props.adminAccountRecoveryContext.currentPolicy?.policy ); } /** * Get the account recovery organization public key. * @returns {null|string} The armored organization public key. */ get organizationKey() { return ( this.props.adminAccountRecoveryContext.policyChanges.publicKey || this.props.adminAccountRecoveryContext.currentPolicy?.account_recovery_organization_public_key.armored_key ); } /** * Get the account recovery organization key info * @returns {null|object} */ get organizationKeyInfo() { return this.state.keyInfoDto; } /** * fetch the account recovery organization settings */ async findAccountRecoveryPolicy() { await this.props.adminAccountRecoveryContext.findAccountRecoveryPolicy(); this.setState({ loading: false, }); } /** * Handle policy input change. * @params {ReactEvent} The react event * @returns {void} */ async handlePolicyInputChange(event) { const policy = event.target.value; if (policy === "disabled") { // the keyinfo will be updated in componentDidUpdate. this.resetKeyInfo(); } this.props.adminAccountRecoveryContext.changePolicy(policy); } /** * Handle click on "Rotate key" button */ HandleUpdatePublicKeyClick() { this.props.dialogContext.open(SelectAccountRecoveryOrganizationKey, { handleUpdateOrganizationKey: this.handleUpdateOrganizationKey, }); } /** * Handle registration in current state of the new Organization public key * @param {string} publicKey The new organization public key * @param {string} privateKey (Optional) The new organization private key. Presents if the user generate a key via the UI. */ async handleUpdateOrganizationKey(publicKey, privateKey) { this.resetKeyInfo(); this.props.adminAccountRecoveryContext.changePublicKey(publicKey); if (privateKey) { await this.downloadPrivateKey(privateKey); } } /** * Download the generated organization private key. * @param {string} privateKey The new organization private key. * @returns {Promise<void>} */ async downloadPrivateKey(privateKey) { await this.props.adminAccountRecoveryContext.downloadPrivateKey(privateKey); this.props.dialogContext.open(DownloadOrganizationKey, { privateKey: privateKey, handleDownloadAgain: async () => { await this.props.adminAccountRecoveryContext.downloadPrivateKey(privateKey); }, }); } /** * Should input be disabled? True if state is loading or processing * @returns {boolean} */ hasAllInputDisabled() { return this.state.processing || this.state.loading; } /** * Has a recovery key * @returns {boolean} */ hasOrganisationRecoveryKey() { const publicORK = this.state.keyInfoDto; return Boolean(publicORK); } /** * Is policy enabled. * @returns {boolean} */ isPolicyEnabled() { return Boolean(this.policy !== "disabled"); } /** * Reset the key details. */ resetKeyInfo() { this.setState({ keyInfoDto: null }); } /** * get fingerprint * @param fingerprint * @returns {JSX.Element} */ formatFingerprint(fingerprint) { if (!fingerprint) { return null; } const result = fingerprint.toUpperCase().replace(/.{4}/g, "$& "); return ( <> {result.substr(0, 24)} <br /> {result.substr(25)} </> ); } /** * Format organization key user ids. * @param user_ids * @returns {null|*} */ formatUserIds(user_ids) { if (!user_ids) { return null; } return user_ids.map((user, id) => ( <Fragment key={id}> {user.name} &lt;{user.email}&gt; <br /> </Fragment> )); } /** * Get the translate function * @returns {function(...[*]=)} */ get translate() { return this.props.t; } /** * Render the component * @returns {JSX} */ render() { const hasWarnings = this.props.adminAccountRecoveryContext.hasPolicyChanges() || (!this.hasOrganisationRecoveryKey() && this.isPolicyEnabled()); return ( <div className="row"> <div className="recover-account-settings main-column"> <div className="main-content"> <h3 className="title"> <Trans>Account Recovery</Trans> </h3> <form className="form"> <h4 className="no-border"> <Trans>Account Recovery Policy</Trans> </h4> <p> <Trans>In this section you can choose the default behavior of account recovery for all users.</Trans> </p> <div className="radiolist-alt"> <div className={`input radio ${this.policy === "mandatory" ? "checked" : ""}`}> <input type="radio" value="mandatory" onChange={this.handlePolicyInputChange} name="accountRecoveryPolicy" checked={this.policy === "mandatory"} id="accountRecoveryPolicyMandatory" disabled={this.hasAllInputDisabled()} /> <label htmlFor="accountRecoveryPolicyMandatory"> <span className="name"> <Trans>Prompt</Trans> </span> <span className="info"> <Trans> Every user is required to provide a copy of their private key and passphrase during setup. </Trans> <br /> <Trans>You should inform your users not to store personal passwords.</Trans> </span> </label> </div> <div className={`input radio ${this.policy === "opt-out" ? "checked" : ""}`}> <input type="radio" value="opt-out" onChange={this.handlePolicyInputChange} name="accountRecoveryPolicy" checked={this.policy === "opt-out"} id="accountRecoveryPolicyOptOut" disabled={this.hasAllInputDisabled()} /> <label htmlFor="accountRecoveryPolicyOptOut"> <span className="name"> <Trans>Optional, Opt-out</Trans> </span> <span className="info"> <Trans> Every user will be prompted to provide a copy of their private key and passphrase by default during the setup, but they can opt out. </Trans> </span> </label> </div> <div className={`input radio ${this.policy === "opt-in" ? "checked" : ""}`}> <input type="radio" value="opt-in" onChange={this.handlePolicyInputChange} name="accountRecoveryPolicy" checked={this.policy === "opt-in"} id="accountRecoveryPolicyOptIn" disabled={this.hasAllInputDisabled()} /> <label htmlFor="accountRecoveryPolicyOptIn"> <span className="name"> <Trans>Optional, Opt-in</Trans> </span> <span className="info"> <Trans> Every user can decide to provide a copy of their private key and passphrase by default during the setup, but they can opt in. </Trans> </span> </label> </div> <div className={`input radio ${this.policy === "disabled" ? "checked" : ""}`}> <input type="radio" value="disabled" onChange={this.handlePolicyInputChange} name="accountRecoveryPolicy" checked={this.policy === "disabled"} id="accountRecoveryPolicyDisable" disabled={this.hasAllInputDisabled()} /> <label htmlFor="accountRecoveryPolicyDisable"> <span className="name"> <Trans>Disable (Default)</Trans> </span> <span className="info"> <Trans> Backup of the private key and passphrase will not be stored. This is the safest option. </Trans> <Trans> If users lose their private key and passphrase they will not be able to recover their account. </Trans> </span> </label> </div> </div> <h4> <span className="input toggle-switch form-element "> <input type="checkbox" className="toggle-switch-checkbox checkbox" name="organisationRecoveryKeyToggle" disabled={this.hasAllInputDisabled()} checked={this.isPolicyEnabled()} id="recovery-key-toggle-button" /> <label htmlFor="recovery-key-toggle-button"> <Trans>Organization Recovery Key</Trans> </label> </span> </h4> {this.isPolicyEnabled() && ( <> <p> <Trans> Your organization recovery key will be used to decrypt and recover the private key and passphrase of the users that are participating in the account recovery program. </Trans>{" "} <Trans>The organization private recovery key should not be stored in passbolt.</Trans>{" "} <Trans>You should keep it offline in a safe place.</Trans> </p> <div className="recovery-key-details"> <table className="table-info recovery-key"> <tbody> <tr className="user-ids"> <td className="label"> <Trans>User ids</Trans> </td> {this.organizationKeyInfo?.user_ids && ( <td className="value">{this.formatUserIds(this.organizationKeyInfo.user_ids)}</td> )} {!this.organizationKeyInfo?.user_ids && ( <td className="empty-value"> <Trans>not available</Trans> </td> )} <td className="table-button"> <button className="button primary" type="button" disabled={this.hasAllInputDisabled()} onClick={this.HandleUpdatePublicKeyClick} > {this.hasOrganisationRecoveryKey() && <Trans>Rotate Key</Trans>} {!this.hasOrganisationRecoveryKey() && <Trans>Add an Organization Recovery Key</Trans>} </button> </td> </tr> <tr className="fingerprint"> <td className="label"> <Trans>Fingerprint</Trans> </td> {this.organizationKeyInfo?.fingerprint && ( <td className="value">{this.formatFingerprint(this.organizationKeyInfo.fingerprint)}</td> )} {!this.organizationKeyInfo?.fingerprint && ( <td className="empty-value"> <Trans>not available</Trans> </td> )} </tr> <tr className="algorithm"> <td className="label"> <Trans>Algorithm</Trans> </td> {this.organizationKeyInfo?.algorithm && ( <td className="value">{this.organizationKeyInfo.algorithm}</td> )} {!this.organizationKeyInfo?.algorithm && ( <td className="empty-value"> <Trans>not available</Trans> </td> )} </tr> <tr className="key-length"> <td className="label"> <Trans>Key length</Trans> </td> {this.organizationKeyInfo?.length && ( <td className="value">{this.organizationKeyInfo.length}</td> )} {!this.organizationKeyInfo?.length && ( <td className="empty-value"> <Trans>not available</Trans> </td> )} </tr> <tr className="created"> <td className="label"> <Trans>Created</Trans> </td> {this.organizationKeyInfo?.created && ( <td className="value" title={this.organizationKeyInfo.created}> {formatDateTimeAgo( this.organizationKeyInfo.created, this.props.t, this.props.context.locale, )} </td> )} {!this.organizationKeyInfo?.created && ( <td className="empty-value"> <Trans>not available</Trans> </td> )} </tr> <tr className="expires"> <td className="label"> <Trans>Expires</Trans> </td> {this.organizationKeyInfo?.expires && ( <td className="value" title={this.organizationKeyInfo.expires}> {formatDateTimeAgo( this.organizationKeyInfo.expires, this.props.t, this.props.context.locale, )} </td> )} {!this.organizationKeyInfo?.expires && ( <td className="empty-value"> <Trans>not available</Trans> </td> )} </tr> </tbody> </table> </div> </> )} </form> </div> {hasWarnings && ( <div className="warning message" id="email-notification-setting-overridden-banner"> {this.props.adminAccountRecoveryContext.hasPolicyChanges() && ( <div id="email-notification-setting-overridden-banner"> <div> <p> <Trans>Don&apos;t forget to save your settings to apply your modification.</Trans> </p> </div> </div> )} {!this.hasOrganisationRecoveryKey() && this.isPolicyEnabled() && ( <div id="email-notification-setting-overridden-banner"> <div> <p> <Trans>Warning, Don&apos;t forget to add an organization recovery key.</Trans> </p> </div> </div> )} </div> )} </div> <DisplayAdministrationAccountRecoveryActions /> {createSafePortal( <div className="sidebar-help-section"> <h3> <Trans>Need some help?</Trans> </h3> <p> <Trans> For more information about account recovery, checkout the dedicated page on the help website. </Trans> </p> <a className="button" href="https://passbolt.com/docs/admin/authentication/account-recovery/" target="_blank" rel="noopener noreferrer" > <BuoySVG /> <span> <Trans>Read the documentation</Trans> </span> </a> </div>, document.getElementById("administration-help-panel"), )} </div> ); } } ManageAccountRecoveryAdministrationSettings.propTypes = { context: PropTypes.object, // Application context dialogContext: PropTypes.any, // The dialog context administrationWorkspaceContext: PropTypes.object, // The administration workspace context adminAccountRecoveryContext: PropTypes.object, // The admin account recovery context t: PropTypes.func, // The translation function }; export default withAppContext( withDialog( withAdministrationWorkspace( withAdminAccountRecovery(withTranslation("common")(ManageAccountRecoveryAdministrationSettings)), ), ), );