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",