UNPKG

passbolt-styleguide

Version:

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

887 lines (850 loc) 36.6 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.8.0 */ import React from "react"; import PropTypes from "prop-types"; import { Trans, withTranslation } from "react-i18next"; import { withAppContext } from "../../../../shared/context/AppContext/AppContext"; import { withAdministrationWorkspace } from "../../../contexts/AdministrationWorkspaceContext"; import { withDialog } from "../../../contexts/DialogContext"; import SmtpProviders from "./SmtpProviders.data"; import Password from "../../../../shared/components/Password/Password"; import Select from "../../Common/Select/Select"; import { withAdminSmtpSettings } from "../../../contexts/AdminSmtpSettingsContext"; import SmtpSettingsFormEntity from "../../../../shared/models/entity/smtpSettings/smtpSettingsFormEntity"; import DisplayAdministrationSmtpSettingsActions from "../DisplayAdministrationWorkspaceActions/DisplayAdministrationSmtpSettingsActions/DisplayAdministrationSmtpSettingsActions"; import { createSafePortal } from "../../../../shared/utils/portals"; import CaretDownSVG from "../../../../img/svg/caret_down.svg"; import CaretRightSVG from "../../../../img/svg/caret_right.svg"; import EmailSVG from "../../../../img/svg/email.svg"; import FileTextSVG from "../../../../img/svg/file_text.svg"; import LinkSVG from "../../../../img/svg/link.svg"; /* * Supported authentication methods. */ const AUTHENTICATION_METHOD_NONE = SmtpSettingsFormEntity.AUTHENTICATION_METHOD_NONE; const AUTHENTICATION_METHOD_USERNAME = SmtpSettingsFormEntity.AUTHENTICATION_METHOD_USERNAME; const AUTHENTICATION_METHOD_USERNAME_PASSWORD = SmtpSettingsFormEntity.AUTHENTICATION_METHOD_USERNAME_PASSWORD; const AUTHENTICATION_METHOD_OAUTH = SmtpSettingsFormEntity.AUTHENTICATION_METHOD_OAUTH; export class ManageSmtpAdministrationSettings extends React.Component { /** * The no authentication method. * @returns {string} */ static get AUTHENTICATION_METHOD_NONE() { return AUTHENTICATION_METHOD_NONE; } /** * The authentication method username only * @returns {string} */ static get AUTHENTICATION_METHOD_USERNAME() { return AUTHENTICATION_METHOD_USERNAME; } /** * The authentication method username and password * @returns {string} */ static get AUTHENTICATION_METHOD_USERNAME_PASSWORD() { return AUTHENTICATION_METHOD_USERNAME_PASSWORD; } /** * The authentication method OAuth2 Client Credentials * @returns {string} */ static get AUTHENTICATION_METHOD_OAUTH() { return AUTHENTICATION_METHOD_OAUTH; } /** * Constructor * @param {Object} props * @constructor */ constructor(props) { super(props); this.state = this.defaultState; this.bindCallbacks(); this.createRefs(); } /** * Get default state * @returns {Object} */ get defaultState() { return { showAdvancedSettings: false, initialized: false, source: "db", }; } /** * Creates the React ref for the forms to be able to focus fields having errors */ createRefs() { this.usernameFieldRef = React.createRef(); this.passwordFieldRef = React.createRef(); this.hostFieldRef = React.createRef(); this.portFieldRef = React.createRef(); this.clientFieldRef = React.createRef(); this.senderEmailFieldRef = React.createRef(); this.senderNameFieldRef = React.createRef(); this.oauth_usernameFieldRef = React.createRef(); this.tenant_idFieldRef = React.createRef(); this.client_idFieldRef = React.createRef(); this.client_secretFieldRef = React.createRef(); } /** * ComponentDidMount * Invoked immediately after component is inserted into the tree * @return {void} */ componentDidMount() { this.props.administrationWorkspaceContext.setDisplayAdministrationWorkspaceAction( DisplayAdministrationSmtpSettingsActions, ); this.props.adminSmtpSettingsContext.findSmtpSettings(); } /** * componentWillUnmount * Use to clear the data from the form in case the user put something that needs to be cleared. */ componentWillUnmount() { this.props.administrationWorkspaceContext.resetDisplayAdministrationWorkspaceAction(); this.props.adminSmtpSettingsContext.clearContext(); } /** * componentDidUpdate * Invoked immediately after state or props is updated. * It is used to focus on a field if needed or to show the advanced settings panel if needed. */ componentDidUpdate() { const smtpContext = this.props.adminSmtpSettingsContext; const settings = smtpContext.getCurrentSmtpSettings(); // When data becomes loaded for the first time, set showAdvancedSettings based on provider if (smtpContext.isDataReady() && !this.state.initialized) { this.setState({ initialized: true, showAdvancedSettings: settings?.provider === "other", }); } const fieldToFocus = smtpContext.getFieldToFocus(); if (fieldToFocus) { this[`${fieldToFocus}FieldRef`]?.current?.focus(); } if (smtpContext.hasProviderChanged()) { this.setState({ showAdvancedSettings: settings?.provider === "other" }); } } /** * Bind callbacks */ bindCallbacks() { this.handleAdvancedSettingsToggle = this.handleAdvancedSettingsToggle.bind(this); this.handleInputChange = this.handleInputChange.bind(this); this.handleProviderChange = this.handleProviderChange.bind(this); this.handleAuthenticationMethodChange = this.handleAuthenticationMethodChange.bind(this); } /** * Handle provider change from select field. * @params {ReactEvent} The react event */ handleProviderChange(event) { const providerId = event.target.value; const provider = SmtpProviders.find((item) => item.id === providerId); this.props.adminSmtpSettingsContext.changeProvider(provider); } /** * Handle provider change from select field. * @params {ReactEvent} The react event */ handleAuthenticationMethodChange(event) { this.props.adminSmtpSettingsContext.changeAuthenticationMethod(event.target.value); } /** * Handle form input changes. * @params {ReactEvent} The react event * @returns {void} */ handleInputChange(event) { const target = event.target; this.props.adminSmtpSettingsContext.setData({ [target.name]: target.value }); } /** * Handles the click on the advanced settings toggle. */ handleAdvancedSettingsToggle() { this.setState({ showAdvancedSettings: !this.state.showAdvancedSettings }); } /** * Returns true if there is a processing happening with the SMTP settings. * @returns {boolean} */ isProcessing() { return this.props.adminSmtpSettingsContext.isProcessing(); } /** * Return the list of available providers. * @returns {Array<object>} */ get providerList() { return SmtpProviders.map((item) => ({ value: item.id, label: item.name, })); } /** * Return the list of available authentication method. * @returns {Array<object>} */ get authenticationMethodList() { const settings = this.props.adminSmtpSettingsContext.getCurrentSmtpSettings(); const list = [ { value: AUTHENTICATION_METHOD_NONE, label: this.translate("None") }, { value: AUTHENTICATION_METHOD_USERNAME, label: this.translate("Username only") }, { value: AUTHENTICATION_METHOD_USERNAME_PASSWORD, label: this.translate("Username & password") }, ]; if (settings?.provider === "office-365") { list.push({ value: AUTHENTICATION_METHOD_OAUTH, label: this.translate("OAuth (Client Credentials Grant)") }); } return list; } /** * Returns a list of available choice for the 'TLS' select field. * @returns {Array<object>} */ get tlsSelectList() { return [ { value: true, label: this.translate("Yes"), }, { value: false, label: this.translate("No"), }, ]; } /** * Return the selected authentication method. * @return {string} */ get authenticationMethod() { return this.props.adminSmtpSettingsContext.getAuthenticationMethod(); } /** * Return true if the username field should be displayed * @return {boolean} */ shouldDisplayUsername() { return ( this.authenticationMethod === AUTHENTICATION_METHOD_USERNAME || this.authenticationMethod === AUTHENTICATION_METHOD_USERNAME_PASSWORD ); } /** * Return true if the password field should be displayed * @return {boolean} */ shouldDisplayPassword() { return this.authenticationMethod === AUTHENTICATION_METHOD_USERNAME_PASSWORD; } /** * Return true if the OAuth fields should be displayed * @return {boolean} */ shouldDisplayOAuth() { return this.authenticationMethod === AUTHENTICATION_METHOD_OAUTH; } /** * Returns true if the source of the settings is from a config file instead of DB and the user has modified a field. * @returns {boolean} */ shouldShowSourceWarningMessage() { const smtpContext = this.props.adminSmtpSettingsContext; return smtpContext.getCurrentSmtpSettings().source !== "db" && smtpContext.isSettingsModified(); } /** * Returns true if the data is loaded. * Useful to avoid UI blinking during data loading. * @returns {boolean} */ isReady() { return this.props.adminSmtpSettingsContext.isDataReady(); } /** * Returns the source of the settings * @returns {string} */ get settingsSource() { return this.props.adminSmtpSettingsContext?.getCurrentSmtpSettings()?.source; } /** * Returns the source of the current configuration * @returns {string} */ get configurationSource() { return ( { env: this.props.t("environment variables"), file: this.props.t("file"), db: this.props.t("database"), }[this.settingsSource] || this.props.t("unknown") ); } /** * Get the translate function * @returns {function(...[*]=)} */ get translate() { return this.props.t; } /** * Render the component * @returns {JSX} */ render() { const settings = this.props.adminSmtpSettingsContext.getCurrentSmtpSettings(); const errors = this.props.adminSmtpSettingsContext.getErrors(); const providerObject = SmtpProviders.find((p) => p.id === settings?.provider); const smtpProviderName = providerObject?.name; const hasChanges = this.props.adminSmtpSettingsContext.isSettingsModified(); const hasWarnings = hasChanges || this.shouldShowSourceWarningMessage(); return ( <div className="row"> <> <div className="third-party-provider-settings smtp-settings main-column"> <div className="main-content"> <h3 className="title"> <Trans>Email server</Trans> </h3> {this.isReady() && !settings?.provider && ( <> <h4 className="no-border"> <Trans>Select a provider</Trans> </h4> <div className="provider-list"> {SmtpProviders.map((provider) => ( <div key={provider.id} className="provider button" id={provider.id} onClick={() => this.props.adminSmtpSettingsContext.changeProvider(provider)} > <div className="provider-logo"> {provider.id === "other" && <EmailSVG />} {provider.id !== "other" && ( <img src={`${this.props.context.trustedDomain}/img/third_party/${provider.icon}`} /> )} </div> <p className="provider-name">{provider.name}</p> </div> ))} </div> </> )} {this.isReady() && settings?.provider && ( <> <form className="form"> <h4 className="no-border"> <Trans>SMTP server configuration</Trans> </h4> <div className={`select-wrapper input required ${this.isProcessing() ? "disabled" : ""}`}> <label htmlFor="smtp-settings-form-provider"> <Trans>Email provider</Trans> </label> <Select id="smtp-settings-form-provider" name="provider" items={this.providerList} value={settings.provider} onChange={this.handleProviderChange} disabled={this.isProcessing()} /> </div> <div className={`select-wrapper input required ${this.isProcessing() ? "disabled" : ""}`}> <label htmlFor="smtp-settings-form-authentication-method"> <Trans>Authentication method</Trans> </label> <Select id="smtp-settings-form-authentication-method" name="authentication-method" items={this.authenticationMethodList} value={this.authenticationMethod} onChange={this.handleAuthenticationMethodChange} disabled={this.isProcessing()} /> </div> {this.shouldDisplayUsername() && ( <div className={`input text ${errors?.hasError("username") ? "error" : ""} ${this.isProcessing() ? "disabled" : ""}`} > <label htmlFor="smtp-settings-form-username"> <Trans>Username</Trans> </label> <input id="smtp-settings-form-username" ref={this.usernameFieldRef} name="username" className="fluid" maxLength="256" type="text" autoComplete="off" value={settings.username} onChange={this.handleInputChange} placeholder={this.translate("Username")} disabled={this.isProcessing()} /> {errors?.hasError("username") && ( <div className="error-message"> <Trans> This is the maximum size for this field, make sure your data was not truncated. </Trans> </div> )} </div> )} {this.shouldDisplayPassword() && ( <div className={`input-password-wrapper input ${errors?.hasError("password") ? "error" : ""} ${this.isProcessing() ? "disabled" : ""}`} > <label htmlFor="smtp-settings-form-password"> <Trans>Password</Trans> </label> <Password id="smtp-settings-form-password" name="password" autoComplete="new-password" placeholder={this.translate("Password")} preview={true} value={settings.password} onChange={this.handleInputChange} disabled={this.isProcessing()} inputRef={this.passwordFieldRef} /> {errors?.hasError("password") && ( <div className="password error-message"> <Trans> This is the maximum size for this field, make sure your data was not truncated. </Trans> </div> )} </div> )} {this.shouldDisplayOAuth() && ( <> <div className={`input text required ${errors?.hasError("oauth_username") ? "error" : ""} ${this.isProcessing() ? "disabled" : ""}`} > <label htmlFor="smtp-settings-form-oauth-username"> <Trans>OAuth Username</Trans> </label> <input id="smtp-settings-form-oauth-username" ref={this.oauth_usernameFieldRef} name="oauth_username" aria-required={true} className="fluid" maxLength="256" type="text" autoComplete="off" value={settings.oauth_username} onChange={this.handleInputChange} placeholder={this.translate("OAuth Username")} disabled={this.isProcessing()} /> {errors?.hasError("oauth_username") && ( <div className="error-message"> {!settings.oauth_username || errors.hasError("oauth_username", "required") ? ( <Trans>OAuth Username is required.</Trans> ) : ( <Trans>OAuth Username must be a valid email.</Trans> )} </div> )} </div> <div className={`input text required ${errors?.hasError("tenant_id") ? "error" : ""} ${this.isProcessing() ? "disabled" : ""}`} > <label htmlFor="smtp-settings-form-tenant-id"> <Trans>Tenant ID</Trans> </label> <input id="smtp-settings-form-tenant-id" ref={this.tenant_idFieldRef} name="tenant_id" aria-required={true} className="fluid" maxLength="256" type="text" autoComplete="off" value={settings.tenant_id} onChange={this.handleInputChange} placeholder={this.translate("Tenant ID")} disabled={this.isProcessing()} /> {errors?.hasError("tenant_id") && ( <div className="error-message"> <Trans>Tenant ID is required.</Trans> </div> )} </div> <div className={`input text required ${errors?.hasError("client_id") ? "error" : ""} ${this.isProcessing() ? "disabled" : ""}`} > <label htmlFor="smtp-settings-form-client-id"> <Trans>Client ID</Trans> </label> <input id="smtp-settings-form-client-id" ref={this.client_idFieldRef} name="client_id" aria-required={true} className="fluid" maxLength="256" type="text" autoComplete="off" value={settings.client_id} onChange={this.handleInputChange} placeholder={this.translate("Client ID")} disabled={this.isProcessing()} /> {errors?.hasError("client_id") && ( <div className="error-message"> <Trans>Client ID is required.</Trans> </div> )} </div> <div className={`input-password-wrapper input required ${errors?.hasError("client_secret") ? "error" : ""} ${this.isProcessing() ? "disabled" : ""}`} > <label htmlFor="smtp-settings-form-client-secret"> <Trans>Client Secret</Trans> </label> <Password id="smtp-settings-form-client-secret" name="client_secret" autoComplete="new-password" placeholder={this.translate("Client Secret")} preview={true} value={settings.client_secret} onChange={this.handleInputChange} disabled={this.isProcessing()} inputRef={this.client_secretFieldRef} /> {errors?.hasError("client_secret") && ( <div className="password error-message"> <Trans>Client Secret is required.</Trans> </div> )} </div> </> )} <div className="accordion-header"> <button type="button" className="link no-border" onClick={this.handleAdvancedSettingsToggle}> {this.state.showAdvancedSettings ? <CaretDownSVG /> : <CaretRightSVG />} <Trans>Advanced settings</Trans> </button> </div> {this.state.showAdvancedSettings && ( <div className="advanced-settings"> <div className={`input text required ${errors?.hasError("host") ? "error" : ""} ${this.isProcessing() ? "disabled" : ""}`} > <label htmlFor="smtp-settings-form-host"> <Trans>SMTP host</Trans> </label> <input id="smtp-settings-form-host" ref={this.hostFieldRef} name="host" aria-required={true} className="fluid" maxLength="256" type="text" autoComplete="off" value={settings.host} onChange={this.handleInputChange} placeholder={this.translate("SMTP server address")} disabled={this.isProcessing()} /> {errors?.hasError("host") && ( <div className="error-message"> <Trans>SMTP Host is required</Trans> </div> )} </div> <div className={`input text required ${errors?.hasError("tls") ? "error" : ""} ${this.isProcessing() ? "disabled" : ""}`} > <label htmlFor="smtp-settings-form-tls"> <Trans>Use TLS</Trans> </label> <Select id="smtp-settings-form-tls" name="tls" items={this.tlsSelectList} value={settings.tls} onChange={this.handleInputChange} disabled={this.isProcessing()} /> {errors?.hasError("tls") && ( <div className="error-message"> <Trans>TLS must be set to &apos;Yes&apos; or &apos;No&apos;</Trans> </div> )} </div> <div className={`input text required ${errors?.hasError("port") ? "error" : ""} ${this.isProcessing() ? "disabled" : ""}`} > <label htmlFor="smtp-settings-form-port"> <Trans>Port</Trans> </label> <input id="smtp-settings-form-port" aria-required={true} ref={this.portFieldRef} name="port" className="fluid" maxLength="256" type="text" autoComplete="off" value={settings.port} onChange={this.handleInputChange} placeholder={this.translate("Port number")} disabled={this.isProcessing()} /> {errors?.hasError("port") && ( <div className="error-message"> {errors.hasError("port", "minimum") || errors.hasError("port", "maximum") ? ( <Trans>Port must be a number between 1 and 65535</Trans> ) : ( <Trans>Port must be a valid number</Trans> )} </div> )} </div> <div className={`input text ${errors?.hasError("client") ? "error" : ""} ${this.isProcessing() ? "disabled" : ""}`} > <label htmlFor="smtp-settings-form-client"> <Trans>SMTP client</Trans> </label> <input id="smtp-settings-form-client" ref={this.clientFieldRef} name="client" maxLength="2048" type="text" autoComplete="off" value={settings.client} onChange={this.handleInputChange} placeholder={this.translate("SMTP client address")} disabled={this.isProcessing()} /> {errors?.hasError("client") && ( <div className="error-message"> <Trans>SMTP client should be a valid domain or IP address</Trans> </div> )} </div> </div> )} <h4> <Trans>Sender configuration</Trans> </h4> <div className={`input text required ${errors?.hasError("sender_name") ? "error" : ""} ${this.isProcessing() ? "disabled" : ""}`} > <label htmlFor="smtp-settings-form-sender-name"> <Trans>Sender name</Trans> </label> <input id="smtp-settings-form-sender-name" ref={this.senderNameFieldRef} name="sender_name" aria-required={true} className="fluid" maxLength="256" type="text" autoComplete="off" value={settings.sender_name} onChange={this.handleInputChange} placeholder={this.translate("Sender name")} disabled={this.isProcessing()} /> {errors?.hasError("sender_name") && ( <div className="error-message"> <Trans>Sender name is required</Trans> </div> )} <p> <Trans> This is the name users will see in their mailbox when passbolt sends a notification. </Trans> </p> </div> <div className={`input text required ${errors?.hasError("sender_email") ? "error" : ""} ${this.isProcessing() ? "disabled" : ""}`} > <label htmlFor="smtp-settings-form-sender-name"> <Trans>Sender email</Trans> </label> <input id="smtp-settings-form-sender-email" ref={this.senderEmailFieldRef} name="sender_email" aria-required={true} className="fluid" maxLength="256" type="text" autoComplete="off" value={settings.sender_email} onChange={this.handleInputChange} placeholder={this.translate("Sender email")} disabled={this.isProcessing()} /> {errors?.hasError("sender_email") && ( <div className="error-message"> {!settings.sender_email || errors.hasError("sender_email", "required") ? ( <Trans>Sender email is required</Trans> ) : ( <Trans>Sender email must be a valid email</Trans> )} </div> )} <p> <Trans> This is the email address users will see in their mail box when passbolt sends a notification. <br /> It&apos;s a good practice to provide a working email address that users can reply to. </Trans> </p> </div> </form> </> )} </div> {hasWarnings && ( <div className="warning message"> {this.shouldShowSourceWarningMessage() && ( <div id="smtp-settings-source-warning"> <Trans> These are the settings provided by a configuration file. If you save it, will ignore the settings on file and use the ones from the database. </Trans> </div> )} {hasChanges && ( <div> <p> <Trans>Don&apos;t forget to save your settings to apply your modification.</Trans> </p> </div> )} </div> )} </div> <DisplayAdministrationSmtpSettingsActions /> </> {createSafePortal( <> <div className="sidebar-help-section" id="smtp-settings-source"> <h3> <Trans>Configuration source</Trans> </h3> <p> <Trans>This current configuration source is: </Trans> {this.configurationSource}. </p> </div> <div className="sidebar-help-section"> <h3> <Trans>Why do I need an SMTP server?</Trans> </h3> <p> <Trans> Passbolt needs an smtp server in order to send invitation emails after an account creation and to send email notifications. </Trans> </p> <a className="button" href="https://passbolt.com/docs/admin/emails/email-server/" target="_blank" rel="noopener noreferrer" > <FileTextSVG /> <span> <Trans>Read the documentation</Trans> </span> </a> </div> {providerObject && providerObject.id !== "other" && ( <div className="sidebar-help-section"> <h3> <Trans>How do I configure a {{ smtpProviderName }} SMTP server?</Trans> </h3> <a className="button" href={providerObject.help_page} target="_blank" rel="noopener noreferrer"> <LinkSVG /> <span> <Trans>See the {{ smtpProviderName }} documentation</Trans> </span> </a> </div> )} {settings?.provider && (settings.provider === "google-mail" || settings.provider === "google-workspace") && ( <div className="sidebar-help-section"> <h3> <Trans>Why shouldn&apos;t I use my login password ?</Trans> </h3> <p> <Trans> In order to use the &quot;Username & Password&quot; authentication method with Google, you will need to enable MFA on your Google Account. The password should not be your login password, you have to create an &quot;App Password&quot; generated by Google.. However, the email remain the same. </Trans> </p> <a className="button" href="https://support.google.com/mail/answer/185833" target="_blank" rel="noopener noreferrer" > <FileTextSVG /> <span> <Trans>More informations</Trans> </span> </a> </div> )} </>, document.getElementById("administration-help-panel"), )} </div> ); } } ManageSmtpAdministrationSettings.propTypes = { context: PropTypes.object, // Application context dialogContext: PropTypes.any, // The dialog context administrationWorkspaceContext: PropTypes.object, // The administration workspace context adminSmtpSettingsContext: PropTypes.object, // The administration SMTP settings context t: PropTypes.func, // The translation function }; export default withAppContext( withAdminSmtpSettings( withDialog(withAdministrationWorkspace(withTranslation("common")(ManageSmtpAdministrationSettings))), ), );