diff --git a/package-lock.json b/package-lock.json index 1b67fe56b..5df769683 100644 --- a/package-lock.json +++ b/package-lock.json @@ -99,8 +99,8 @@ "regenerator-runtime": "0.13.9", "sass": "1.49.7", "sass-loader": "10.2.1", - "scratch-gui": "0.1.0-prerelease.20220914202542", - "scratch-l10n": "3.15.20220913031617", + "scratch-gui": "0.1.0-prerelease.20220928050448", + "scratch-l10n": "3.15.20220927031631", "selenium-webdriver": "4.1.0", "slick-carousel": "1.6.0", "style-loader": "0.12.3", @@ -22445,21 +22445,21 @@ } }, "node_modules/scratch-blocks": { - "version": "0.1.0-prerelease.20220913171331", - "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20220913171331.tgz", - "integrity": "sha512-G6GC7LfA7S2IspsMfLzTJz5KgdOmr9A6MAnRQLKV2A3TslAaH+V4HTZv+xoiEgmLtjM8XCDsHsuZXkJuBlqoAw==", + "version": "0.1.0-prerelease.20220927130010", + "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20220927130010.tgz", + "integrity": "sha512-NgZcBOmiOCqpMcIrjbaidNuHA+WoCDfkc20FZtErngXR6vzkBuB6WIVs1F6jo0yHTEDdnjQskjm7J0RfcP3tKg==", "dev": true, "dependencies": { "exports-loader": "0.7.0", "google-closure-library": "20190301.0.0", "imports-loader": "0.8.0", - "scratch-l10n": "3.15.20220913031617" + "scratch-l10n": "3.15.20220927031631" } }, "node_modules/scratch-gui": { - "version": "0.1.0-prerelease.20220914202542", - "resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20220914202542.tgz", - "integrity": "sha512-LXdqqtCaAWU+43gYITMupFcxOStEPrprpoJa8D15HhTfMhC/cg1g2i0zx5d72Ll/uqfrjMO64PKVaFEoJvq0Rw==", + "version": "0.1.0-prerelease.20220928050448", + "resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20220928050448.tgz", + "integrity": "sha512-xlZvwQAUNlPHLZ0eK6Yl3KMlfLzhoG1u/qK25U9x+5fibr9vIKK/w+4r7K8kSt/i1/fszNLkMkF6ptRjikvJGw==", "dev": true, "dependencies": { "arraybuffer-loader": "^1.0.6", @@ -22510,14 +22510,14 @@ "redux": "3.7.2", "redux-throttle": "0.1.1", "scratch-audio": "0.1.0-prerelease.20200528195344", - "scratch-blocks": "0.1.0-prerelease.20220913171331", - "scratch-l10n": "3.15.20220913031617", - "scratch-paint": "1.1.3", + "scratch-blocks": "0.1.0-prerelease.20220927130010", + "scratch-l10n": "3.15.20220927031631", + "scratch-paint": "1.1.13", "scratch-render": "0.1.0-prerelease.20211028200436", "scratch-render-fonts": "1.0.0-prerelease.20210401210003", "scratch-storage": "2.0.2", "scratch-svg-renderer": "0.2.0-prerelease.20220912180225", - "scratch-vm": "1.2.4", + "scratch-vm": "1.2.22", "startaudiocontext": "1.2.1", "style-loader": "^0.23.0", "text-encoding": "0.7.0", @@ -22795,9 +22795,9 @@ } }, "node_modules/scratch-gui/node_modules/scratch-paint": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/scratch-paint/-/scratch-paint-1.1.3.tgz", - "integrity": "sha512-O++0hNP60CsNYkS8LsX7cl/FJoWUT71gei2NCuw4VREbvljOQAXWegF4JNVbJxx6TUw4ab8xOULQwo4rMtUYHg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/scratch-paint/-/scratch-paint-1.1.13.tgz", + "integrity": "sha512-kev9atMqvigntkxzKdaZvU0J0FRtqkHSu5q3sjrX+6dOCFvTV4HzhPJd4OlWO5AVeiS3FF1QMqt8P2IRtdPeIA==", "dev": true, "dependencies": { "@scratch/paper": "0.11.20200728195508", @@ -22816,7 +22816,7 @@ "react-intl-redux": "^0.7", "react-popover": "^0.5", "react-redux": "^5", - "react-responsive": "^4", + "react-responsive": "^5", "react-style-proptype": "^3", "react-tooltip": "^3", "redux": "^3", @@ -22893,9 +22893,9 @@ } }, "node_modules/scratch-l10n": { - "version": "3.15.20220913031617", - "resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.15.20220913031617.tgz", - "integrity": "sha512-2WkeXZBc7yzUOq4UbGTnwjnvkoDbut1nxhYprOcMHLMck2UuVyWx2jyfu3fAlUqGtJs3dVz5A79uS1FwGXx29A==", + "version": "3.15.20220927031631", + "resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.15.20220927031631.tgz", + "integrity": "sha512-KTNIAMnpJ+BSpxbK0r8dMSZWLAASDAfcG467UWar+1wz5v85GNPwUWkNTzSetbUHnm7vriZE9QOgX+eN1qaIfQ==", "dev": true, "dependencies": { "@babel/cli": "^7.1.2", @@ -23090,9 +23090,9 @@ "dev": true }, "node_modules/scratch-vm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/scratch-vm/-/scratch-vm-1.2.4.tgz", - "integrity": "sha512-gBF8g99M1i0uRqwhoPvjwHrt5+po2mEHBoYd5W7osFYKlVrSRgF5WpNnU4KOOkq5CpXvPgGjdst25Qh2Rk0qOw==", + "version": "1.2.22", + "resolved": "https://registry.npmjs.org/scratch-vm/-/scratch-vm-1.2.22.tgz", + "integrity": "sha512-dmcXb2SOgu4ocQC+HEUJZQEf5aHDzRVKYIY7eCerg20lGGmagIsiYEWq7kn31niVbVBx9RdpcHjL4JEwZ+Aqxw==", "dev": true, "dependencies": { "@vernier/godirect": "1.5.0", @@ -49761,21 +49761,21 @@ } }, "scratch-blocks": { - "version": "0.1.0-prerelease.20220913171331", - "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20220913171331.tgz", - "integrity": "sha512-G6GC7LfA7S2IspsMfLzTJz5KgdOmr9A6MAnRQLKV2A3TslAaH+V4HTZv+xoiEgmLtjM8XCDsHsuZXkJuBlqoAw==", + "version": "0.1.0-prerelease.20220927130010", + "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20220927130010.tgz", + "integrity": "sha512-NgZcBOmiOCqpMcIrjbaidNuHA+WoCDfkc20FZtErngXR6vzkBuB6WIVs1F6jo0yHTEDdnjQskjm7J0RfcP3tKg==", "dev": true, "requires": { "exports-loader": "0.7.0", "google-closure-library": "20190301.0.0", "imports-loader": "0.8.0", - "scratch-l10n": "3.15.20220913031617" + "scratch-l10n": "3.15.20220927031631" } }, "scratch-gui": { - "version": "0.1.0-prerelease.20220914202542", - "resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20220914202542.tgz", - "integrity": "sha512-LXdqqtCaAWU+43gYITMupFcxOStEPrprpoJa8D15HhTfMhC/cg1g2i0zx5d72Ll/uqfrjMO64PKVaFEoJvq0Rw==", + "version": "0.1.0-prerelease.20220928050448", + "resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20220928050448.tgz", + "integrity": "sha512-xlZvwQAUNlPHLZ0eK6Yl3KMlfLzhoG1u/qK25U9x+5fibr9vIKK/w+4r7K8kSt/i1/fszNLkMkF6ptRjikvJGw==", "dev": true, "requires": { "arraybuffer-loader": "^1.0.6", @@ -49826,14 +49826,14 @@ "redux": "3.7.2", "redux-throttle": "0.1.1", "scratch-audio": "0.1.0-prerelease.20200528195344", - "scratch-blocks": "0.1.0-prerelease.20220913171331", - "scratch-l10n": "3.15.20220913031617", - "scratch-paint": "1.1.3", + "scratch-blocks": "0.1.0-prerelease.20220927130010", + "scratch-l10n": "3.15.20220927031631", + "scratch-paint": "1.1.13", "scratch-render": "0.1.0-prerelease.20211028200436", "scratch-render-fonts": "1.0.0-prerelease.20210401210003", "scratch-storage": "2.0.2", "scratch-svg-renderer": "0.2.0-prerelease.20220912180225", - "scratch-vm": "1.2.4", + "scratch-vm": "1.2.22", "startaudiocontext": "1.2.1", "style-loader": "^0.23.0", "text-encoding": "0.7.0", @@ -50057,9 +50057,9 @@ } }, "scratch-paint": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/scratch-paint/-/scratch-paint-1.1.3.tgz", - "integrity": "sha512-O++0hNP60CsNYkS8LsX7cl/FJoWUT71gei2NCuw4VREbvljOQAXWegF4JNVbJxx6TUw4ab8xOULQwo4rMtUYHg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/scratch-paint/-/scratch-paint-1.1.13.tgz", + "integrity": "sha512-kev9atMqvigntkxzKdaZvU0J0FRtqkHSu5q3sjrX+6dOCFvTV4HzhPJd4OlWO5AVeiS3FF1QMqt8P2IRtdPeIA==", "dev": true, "requires": { "@scratch/paper": "0.11.20200728195508", @@ -50134,9 +50134,9 @@ } }, "scratch-l10n": { - "version": "3.15.20220913031617", - "resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.15.20220913031617.tgz", - "integrity": "sha512-2WkeXZBc7yzUOq4UbGTnwjnvkoDbut1nxhYprOcMHLMck2UuVyWx2jyfu3fAlUqGtJs3dVz5A79uS1FwGXx29A==", + "version": "3.15.20220927031631", + "resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.15.20220927031631.tgz", + "integrity": "sha512-KTNIAMnpJ+BSpxbK0r8dMSZWLAASDAfcG467UWar+1wz5v85GNPwUWkNTzSetbUHnm7vriZE9QOgX+eN1qaIfQ==", "dev": true, "requires": { "@babel/cli": "^7.1.2", @@ -50328,9 +50328,9 @@ "dev": true }, "scratch-vm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/scratch-vm/-/scratch-vm-1.2.4.tgz", - "integrity": "sha512-gBF8g99M1i0uRqwhoPvjwHrt5+po2mEHBoYd5W7osFYKlVrSRgF5WpNnU4KOOkq5CpXvPgGjdst25Qh2Rk0qOw==", + "version": "1.2.22", + "resolved": "https://registry.npmjs.org/scratch-vm/-/scratch-vm-1.2.22.tgz", + "integrity": "sha512-dmcXb2SOgu4ocQC+HEUJZQEf5aHDzRVKYIY7eCerg20lGGmagIsiYEWq7kn31niVbVBx9RdpcHjL4JEwZ+Aqxw==", "dev": true, "requires": { "@vernier/godirect": "1.5.0", diff --git a/package.json b/package.json index 4937e9e20..471489b95 100644 --- a/package.json +++ b/package.json @@ -134,8 +134,8 @@ "regenerator-runtime": "0.13.9", "sass": "1.49.7", "sass-loader": "10.2.1", - "scratch-gui": "0.1.0-prerelease.20220914202542", - "scratch-l10n": "3.15.20220913031617", + "scratch-gui": "0.1.0-prerelease.20220928050448", + "scratch-l10n": "3.15.20220927031631", "selenium-webdriver": "4.1.0", "slick-carousel": "1.6.0", "style-loader": "0.12.3", diff --git a/src/components/join-flow/username-step.jsx b/src/components/join-flow/username-step.jsx index ee8b72008..15cf1b7ac 100644 --- a/src/components/join-flow/username-step.jsx +++ b/src/components/join-flow/username-step.jsx @@ -24,6 +24,7 @@ class UsernameStep extends React.Component { 'handleSetUsernameRef', 'handleValidSubmit', 'validatePasswordIfPresent', + 'validatePasswordRemotelyWithCache', 'validatePasswordConfirmIfPresent', 'validateUsernameIfPresent', 'validateUsernameRemotelyWithCache', @@ -32,9 +33,10 @@ class UsernameStep extends React.Component { this.state = { focused: null }; - // simple object to memoize remote requests for usernames. + // memoize remote requests for username check and password weakness // keeps us from submitting multiple requests for same data. this.usernameRemoteCache = Object.create(null); + this.passwordRemoteCache = Object.create(null); } componentDidMount () { // Send info to analytics when we aren't on the standalone page. @@ -92,12 +94,38 @@ class UsernameStep extends React.Component { } ); } + // memoize remote requests for weak password check + validatePasswordRemotelyWithCache (password) { + if (typeof this.passwordRemoteCache[password] === 'object') { + return Promise.resolve(this.passwordRemoteCache[password]); + } + // password is not in our cache + return validate.validatePasswordRemotely(password).then( + remoteResult => { + // cache result, if it successfully heard back from server + if (remoteResult.requestSucceeded) { + this.passwordRemoteCache[password] = remoteResult; + } + return remoteResult; + } + ); + } validatePasswordIfPresent (password, username) { if (!password) return null; // skip validation if password is blank; null indicates valid - const localResult = validate.validatePassword(password, username); - if (localResult.valid) return null; - return this.props.intl.formatMessage({id: localResult.errMsgId}); - } + // if password is not blank, run both local and remote validations + const localResult = validate.validatePasswordLocally(password, username); + if (localResult.valid === false) { // defer to local check first + return this.props.intl.formatMessage({id: localResult.errMsgId}); + } + return this.validatePasswordRemotelyWithCache(password).then( + remoteResult => { + if (remoteResult.valid === false) { + return this.props.intl.formatMessage({id: remoteResult.errMsgId}); + } + return null; + } // remoteResult + ); // validatePasswordRemotelyWithCache + } // validatePasswordIfPresent validatePasswordConfirmIfPresent (password, passwordConfirm) { if (!passwordConfirm) return null; // allow blank password if not submitting yet const localResult = validate.validatePasswordConfirm(password, passwordConfirm); @@ -114,7 +142,7 @@ class UsernameStep extends React.Component { if (!usernameResult.valid) { errors.username = this.props.intl.formatMessage({id: usernameResult.errMsgId}); } - const passwordResult = validate.validatePassword(values.password, values.username); + const passwordResult = validate.validatePasswordLocally(values.password, values.username); if (!passwordResult.valid) { errors.password = this.props.intl.formatMessage({id: passwordResult.errMsgId}); } diff --git a/src/lib/validate.js b/src/lib/validate.js index d97b46a64..56c0ae638 100644 --- a/src/lib/validate.js +++ b/src/lib/validate.js @@ -54,7 +54,7 @@ module.exports.validateUsernameRemotely = username => ( * @param {string} username username value to compare * @return {object} {valid: boolean, errMsgId: string} */ -module.exports.validatePassword = (password, username) => { +module.exports.validatePasswordLocally = (password, username) => { if (!password) { return {valid: false, errMsgId: 'general.required'}; // Using Array.from(string).length, instead of string.length, improves unicode @@ -67,7 +67,7 @@ module.exports.validatePassword = (password, username) => { // https://stackoverflow.com/a/54370584/2308190 } else if (Array.from(password).length < 6) { return {valid: false, errMsgId: 'registration.validationPasswordLength'}; - } else if (password === 'password') { + } else if (password.toLowerCase() === 'password') { return {valid: false, errMsgId: 'registration.validationPasswordNotEquals'}; } else if (username && password === username) { return {valid: false, errMsgId: 'registration.validationPasswordNotUsername'}; @@ -75,6 +75,43 @@ module.exports.validatePassword = (password, username) => { return {valid: true}; }; +module.exports.validatePasswordRemotely = password => ( + new Promise(resolve => { + api({ + method: 'POST', + uri: `/accounts/checkpassword`, + json: { + password: `${password}` + } + }, (err, body, res) => { + if (err || res.statusCode !== 200) { + return resolve({ + requestSucceeded: false, + valid: false, + errMsgId: 'general.error' + }); + } + let msg = ''; + if (body && body.msg) msg = body.msg; + else if (body && body[0]) msg = body[0].msg; + switch (msg) { + case 'valid password': + return resolve({ + requestSucceeded: true, + valid: true + }); + case 'invalid password': + default: + return resolve({ + requestSucceeded: true, + valid: false, + errMsgId: 'registration.validationPasswordNotEquals' + }); + } // switch + }); // api + }) // promise +); + module.exports.validatePasswordConfirm = (password, passwordConfirm) => { if (!passwordConfirm) { return {valid: false, errMsgId: 'general.required'}; diff --git a/test/unit/lib/validate.test.js b/test/unit/lib/validate.test.js index ab3f5fc97..17e8e4998 100644 --- a/test/unit/lib/validate.test.js +++ b/test/unit/lib/validate.test.js @@ -2,6 +2,10 @@ const validate = require('../../../src/lib/validate'); describe('unit test lib/validate.js', () => { + test('validate username remote existence', () => { + expect(typeof validate.validateUsernameRemotely).toBe('function'); + }); + test('validate username exists locally', () => { let response; expect(typeof validate.validateUsernameLocally).toBe('function'); @@ -52,41 +56,45 @@ describe('unit test lib/validate.js', () => { expect(response).toEqual({valid: false, errMsgId: 'registration.validationUsernameRegexp'}); }); + test('validate password remote existence', () => { + expect(typeof validate.validatePasswordRemotely).toBe('function'); + }); + test('validate password existence', () => { let response; - expect(typeof validate.validatePassword).toBe('function'); - response = validate.validatePassword('abcdef'); + expect(typeof validate.validatePasswordLocally).toBe('function'); + response = validate.validatePasswordLocally('abcdef'); expect(response).toEqual({valid: true}); - response = validate.validatePassword(''); + response = validate.validatePasswordLocally(''); expect(response).toEqual({valid: false, errMsgId: 'general.required'}); }); test('validate password length', () => { let response; - response = validate.validatePassword('abcdefghijklmnopqrst'); + response = validate.validatePasswordLocally('abcdefghijklmnopqrst'); expect(response).toEqual({valid: true}); - response = validate.validatePassword('abcde'); + response = validate.validatePasswordLocally('abcde'); expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordLength'}); - response = validate.validatePassword('😺'); + response = validate.validatePasswordLocally('😺'); expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordLength'}); - response = validate.validatePassword('😺🦆🐝'); + response = validate.validatePasswordLocally('😺🦆🐝'); expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordLength'}); - response = validate.validatePassword('😺🦆🐝🐮🐠'); + response = validate.validatePasswordLocally('😺🦆🐝🐮🐠'); expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordLength'}); - response = validate.validatePassword('😺🦆🐝🐮🐠🐻'); + response = validate.validatePasswordLocally('😺🦆🐝🐮🐠🐻'); expect(response).toEqual({valid: true}); }); test('validate password cannot be "password"', () => { - const response = validate.validatePassword('password'); + const response = validate.validatePasswordLocally('password'); expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordNotEquals'}); }); test('validate password cannot be same as username', () => { let response; - response = validate.validatePassword('abcdefg', 'abcdefg'); + response = validate.validatePasswordLocally('abcdefg', 'abcdefg'); expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordNotUsername'}); - response = validate.validatePassword('abcdefg', 'abcdefG'); + response = validate.validatePasswordLocally('abcdefg', 'abcdefG'); expect(response).toEqual({valid: true}); });