From cfa4a4a66c4c670f82b16ef4663b5fcb8978e9c0 Mon Sep 17 00:00:00 2001 From: Maxim Chervonny Date: Mon, 21 Feb 2022 18:07:26 +0300 Subject: [PATCH] Password strength (#2682) --- .../app/containers/Auth/PasswordStrength.tsx | 96 +++++++++++++++++++ catalog/app/containers/Auth/SignUp.js | 50 +++++++++- catalog/package-lock.json | 26 ++++- catalog/package.json | 4 +- 4 files changed, 171 insertions(+), 5 deletions(-) create mode 100644 catalog/app/containers/Auth/PasswordStrength.tsx diff --git a/catalog/app/containers/Auth/PasswordStrength.tsx b/catalog/app/containers/Auth/PasswordStrength.tsx new file mode 100644 index 00000000000..8533eb04bad --- /dev/null +++ b/catalog/app/containers/Auth/PasswordStrength.tsx @@ -0,0 +1,96 @@ +import cx from 'classnames' +import * as React from 'react' +import zxcvbn from 'zxcvbn' +import { fade } from '@material-ui/core/styles/colorManipulator' +import * as M from '@material-ui/core' + +const useStyles = M.makeStyles((t) => ({ + root: { + backgroundColor: t.palette.divider, + height: t.spacing(0.5), + position: 'relative', + transition: '.3s ease background-color', + '&:after': { + bottom: 0, + content: '""', + left: 0, + position: 'absolute', + top: 0, + transition: '.3s ease background-color, .3s ease width', + }, + '&$tooGuessable': { + backgroundColor: fade(t.palette.error.dark, 0.3), + }, + '&$tooGuessable:after': { + backgroundColor: t.palette.error.dark, + width: '10%', + }, + '&$veryGuessable': { + backgroundColor: fade(t.palette.error.main, 0.3), + }, + '&$veryGuessable:after': { + backgroundColor: t.palette.error.main, + width: '33%', + }, + '&$somewhatGuessable': { + backgroundColor: fade(t.palette.warning.dark, 0.3), + }, + '&$somewhatGuessable:after': { + backgroundColor: t.palette.warning.dark, + width: '55%', + }, + '&$safelyUnguessable': { + backgroundColor: fade(t.palette.success.light, 0.3), + }, + '&$safelyUnguessable:after': { + backgroundColor: t.palette.success.light, + width: '78%', + }, + '&$veryUnguessable': { + backgroundColor: fade(t.palette.success.dark, 0.3), + }, + '&$veryUnguessable:after': { + backgroundColor: t.palette.success.dark, + width: '100%', + }, + }, + tooGuessable: {}, + veryGuessable: {}, + somewhatGuessable: {}, + safelyUnguessable: {}, + veryUnguessable: {}, +})) + +type PasswordStrength = zxcvbn.ZXCVBNResult | null + +export function useStrength(value: string): PasswordStrength { + return React.useMemo(() => { + if (!value) return null + return zxcvbn(value) + }, [value]) +} + +interface IndicatorProps { + strength: PasswordStrength +} + +type ScoreState = + | 'tooGuessable' + | 'veryGuessable' + | 'somewhatGuessable' + | 'safelyUnguessable' + | 'veryUnguessable' + +const ScoreMap: ScoreState[] = [ + 'tooGuessable', + 'veryGuessable', + 'somewhatGuessable', + 'safelyUnguessable', + 'veryUnguessable', +] + +export function Indicator({ strength }: IndicatorProps) { + const classes = useStyles() + const stateClassName = strength ? classes[ScoreMap[strength.score]] : '' + return
+} diff --git a/catalog/app/containers/Auth/SignUp.js b/catalog/app/containers/Auth/SignUp.js index 015f99a0d39..efdf5dbe0a3 100644 --- a/catalog/app/containers/Auth/SignUp.js +++ b/catalog/app/containers/Auth/SignUp.js @@ -15,6 +15,7 @@ import parseSearch from 'utils/parseSearch' import useMutex from 'utils/useMutex' import validate, * as validators from 'utils/validators' +import * as PasswordStrength from './PasswordStrength' import * as Layout from './Layout' import SSOAzure from './SSOAzure' import SSOGoogle from './SSOGoogle' @@ -28,6 +29,51 @@ const Container = Layout.mkLayout('Complete sign-up') const MUTEX_ID = 'password' +const useWeakPasswordIconStyles = M.makeStyles((t) => ({ + icon: { + color: t.palette.warning.dark, + }, +})) + +function WeakPasswordIcon() { + const classes = useWeakPasswordIconStyles() + return ( + + + error_outline + + + ) +} + +function PasswordField({ input, ...rest }) { + const { value } = input + const strength = PasswordStrength.useStrength(value) + const isWeak = strength?.score <= 2 + const helperText = strength?.feedback.suggestions.length + ? `Hint: ${strength?.feedback.suggestions.join(' ')}` + : '' + return ( + <> + + + + ), + }} + helperText={helperText} + type="password" + floatingLabelText="Password" + {...input} + {...rest} + /> + + + ) +} + function PasswordSignUp({ mutex, next, onSuccess }) { const sentry = Sentry.use() const dispatch = redux.useDispatch() @@ -143,12 +189,10 @@ function PasswordSignUp({ mutex, next, onSuccess }) { }} />