passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
459 lines (418 loc) • 16.8 kB
JavaScript
import React from "react";
import {Link} from "react-router-dom";
import PropTypes from "prop-types";
import {withRouter} from "react-router-dom";
import {Trans, withTranslation} from "react-i18next";
import {withAppContext} from "../../contexts/AppContext";
import {SecretGenerator} from "../../../shared/lib/SecretGenerator/SecretGenerator";
import {withPrepareResourceContext} from "../../contexts/PrepareResourceContext";
import Icon from "../../../shared/components/Icons/Icon";
import Password from "../../../shared/components/Password/Password";
import PasswordComplexity from "../../../shared/components/PasswordComplexity/PasswordComplexity";
import debounce from 'debounce-promise';
import PownedService from "../../../shared/services/api/secrets/pownedService";
class ResourceCreatePage extends React.Component {
constructor(props) {
super(props);
this.initEventHandlers();
this.state = this.getDefaultState();
this.createInputRef();
this.isPwndProcessingPromise = null;
this.evaluatePasswordIsInDictionaryDebounce = debounce(this.evaluatePasswordIsInDictionaryDebounce, 300);
}
async componentDidMount() {
this.pownedService = new PownedService(this.props.context.port);
await this.handlePreparedResource();
this.handleLastGeneratedPassword();
this.evaluatePasswordIsInDictionaryDebounce();
}
initEventHandlers() {
this.handleGoBackClick = this.handleGoBackClick.bind(this);
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
this.handlePasswordChange = this.handlePasswordChange.bind(this);
this.handleGeneratePasswordButtonClick = this.handleGeneratePasswordButtonClick.bind(this);
this.handleOpenGenerator = this.handleOpenGenerator.bind(this);
}
getDefaultState() {
return {
loaded: false,
error: "",
name: "",
nameError: "",
username: "",
usernameError: "",
uri: "",
uriError: "",
password: "",
passwordError: "",
passwordEntropy: null,
isPwnedServiceAvailable: true,
passwordInDictionary: false
};
}
/**
* Get the translate function
* @returns {function(...[*]=)}
*/
get translate() {
return this.props.t;
}
createInputRef() {
this.nameInputRef = React.createRef();
this.uriInputRef = React.createRef();
this.usernameInputRef = React.createRef();
this.passwordInputRef = React.createRef();
}
/*
* =============================================================
* Resource password generator
* =============================================================
*/
async getCurrentGeneratorConfiguration() {
const type = (await this.props.prepareResourceContext.getSettings()).default_generator;
return this.props.prepareResourceContext.settings.generators.find(generator => generator.type === type);
}
/**
* Whenever a new password has been generated through the generator
*/
handleLastGeneratedPassword() {
const currentLastGeneratedPassword = this.props.prepareResourceContext.getLastGeneratedPassword();
if (currentLastGeneratedPassword?.length > 0) {
this.loadPassword(currentLastGeneratedPassword);
}
}
/**
* Whenever a resource has been prepared
*/
async handlePreparedResource() {
const resource = this.props.prepareResourceContext.getPreparedResource();
if (resource) {
this.setState({name: resource.name, uri: resource.uri, username: resource.username});
this.loadPassword(resource.password);
} else {
await this.loadPasswordMetaFromTabInfo();
}
}
/*
* =============================================================
* Autofill fields from tab
* =============================================================
*/
async loadPasswordMetaFromTabInfo() {
const {name, uri, username, password} = await this.getPasswordMetaFromTabInfo();
this.setState({name, uri, username});
if (password?.length > 0) {
this.loadPassword(password);
}
await this.focusFirstEmptyField(name, uri, username, password);
this.setState({loaded: true});
}
async getPasswordMetaFromTabInfo() {
let name = "";
let uri = "";
let username = "";
let password = "";
const ignoreNames = ["newtab"];
const ignoreUris = ["chrome://newtab/", "about:newtab"];
try {
const tabInfo = await this.props.context.port.request("passbolt.quickaccess.prepare-resource", this.props.context.tabId);
if (!ignoreNames.includes(tabInfo["name"])) {
name = tabInfo["name"].substring(0, 255);
}
if (!ignoreUris.includes(tabInfo["uri"])) {
uri = tabInfo["uri"];
}
if (tabInfo.username?.length > 0) {
username = tabInfo.username;
} else {
username = this.props.context.userSettings.username;
}
if (tabInfo.secret_clear?.length > 0) {
password = tabInfo.secret_clear;
} else {
password = SecretGenerator.generate(await this.getCurrentGeneratorConfiguration());
}
} catch (error) {
console.error(error);
}
return {name, uri, username, password};
}
focusFirstEmptyField(name, uri, username, password) {
return new Promise(resolve => {
/*
* Wait 210ms, the time for the animation to be completed.
* If we don't wait the animation to be completed, then the focus will screw the animation. Some browsers need
* elements to be visible to give them focus, therefore the browser makes it visible while the animation is
* running, making the element blinking.
*/
setTimeout(() => {
if (name === "") {
this.nameInputRef.current.focus();
} else if (uri === "") {
this.uriInputRef.current.focus();
} else if (username === "") {
this.usernameInputRef.current.focus();
} else if (password === "") {
this.passwordInputRef.current.focus();
}
resolve();
}, 210);
});
}
handleGoBackClick(ev) {
ev.preventDefault();
this.props.history.goBack();
}
validateFields() {
const state = {
nameError: "",
passwordError: ""
};
let isValid = true;
if (this.state.name === "") {
state.nameError = this.translate("A name is required.");
isValid = false;
}
if (this.state.password === "") {
state.passwordError = this.translate("A password is required.");
isValid = false;
}
this.setState(state);
return isValid;
}
/*
* =============================================================
* Form submit
* =============================================================
*/
async handleFormSubmit(event) {
event.preventDefault();
this.setState({
processing: true,
error: "",
nameError: "",
usernameError: "",
uriError: "",
});
if (!this.validateFields()) {
this.setState({processing: false});
return;
}
const resourceDto = {
name: this.state.name,
username: this.state.username,
uri: this.state.uri
};
const secretDto = this.state.password;
try {
const resource = await this.props.context.port.request("passbolt.resources.create", resourceDto, secretDto);
/*
* Remove the create step from the history.
* The user needs to be redirected to the home page and not the create page while clicking on go back
* password details page.
*/
const goToComponentState = {
goBackEntriesCount: -2
};
this.props.history.push(`/webAccessibleResources/quickaccess/resources/view/${resource.id}`, goToComponentState);
} catch (error) {
this.handleSubmitError(error);
}
}
handleSubmitError(error) {
if (error.name === "UserAbortsOperationError") {
this.setState({processing: false});
} else if (error.name === "PassboltApiFetchError"
&& error.data.code === 400 && error.data.body
&& (error.data.body.name || error.data.body.username || error.data.body.uri)) {
// Could not validate resource data.
this.setState({
nameError: this.formatValidationFieldError(error.data.body.name),
usernameError: this.formatValidationFieldError(error.data.body.username),
uriError: this.formatValidationFieldError(error.data.body.uri),
processing: false
});
} else {
// An unexpected error occured.
this.setState({
error: error.message,
processing: false
});
}
}
/**
* Evaluate to check if password is in a dictionary.
* @return {Promise}
*/
async evaluatePasswordIsInDictionaryDebounce() {
let passwordEntropy = null;
if (this.state.isPwnedServiceAvailable) {
passwordEntropy = this.state.password.length > 0 ? SecretGenerator.entropy(this.state.password) : null;
const result = await this.pownedService.evaluateSecret(this.state.password);
const passwordInDictionary = this.state.password.length > 0 ? result.inDictionary : false;
this.setState({isPwnedServiceAvailable: result.isPwnedServiceAvailable, passwordInDictionary});
}
this.setState({passwordEntropy});
}
formatValidationFieldError(fieldErrors) {
if (!fieldErrors) {
return "";
}
return Object.values(fieldErrors).join(', ');
}
handlePasswordChange(event) {
if (event.target.value.length) {
this.isPwndProcessingPromise = this.evaluatePasswordIsInDictionaryDebounce();
} else {
this.setState({
passwordInDictionary: false,
});
}
this.loadPassword(event.target.value);
}
handleInputChange(event) {
const target = event.target;
const value = target.type === "checkbox" ? target.checked : target.value;
const name = target.name;
this.setState({
[name]: value
});
}
async handleGeneratePasswordButtonClick() {
if (this.state.processing) {
return;
}
this.setState({
passwordInDictionary: false,
});
const password = SecretGenerator.generate(await this.getCurrentGeneratorConfiguration());
this.loadPassword(password);
}
/**
* Whenever the user wants to go to the password generator
*/
handleOpenGenerator() {
if (this.state.processing) {
return;
}
const resource = {
name: this.state.name,
username: this.state.username,
uri: this.state.uri,
password: this.state.password
};
this.props.prepareResourceContext.onPrepareResource(resource);
this.props.history.push('/webAccessibleResources/quickaccess/resources/generate-password');
}
loadPassword(password) {
const passwordEntropy = password ? SecretGenerator.entropy(password) : null;
this.setState({password, passwordEntropy});
}
/**
* Returns true if the logged in user can use the password generator capability.
* @returns {boolean}
*/
get canUsePasswordGenerator() {
return this.props.context.siteSettings.canIUse('passwordGenerator');
}
render() {
const passwordEntropy = this.state.passwordInDictionary ? 0 : this.state.passwordEntropy;
return (
<div className="resource-create">
<div className="back-link">
<a href="#" className="primary-action" onClick={this.handleGoBackClick} title={this.translate("Cancel the operation")}>
<Icon name="chevron-left"/>
<span className="primary-action-title"><Trans>Create password</Trans></span>
</a>
<Link to="/webAccessibleResources/quickaccess.html" className="secondary-action button-transparent button" title={this.translate("Cancel")}>
<Icon name="close"/>
<span className="visually-hidden"><Trans>Cancel</Trans></span>
</Link>
</div>
<form onSubmit={this.handleFormSubmit}>
<div className="resource-create-form">
<div className="form-container">
<div className={`input text required ${this.state.nameError ? "error" : ""}`}>
<label htmlFor="name"><Trans>Name</Trans></label>
<input name="name" value={this.state.name} onChange={this.handleInputChange} disabled={this.state.processing}
ref={this.nameInputRef} className="required fluid" maxLength="255" type="text" id="name" autoComplete="off" />
{this.state.nameError &&
<div className="error-message">{this.state.nameError}</div>
}
</div>
<div className={`input text ${this.state.uriError ? "error" : ""}`}>
<label htmlFor="uri"><Trans>URL</Trans></label>
<input name="uri" value={this.state.uri} onChange={this.handleInputChange} disabled={this.state.processing}
ref={this.uriInputRef} className="fluid" maxLength="1024" type="text" id="uri" autoComplete="off" />
{this.state.uriError &&
<div className="error-message">{this.state.uriError}</div>
}
</div>
<div className="input text">
<label htmlFor="username"><Trans>Username</Trans></label>
<input name="username" value={this.state.username} onChange={this.handleInputChange} disabled={this.state.processing}
ref={this.usernameInputRef} className="fluid" maxLength="255" type="text" id="username" autoComplete="off" />
{this.state.usernameError &&
<div className="error-message">{this.state.usernameError}</div>
}
</div>
<div className={`input-password-wrapper input required ${this.state.passwordError ? "error" : ""}`}>
<label htmlFor="password"><Trans>Password</Trans>
{(this.state.passwordInDictionary || !this.state.isPwnedServiceAvailable) &&
<Icon name="exclamation"/>
}</label>
<div className="password-button-inline">
<Password name="password" value={this.state.password} preview={true} onChange={this.handlePasswordChange} disabled={this.state.processing}
autoComplete="new-password" placeholder={this.translate('Password')} id="password" inputRef={this.passwordInputRef}/>
<a onClick={this.handleGeneratePasswordButtonClick}
className={`password-generate button-icon button ${this.state.processing ? "disabled" : ""}`}>
<Icon name='dice'/>
<span className="visually-hidden"><Trans>Generate</Trans></span>
</a>
{this.canUsePasswordGenerator &&
<a onClick={this.handleOpenGenerator}
className="password-generator button-icon button">
<Icon name='settings'/>
<span className="visually-hidden"><Trans>Open generator</Trans></span>
</a>
}
</div>
<PasswordComplexity entropy={passwordEntropy} error={Boolean(this.state.passwordError)}/>
{this.state.passwordError &&
<div className="error-message">{this.state.passwordError}</div>
}
{!this.state.isPwnedServiceAvailable &&
<div className="pwned-password warning-message"><Trans>The pwnedpasswords service is unavailable, your password might be part of an exposed data breach</Trans></div>
}
{this.state.passwordInDictionary &&
<div className="pwned-password warning-message"><Trans>The password is part of an exposed data breach.</Trans></div>
}
</div>
</div>
</div>
<div className="submit-wrapper input">
<button type="submit" className={`button primary big full-width ${this.state.processing ? "processing" : ""}`} role="button"
disabled={this.state.processing}>
<Trans>Save</Trans>
{this.state.processing &&
<Icon name="spinner"/>
}
</button>
{this.state.error &&
<div className="error-message">{this.state.error}</div>
}
</div>
</form>
</div>
);
}
}
ResourceCreatePage.propTypes = {
context: PropTypes.any, // The application context
prepareResourceContext: PropTypes.any, // The password generator context
history: PropTypes.object,
location: PropTypes.any,
t: PropTypes.func, // The translation function
};
export default withAppContext(withRouter(withPrepareResourceContext(withTranslation('common')(ResourceCreatePage))));