Extract Password field from Registration into a reusable component
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
eb6796bd0e
commit
cf3c4d9e5f
8 changed files with 148 additions and 69 deletions
121
src/components/views/auth/PassphraseField.tsx
Normal file
121
src/components/views/auth/PassphraseField.tsx
Normal file
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {PureComponent, RefCallback, RefObject} from "react";
|
||||
import classNames from "classnames";
|
||||
import zxcvbn from "zxcvbn";
|
||||
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import withValidation, {IFieldState, IValidationResult} from "../elements/Validation";
|
||||
import {_t, _td} from "../../../languageHandler";
|
||||
import Field from "../elements/Field";
|
||||
|
||||
interface IProps {
|
||||
id?: string;
|
||||
className?: string;
|
||||
minScore: 0 | 1 | 2 | 3 | 4;
|
||||
value: string;
|
||||
fieldRef: RefCallback<Field> | RefObject<Field>;
|
||||
|
||||
label?: string;
|
||||
labelEnterPassword?: string;
|
||||
labelStrongPassword?: string;
|
||||
labelAllowedButUnsafe?: string;
|
||||
|
||||
onChange(ev: KeyboardEvent);
|
||||
onValidate(result: IValidationResult);
|
||||
}
|
||||
|
||||
interface IState {
|
||||
complexity: zxcvbn.ZXCVBNResult;
|
||||
}
|
||||
|
||||
class PassphraseField extends PureComponent<IProps, IState> {
|
||||
static defaultProps = {
|
||||
label: _td("Password"),
|
||||
labelEnterPassword: _td("Enter password"),
|
||||
labelStrongPassword: _td("Nice, strong password!"),
|
||||
labelAllowedButUnsafe: _td("Password is allowed, but unsafe"),
|
||||
};
|
||||
|
||||
public readonly validate = withValidation<this>({
|
||||
description: function() {
|
||||
const complexity = this.state.complexity;
|
||||
const score = complexity ? complexity.score : 0;
|
||||
return <progress className="mx_PassphraseField_progress" max={4} value={score} />;
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t(this.props.labelEnterPassword),
|
||||
},
|
||||
{
|
||||
key: "complexity",
|
||||
test: async function({ value }) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
const { scorePassword } = await import('../../../utils/PasswordScorer');
|
||||
const complexity = scorePassword(value);
|
||||
this.setState({ complexity });
|
||||
const safe = complexity.score >= this.props.minScore;
|
||||
const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
|
||||
return allowUnsafe || safe;
|
||||
},
|
||||
valid: function() {
|
||||
// Unsafe passwords that are valid are only possible through a
|
||||
// configuration flag. We'll print some helper text to signal
|
||||
// to the user that their password is allowed, but unsafe.
|
||||
if (this.state.complexity.score >= this.props.minScore) {
|
||||
return _t(this.props.labelStrongPassword);
|
||||
}
|
||||
return _t(this.props.labelAllowedButUnsafe);
|
||||
},
|
||||
invalid: function() {
|
||||
const complexity = this.state.complexity;
|
||||
if (!complexity) {
|
||||
return null;
|
||||
}
|
||||
const { feedback } = complexity;
|
||||
return feedback.warning || feedback.suggestions[0] || _t("Keep going...");
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
onValidate = async (fieldState: IFieldState) => {
|
||||
const result = await this.validate(fieldState);
|
||||
this.props.onValidate(result);
|
||||
return result;
|
||||
};
|
||||
|
||||
render() {
|
||||
return <Field
|
||||
id={this.props.id}
|
||||
className={classNames("mx_PassphraseField", this.props.className)}
|
||||
ref={this.props.fieldRef}
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
label={_t(this.props.label)}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
onValidate={this.onValidate}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default PassphraseField;
|
|
@ -29,7 +29,7 @@ import SdkConfig from '../../../SdkConfig';
|
|||
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
|
||||
import withValidation from '../elements/Validation';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import ZxcvbnProgressBar from "../elements/ZxcvbnProgressBar";
|
||||
import PassphraseField from "./PassphraseField";
|
||||
|
||||
const FIELD_EMAIL = 'field_email';
|
||||
const FIELD_PHONE_NUMBER = 'field_phone_number';
|
||||
|
@ -264,60 +264,10 @@ export default createReactClass({
|
|||
});
|
||||
},
|
||||
|
||||
async onPasswordValidate(fieldState) {
|
||||
const result = await this.validatePasswordRules(fieldState);
|
||||
onPasswordValidate(result) {
|
||||
this.markFieldValid(FIELD_PASSWORD, result.valid);
|
||||
return result;
|
||||
},
|
||||
|
||||
validatePasswordRules: withValidation({
|
||||
description: function() {
|
||||
const complexity = this.state.passwordComplexity;
|
||||
const score = complexity ? complexity.score : 0;
|
||||
return <ZxcvbnProgressBar value={score} className="mx_AuthBody_passwordScore" />;
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t("Enter password"),
|
||||
},
|
||||
{
|
||||
key: "complexity",
|
||||
test: async function({ value }) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
const { scorePassword } = await import('../../../utils/PasswordScorer');
|
||||
const complexity = scorePassword(value);
|
||||
this.setState({
|
||||
passwordComplexity: complexity,
|
||||
});
|
||||
const safe = complexity.score >= PASSWORD_MIN_SCORE;
|
||||
const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
|
||||
return allowUnsafe || safe;
|
||||
},
|
||||
valid: function() {
|
||||
// Unsafe passwords that are valid are only possible through a
|
||||
// configuration flag. We'll print some helper text to signal
|
||||
// to the user that their password is allowed, but unsafe.
|
||||
if (this.state.passwordComplexity.score >= PASSWORD_MIN_SCORE) {
|
||||
return _t("Nice, strong password!");
|
||||
}
|
||||
return _t("Password is allowed, but unsafe");
|
||||
},
|
||||
invalid: function() {
|
||||
const complexity = this.state.passwordComplexity;
|
||||
if (!complexity) {
|
||||
return null;
|
||||
}
|
||||
const { feedback } = complexity;
|
||||
return feedback.warning || feedback.suggestions[0] || _t("Keep going...");
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
onPasswordConfirmChange(ev) {
|
||||
this.setState({
|
||||
passwordConfirm: ev.target.value,
|
||||
|
@ -479,13 +429,10 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
renderPassword() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
return <Field
|
||||
return <PassphraseField
|
||||
id="mx_RegistrationForm_password"
|
||||
ref={field => this[FIELD_PASSWORD] = field}
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
label={_t("Password")}
|
||||
fieldRef={field => this[FIELD_PASSWORD] = field}
|
||||
minScore={PASSWORD_MIN_SCORE}
|
||||
value={this.state.password}
|
||||
onChange={this.onPasswordChange}
|
||||
onValidate={this.onPasswordValidate}
|
||||
|
|
|
@ -19,7 +19,7 @@ limitations under the License.
|
|||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Data = Pick<IValidateArgs, "value" | "allowEmpty">;
|
||||
type Data = Pick<IFieldState, "value" | "allowEmpty">;
|
||||
|
||||
interface IRule<T> {
|
||||
key: string;
|
||||
|
@ -32,15 +32,20 @@ interface IRule<T> {
|
|||
|
||||
interface IArgs<T> {
|
||||
rules: IRule<T>[];
|
||||
description(): React.ReactChild;
|
||||
description(this: T): React.ReactChild;
|
||||
}
|
||||
|
||||
interface IValidateArgs {
|
||||
export interface IFieldState {
|
||||
value: string;
|
||||
focused: boolean;
|
||||
allowEmpty: boolean;
|
||||
}
|
||||
|
||||
export interface IValidationResult {
|
||||
valid?: boolean;
|
||||
feedback?: React.ReactChild;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a validation function from a set of rules describing what to validate.
|
||||
* Generic T is the "this" type passed to the rule methods
|
||||
|
@ -62,7 +67,7 @@ interface IValidateArgs {
|
|||
* the overall validity and a feedback UI that can be rendered for more detail.
|
||||
*/
|
||||
export default function withValidation<T = undefined>({ description, rules }: IArgs<T>) {
|
||||
return async function onValidate({ value, focused, allowEmpty = true }: IValidateArgs) {
|
||||
return async function onValidate({ value, focused, allowEmpty = true }: IFieldState): Promise<IValidationResult> {
|
||||
if (!value && allowEmpty) {
|
||||
return {
|
||||
valid: null,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue