1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-22 14:49:43 +02:00

fix: Use password strength estimator

Closes #294
This commit is contained in:
Maksim Eltyshev 2022-09-03 22:47:06 +05:00
parent 543a992d98
commit 3df07c10fa
14 changed files with 134 additions and 65 deletions

View file

@ -44,7 +44,8 @@
"semantic-ui-react": "^2.1.3", "semantic-ui-react": "^2.1.3",
"socket.io-client": "^2.3.1", "socket.io-client": "^2.3.1",
"validator": "^13.7.0", "validator": "^13.7.0",
"whatwg-fetch": "^3.6.2" "whatwg-fetch": "^3.6.2",
"zxcvbn": "^4.4.2"
}, },
"devDependencies": { "devDependencies": {
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.7", "@wojtekmaj/enzyme-adapter-react-17": "^0.6.7",
@ -25310,6 +25311,11 @@
"type": "github", "type": "github",
"url": "https://github.com/sponsors/wooorm" "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": { "dependencies": {
@ -43805,6 +43811,11 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.2.tgz", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.2.tgz",
"integrity": "sha512-JZxotl7SxAJH0j7dN4pxsTV6ZLXoLdGME+PsjkL/DaBrVryK9kTGq06GfKrwcSOqypP+fdXGoCHE36b99fWVoA==" "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=="
} }
} }
} }

View file

@ -101,7 +101,8 @@
"semantic-ui-react": "^2.1.3", "semantic-ui-react": "^2.1.3",
"socket.io-client": "^2.3.1", "socket.io-client": "^2.3.1",
"validator": "^13.7.0", "validator": "^13.7.0",
"whatwg-fetch": "^3.6.2" "whatwg-fetch": "^3.6.2",
"zxcvbn": "^4.4.2"
}, },
"devDependencies": { "devDependencies": {
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.7", "@wojtekmaj/enzyme-adapter-react-17": "^0.6.7",

View file

@ -142,19 +142,16 @@ const UserAddStep = React.memo(
onChange={handleFieldChange} onChange={handleFieldChange}
/> />
<div className={styles.text}>{t('common.password')}</div> <div className={styles.text}>{t('common.password')}</div>
<div className={styles.field}> <Input.Password
<Input.Password withStrengthBar
fluid fluid
ref={passwordField} ref={passwordField}
name="password" name="password"
value={data.password} value={data.password}
readOnly={isSubmitting} readOnly={isSubmitting}
onChange={handleFieldChange} className={styles.field}
/> onChange={handleFieldChange}
<div className={styles.note}> />
{t('common.mustBeAtLeast6CharactersLongAndContainAtLeastOneLetterAndNumber')}
</div>
</div>
<div className={styles.text}>{t('common.name')}</div> <div className={styles.text}>{t('common.name')}</div>
<Input <Input
fluid fluid

View file

@ -113,18 +113,15 @@ const UserPasswordEditStep = React.memo(
)} )}
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
<div className={styles.text}>{t('common.newPassword')}</div> <div className={styles.text}>{t('common.newPassword')}</div>
<div className={styles.field}> <Input.Password
<Input.Password withStrengthBar
fluid fluid
ref={passwordField} ref={passwordField}
name="password" name="password"
value={data.password} value={data.password}
onChange={handleFieldChange} className={styles.field}
/> onChange={handleFieldChange}
<div className={styles.note}> />
{t('common.mustBeAtLeast6CharactersLongAndContainAtLeastOneLetterAndNumber')}
</div>
</div>
{usePasswordConfirmation && ( {usePasswordConfirmation && (
<> <>
<div className={styles.text}>{t('common.currentPassword')}</div> <div className={styles.text}>{t('common.currentPassword')}</div>

View file

@ -1,22 +1,74 @@
import React, { useCallback } from 'react'; import zxcvbn from 'zxcvbn';
import { Icon, Input } from 'semantic-ui-react'; import React, { useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
import { Icon, Input, Progress } from 'semantic-ui-react';
import { useToggle } from '../../../hooks'; import { useToggle } from '../../../hooks';
const InputPassword = React.forwardRef((props, ref) => { import styles from './InputPassword.module.css';
const [isVisible, toggleVisible] = useToggle();
const handleToggleClick = useCallback(() => { const STRENGTH_SCORE_COLORS = ['red', 'orange', 'yellow', 'olive', 'green'];
toggleVisible();
}, [toggleVisible]);
return ( const InputPassword = React.forwardRef(
<Input ({ value, withStrengthBar, minStrengthScore, className, ...props }, ref) => {
{...props} // eslint-disable-line react/jsx-props-no-spreading const [isVisible, toggleVisible] = useToggle();
ref={ref}
type={isVisible ? 'text' : 'password'} const strengthScore = useMemo(() => {
icon={<Icon link name={isVisible ? 'eye' : 'eye slash'} onClick={handleToggleClick} />} if (!withStrengthBar) {
/> return undefined;
); }
});
return zxcvbn(value).score;
}, [value, withStrengthBar]);
const handleToggleClick = useCallback(() => {
toggleVisible();
}, [toggleVisible]);
const inputProps = {
...props,
ref,
type: isVisible ? 'text' : 'password',
icon: <Icon link name={isVisible ? 'eye' : 'eye slash'} onClick={handleToggleClick} />,
};
if (!withStrengthBar) {
return (
<Input
{...inputProps} // eslint-disable-line react/jsx-props-no-spreading
className={className}
/>
);
}
return (
<div className={className}>
<Input
{...inputProps} // eslint-disable-line react/jsx-props-no-spreading
error={!!value && strengthScore < minStrengthScore}
/>
<Progress
value={value ? strengthScore + 1 : 0}
total={5}
color={STRENGTH_SCORE_COLORS[strengthScore]}
size="tiny"
className={styles.strengthBar}
/>
</div>
);
},
);
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); export default React.memo(InputPassword);

View file

@ -0,0 +1,4 @@
.strengthBar {
margin: 4px 0 0 !important;
opacity: 0.5;
}

View file

@ -14453,11 +14453,11 @@ img.ui.bordered.image {
---------------------*/ ---------------------*/
.ui.input.error > input { .ui.input.error > input {
background-color: #fff6f6; background-color: #fff6f6 !important;
border-color: #e0b4b4; border-color: #e0b4b4 !important;
color: #9f3a38; color: #9f3a38 !important;
-webkit-box-shadow: none; -webkit-box-shadow: none !important;
box-shadow: none; box-shadow: none !important;
} }
/* Error Placeholder */ /* Error Placeholder */

View file

@ -105,8 +105,6 @@ export default {
members: 'Members', members: 'Members',
minutes: 'Minutes', minutes: 'Minutes',
moveCard_title: 'Move Card', moveCard_title: 'Move Card',
mustBeAtLeast6CharactersLongAndContainAtLeastOneLetterAndNumber:
'Must be at least 6 characters long and contain at least one letter and number',
name: 'Name', name: 'Name',
newEmail: 'New e-mail', newEmail: 'New e-mail',
newPassword: 'New password', newPassword: 'New password',

View file

@ -100,8 +100,6 @@ export default {
members: 'Участники', members: 'Участники',
minutes: 'Минуты', minutes: 'Минуты',
moveCard: 'Перемещение карточки', moveCard: 'Перемещение карточки',
mustBeAtLeast6CharactersLongAndContainAtLeastOneLetterAndNumber:
'Должен быть не менее 6 символов и содержать хотя бы одну букву и цифру',
name: 'Имя', name: 'Имя',
newEmail: 'Новый e-mail', newEmail: 'Новый e-mail',
newPassword: 'Новый пароль', newPassword: 'Новый пароль',

View file

@ -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])*$/; const USERNAME_REGEX = /^[a-zA-Z0-9]+((_|\.)?[a-zA-Z0-9])*$/;
export const isPassword = (string) => { export const isPassword = (string) => zxcvbn(string).score >= 2; // TODO: move to config
return string.length >= 6 && PASSWORD_REGEX.test(string);
};
export const isUsername = (string) => { export const isUsername = (string) =>
return string.length >= 3 && string.length <= 16 && USERNAME_REGEX.test(string); string.length >= 3 && string.length <= 16 && USERNAME_REGEX.test(string);
};

View file

@ -1,3 +1,5 @@
const zxcvbn = require('zxcvbn');
const Errors = { const Errors = {
EMAIL_ALREADY_IN_USE: { EMAIL_ALREADY_IN_USE: {
emailAlreadyInUse: 'Email already in use', emailAlreadyInUse: 'Email already in use',
@ -16,8 +18,7 @@ module.exports = {
}, },
password: { password: {
type: 'string', type: 'string',
minLength: 6, custom: (value) => zxcvbn(value).score >= 2, // TODO: move to config
regex: /^(?=.*[A-Za-z])(?=.*\d).+$/,
required: true, required: true,
}, },
name: { name: {

View file

@ -1,4 +1,5 @@
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
const zxcvbn = require('zxcvbn');
const Errors = { const Errors = {
USER_NOT_FOUND: { USER_NOT_FOUND: {
@ -18,8 +19,7 @@ module.exports = {
}, },
password: { password: {
type: 'string', type: 'string',
minLength: 6, custom: (value) => zxcvbn(value).score >= 2, // TODO: move to config
regex: /^(?=.*[A-Za-z])(?=.*\d).+$/,
required: true, required: true,
}, },
currentPassword: { currentPassword: {

View file

@ -24,7 +24,8 @@
"stream-to-array": "^2.3.0", "stream-to-array": "^2.3.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"validator": "^13.7.0", "validator": "^13.7.0",
"winston": "^3.8.1" "winston": "^3.8.1",
"zxcvbn": "^4.4.2"
}, },
"devDependencies": { "devDependencies": {
"chai": "^4.3.6", "chai": "^4.3.6",
@ -7984,6 +7985,11 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "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": { "dependencies": {
@ -14244,6 +14250,11 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true "dev": true
},
"zxcvbn": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz",
"integrity": "sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ=="
} }
} }
} }

View file

@ -56,7 +56,8 @@
"stream-to-array": "^2.3.0", "stream-to-array": "^2.3.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"validator": "^13.7.0", "validator": "^13.7.0",
"winston": "^3.8.1" "winston": "^3.8.1",
"zxcvbn": "^4.4.2"
}, },
"devDependencies": { "devDependencies": {
"chai": "^4.3.6", "chai": "^4.3.6",