Merge pull request #5436 from SimonBrandner/feature-change-password-validation

Add live password validation to change password dialog
This commit is contained in:
Michael Telatynski
2020-11-27 14:01:15 +00:00
committed by GitHub
2 changed files with 149 additions and 8 deletions
+146 -5
View File
@@ -21,9 +21,18 @@ import PropTypes from 'prop-types';
import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../MatrixClientPeg";
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import Spinner from '../elements/Spinner'; import Spinner from '../elements/Spinner';
import withValidation from '../elements/Validation';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as sdk from "../../../index"; import * as sdk from "../../../index";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import PassphraseField from "../auth/PassphraseField";
import CountlyAnalytics from "../../../CountlyAnalytics";
const FIELD_OLD_PASSWORD = 'field_old_password';
const FIELD_NEW_PASSWORD = 'field_new_password';
const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm';
const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario.
export default class ChangePassword extends React.Component { export default class ChangePassword extends React.Component {
static propTypes = { static propTypes = {
@@ -63,6 +72,7 @@ export default class ChangePassword extends React.Component {
} }
state = { state = {
fieldValid: {},
phase: ChangePassword.Phases.Edit, phase: ChangePassword.Phases.Edit,
oldPassword: "", oldPassword: "",
newPassword: "", newPassword: "",
@@ -168,26 +178,84 @@ export default class ChangePassword extends React.Component {
); );
}; };
markFieldValid(fieldID, valid) {
const { fieldValid } = this.state;
fieldValid[fieldID] = valid;
this.setState({
fieldValid,
});
}
onChangeOldPassword = (ev) => { onChangeOldPassword = (ev) => {
this.setState({ this.setState({
oldPassword: ev.target.value, oldPassword: ev.target.value,
}); });
}; };
onOldPasswordValidate = async fieldState => {
const result = await this.validateOldPasswordRules(fieldState);
this.markFieldValid(FIELD_OLD_PASSWORD, result.valid);
return result;
};
validateOldPasswordRules = withValidation({
rules: [
{
key: "required",
test: ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t("Passwords can't be empty"),
},
],
});
onChangeNewPassword = (ev) => { onChangeNewPassword = (ev) => {
this.setState({ this.setState({
newPassword: ev.target.value, newPassword: ev.target.value,
}); });
}; };
onNewPasswordValidate = result => {
this.markFieldValid(FIELD_NEW_PASSWORD, result.valid);
};
onChangeNewPasswordConfirm = (ev) => { onChangeNewPasswordConfirm = (ev) => {
this.setState({ this.setState({
newPasswordConfirm: ev.target.value, newPasswordConfirm: ev.target.value,
}); });
}; };
onClickChange = (ev) => { onNewPasswordConfirmValidate = async fieldState => {
const result = await this.validatePasswordConfirmRules(fieldState);
this.markFieldValid(FIELD_NEW_PASSWORD_CONFIRM, result.valid);
return result;
};
validatePasswordConfirmRules = withValidation({
rules: [
{
key: "required",
test: ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t("Confirm password"),
},
{
key: "match",
test({ value }) {
return !value || value === this.state.newPassword;
},
invalid: () => _t("Passwords don't match"),
},
],
});
onClickChange = async (ev) => {
ev.preventDefault(); ev.preventDefault();
const allFieldsValid = await this.verifyFieldsBeforeSubmit();
if (!allFieldsValid) {
CountlyAnalytics.instance.track("onboarding_registration_submit_failed");
return;
}
const oldPassword = this.state.oldPassword; const oldPassword = this.state.oldPassword;
const newPassword = this.state.newPassword; const newPassword = this.state.newPassword;
const confirmPassword = this.state.newPasswordConfirm; const confirmPassword = this.state.newPasswordConfirm;
@@ -201,9 +269,75 @@ export default class ChangePassword extends React.Component {
} }
}; };
render() { async verifyFieldsBeforeSubmit() {
// TODO: Live validation on `new pw == confirm pw` // Blur the active element if any, so we first run its blur validation,
// which is less strict than the pass we're about to do below for all fields.
const activeElement = document.activeElement;
if (activeElement) {
activeElement.blur();
}
const fieldIDsInDisplayOrder = [
FIELD_OLD_PASSWORD,
FIELD_NEW_PASSWORD,
FIELD_NEW_PASSWORD_CONFIRM,
];
// Run all fields with stricter validation that no longer allows empty
// values for required fields.
for (const fieldID of fieldIDsInDisplayOrder) {
const field = this[fieldID];
if (!field) {
continue;
}
// We must wait for these validations to finish before queueing
// up the setState below so our setState goes in the queue after
// all the setStates from these validate calls (that's how we
// know they've finished).
await field.validate({ allowEmpty: false });
}
// Validation and state updates are async, so we need to wait for them to complete
// first. Queue a `setState` callback and wait for it to resolve.
await new Promise(resolve => this.setState({}, resolve));
if (this.allFieldsValid()) {
return true;
}
const invalidField = this.findFirstInvalidField(fieldIDsInDisplayOrder);
if (!invalidField) {
return true;
}
// Focus the first invalid field and show feedback in the stricter mode
// that no longer allows empty values for required fields.
invalidField.focus();
invalidField.validate({ allowEmpty: false, focused: true });
return false;
}
allFieldsValid() {
const keys = Object.keys(this.state.fieldValid);
for (let i = 0; i < keys.length; ++i) {
if (!this.state.fieldValid[keys[i]]) {
return false;
}
}
return true;
}
findFirstInvalidField(fieldIDs) {
for (const fieldID of fieldIDs) {
if (!this.state.fieldValid[fieldID] && this[fieldID]) {
return this[fieldID];
}
}
return null;
}
render() {
const rowClassName = this.props.rowClassName; const rowClassName = this.props.rowClassName;
const buttonClassName = this.props.buttonClassName; const buttonClassName = this.props.buttonClassName;
@@ -213,28 +347,35 @@ export default class ChangePassword extends React.Component {
<form className={this.props.className} onSubmit={this.onClickChange}> <form className={this.props.className} onSubmit={this.onClickChange}>
<div className={rowClassName}> <div className={rowClassName}>
<Field <Field
ref={field => this[FIELD_OLD_PASSWORD] = field}
type="password" type="password"
label={_t('Current password')} label={_t('Current password')}
value={this.state.oldPassword} value={this.state.oldPassword}
onChange={this.onChangeOldPassword} onChange={this.onChangeOldPassword}
onValidate={this.onOldPasswordValidate}
/> />
</div> </div>
<div className={rowClassName}> <div className={rowClassName}>
<Field <PassphraseField
fieldRef={field => this[FIELD_NEW_PASSWORD] = field}
type="password" type="password"
label={_t('New Password')} label='New Password'
minScore={PASSWORD_MIN_SCORE}
value={this.state.newPassword} value={this.state.newPassword}
autoFocus={this.props.autoFocusNewPasswordInput} autoFocus={this.props.autoFocusNewPasswordInput}
onChange={this.onChangeNewPassword} onChange={this.onChangeNewPassword}
onValidate={this.onNewPasswordValidate}
autoComplete="new-password" autoComplete="new-password"
/> />
</div> </div>
<div className={rowClassName}> <div className={rowClassName}>
<Field <Field
ref={field => this[FIELD_NEW_PASSWORD_CONFIRM] = field}
type="password" type="password"
label={_t("Confirm password")} label={_t("Confirm password")}
value={this.state.newPasswordConfirm} value={this.state.newPasswordConfirm}
onChange={this.onChangeNewPasswordConfirm} onChange={this.onChangeNewPasswordConfirm}
onValidate={this.onNewPasswordConfirmValidate}
autoComplete="new-password" autoComplete="new-password"
/> />
</div> </div>
+3 -3
View File
@@ -955,9 +955,9 @@
"Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.", "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.",
"Export E2E room keys": "Export E2E room keys", "Export E2E room keys": "Export E2E room keys",
"Do you want to set an email address?": "Do you want to set an email address?", "Do you want to set an email address?": "Do you want to set an email address?",
"Current password": "Current password",
"New Password": "New Password",
"Confirm password": "Confirm password", "Confirm password": "Confirm password",
"Passwords don't match": "Passwords don't match",
"Current password": "Current password",
"Change Password": "Change Password", "Change Password": "Change Password",
"Your homeserver does not support cross-signing.": "Your homeserver does not support cross-signing.", "Your homeserver does not support cross-signing.": "Your homeserver does not support cross-signing.",
"Cross-signing is ready for use.": "Cross-signing is ready for use.", "Cross-signing is ready for use.": "Cross-signing is ready for use.",
@@ -2304,7 +2304,6 @@
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?", "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?",
"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)",
"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)",
"Use lowercase letters, numbers, dashes and underscores only": "Use lowercase letters, numbers, dashes and underscores only", "Use lowercase letters, numbers, dashes and underscores only": "Use lowercase letters, numbers, dashes and underscores only",
@@ -2484,6 +2483,7 @@
"Your Matrix account on <underlinedServerName />": "Your Matrix account on <underlinedServerName />", "Your Matrix account on <underlinedServerName />": "Your Matrix account on <underlinedServerName />",
"No identity server is configured: add one in server settings to reset your password.": "No identity server is configured: add one in server settings to reset your password.", "No identity server is configured: add one in server settings to reset your password.": "No identity server is configured: add one in server settings to reset your password.",
"Sign in instead": "Sign in instead", "Sign in instead": "Sign in instead",
"New Password": "New Password",
"A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.", "A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.",
"Send Reset Email": "Send Reset Email", "Send Reset Email": "Send Reset Email",
"An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.", "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.",