Merge pull request #4599 from matrix-org/t3chguy/progress_colour
Consolidate password/passphrase fields into a component & add dynamic colour to progress
This commit is contained in:
commit
1eea203db6
14 changed files with 292 additions and 272 deletions
|
@ -120,6 +120,7 @@
|
||||||
"@types/modernizr": "^3.5.3",
|
"@types/modernizr": "^3.5.3",
|
||||||
"@types/qrcode": "^1.3.4",
|
"@types/qrcode": "^1.3.4",
|
||||||
"@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,27 +146,3 @@ limitations under the License.
|
||||||
.mx_AuthBody_spinner {
|
.mx_AuthBody_spinner {
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AuthBody_passwordScore {
|
|
||||||
width: 100%;
|
|
||||||
appearance: none;
|
|
||||||
height: 4px;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 2px;
|
|
||||||
position: absolute;
|
|
||||||
top: -12px;
|
|
||||||
|
|
||||||
&::-moz-progress-bar {
|
|
||||||
border-radius: 2px;
|
|
||||||
background-color: $accent-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-progress-bar,
|
|
||||||
&::-webkit-progress-value {
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-progress-value {
|
|
||||||
background-color: $accent-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
55
res/css/views/auth/_PassphraseField.scss
Normal file
55
res/css/views/auth/_PassphraseField.scss
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$PassphraseStrengthHigh: $accent-color;
|
||||||
|
$PassphraseStrengthMedium: $username-variant5-color;
|
||||||
|
$PassphraseStrengthLow: $notice-primary-color;
|
||||||
|
|
||||||
|
@define-mixin ProgressBarColour $colour {
|
||||||
|
color: $colour;
|
||||||
|
&::-moz-progress-bar {
|
||||||
|
background-color: $colour;
|
||||||
|
}
|
||||||
|
&::-webkit-progress-value {
|
||||||
|
background-color: $colour;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.mx_PassphraseField_progress {
|
||||||
|
appearance: none;
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
height: 4px;
|
||||||
|
position: absolute;
|
||||||
|
top: -12px;
|
||||||
|
|
||||||
|
border-radius: 2px;
|
||||||
|
&::-moz-progress-bar {
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
&::-webkit-progress-bar,
|
||||||
|
&::-webkit-progress-value {
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin ProgressBarColour $PassphraseStrengthLow;
|
||||||
|
&[value="2"], &[value="3"] {
|
||||||
|
@mixin ProgressBarColour $PassphraseStrengthMedium;
|
||||||
|
}
|
||||||
|
&[value="4"] {
|
||||||
|
@mixin ProgressBarColour $PassphraseStrengthHigh;
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,17 +35,6 @@ limitations under the License.
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CreateKeyBackupDialog_passPhraseHelp {
|
|
||||||
flex: 1;
|
|
||||||
height: 85px;
|
|
||||||
margin-left: 20px;
|
|
||||||
font-size: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CreateKeyBackupDialog_passPhraseHelp progress {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CreateKeyBackupDialog_passPhraseInput {
|
.mx_CreateKeyBackupDialog_passPhraseInput {
|
||||||
flex: none;
|
flex: none;
|
||||||
width: 250px;
|
width: 250px;
|
||||||
|
|
|
@ -68,17 +68,6 @@ limitations under the License.
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CreateSecretStorageDialog_passPhraseHelp {
|
|
||||||
flex: 1;
|
|
||||||
height: 64px;
|
|
||||||
margin-left: 20px;
|
|
||||||
font-size: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CreateSecretStorageDialog_passPhraseHelp progress {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CreateSecretStorageDialog_passPhraseMatch {
|
.mx_CreateSecretStorageDialog_passPhraseMatch {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
|
|
|
@ -15,17 +15,17 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {createRef} from 'react';
|
||||||
import FileSaver from 'file-saver';
|
import FileSaver from 'file-saver';
|
||||||
import * as sdk from '../../../../index';
|
import * as sdk from '../../../../index';
|
||||||
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { scorePassword } from '../../../../utils/PasswordScorer';
|
import {_t, _td} from '../../../../languageHandler';
|
||||||
import { _t } from '../../../../languageHandler';
|
|
||||||
import { accessSecretStorage } from '../../../../CrossSigningManager';
|
import { accessSecretStorage } from '../../../../CrossSigningManager';
|
||||||
import SettingsStore from '../../../../settings/SettingsStore';
|
import SettingsStore from '../../../../settings/SettingsStore';
|
||||||
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
|
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
|
||||||
import {copyNode} from "../../../../utils/strings";
|
import {copyNode} from "../../../../utils/strings";
|
||||||
|
import PassphraseField from "../../../../components/views/auth/PassphraseField";
|
||||||
|
|
||||||
const PHASE_PASSPHRASE = 0;
|
const PHASE_PASSPHRASE = 0;
|
||||||
const PHASE_PASSPHRASE_CONFIRM = 1;
|
const PHASE_PASSPHRASE_CONFIRM = 1;
|
||||||
|
@ -36,7 +36,6 @@ const PHASE_DONE = 5;
|
||||||
const PHASE_OPTOUT_CONFIRM = 6;
|
const PHASE_OPTOUT_CONFIRM = 6;
|
||||||
|
|
||||||
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
|
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
|
||||||
const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms.
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Walks the user through the process of creating an e2e key backup
|
* Walks the user through the process of creating an e2e key backup
|
||||||
|
@ -52,17 +51,18 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
|
|
||||||
this._recoveryKeyNode = null;
|
this._recoveryKeyNode = null;
|
||||||
this._keyBackupInfo = null;
|
this._keyBackupInfo = null;
|
||||||
this._setZxcvbnResultTimeout = null;
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
secureSecretStorage: null,
|
secureSecretStorage: null,
|
||||||
phase: PHASE_PASSPHRASE,
|
phase: PHASE_PASSPHRASE,
|
||||||
passPhrase: '',
|
passPhrase: '',
|
||||||
|
passPhraseValid: false,
|
||||||
passPhraseConfirm: '',
|
passPhraseConfirm: '',
|
||||||
copied: false,
|
copied: false,
|
||||||
downloaded: false,
|
downloaded: false,
|
||||||
zxcvbnResult: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this._passphraseField = createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
|
@ -81,12 +81,6 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this._setZxcvbnResultTimeout !== null) {
|
|
||||||
clearTimeout(this._setZxcvbnResultTimeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_collectRecoveryKeyNode = (n) => {
|
_collectRecoveryKeyNode = (n) => {
|
||||||
this._recoveryKeyNode = n;
|
this._recoveryKeyNode = n;
|
||||||
}
|
}
|
||||||
|
@ -180,22 +174,16 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
|
|
||||||
_onPassPhraseNextClick = async (e) => {
|
_onPassPhraseNextClick = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!this._passphraseField.current) return; // unmounting
|
||||||
|
|
||||||
// If we're waiting for the timeout before updating the result at this point,
|
await this._passphraseField.current.validate({ allowEmpty: false });
|
||||||
// skip ahead and do it now, otherwise we'll deny the attempt to proceed
|
if (!this._passphraseField.current.state.valid) {
|
||||||
// even if the user entered a valid passphrase
|
this._passphraseField.current.focus();
|
||||||
if (this._setZxcvbnResultTimeout !== null) {
|
this._passphraseField.current.validate({ allowEmpty: false, focused: true });
|
||||||
clearTimeout(this._setZxcvbnResultTimeout);
|
return;
|
||||||
this._setZxcvbnResultTimeout = null;
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
this.setState({
|
|
||||||
zxcvbnResult: scorePassword(this.state.passPhrase),
|
|
||||||
}, resolve);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (this._passPhraseIsValid()) {
|
|
||||||
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onPassPhraseConfirmNextClick = async (e) => {
|
_onPassPhraseConfirmNextClick = async (e) => {
|
||||||
|
@ -214,9 +202,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
_onSetAgainClick = () => {
|
_onSetAgainClick = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
passPhrase: '',
|
passPhrase: '',
|
||||||
|
passPhraseValid: false,
|
||||||
passPhraseConfirm: '',
|
passPhraseConfirm: '',
|
||||||
phase: PHASE_PASSPHRASE,
|
phase: PHASE_PASSPHRASE,
|
||||||
zxcvbnResult: null,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,23 +214,16 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onPassPhraseValidate = (result) => {
|
||||||
|
this.setState({
|
||||||
|
passPhraseValid: result.valid,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
_onPassPhraseChange = (e) => {
|
_onPassPhraseChange = (e) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
passPhrase: e.target.value,
|
passPhrase: e.target.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this._setZxcvbnResultTimeout !== null) {
|
|
||||||
clearTimeout(this._setZxcvbnResultTimeout);
|
|
||||||
}
|
|
||||||
this._setZxcvbnResultTimeout = setTimeout(() => {
|
|
||||||
this._setZxcvbnResultTimeout = null;
|
|
||||||
this.setState({
|
|
||||||
// precompute this and keep it in state: zxcvbn is fast but
|
|
||||||
// we use it in a couple of different places so no point recomputing
|
|
||||||
// it unnecessarily.
|
|
||||||
zxcvbnResult: scorePassword(this.state.passPhrase),
|
|
||||||
});
|
|
||||||
}, PASSPHRASE_FEEDBACK_DELAY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPassPhraseConfirmChange = (e) => {
|
_onPassPhraseConfirmChange = (e) => {
|
||||||
|
@ -251,35 +232,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_passPhraseIsValid() {
|
|
||||||
return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
|
|
||||||
}
|
|
||||||
|
|
||||||
_renderPhasePassPhrase() {
|
_renderPhasePassPhrase() {
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
|
|
||||||
let strengthMeter;
|
|
||||||
let helpText;
|
|
||||||
if (this.state.zxcvbnResult) {
|
|
||||||
if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) {
|
|
||||||
helpText = _t("Great! This recovery passphrase looks strong enough.");
|
|
||||||
} else {
|
|
||||||
const suggestions = [];
|
|
||||||
for (let i = 0; i < this.state.zxcvbnResult.feedback.suggestions.length; ++i) {
|
|
||||||
suggestions.push(<div key={i}>{this.state.zxcvbnResult.feedback.suggestions[i]}</div>);
|
|
||||||
}
|
|
||||||
const suggestionBlock = <div>{suggestions.length > 0 ? suggestions : _t("Keep going...")}</div>;
|
|
||||||
|
|
||||||
helpText = <div>
|
|
||||||
{this.state.zxcvbnResult.feedback.warning}
|
|
||||||
{suggestionBlock}
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
strengthMeter = <div>
|
|
||||||
<progress max={PASSWORD_MIN_SCORE} value={this.state.zxcvbnResult.score} />
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <form onSubmit={this._onPassPhraseNextClick}>
|
return <form onSubmit={this._onPassPhraseNextClick}>
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
"<b>Warning</b>: You should only set up key backup from a trusted computer.", {},
|
"<b>Warning</b>: You should only set up key backup from a trusted computer.", {},
|
||||||
|
@ -293,17 +248,19 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
|
|
||||||
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
||||||
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
|
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
|
||||||
<input type="password"
|
<PassphraseField
|
||||||
onChange={this._onPassPhraseChange}
|
|
||||||
value={this.state.passPhrase}
|
|
||||||
className="mx_CreateKeyBackupDialog_passPhraseInput"
|
className="mx_CreateKeyBackupDialog_passPhraseInput"
|
||||||
placeholder={_t("Enter a recovery passphrase...")}
|
onChange={this._onPassPhraseChange}
|
||||||
|
minScore={PASSWORD_MIN_SCORE}
|
||||||
|
value={this.state.passPhrase}
|
||||||
|
onValidate={this._onPassPhraseValidate}
|
||||||
|
fieldRef={this._passphraseField}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
|
label={_td("Enter a recovery passphrase")}
|
||||||
|
labelEnterPassword={_td("Enter a recovery passphrase")}
|
||||||
|
labelStrongPassword={_td("Great! This recovery passphrase looks strong enough.")}
|
||||||
|
labelAllowedButUnsafe={_td("Great! This recovery passphrase looks strong enough.")}
|
||||||
/>
|
/>
|
||||||
<div className="mx_CreateKeyBackupDialog_passPhraseHelp">
|
|
||||||
{strengthMeter}
|
|
||||||
{helpText}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -311,7 +268,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
primaryButton={_t('Next')}
|
primaryButton={_t('Next')}
|
||||||
onPrimaryButtonClick={this._onPassPhraseNextClick}
|
onPrimaryButtonClick={this._onPassPhraseNextClick}
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
disabled={!this._passPhraseIsValid()}
|
disabled={!this.state.passPhraseValid}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
|
|
|
@ -15,17 +15,17 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {createRef} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import * as sdk from '../../../../index';
|
import * as sdk from '../../../../index';
|
||||||
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
||||||
import { scorePassword } from '../../../../utils/PasswordScorer';
|
|
||||||
import FileSaver from 'file-saver';
|
import FileSaver from 'file-saver';
|
||||||
import { _t } from '../../../../languageHandler';
|
import {_t, _td} from '../../../../languageHandler';
|
||||||
import Modal from '../../../../Modal';
|
import Modal from '../../../../Modal';
|
||||||
import { promptForBackupPassphrase } from '../../../../CrossSigningManager';
|
import { promptForBackupPassphrase } from '../../../../CrossSigningManager';
|
||||||
import {copyNode} from "../../../../utils/strings";
|
import {copyNode} from "../../../../utils/strings";
|
||||||
import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents";
|
import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents";
|
||||||
|
import PassphraseField from "../../../../components/views/auth/PassphraseField";
|
||||||
|
|
||||||
const PHASE_LOADING = 0;
|
const PHASE_LOADING = 0;
|
||||||
const PHASE_LOADERROR = 1;
|
const PHASE_LOADERROR = 1;
|
||||||
|
@ -39,7 +39,6 @@ const PHASE_DONE = 8;
|
||||||
const PHASE_CONFIRM_SKIP = 9;
|
const PHASE_CONFIRM_SKIP = 9;
|
||||||
|
|
||||||
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
|
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
|
||||||
const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms.
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Walks the user through the process of creating a passphrase to guard Secure
|
* Walks the user through the process of creating a passphrase to guard Secure
|
||||||
|
@ -62,16 +61,15 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
|
|
||||||
this._recoveryKey = null;
|
this._recoveryKey = null;
|
||||||
this._recoveryKeyNode = null;
|
this._recoveryKeyNode = null;
|
||||||
this._setZxcvbnResultTimeout = null;
|
|
||||||
this._backupKey = null;
|
this._backupKey = null;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
phase: PHASE_LOADING,
|
phase: PHASE_LOADING,
|
||||||
passPhrase: '',
|
passPhrase: '',
|
||||||
|
passPhraseValid: false,
|
||||||
passPhraseConfirm: '',
|
passPhraseConfirm: '',
|
||||||
copied: false,
|
copied: false,
|
||||||
downloaded: false,
|
downloaded: false,
|
||||||
zxcvbnResult: null,
|
|
||||||
backupInfo: null,
|
backupInfo: null,
|
||||||
backupSigStatus: null,
|
backupSigStatus: null,
|
||||||
// does the server offer a UI auth flow with just m.login.password
|
// does the server offer a UI auth flow with just m.login.password
|
||||||
|
@ -83,6 +81,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
useKeyBackup: true,
|
useKeyBackup: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this._passphraseField = createRef();
|
||||||
|
|
||||||
this._fetchBackupInfo();
|
this._fetchBackupInfo();
|
||||||
if (this.state.accountPassword) {
|
if (this.state.accountPassword) {
|
||||||
// If we have an account password in memory, let's simplify and
|
// If we have an account password in memory, let's simplify and
|
||||||
|
@ -99,9 +99,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
|
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
|
||||||
if (this._setZxcvbnResultTimeout !== null) {
|
|
||||||
clearTimeout(this._setZxcvbnResultTimeout);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _fetchBackupInfo() {
|
async _fetchBackupInfo() {
|
||||||
|
@ -364,22 +361,16 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
|
|
||||||
_onPassPhraseNextClick = async (e) => {
|
_onPassPhraseNextClick = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!this._passphraseField.current) return; // unmounting
|
||||||
|
|
||||||
// If we're waiting for the timeout before updating the result at this point,
|
await this._passphraseField.current.validate({ allowEmpty: false });
|
||||||
// skip ahead and do it now, otherwise we'll deny the attempt to proceed
|
if (!this._passphraseField.current.state.valid) {
|
||||||
// even if the user entered a valid passphrase
|
this._passphraseField.current.focus();
|
||||||
if (this._setZxcvbnResultTimeout !== null) {
|
this._passphraseField.current.validate({ allowEmpty: false, focused: true });
|
||||||
clearTimeout(this._setZxcvbnResultTimeout);
|
return;
|
||||||
this._setZxcvbnResultTimeout = null;
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
this.setState({
|
|
||||||
zxcvbnResult: scorePassword(this.state.passPhrase),
|
|
||||||
}, resolve);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (this._passPhraseIsValid()) {
|
|
||||||
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onPassPhraseConfirmNextClick = async (e) => {
|
_onPassPhraseConfirmNextClick = async (e) => {
|
||||||
|
@ -399,9 +390,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
_onSetAgainClick = () => {
|
_onSetAgainClick = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
passPhrase: '',
|
passPhrase: '',
|
||||||
|
passPhraseValid: false,
|
||||||
passPhraseConfirm: '',
|
passPhraseConfirm: '',
|
||||||
phase: PHASE_PASSPHRASE,
|
phase: PHASE_PASSPHRASE,
|
||||||
zxcvbnResult: null,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -411,23 +402,16 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onPassPhraseValidate = (result) => {
|
||||||
|
this.setState({
|
||||||
|
passPhraseValid: result.valid,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
_onPassPhraseChange = (e) => {
|
_onPassPhraseChange = (e) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
passPhrase: e.target.value,
|
passPhrase: e.target.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this._setZxcvbnResultTimeout !== null) {
|
|
||||||
clearTimeout(this._setZxcvbnResultTimeout);
|
|
||||||
}
|
|
||||||
this._setZxcvbnResultTimeout = setTimeout(() => {
|
|
||||||
this._setZxcvbnResultTimeout = null;
|
|
||||||
this.setState({
|
|
||||||
// precompute this and keep it in state: zxcvbn is fast but
|
|
||||||
// we use it in a couple of different places so no point recomputing
|
|
||||||
// it unnecessarily.
|
|
||||||
zxcvbnResult: scorePassword(this.state.passPhrase),
|
|
||||||
});
|
|
||||||
}, PASSPHRASE_FEEDBACK_DELAY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPassPhraseConfirmChange = (e) => {
|
_onPassPhraseConfirmChange = (e) => {
|
||||||
|
@ -436,10 +420,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_passPhraseIsValid() {
|
|
||||||
return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
|
|
||||||
}
|
|
||||||
|
|
||||||
_onAccountPasswordChange = (e) => {
|
_onAccountPasswordChange = (e) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
accountPassword: e.target.value,
|
accountPassword: e.target.value,
|
||||||
|
@ -502,37 +482,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
|
|
||||||
_renderPhasePassPhrase() {
|
_renderPhasePassPhrase() {
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
const Field = sdk.getComponent('views.elements.Field');
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
|
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
|
||||||
|
|
||||||
let strengthMeter;
|
|
||||||
let helpText;
|
|
||||||
if (this.state.zxcvbnResult) {
|
|
||||||
if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) {
|
|
||||||
helpText = _t("Great! This recovery passphrase looks strong enough.");
|
|
||||||
} else {
|
|
||||||
// We take the warning from zxcvbn or failing that, the first
|
|
||||||
// suggestion. In practice The first is generally the most relevant
|
|
||||||
// and it's probably better to present the user with one thing to
|
|
||||||
// improve about their password than a whole collection - it can
|
|
||||||
// spit out a warning and multiple suggestions which starts getting
|
|
||||||
// very information-dense.
|
|
||||||
const suggestion = (
|
|
||||||
this.state.zxcvbnResult.feedback.warning ||
|
|
||||||
this.state.zxcvbnResult.feedback.suggestions[0]
|
|
||||||
);
|
|
||||||
const suggestionBlock = <div>{suggestion || _t("Keep going...")}</div>;
|
|
||||||
|
|
||||||
helpText = <div>
|
|
||||||
{suggestionBlock}
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
strengthMeter = <div>
|
|
||||||
<progress max={PASSWORD_MIN_SCORE} value={this.state.zxcvbnResult.score} />
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <form onSubmit={this._onPassPhraseNextClick}>
|
return <form onSubmit={this._onPassPhraseNextClick}>
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
"Set a recovery passphrase to secure encrypted information and recover it if you log out. " +
|
"Set a recovery passphrase to secure encrypted information and recover it if you log out. " +
|
||||||
|
@ -540,19 +492,19 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
)}</p>
|
)}</p>
|
||||||
|
|
||||||
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
||||||
<Field
|
<PassphraseField
|
||||||
type="password"
|
|
||||||
className="mx_CreateSecretStorageDialog_passPhraseField"
|
className="mx_CreateSecretStorageDialog_passPhraseField"
|
||||||
onChange={this._onPassPhraseChange}
|
onChange={this._onPassPhraseChange}
|
||||||
|
minScore={PASSWORD_MIN_SCORE}
|
||||||
value={this.state.passPhrase}
|
value={this.state.passPhrase}
|
||||||
label={_t("Enter a recovery passphrase")}
|
onValidate={this._onPassPhraseValidate}
|
||||||
|
fieldRef={this._passphraseField}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
autoComplete="new-password"
|
label={_td("Enter a recovery passphrase")}
|
||||||
|
labelEnterPassword={_td("Enter a recovery passphrase")}
|
||||||
|
labelStrongPassword={_td("Great! This recovery passphrase looks strong enough.")}
|
||||||
|
labelAllowedButUnsafe={_td("Great! This recovery passphrase looks strong enough.")}
|
||||||
/>
|
/>
|
||||||
<div className="mx_CreateSecretStorageDialog_passPhraseHelp">
|
|
||||||
{strengthMeter}
|
|
||||||
{helpText}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LabelledToggleSwitch
|
<LabelledToggleSwitch
|
||||||
|
@ -564,7 +516,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
primaryButton={_t('Continue')}
|
primaryButton={_t('Continue')}
|
||||||
onPrimaryButtonClick={this._onPassPhraseNextClick}
|
onPrimaryButtonClick={this._onPassPhraseNextClick}
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
disabled={!this._passPhraseIsValid()}
|
disabled={!this.state.passPhraseValid}
|
||||||
>
|
>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
onClick={this._onSkipSetupClick}
|
onClick={this._onSkipSetupClick}
|
||||||
|
|
125
src/components/views/auth/PassphraseField.tsx
Normal file
125
src/components/views/auth/PassphraseField.tsx
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
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 {
|
||||||
|
autoFocus?: boolean;
|
||||||
|
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"),
|
||||||
|
};
|
||||||
|
|
||||||
|
state = { complexity: null };
|
||||||
|
|
||||||
|
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}
|
||||||
|
autoFocus={this.props.autoFocus}
|
||||||
|
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,6 +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 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';
|
||||||
|
@ -78,7 +79,6 @@ export default createReactClass({
|
||||||
password: this.props.defaultPassword || "",
|
password: this.props.defaultPassword || "",
|
||||||
passwordConfirm: this.props.defaultPassword || "",
|
passwordConfirm: this.props.defaultPassword || "",
|
||||||
passwordComplexity: null,
|
passwordComplexity: null,
|
||||||
passwordSafe: false,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -264,65 +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 <progress
|
|
||||||
className="mx_AuthBody_passwordScore"
|
|
||||||
max={PASSWORD_MIN_SCORE}
|
|
||||||
value={score}
|
|
||||||
/>;
|
|
||||||
},
|
|
||||||
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);
|
|
||||||
const safe = complexity.score >= PASSWORD_MIN_SCORE;
|
|
||||||
const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
|
|
||||||
this.setState({
|
|
||||||
passwordComplexity: complexity,
|
|
||||||
passwordSafe: safe,
|
|
||||||
});
|
|
||||||
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.passwordSafe) {
|
|
||||||
return _t("Password is allowed, but unsafe");
|
|
||||||
}
|
|
||||||
return _t("Nice, strong password!");
|
|
||||||
},
|
|
||||||
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,
|
||||||
|
@ -484,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}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 New Vector Ltd
|
Copyright 2019 New Vector Ltd
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,11 +16,39 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-disable babel/no-invalid-this */
|
/* eslint-disable babel/no-invalid-this */
|
||||||
|
import React from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
import classNames from 'classnames';
|
type Data = Pick<IFieldState, "value" | "allowEmpty">;
|
||||||
|
|
||||||
|
interface IRule<T> {
|
||||||
|
key: string;
|
||||||
|
final?: boolean;
|
||||||
|
skip?(this: T, data: Data): boolean;
|
||||||
|
test(this: T, data: Data): boolean | Promise<boolean>;
|
||||||
|
valid?(this: T): string;
|
||||||
|
invalid?(this: T): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IArgs<T> {
|
||||||
|
rules: IRule<T>[];
|
||||||
|
description(this: T): React.ReactChild;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
* Creates a validation function from a set of rules describing what to validate.
|
||||||
|
* Generic T is the "this" type passed to the rule methods
|
||||||
*
|
*
|
||||||
* @param {Function} description
|
* @param {Function} description
|
||||||
* Function that returns a string summary of the kind of value that will
|
* Function that returns a string summary of the kind of value that will
|
||||||
|
@ -37,8 +66,8 @@ import classNames from 'classnames';
|
||||||
* A validation function that takes in the current input value and returns
|
* A validation function that takes in the current input value and returns
|
||||||
* 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({ description, rules }) {
|
export default function withValidation<T = undefined>({ description, rules }: IArgs<T>) {
|
||||||
return async function onValidate({ value, focused, allowEmpty = true }) {
|
return async function onValidate({ value, focused, allowEmpty = true }: IFieldState): Promise<IValidationResult> {
|
||||||
if (!value && allowEmpty) {
|
if (!value && allowEmpty) {
|
||||||
return {
|
return {
|
||||||
valid: null,
|
valid: null,
|
|
@ -1889,6 +1889,10 @@
|
||||||
"Your Modular server": "Your Modular server",
|
"Your Modular server": "Your Modular server",
|
||||||
"Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of <a>modular.im</a>.": "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of <a>modular.im</a>.",
|
"Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of <a>modular.im</a>.": "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of <a>modular.im</a>.",
|
||||||
"Server Name": "Server Name",
|
"Server Name": "Server Name",
|
||||||
|
"Enter password": "Enter password",
|
||||||
|
"Nice, strong password!": "Nice, strong password!",
|
||||||
|
"Password is allowed, but unsafe": "Password is allowed, but unsafe",
|
||||||
|
"Keep going...": "Keep going...",
|
||||||
"The email field must not be blank.": "The email field must not be blank.",
|
"The email field must not be blank.": "The email field must not be blank.",
|
||||||
"The username field must not be blank.": "The username field must not be blank.",
|
"The username field must not be blank.": "The username field must not be blank.",
|
||||||
"The phone number field must not be blank.": "The phone number field must not be blank.",
|
"The phone number field must not be blank.": "The phone number field must not be blank.",
|
||||||
|
@ -1903,10 +1907,6 @@
|
||||||
"Use an email address to recover your account": "Use an email address to recover your account",
|
"Use an email address to recover your account": "Use an email address to recover your account",
|
||||||
"Enter email address (required on this homeserver)": "Enter email address (required on this homeserver)",
|
"Enter email address (required on this homeserver)": "Enter email address (required on this homeserver)",
|
||||||
"Doesn't look like a valid email address": "Doesn't look like a valid email address",
|
"Doesn't look like a valid email address": "Doesn't look like a valid email address",
|
||||||
"Enter password": "Enter password",
|
|
||||||
"Password is allowed, but unsafe": "Password is allowed, but unsafe",
|
|
||||||
"Nice, strong password!": "Nice, strong password!",
|
|
||||||
"Keep going...": "Keep going...",
|
|
||||||
"Passwords don't match": "Passwords don't match",
|
"Passwords don't match": "Passwords don't match",
|
||||||
"Other users can invite you to rooms using your contact details": "Other users can invite you to rooms using your contact details",
|
"Other users can invite you to rooms using your contact details": "Other users can invite you to rooms using your contact details",
|
||||||
"Enter phone number (required on this homeserver)": "Enter phone number (required on this homeserver)",
|
"Enter phone number (required on this homeserver)": "Enter phone number (required on this homeserver)",
|
||||||
|
@ -2200,9 +2200,9 @@
|
||||||
"Restore": "Restore",
|
"Restore": "Restore",
|
||||||
"You'll need to authenticate with the server to confirm the upgrade.": "You'll need to authenticate with the server to confirm the upgrade.",
|
"You'll need to authenticate with the server to confirm the upgrade.": "You'll need to authenticate with the server to confirm the upgrade.",
|
||||||
"Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.",
|
"Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.",
|
||||||
"Great! This recovery passphrase looks strong enough.": "Great! This recovery passphrase looks strong enough.",
|
|
||||||
"Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:": "Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:",
|
"Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:": "Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:",
|
||||||
"Enter a recovery passphrase": "Enter a recovery passphrase",
|
"Enter a recovery passphrase": "Enter a recovery passphrase",
|
||||||
|
"Great! This recovery passphrase looks strong enough.": "Great! This recovery passphrase looks strong enough.",
|
||||||
"Back up encrypted message keys": "Back up encrypted message keys",
|
"Back up encrypted message keys": "Back up encrypted message keys",
|
||||||
"Set up with a recovery key": "Set up with a recovery key",
|
"Set up with a recovery key": "Set up with a recovery key",
|
||||||
"That matches!": "That matches!",
|
"That matches!": "That matches!",
|
||||||
|
@ -2230,7 +2230,6 @@
|
||||||
"Unable to set up secret storage": "Unable to set up secret storage",
|
"Unable to set up secret storage": "Unable to set up secret storage",
|
||||||
"We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.",
|
"We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.",
|
||||||
"For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.",
|
"For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.",
|
||||||
"Enter a recovery passphrase...": "Enter a recovery passphrase...",
|
|
||||||
"Please enter your recovery passphrase a second time to confirm.": "Please enter your recovery passphrase a second time to confirm.",
|
"Please enter your recovery passphrase a second time to confirm.": "Please enter your recovery passphrase a second time to confirm.",
|
||||||
"Repeat your recovery passphrase...": "Repeat your recovery passphrase...",
|
"Repeat your recovery passphrase...": "Repeat your recovery passphrase...",
|
||||||
"Your keys are being backed up (the first backup could take a few minutes).": "Your keys are being backed up (the first backup could take a few minutes).",
|
"Your keys are being backed up (the first backup could take a few minutes).": "Your keys are being backed up (the first backup could take a few minutes).",
|
||||||
|
|
|
@ -63,7 +63,7 @@ _td("Short keyboard patterns are easy to guess");
|
||||||
* @param {string} password Password to score
|
* @param {string} password Password to score
|
||||||
* @returns {object} Score result with `score` and `feedback` properties
|
* @returns {object} Score result with `score` and `feedback` properties
|
||||||
*/
|
*/
|
||||||
export function scorePassword(password) {
|
export function scorePassword(password: string) {
|
||||||
if (password.length === 0) return null;
|
if (password.length === 0) return null;
|
||||||
|
|
||||||
const userInputs = ZXCVBN_USER_INPUTS.slice();
|
const userInputs = ZXCVBN_USER_INPUTS.slice();
|
|
@ -1325,6 +1325,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