passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
366 lines (337 loc) • 12.1 kB
JavaScript
/**
* 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 debounce from "debounce-promise";
import {Trans, withTranslation} from "react-i18next";
import Password from "../../../../shared/components/Password/Password";
import PasswordComplexity from "../../../../shared/components/PasswordComplexity/PasswordComplexity";
import {SecretGenerator} from "../../../../shared/lib/SecretGenerator/SecretGenerator";
import ExternalServiceError from "../../../../shared/lib/Error/ExternalServiceError";
import ExternalServiceUnavailableError from "../../../../shared/lib/Error/ExternalServiceUnavailableError";
import PownedService from "../../../../shared/services/api/secrets/pownedService";
import {withAppContext} from "../../../contexts/AppContext";
import Icon from "../../../../shared/components/Icons/Icon";
/**
* The component display variations.
* @type {Object}
*/
export const CheckPassphraseVariations = {
SETUP: 'Setup',
RECOVER: 'Recover'
};
/**
* This component checks the passphrase of an user gpg key
*/
class CheckPassphrase extends Component {
/**
* Default constructor
* @param props Component props
*/
constructor(props) {
super(props);
this.state = this.defaultState;
this.isPwndProcessingPromise = null;
this.evaluatePassphraseIsInDictionaryDebounce = debounce(this.evaluatePassphraseIsInDictionary, 300);
this.bindEventHandlers();
this.createReferences();
}
/**
* Returns the default state
*/
get defaultState() {
return {
passphrase: '', // The passphrase
rememberMe: false, // The remember passphrase flag
isObfuscated: true, // True if the passphrase should not be visible
actions: {
processing: false // True if one's processing passphrase
},
hasBeenValidated: false, // true if the form has already validated once
errors: {
emptyPassphrase: false, // True if the passphrase is empty
invalidPassphrase: false, // True if the passphrase is invalid
},
passphraseInDictionnary: false, // True if the passphrase is part of a data breach
isPwnedServiceAvailable: true, // True if the isPwned service can be reached
passwordEntropy: null
};
}
/**
* Returns true if the user can perform actions on the component
*/
get areActionsAllowed() {
return !this.state.actions.processing;
}
/**
* Returns true if the passphrase is valid
*/
get isValid() {
return Object.values(this.state.errors).every(value => !value);
}
/**
* Returns true if the component must be in a processing mode
*/
get isProcessing() {
return this.state.actions.processing;
}
/**
* Return true if there are errors
*/
get hasErrors() {
return this.state.errors.emptyPassphrase || this.state.errors.invalidPassphrase;
}
/**
* Whenever the component is mounted
*/
componentDidMount() {
this.pownedService = new PownedService(this.props.context.port);
this.focusOnPassphrase();
}
/**
* Handle component event handlers
*/
bindEventHandlers() {
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChangePassphrase = this.handleChangePassphrase.bind(this);
this.handleToggleRememberMe = this.handleToggleRememberMe.bind(this);
}
/**
* Creates the references
*/
createReferences() {
this.passphraseInputRef = React.createRef();
}
/**
* Whenever the users submits his passphrase
* @param event Dom event
*/
async handleSubmit(event) {
event.preventDefault();
this.validate();
await this.isPwndProcessingPromise;
if (this.isValid) {
this.toggleProcessing();
await this.check();
}
}
/**
* Evaluate if the passphrase is in dictionary
* @return {Promise<void>}
*/
async evaluatePassphraseIsInDictionary() {
let isPwnedServiceAvailable = this.state.isPwnedServiceAvailable;
if (!isPwnedServiceAvailable) {
return;
}
let passphraseInDictionnary = false;
let passphraseEntropy = this.state.passphraseEntropy;
try {
const result = await this.pownedService.evaluateSecret(this.state.passphrase);
passphraseInDictionnary = result.inDictionary;
isPwnedServiceAvailable = result.isPwnedServiceAvailable;
if (passphraseInDictionnary) {
passphraseEntropy = 0;
}
} catch (error) {
// If the service is unavailable don't block the user journey.
if (error instanceof ExternalServiceUnavailableError || error instanceof ExternalServiceError) {
isPwnedServiceAvailable = false;
passphraseInDictionnary = false;
} else {
throw error;
}
}
this.setState({
isPwnedServiceAvailable,
passphraseEntropy,
passphraseInDictionnary,
});
}
/**
* Whenever the user changes the private key
* @param event An input event
*/
handleChangePassphrase(event) {
const passphrase = event.target.value;
let passphraseEntropy = null;
if (passphrase.length) {
passphraseEntropy = SecretGenerator.entropy(passphrase);
this.isPwndProcessingPromise = this.evaluatePassphraseIsInDictionaryDebounce();
} else {
this.setState({
passphraseInDictionnary: false,
passwordEntropy: null,
});
}
this.setState({passphrase, passphraseEntropy});
if (this.state.hasBeenValidated) {
this.validate();
}
}
/**
* Whenever the user toggles the remember me flag
*/
handleToggleRememberMe() {
this.toggleRemmemberMe();
}
/**
* Check the private gpg key passphrase
*/
async check() {
await this.props.onComplete(this.state.passphrase, this.state.rememberMe)
.catch(this.onCheckFailure.bind(this));
}
/**
* Whenever the gpg key import failed
* @param {Error} error import Icon from '../../../../shared/components/Icons/Icon';
*The error
* @throw {Error} If an unexpected errors hits the component. Errors not of type: InvalidMasterPasswordError.
*/
onCheckFailure(error) {
// Whenever the passphrase is invalid.
this.toggleProcessing();
if (error.name === "InvalidMasterPasswordError") {
this.setState({errors: {...this.state.errors, invalidPassphrase: true}});
} else {
throw error;
}
}
/**
* Toggle the remember me flag value
*/
toggleRemmemberMe() {
this.setState({rememberMe: !this.state.rememberMe});
}
/**
* Validate the security token data
*/
validate() {
const {passphrase} = this.state;
const errors = {
emptyPassphrase: passphrase.trim() === '',
invalidPassphrase: false,
};
this.setState({hasBeenValidated: true, errors});
}
/**
* Toggle the processing mode
*/
toggleProcessing() {
this.setState({actions: {processing: !this.state.actions.processing}});
}
/**
* Put the focus on the passphrase input
*/
focusOnPassphrase() {
this.passphraseInputRef.current.focus();
}
/**
* Render the component
*/
render() {
const processingClassName = this.isProcessing ? 'processing' : '';
const passphraseEntropy = this.state.passphraseInDictionnary ? 0 : this.state.passphraseEntropy;
return (
<div className="check-passphrase">
<h1><Trans>Please enter your passphrase to continue.</Trans></h1>
<form acceptCharset="utf-8" onSubmit={this.handleSubmit} className="enter-passphrase">
<div className="form-content">
<div className={`input-password-wrapper input required ${this.hasErrors ? "error" : ""} ${!this.areActionsAllowed ? 'disabled' : ''}`}>
<label htmlFor="passphrase"><Trans>Passphrase</Trans>
{!this.state.hasBeenValidated && (!this.state.isPwnedServiceAvailable || this.state.passphraseInDictionnary) &&
<Icon name="exclamation"/>
}</label>
<Password
id="passphrase"
autoComplete="off"
inputRef={this.passphraseInputRef}
name="passphrase"
value={this.state.passphrase}
preview={true}
onChange={this.handleChangePassphrase}
disabled={!this.areActionsAllowed}/>
<PasswordComplexity entropy={passphraseEntropy}/>
{this.state.hasBeenValidated &&
<>
{this.state.errors.emptyPassphrase &&
<div className="empty-passphrase error-message"><Trans>The passphrase should not be empty.</Trans></div>
}
{this.state.errors.invalidPassphrase &&
<div className="invalid-passphrase error-message"><Trans>The passphrase is invalid.</Trans></div>
}
</>
}
{!this.state.hasBeenValidated &&
<>
{!this.state.isPwnedServiceAvailable &&
<div className="invalid-passphrase warning-message"><Trans>The pwnedpasswords service is unavailable, your passphrase might be part of an exposed data breach</Trans></div>
}
{this.state.passphraseInDictionnary &&
<div className="invalid-passphrase warning-message"><Trans>The passphrase is part of an exposed data breach.</Trans></div>
}
</>
}
</div>
{this.props.canRememberMe &&
<div className="input checkbox">
<input
id="remember-me"
type="checkbox"
name="remember-me"
value={this.state.rememberMe}
onChange={this.handleToggleRememberMe}
disabled={!this.areActionsAllowed}/>
<label htmlFor="remember-me">
<Trans>Remember until signed out.</Trans>
</label>
</div>
}
</div>
<div className="form-actions">
<button
type="submit"
className={`button primary big full-width ${processingClassName}`}
role="button"
disabled={this.isProcessing}>
<Trans>Verify</Trans>
</button>
{this.props.onSecondaryActionClick &&
<a onClick={this.props.onSecondaryActionClick}>
{{
[CheckPassphraseVariations.SETUP]: <Trans>I lost my passphrase, generate a new private key.</Trans>,
[CheckPassphraseVariations.RECOVER]: <Trans>Help, I lost my passphrase.</Trans>,
}[this.props.displayAs]}
</a>
}
</div>
</form>
</div>
);
}
}
CheckPassphrase.defaultProps = {
displayAs: CheckPassphraseVariations.SETUP,
};
CheckPassphrase.propTypes = {
context: PropTypes.any, // The application context
onComplete: PropTypes.func.isRequired, // The callback to trigger when the user wants to verify its passphrase
displayAs: PropTypes.PropTypes.oneOf([
CheckPassphraseVariations.SETUP,
CheckPassphraseVariations.RECOVER
]), // Defines how the form should be displayed and behaves
canRememberMe: PropTypes.bool, // True if the remember me flag must be displayed
onSecondaryActionClick: PropTypes.func, // Callback to trigger when the user clicks on the secondary action link.
};
export default withAppContext(withTranslation("common")(CheckPassphrase));