UNPKG

passbolt-styleguide

Version:

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

409 lines (380 loc) 11.3 kB
/** * Passbolt ~ Open source password manager for teams * Copyright (c) 2020 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) 2020 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.0.0 */ import React, { Component } from "react"; import PropTypes from "prop-types"; import { Trans, withTranslation } from "react-i18next"; import { CirclePicker } from "react-color"; import SecretComplexity from "../../../../shared/lib/Secret/SecretComplexity"; import Tooltip from "../../Common/Tooltip/Tooltip"; import { isValidSecurityToken } from "../../../../shared/utils/assertions"; import InfoSVG from "../../../../img/svg/info.svg"; class ChooseSecurityToken extends Component { /** * Default constructor * @param props Component props */ constructor(props) { super(props); this.state = this.defaultState; this.bindEventHandlers(); this.createReferences(); } /** * Whenever the component is mounted */ componentDidMount() { // randomize token and color this.handleRandomize(); } /** * Returns the default state */ get defaultState() { return { background: "", // The token color code: "", // The token code processing: false, // True if one's processing passphrase hasBeenValidated: false, // true if the form has already validated once errors: { emptyCode: false, // True if the token code is empty lengthCode: false, // True if the token code length is > 3 invalidRegex: false, // True if the regex is not valid }, }; } /** * Returns the default set of colors */ get defaultColors() { return [ "#f44336", "#9c27b0", "#3f51b5", "#03a9f4", "#009688", "#8bc34a", "#ffeb3b", "#ff9800", "#795548", "#607d8b", "#000000", "#f6f6f6", ]; } /** * Returns the default set of colors text */ get textColor() { const c = this.state.background.substr(1).match(/(\S{2})/g); const r = parseInt(c[0], 16); const g = parseInt(c[1], 16); const b = parseInt(c[2], 16); const l = (r * 299 + g * 587 + b * 114) / 1000; return l > 125 ? "#000000" : "#ffffff"; } /** * Returns the security token CSS style * @returns {undefined|{backgroundColor: *, color: (string)}} */ get securityTokenStyle() { if (this.state.background) { return { backgroundColor: this.state.background, color: this.textColor, }; } else { return undefined; } } /** * Returns true if the user can perform actions on the component */ get areActionsAllowed() { return !this.state.processing; } /** * Returns true if there is at least one error property is true * @param {object} errors The errors * @return boolean */ isValid(errors) { return Object.values(errors).every((value) => !value); } /** * Returns true if the component must be in a processing mode */ get isProcessing() { return this.state.processing; } /** * Handle component event handlers */ bindEventHandlers() { this.handleSubmit = this.handleSubmit.bind(this); this.handleSelectColor = this.handleSelectColor.bind(this); this.handleRandomize = this.handleRandomize.bind(this); this.handleChangeCode = this.handleChangeCode.bind(this); } /** * Creates the references */ createReferences() { this.tokenCodeInputRef = React.createRef(); } /** * Whenever the users submits his security token choice * @param event Dom event */ async handleSubmit(event) { event.preventDefault(); // Prevent submission while processing if (this.isProcessing) { return; } this.setState({ hasBeenValidated: true }); const errors = this.validate(); if (this.isValid(errors)) { this.setState({ processing: true }); await this.save(); } } /** * Whenever a new color has been selected */ handleSelectColor(color) { if (this.areActionsAllowed) { this.selectColor(color); if (this.state.hasBeenValidated) { this.validate(); } } } /** * Whenever the user wants to randomize the token code and color */ handleRandomize() { this.randomizeCode(); this.randomizeColor(); } /** * Whenever the user changes the token code * @param event An input event */ handleChangeCode(event) { const code = event.target.value; this.selectCode(code); if (this.state.hasBeenValidated) { this.validate(); } } /** * Saves the security token */ async save() { const securityTokenDto = { color: this.state.background, textcolor: this.textColor, code: this.state.code, }; await this.props.onComplete(securityTokenDto); } /** * Select a token color * @param color A color */ selectColor(color) { if (color.hex !== this.state.background) { this.setState({ background: color.hex }); } } /** * Select a token code * @param code A code */ selectCode(code) { this.setState({ code }); } /** * Randomize a token code */ randomizeCode() { const code = SecretComplexity.generate(3, ["uppercase"]); this.selectCode(code); if (this.state.hasBeenValidated) { this.validate(); } } /** * Randomize a color */ randomizeColor() { let color; do { const number = parseInt(SecretComplexity.generate(3, ["digit"])) % this.defaultColors.length; color = { hex: this.defaultColors[number], }; } while (color.hex === this.state.background); this.selectColor(color); } /** * Validate the security token data * @return {object} errors */ validate() { const { code } = this.state; const errors = {}; const emptyCode = code.trim() === ""; if (emptyCode) { errors.emptyCode = true; } else { const lengthCode = code.trim().length !== 3; if (lengthCode) { errors.lengthCode = true; } else { const invalidRegex = !isValidSecurityToken(code.trim()); if (invalidRegex) { errors.invalidRegex = true; } } } this.setState({ errors }); return errors; } /** * Check if there are errors * @return {bool} */ get hasErrors() { return ( this.state.errors && (this.state.errors.emptyCode || this.state.errors.lengthCode || this.state.errors.invalidRegex) ); } /** * Render the component */ render() { const processingClassName = this.isProcessing ? "processing" : ""; return ( <div className="choose-security-token"> <h1> <Trans>Pick a color and enter three characters.</Trans> </h1> <form onSubmit={this.handleSubmit}> <div className={`input-security-token input required ${this.hasErrors ? "error" : ""}`}> <div className="label-required-inline"> <label htmlFor="security-token-text"> <Trans>Security token</Trans> </label> <Tooltip message={this.props.t("Only alphanumeric, dash and underscore characters are accepted.")}> <InfoSVG /> </Tooltip> </div> <input id="security-token-text" ref={this.tokenCodeInputRef} type="text" className="input text required" aria-required={true} name="text" maxLength="3" style={this.securityTokenStyle} value={this.state.code} onChange={this.handleChangeCode} disabled={!this.areActionsAllowed} /> <input type="hidden" id="security-token-background-color" name="security-token-background-color" /> <input type="hidden" id="security-token-text-color" name="security-token-text-color" /> <CirclePicker color={this.state.background} onChange={this.handleSelectColor} width={240} circleSize={24} circleSpacing={16} colors={this.defaultColors} /> <div className="randomize-button-wrapper"> <button type="button" className="randomize-button link" onClick={this.handleRandomize}> <Trans>Randomize</Trans> </button> </div> </div> {this.state.hasBeenValidated && ( <div className="input text"> {this.state.errors.emptyCode && ( <div className="empty-code error-message"> <Trans>The security token code should not be empty.</Trans> </div> )} {this.state.errors.lengthCode && ( <div className="not-good-length-code error-message"> <Trans>The security token code should be 3 characters long.</Trans> </div> )} {this.state.errors.invalidRegex && ( <div className="not-good-regex-code error-message"> <Trans> The security token code should contains only alphanumeric, dash and underscore characters. </Trans> </div> )} </div> )} <p> <Trans> This security token will be displayed when your passphrase is requested, so you can quickly verify the form is coming from passbolt. </Trans> &nbsp; <Trans> This will help protect you from{" "} <a href="https://en.wikipedia.org/wiki/Phishing" target="_blank" rel="noopener noreferrer" data-testid="phishingLink" > phishing attacks </a> . </Trans> </p> <div className="form-actions"> <button type="submit" className={`button primary big full-width ${processingClassName}`} disabled={this.isProcessing} > <Trans>Next</Trans> </button> <a href="https://www.passbolt.com/docs/user/settings/browser/security-token/" target="_blank" rel="noopener noreferrer" data-testid="tokenDocumentationLink" > <Trans>Learn more</Trans> </a> </div> </form> </div> ); } } ChooseSecurityToken.propTypes = { onComplete: PropTypes.func.isRequired, // The callback function to call when the form is submitted t: PropTypes.func, // The translation function }; export default withTranslation("common")(ChooseSecurityToken);