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
|
@ -120,6 +120,7 @@
|
||||||
"@types/classnames": "^2.2.10",
|
"@types/classnames": "^2.2.10",
|
||||||
"@types/modernizr": "^3.5.3",
|
"@types/modernizr": "^3.5.3",
|
||||||
"@types/react": "16.9",
|
"@types/react": "16.9",
|
||||||
|
"@types/zxcvbn": "^4.4.0",
|
||||||
"babel-eslint": "^10.0.3",
|
"babel-eslint": "^10.0.3",
|
||||||
"babel-jest": "^24.9.0",
|
"babel-jest": "^24.9.0",
|
||||||
"chokidar": "^3.3.1",
|
"chokidar": "^3.3.1",
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
@import "./views/auth/_CountryDropdown.scss";
|
@import "./views/auth/_CountryDropdown.scss";
|
||||||
@import "./views/auth/_InteractiveAuthEntryComponents.scss";
|
@import "./views/auth/_InteractiveAuthEntryComponents.scss";
|
||||||
@import "./views/auth/_LanguageSelector.scss";
|
@import "./views/auth/_LanguageSelector.scss";
|
||||||
|
@import "./views/auth/_PassphraseField.scss";
|
||||||
@import "./views/auth/_ServerConfig.scss";
|
@import "./views/auth/_ServerConfig.scss";
|
||||||
@import "./views/auth/_ServerTypeSelector.scss";
|
@import "./views/auth/_ServerTypeSelector.scss";
|
||||||
@import "./views/auth/_Welcome.scss";
|
@import "./views/auth/_Welcome.scss";
|
||||||
|
|
|
@ -146,9 +146,3 @@ limitations under the License.
|
||||||
.mx_AuthBody_spinner {
|
.mx_AuthBody_spinner {
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AuthBody_passwordScore {
|
|
||||||
height: 4px;
|
|
||||||
position: absolute;
|
|
||||||
top: -12px;
|
|
||||||
}
|
|
||||||
|
|
|
@ -18,6 +18,8 @@ $PassphraseStrengthHigh: $accent-color;
|
||||||
$PassphraseStrengthMedium: $username-variant5-color;
|
$PassphraseStrengthMedium: $username-variant5-color;
|
||||||
$PassphraseStrengthLow: $notice-primary-color;
|
$PassphraseStrengthLow: $notice-primary-color;
|
||||||
|
|
||||||
|
.mx_PassphraseField {}
|
||||||
|
|
||||||
@define-mixin ProgressBarColour $colour {
|
@define-mixin ProgressBarColour $colour {
|
||||||
color: $colour;
|
color: $colour;
|
||||||
&::-moz-progress-bar {
|
&::-moz-progress-bar {
|
||||||
|
@ -28,10 +30,13 @@ $PassphraseStrengthLow: $notice-primary-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.mx_ZxcvbnProgressBar {
|
progress.mx_PassphraseField_progress {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
height: 4px;
|
||||||
|
position: absolute;
|
||||||
|
top: -12px;
|
||||||
|
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
&::-moz-progress-bar {
|
&::-moz-progress-bar {
|
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 { SAFE_LOCALPART_REGEX } from '../../../Registration';
|
||||||
import withValidation from '../elements/Validation';
|
import withValidation from '../elements/Validation';
|
||||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||||
import ZxcvbnProgressBar from "../elements/ZxcvbnProgressBar";
|
import PassphraseField from "./PassphraseField";
|
||||||
|
|
||||||
const FIELD_EMAIL = 'field_email';
|
const FIELD_EMAIL = 'field_email';
|
||||||
const FIELD_PHONE_NUMBER = 'field_phone_number';
|
const FIELD_PHONE_NUMBER = 'field_phone_number';
|
||||||
|
@ -264,60 +264,10 @@ export default createReactClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async onPasswordValidate(fieldState) {
|
onPasswordValidate(result) {
|
||||||
const result = await this.validatePasswordRules(fieldState);
|
|
||||||
this.markFieldValid(FIELD_PASSWORD, result.valid);
|
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) {
|
onPasswordConfirmChange(ev) {
|
||||||
this.setState({
|
this.setState({
|
||||||
passwordConfirm: ev.target.value,
|
passwordConfirm: ev.target.value,
|
||||||
|
@ -479,13 +429,10 @@ export default createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
renderPassword() {
|
renderPassword() {
|
||||||
const Field = sdk.getComponent('elements.Field');
|
return <PassphraseField
|
||||||
return <Field
|
|
||||||
id="mx_RegistrationForm_password"
|
id="mx_RegistrationForm_password"
|
||||||
ref={field => this[FIELD_PASSWORD] = field}
|
fieldRef={field => this[FIELD_PASSWORD] = field}
|
||||||
type="password"
|
minScore={PASSWORD_MIN_SCORE}
|
||||||
autoComplete="new-password"
|
|
||||||
label={_t("Password")}
|
|
||||||
value={this.state.password}
|
value={this.state.password}
|
||||||
onChange={this.onPasswordChange}
|
onChange={this.onPasswordChange}
|
||||||
onValidate={this.onPasswordValidate}
|
onValidate={this.onPasswordValidate}
|
||||||
|
|
|
@ -19,7 +19,7 @@ limitations under the License.
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
type Data = Pick<IValidateArgs, "value" | "allowEmpty">;
|
type Data = Pick<IFieldState, "value" | "allowEmpty">;
|
||||||
|
|
||||||
interface IRule<T> {
|
interface IRule<T> {
|
||||||
key: string;
|
key: string;
|
||||||
|
@ -32,15 +32,20 @@ interface IRule<T> {
|
||||||
|
|
||||||
interface IArgs<T> {
|
interface IArgs<T> {
|
||||||
rules: IRule<T>[];
|
rules: IRule<T>[];
|
||||||
description(): React.ReactChild;
|
description(this: T): React.ReactChild;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IValidateArgs {
|
export interface IFieldState {
|
||||||
value: string;
|
value: string;
|
||||||
focused: boolean;
|
focused: boolean;
|
||||||
allowEmpty: boolean;
|
allowEmpty: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IValidationResult {
|
||||||
|
valid?: boolean;
|
||||||
|
feedback?: React.ReactChild;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a validation function from a set of rules describing what to validate.
|
* Creates a validation function from a set of rules describing what to validate.
|
||||||
* Generic T is the "this" type passed to the rule methods
|
* 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.
|
* the overall validity and a feedback UI that can be rendered for more detail.
|
||||||
*/
|
*/
|
||||||
export default function withValidation<T = undefined>({ description, rules }: IArgs<T>) {
|
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) {
|
if (!value && allowEmpty) {
|
||||||
return {
|
return {
|
||||||
valid: null,
|
valid: null,
|
||||||
|
|
|
@ -1318,6 +1318,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/yargs-parser" "*"
|
"@types/yargs-parser" "*"
|
||||||
|
|
||||||
|
"@types/zxcvbn@^4.4.0":
|
||||||
|
version "4.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/zxcvbn/-/zxcvbn-4.4.0.tgz#fbc1d941cc6d9d37d18405c513ba6b294f89b609"
|
||||||
|
integrity sha512-GQLOT+SN20a+AI51y3fAimhyTF4Y0RG+YP3gf91OibIZ7CJmPFgoZi+ZR5a+vRbS01LbQosITWum4ATmJ1Z6Pg==
|
||||||
|
|
||||||
"@typescript-eslint/experimental-utils@^2.5.0":
|
"@typescript-eslint/experimental-utils@^2.5.0":
|
||||||
version "2.27.0"
|
version "2.27.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.27.0.tgz#801a952c10b58e486c9a0b36cf21e2aab1e9e01a"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.27.0.tgz#801a952c10b58e486c9a0b36cf21e2aab1e9e01a"
|
||||||
|
|
Loading…
Reference in a new issue