passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
397 lines (365 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 UserAvatar from "../../Common/Avatar/UserAvatar";
import {Trans, withTranslation} from "react-i18next";
import {withAppContext} from "../../../contexts/AppContext";
import Password from "../../../../shared/components/Password/Password";
/**
* The component display variations.
* @type {Object}
*/
export const LoginVariations = {
SIGN_IN: 'Sign in',
ACCOUNT_RECOVERY: 'Account recovery'
};
/**
* This component allows the user to log in with his account
*/
class Login extends Component {
/**
* Default constructor
* @param props Component props
*/
constructor(props) {
super(props);
this.state = this.defaultState;
this.bindEventHandlers();
this.createReferences();
}
/**
* Returns the default state
*/
get defaultState() {
return {
passphrase: '', // The passphrase
rememberMe: false, // The remember me flag
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
invalidGpgKey: false, // True if the gpg key is invalid
},
isSsoAvailable: false, // true if the current user has an SSO kit built locally
};
}
/**
* Returns true if the user can perform actions on the component
* @returns {boolean}
*/
get areActionsAllowed() {
return !this.state.actions.processing;
}
/**
* Returns true if the passphrase is valid
* @returns {boolean}
*/
get isValid() {
return Object.values(this.state.errors).every(value => !value);
}
/**
* Returns true if the component must be in a processing mode
* @returns {boolean}
*/
get isProcessing() {
return this.state.actions.processing;
}
/**
* Return true if there is a validation error
* @returns {boolean}
*/
get hasErrors() {
return this.state.errors.emptyPassphrase || this.state.errors.invalidPassphrase || this.state.errors.invalidGpgKey;
}
/**
* Returns the user full name
*/
get fullname() {
return this.props.userSettings?.fullName
|| `${this.props.account?.first_name} ${this.props.account?.last_name}`;
}
/**
* Returns the username
*/
get username() {
return this.props.userSettings?.username || this.props.account?.username;
}
/**
* Returns the trusted domain
*/
get trustedDomain() {
return this.props.userSettings?.getTrustedDomain()
|| this.props.account?.domain;
}
/**
* Handle component event handlers
*/
bindEventHandlers() {
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChangePassphrase = this.handleChangePassphrase.bind(this);
this.handleToggleRememberMe = this.handleToggleRememberMe.bind(this);
this.handleSwitchToSso = this.handleSwitchToSso.bind(this);
}
/**
* Creates the references
*/
createReferences() {
this.passphraseInputRef = React.createRef();
}
/**
* Whenever the component is mounted
*/
componentDidMount() {
this.focusOnPassphrase();
}
/**
* Put the focus on the passphrase input
*/
focusOnPassphrase() {
this.passphraseInputRef.current.focus();
}
/**
* Whenever the users submits his passphrase
* @param {Event} event Dom event
*/
async handleSubmit(event) {
event.preventDefault();
await this.validate();
if (this.isValid) {
this.toggleProcessing();
if (await this.checkPassphrase()) {
await this.login();
}
}
}
/**
* Whenever the user changes the private key
* @param event An input event
*/
async handleChangePassphrase(event) {
const passphrase = event.target.value;
await this.fillPassphrase(passphrase);
if (this.state.hasBeenValidated) {
await this.validate();
}
}
/**
* Whenever the user tosggles the remember me flag
*/
async handleToggleRememberMe() {
await this.toggleRememberMe();
}
/**
* Check the private gpg key passphrase
* @returns {Promise<boolean>}
*/
async checkPassphrase() {
try {
await this.props.onCheckPassphrase(this.state.passphrase);
return true;
} catch (error) {
await this.onCheckPassphraseFailure(error);
return false;
}
}
/**
* Whenever the passphrase check failed
* @param {Error} error The error
* @throw {Error} If an unexpected errors hits the component. Errors not of type: InvalidMasterPasswordError, GpgKeyError.
*/
onCheckPassphraseFailure(error) {
// It can happen when the user has entered the wrong passphrase.
if (error.name === "InvalidMasterPasswordError") {
this.setState({actions: {processing: false}, errors: {invalidPassphrase: true}});
} else if (error.name === 'GpgKeyError') {
this.setState({actions: {processing: false}, errors: {invalidGpgKey: true}});
} else {
// Only controlled errors should hit the component.
throw error;
}
}
/**
* Sign in the user
* @returns {Promise<void>}
*/
async login() {
await this.props.onSignIn(this.state.passphrase, this.state.rememberMe);
}
/**
* Fill the passphrase
* @param passphrase A passphrase
*/
async fillPassphrase(passphrase) {
await this.setState({passphrase});
}
/**
* Toggle the remember me flag value
*/
async toggleRememberMe() {
await this.setState({rememberMe: !this.state.rememberMe});
}
/**
* Validate the security token data
*/
async validate() {
const {passphrase} = this.state;
const emptyPassphrase = passphrase.trim() === '';
if (emptyPassphrase) {
await this.setState({hasBeenValidated: true, errors: {emptyPassphrase}});
return;
}
await this.setState({hasBeenValidated: true, errors: {}});
}
/**
* Toggle the processing mode
*/
toggleProcessing() {
this.setState({actions: {processing: !this.state.actions.processing}});
}
/**
* Switches the UI to the SSO mode.
*/
handleSwitchToSso(event) {
event.preventDefault();
this.props.switchToSsoLogin();
}
/**
* Get the security token
* @returns {{backgroundColor, code, textColor}}
*/
get securityToken() {
return this.props.userSettings?.getSecurityToken() || {
code: this.props.account.security_token.code,
backgroundColor: this.props.account.security_token.color,
textColor: this.props.account.security_token.textcolor
};
}
/**
* Returns the provider information of the current SSO provider configured.
* @return {object}
*/
get ssoProviderData() {
return this.props.ssoProvider;
}
/**
* Render the component
*/
render() {
const processingClassName = this.isProcessing ? 'processing' : '';
const securityToken = this.securityToken;
return (
<div className="login">
<div className="login-user">
<UserAvatar user={this.props.account?.user} baseUrl={this.trustedDomain} className="big avatar user-avatar"/>
<p className="login-user-name">{this.fullname}</p>
<p className="login-user-email">{this.username}</p>
</div>
<form acceptCharset="utf-8" onSubmit={this.handleSubmit} className="enter-passphrase">
<div className={`input-password-wrapper input required ${this.hasErrors ? "error" : ""}`}>
<label htmlFor="passphrase">
<Trans>Passphrase</Trans>
</label>
<Password
id="passphrase"
autoComplete="off"
inputRef={this.passphraseInputRef}
name="passphrase"
placeholder={this.props.t('Passphrase')}
value={this.state.passphrase}
onChange={this.handleChangePassphrase}
disabled={!this.areActionsAllowed}
preview={true}
securityToken={securityToken}/>
{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> {this.props.isSsoAvailable && <a onClick={this.props.onSecondaryActionClick}><Trans>Do you need help?</Trans></a>}
</div>
}
{this.state.errors.invalidGpgKey &&
<div className="invalid-gpg-key error-message"><Trans>The private key is invalid.</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 className="form-actions">
<button
type="submit"
className={`button primary big full-width ${processingClassName}`}
role="button"
disabled={this.isProcessing}>
{{
[LoginVariations.SIGN_IN]: <Trans>Sign in</Trans>,
[LoginVariations.ACCOUNT_RECOVERY]: <Trans>Complete recovery</Trans>,
}[this.props.displayAs]}
</button>
{this.props.isSsoAvailable &&
<a className="switchToSso" onClick={this.handleSwitchToSso}>
<Trans>Sign in with Single Sign-On.</Trans>
</a>
}
{!this.props.isSsoAvailable &&
<a onClick={this.props.onSecondaryActionClick}>
<Trans>Help, I lost my passphrase.</Trans>
</a>
}
</div>
</form>
</div>
);
}
}
Login.defaultProps = {
displayAs: LoginVariations.SIGN_IN,
};
Login.propTypes = {
displayAs: PropTypes.oneOf([
LoginVariations.SIGN_IN,
LoginVariations.ACCOUNT_RECOVERY,
]), // Defines how the form should be displayed and behaves
isSsoAvailable: PropTypes.bool, // true if SSO is available
context: PropTypes.any, // The application context
account: PropTypes.object, // The user account
userSettings: PropTypes.object, // The user settings
canRememberMe: PropTypes.bool, // True if the remember me flag must be displayed
onSignIn: PropTypes.func.isRequired, // Callback to trigger whenever the user wants to sign-in
onCheckPassphrase: PropTypes.func.isRequired, // Callback to trigger whenever the user wants to check the passphrase
onSecondaryActionClick: PropTypes.func, // Callback to trigger when the user clicks on the secondary action link.
switchToSsoLogin: PropTypes.func, // Whenever the user want to sign-in via SSO
ssoProvider: PropTypes.object, // The SSO provider if any
t: PropTypes.func, // The translation function
};
export default withAppContext(withTranslation('common')(Login));