diff --git a/client/package-lock.json b/client/package-lock.json index 2605b6be..c8cc96e9 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -44,7 +44,8 @@ "semantic-ui-react": "^2.1.3", "socket.io-client": "^2.3.1", "validator": "^13.7.0", - "whatwg-fetch": "^3.6.2" + "whatwg-fetch": "^3.6.2", + "zxcvbn": "^4.4.2" }, "devDependencies": { "@wojtekmaj/enzyme-adapter-react-17": "^0.6.7", @@ -25310,6 +25311,11 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } + }, + "node_modules/zxcvbn": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz", + "integrity": "sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ==" } }, "dependencies": { @@ -43805,6 +43811,11 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.2.tgz", "integrity": "sha512-JZxotl7SxAJH0j7dN4pxsTV6ZLXoLdGME+PsjkL/DaBrVryK9kTGq06GfKrwcSOqypP+fdXGoCHE36b99fWVoA==" + }, + "zxcvbn": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz", + "integrity": "sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ==" } } } diff --git a/client/package.json b/client/package.json index c6e4e9ec..ec109f89 100755 --- a/client/package.json +++ b/client/package.json @@ -101,7 +101,8 @@ "semantic-ui-react": "^2.1.3", "socket.io-client": "^2.3.1", "validator": "^13.7.0", - "whatwg-fetch": "^3.6.2" + "whatwg-fetch": "^3.6.2", + "zxcvbn": "^4.4.2" }, "devDependencies": { "@wojtekmaj/enzyme-adapter-react-17": "^0.6.7", diff --git a/client/src/components/UserAddPopup/UserAddPopup.jsx b/client/src/components/UserAddPopup/UserAddPopup.jsx index a164448f..dc3572f2 100755 --- a/client/src/components/UserAddPopup/UserAddPopup.jsx +++ b/client/src/components/UserAddPopup/UserAddPopup.jsx @@ -142,19 +142,16 @@ const UserAddStep = React.memo( onChange={handleFieldChange} />
{t('common.password')}
-
- -
- {t('common.mustBeAtLeast6CharactersLongAndContainAtLeastOneLetterAndNumber')} -
-
+
{t('common.name')}
{t('common.newPassword')}
-
- -
- {t('common.mustBeAtLeast6CharactersLongAndContainAtLeastOneLetterAndNumber')} -
-
+ {usePasswordConfirmation && ( <>
{t('common.currentPassword')}
diff --git a/client/src/lib/custom-ui/components/Input/InputPassword.jsx b/client/src/lib/custom-ui/components/Input/InputPassword.jsx index ccfd50af..43952c86 100644 --- a/client/src/lib/custom-ui/components/Input/InputPassword.jsx +++ b/client/src/lib/custom-ui/components/Input/InputPassword.jsx @@ -1,22 +1,74 @@ -import React, { useCallback } from 'react'; -import { Icon, Input } from 'semantic-ui-react'; +import zxcvbn from 'zxcvbn'; +import React, { useCallback, useMemo } from 'react'; +import PropTypes from 'prop-types'; +import { Icon, Input, Progress } from 'semantic-ui-react'; import { useToggle } from '../../../hooks'; -const InputPassword = React.forwardRef((props, ref) => { - const [isVisible, toggleVisible] = useToggle(); +import styles from './InputPassword.module.css'; - const handleToggleClick = useCallback(() => { - toggleVisible(); - }, [toggleVisible]); +const STRENGTH_SCORE_COLORS = ['red', 'orange', 'yellow', 'olive', 'green']; - return ( - } - /> - ); -}); +const InputPassword = React.forwardRef( + ({ value, withStrengthBar, minStrengthScore, className, ...props }, ref) => { + const [isVisible, toggleVisible] = useToggle(); + + const strengthScore = useMemo(() => { + if (!withStrengthBar) { + return undefined; + } + + return zxcvbn(value).score; + }, [value, withStrengthBar]); + + const handleToggleClick = useCallback(() => { + toggleVisible(); + }, [toggleVisible]); + + const inputProps = { + ...props, + ref, + type: isVisible ? 'text' : 'password', + icon: , + }; + + if (!withStrengthBar) { + return ( + + ); + } + + return ( +
+ + +
+ ); + }, +); + +InputPassword.propTypes = { + value: PropTypes.string.isRequired, + withStrengthBar: PropTypes.bool, + minStrengthScore: PropTypes.number, + className: PropTypes.string, +}; + +InputPassword.defaultProps = { + withStrengthBar: false, + minStrengthScore: 2, + className: undefined, +}; export default React.memo(InputPassword); diff --git a/client/src/lib/custom-ui/components/Input/InputPassword.module.css b/client/src/lib/custom-ui/components/Input/InputPassword.module.css new file mode 100644 index 00000000..317b669f --- /dev/null +++ b/client/src/lib/custom-ui/components/Input/InputPassword.module.css @@ -0,0 +1,4 @@ +.strengthBar { + margin: 4px 0 0 !important; + opacity: 0.5; +} diff --git a/client/src/lib/custom-ui/styles.css b/client/src/lib/custom-ui/styles.css index 2fdcac1b..dab0eda9 100644 --- a/client/src/lib/custom-ui/styles.css +++ b/client/src/lib/custom-ui/styles.css @@ -14453,11 +14453,11 @@ img.ui.bordered.image { ---------------------*/ .ui.input.error > input { - background-color: #fff6f6; - border-color: #e0b4b4; - color: #9f3a38; - -webkit-box-shadow: none; - box-shadow: none; + background-color: #fff6f6 !important; + border-color: #e0b4b4 !important; + color: #9f3a38 !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; } /* Error Placeholder */ diff --git a/client/src/locales/en/core.js b/client/src/locales/en/core.js index 44f49ff7..e43fa667 100644 --- a/client/src/locales/en/core.js +++ b/client/src/locales/en/core.js @@ -105,8 +105,6 @@ export default { members: 'Members', minutes: 'Minutes', moveCard_title: 'Move Card', - mustBeAtLeast6CharactersLongAndContainAtLeastOneLetterAndNumber: - 'Must be at least 6 characters long and contain at least one letter and number', name: 'Name', newEmail: 'New e-mail', newPassword: 'New password', diff --git a/client/src/locales/ru/core.js b/client/src/locales/ru/core.js index f614dbc7..87b37303 100644 --- a/client/src/locales/ru/core.js +++ b/client/src/locales/ru/core.js @@ -100,8 +100,6 @@ export default { members: 'Участники', minutes: 'Минуты', moveCard: 'Перемещение карточки', - mustBeAtLeast6CharactersLongAndContainAtLeastOneLetterAndNumber: - 'Должен быть не менее 6 символов и содержать хотя бы одну букву и цифру', name: 'Имя', newEmail: 'Новый e-mail', newPassword: 'Новый пароль', diff --git a/client/src/utils/validator.js b/client/src/utils/validator.js index 7fb171c6..6d391a79 100644 --- a/client/src/utils/validator.js +++ b/client/src/utils/validator.js @@ -1,10 +1,8 @@ -const PASSWORD_REGEX = /^(?=.*[A-Za-z])(?=.*\d).+$/; +import zxcvbn from 'zxcvbn'; + const USERNAME_REGEX = /^[a-zA-Z0-9]+((_|\.)?[a-zA-Z0-9])*$/; -export const isPassword = (string) => { - return string.length >= 6 && PASSWORD_REGEX.test(string); -}; +export const isPassword = (string) => zxcvbn(string).score >= 2; // TODO: move to config -export const isUsername = (string) => { - return string.length >= 3 && string.length <= 16 && USERNAME_REGEX.test(string); -}; +export const isUsername = (string) => + string.length >= 3 && string.length <= 16 && USERNAME_REGEX.test(string); diff --git a/server/api/controllers/users/create.js b/server/api/controllers/users/create.js index 9b397a1c..1fa0f1f5 100755 --- a/server/api/controllers/users/create.js +++ b/server/api/controllers/users/create.js @@ -1,3 +1,5 @@ +const zxcvbn = require('zxcvbn'); + const Errors = { EMAIL_ALREADY_IN_USE: { emailAlreadyInUse: 'Email already in use', @@ -16,8 +18,7 @@ module.exports = { }, password: { type: 'string', - minLength: 6, - regex: /^(?=.*[A-Za-z])(?=.*\d).+$/, + custom: (value) => zxcvbn(value).score >= 2, // TODO: move to config required: true, }, name: { diff --git a/server/api/controllers/users/update-password.js b/server/api/controllers/users/update-password.js index 0ea1afad..a99258b7 100644 --- a/server/api/controllers/users/update-password.js +++ b/server/api/controllers/users/update-password.js @@ -1,4 +1,5 @@ const bcrypt = require('bcrypt'); +const zxcvbn = require('zxcvbn'); const Errors = { USER_NOT_FOUND: { @@ -18,8 +19,7 @@ module.exports = { }, password: { type: 'string', - minLength: 6, - regex: /^(?=.*[A-Za-z])(?=.*\d).+$/, + custom: (value) => zxcvbn(value).score >= 2, // TODO: move to config required: true, }, currentPassword: { diff --git a/server/package-lock.json b/server/package-lock.json index baac2088..c5e1c931 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -24,7 +24,8 @@ "stream-to-array": "^2.3.0", "uuid": "^8.3.2", "validator": "^13.7.0", - "winston": "^3.8.1" + "winston": "^3.8.1", + "zxcvbn": "^4.4.2" }, "devDependencies": { "chai": "^4.3.6", @@ -7984,6 +7985,11 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zxcvbn": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz", + "integrity": "sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ==" } }, "dependencies": { @@ -14244,6 +14250,11 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true + }, + "zxcvbn": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz", + "integrity": "sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ==" } } } diff --git a/server/package.json b/server/package.json index 52c143a4..2a291ffe 100644 --- a/server/package.json +++ b/server/package.json @@ -56,7 +56,8 @@ "stream-to-array": "^2.3.0", "uuid": "^8.3.2", "validator": "^13.7.0", - "winston": "^3.8.1" + "winston": "^3.8.1", + "zxcvbn": "^4.4.2" }, "devDependencies": { "chai": "^4.3.6",