From 7e786e67a8a8d615999d336e15c2c4a7ec14b0cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 21 Nov 2020 20:10:38 +0100 Subject: [PATCH 1/6] Added live validation --- .../views/settings/ChangePassword.js | 54 +++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index bafbc816b9..3e3254c666 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -21,9 +21,16 @@ import PropTypes from 'prop-types'; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import AccessibleButton from '../elements/AccessibleButton'; import Spinner from '../elements/Spinner'; +import withValidation from '../elements/Validation'; import { _t } from '../../../languageHandler'; import * as sdk from "../../../index"; import Modal from "../../../Modal"; +import PassphraseField from "../auth/PassphraseField"; + +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 { static propTypes = { @@ -63,6 +70,7 @@ export default class ChangePassword extends React.Component { } state = { + fieldValid: {}, phase: ChangePassword.Phases.Edit, oldPassword: "", newPassword: "", @@ -168,6 +176,14 @@ export default class ChangePassword extends React.Component { ); }; + markFieldValid(fieldID, valid) { + const { fieldValid } = this.state; + fieldValid[fieldID] = valid; + this.setState({ + fieldValid, + }); + } + onChangeOldPassword = (ev) => { this.setState({ oldPassword: ev.target.value, @@ -180,12 +196,39 @@ export default class ChangePassword extends React.Component { }); }; + onNewPasswordValidate = result => { + this.markFieldValid(FIELD_NEW_PASSWORD, result.valid); + }; + onChangeNewPasswordConfirm = (ev) => { this.setState({ newPasswordConfirm: ev.target.value, }); }; + 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 = (ev) => { ev.preventDefault(); const oldPassword = this.state.oldPassword; @@ -202,8 +245,6 @@ export default class ChangePassword extends React.Component { }; render() { - // TODO: Live validation on `new pw == confirm pw` - const rowClassName = this.props.rowClassName; const buttonClassName = this.props.buttonClassName; @@ -220,21 +261,26 @@ export default class ChangePassword extends React.Component { />
- this[FIELD_NEW_PASSWORD] = field} type="password" - label={_t('New Password')} + label='New Password' + minScore={PASSWORD_MIN_SCORE} value={this.state.newPassword} autoFocus={this.props.autoFocusNewPasswordInput} onChange={this.onChangeNewPassword} + onValidate={this.onNewPasswordValidate} autoComplete="new-password" />
this[FIELD_NEW_PASSWORD_CONFIRM] = field} type="password" label={_t("Confirm password")} value={this.state.newPasswordConfirm} onChange={this.onChangeNewPasswordConfirm} + onValidate={this.onNewPasswordConfirmValidate} autoComplete="new-password" />
From 4d7886d1773554e1e47cde096431530b8f1eb636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 21 Nov 2020 21:18:26 +0100 Subject: [PATCH 2/6] Fix i18n --- src/i18n/strings/en_EN.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dc707222e7..1b54d33bf9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -952,9 +952,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.", "Export E2E room keys": "Export E2E room keys", "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", + "Passwords don't match": "Passwords don't match", + "Current password": "Current password", "Change Password": "Change Password", "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.", @@ -2301,7 +2301,6 @@ "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)", "Doesn't look like a valid email address": "Doesn't look like a valid email address", - "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", "Enter phone number (required on this homeserver)": "Enter phone number (required on this homeserver)", "Doesn't look like a valid phone number": "Doesn't look like a valid phone number", @@ -2490,6 +2489,7 @@ "Your Matrix account on ": "Your Matrix account on ", "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", + "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.", "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.", From cd197133aadc6e188f7276573ac5f265452223c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 22 Nov 2020 08:49:20 +0100 Subject: [PATCH 3/6] Button click validation Check validity when clicking change password button --- .../views/settings/ChangePassword.js | 79 ++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 3e3254c666..e8ac419c89 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -26,6 +26,7 @@ import { _t } from '../../../languageHandler'; import * as sdk from "../../../index"; import Modal from "../../../Modal"; import PassphraseField from "../auth/PassphraseField"; +import CountlyAnalytics from "../../../CountlyAnalytics"; const FIELD_NEW_PASSWORD = 'field_new_password'; const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm'; @@ -229,8 +230,15 @@ export default class ChangePassword extends React.Component { ], }); - onClickChange = (ev) => { + onClickChange = async (ev) => { ev.preventDefault(); + + const allFieldsValid = await this.verifyFieldsBeforeSubmit(); + if (!allFieldsValid) { + CountlyAnalytics.instance.track("onboarding_registration_submit_failed"); + return; + } + const oldPassword = this.state.oldPassword; const newPassword = this.state.newPassword; const confirmPassword = this.state.newPasswordConfirm; @@ -244,6 +252,73 @@ export default class ChangePassword extends React.Component { } }; + async verifyFieldsBeforeSubmit() { + // 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_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 buttonClassName = this.props.buttonClassName; @@ -271,6 +346,7 @@ export default class ChangePassword extends React.Component { onChange={this.onChangeNewPassword} onValidate={this.onNewPasswordValidate} autoComplete="new-password" + onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email_blur")} />
@@ -282,6 +358,7 @@ export default class ChangePassword extends React.Component { onChange={this.onChangeNewPasswordConfirm} onValidate={this.onNewPasswordConfirmValidate} autoComplete="new-password" + onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email_blur")} />
From dbce418b63b84ab1ef5c955f52cc4877b7366a7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 22 Nov 2020 09:26:51 +0100 Subject: [PATCH 4/6] Check if old password is empty --- .../views/settings/ChangePassword.js | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index e8ac419c89..557ca6298d 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -28,6 +28,7 @@ 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'; @@ -191,6 +192,22 @@ export default class ChangePassword extends React.Component { }); }; + 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) => { this.setState({ newPassword: ev.target.value, @@ -261,8 +278,9 @@ export default class ChangePassword extends React.Component { } const fieldIDsInDisplayOrder = [ + FIELD_OLD_PASSWORD, FIELD_NEW_PASSWORD, - FIELD_NEW_PASSWORD_CONFIRM + FIELD_NEW_PASSWORD_CONFIRM, ]; // Run all fields with stricter validation that no longer allows empty @@ -329,10 +347,13 @@ export default class ChangePassword extends React.Component {
this[FIELD_OLD_PASSWORD] = field} type="password" label={_t('Current password')} value={this.state.oldPassword} onChange={this.onChangeOldPassword} + onValidate={this.onOldPasswordValidate} + onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email_blur")} />
From 15ffdcb6525d0c1fa3fbbde6f1965bbc104a51fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 22 Nov 2020 09:57:22 +0100 Subject: [PATCH 5/6] Added trailing comma --- src/components/views/settings/ChangePassword.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 557ca6298d..b4585452f8 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -204,7 +204,7 @@ export default class ChangePassword extends React.Component { key: "required", test: ({ value, allowEmpty }) => allowEmpty || !!value, invalid: () => _t("Passwords can't be empty"), - } + }, ], }); From acd148d807446cecf71bbe5d68fb87aa3a814edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 26 Nov 2020 16:58:34 +0100 Subject: [PATCH 6/6] Remove nonsense lines --- src/components/views/settings/ChangePassword.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index b4585452f8..22b758b1ca 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -353,7 +353,6 @@ export default class ChangePassword extends React.Component { value={this.state.oldPassword} onChange={this.onChangeOldPassword} onValidate={this.onOldPasswordValidate} - onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email_blur")} />
@@ -367,7 +366,6 @@ export default class ChangePassword extends React.Component { onChange={this.onChangeNewPassword} onValidate={this.onNewPasswordValidate} autoComplete="new-password" - onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email_blur")} />
@@ -379,7 +377,6 @@ export default class ChangePassword extends React.Component { onChange={this.onChangeNewPasswordConfirm} onValidate={this.onNewPasswordConfirmValidate} autoComplete="new-password" - onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email_blur")} />