Merge pull request #5436 from SimonBrandner/feature-change-password-validation
Add live password validation to change password dialog
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
Reference in New Issue
Block a user