Update password strength checking (#2994)

* use zxcvbn password strength checker

* prevent user from proceeding with a weak password + add tips

* create storybook for password strength component

* update storybook

* update password-strength
This commit is contained in:
Fouad
2023-02-13 10:27:04 +00:00
committed by GitHub
parent a4ca94ccef
commit 4652d65874
10 changed files with 137 additions and 75 deletions
+3 -1
View File
@@ -52,7 +52,8 @@
"string-to-color": "^2.2.2",
"use-clipboard-copy": "^0.2.0",
"uuid": "^8.3.2",
"yup": "^0.32.9"
"yup": "^0.32.9",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@babel/core": "^7.15.0",
@@ -76,6 +77,7 @@
"@types/react-dom": "^18.0.10",
"@types/semver": "^7.3.8",
"@types/uuid": "^8.3.4",
"@types/zxcvbn": "^4.4.1",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.13.0",
"babel-loader": "^8.3.0",
@@ -1,52 +1,61 @@
/* eslint-disable no-nested-ternary */
import React, { useEffect, useState } from 'react';
import React from 'react';
import zxcvbn, { ZXCVBNScore } from 'zxcvbn';
import { LockOutlined } from '@mui/icons-material';
import { LinearProgress, Stack, Typography, Box } from '@mui/material';
type TStrength = 'weak' | 'medium' | 'strong' | 'init';
const strong = /^(?=.*[a-z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})/;
const medium = /^(((?=.*[a-z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[0-9])))(?=.{6,})/;
const colorMap = {
init: 'inherit' as 'inherit',
weak: 'error' as 'error',
medium: 'warning' as 'warning',
strong: 'success' as 'success',
4: 'success' as 'success',
3: 'success' as 'success',
2: 'warning' as 'warning',
1: 'error' as 'error',
0: 'error' as 'error',
};
const getText = (strength: TStrength) => {
switch (strength) {
case 'strong':
const getText = (score: ZXCVBNScore) => {
switch (score) {
case 4:
return 'Very strong password';
case 3:
return 'Strong password';
case 'medium':
return 'Medium strength password';
case 'weak':
case 2:
return 'Average password';
case 1:
return 'Weak password';
case 0:
return 'Very weak password';
default:
return 'Password strength';
return '';
}
};
const getTextColor = (strength: TStrength) => {
switch (strength) {
case 'strong':
const getColor = (score: ZXCVBNScore) => {
switch (score) {
case 4:
return 'success.main';
case 'medium':
case 3:
return 'success.main';
case 2:
return 'warning.main';
case 'weak':
case 1:
return 'error.main';
case 0:
return 'error.main';
default:
return 'grey.500';
}
};
const getPasswordStrength = (strength: TStrength) => {
switch (strength) {
case 'strong':
const getPasswordStrength = (score: ZXCVBNScore) => {
switch (score) {
case 4:
return 100;
case 'medium':
case 3:
return 75;
case 2:
return 50;
case 1:
return 25;
default:
return 0;
}
@@ -54,47 +63,32 @@ const getPasswordStrength = (strength: TStrength) => {
export const PasswordStrength = ({
password,
onChange,
withWarnings,
handleIsSafePassword,
}: {
password: string;
onChange: (isStrong: boolean) => void;
withWarnings?: boolean;
handleIsSafePassword: (isSafe: boolean) => void;
}) => {
const [strength, setStrength] = useState<TStrength>('init');
const result = zxcvbn(password);
useEffect(() => {
if (password.length === 0) {
setStrength('init');
return;
}
handleIsSafePassword(result.score > 1);
if (password.match(strong)) {
setStrength('strong');
return;
}
if (password.match(medium)) {
setStrength('medium');
return;
}
setStrength('weak');
}, [password]);
useEffect(() => {
if (strength === 'strong') {
onChange(true);
} else {
onChange(false);
}
}, [strength]);
if (!password.length) return null;
return (
<Stack spacing={0.5}>
<LinearProgress variant="determinate" color={colorMap[strength]} value={getPasswordStrength(strength)} />
<Box display="flex" alignItems="center">
<LockOutlined sx={{ fontSize: 15, color: getTextColor(strength) }} />
<Typography variant="caption" sx={{ ml: 0.5, color: getTextColor(strength) }}>
{getText(strength)}
</Typography>
<LinearProgress variant="determinate" color={colorMap[result.score]} value={getPasswordStrength(result.score)} />
<Box display="flex" alignItems="center" justifyContent="space-between">
<Box display="flex" alignItems="center">
<LockOutlined sx={{ fontSize: 15, color: getColor(result.score) }} />
<Typography variant="caption" sx={{ ml: 0.5, color: getColor(result.score) }}>
{getText(result.score)}
</Typography>
</Box>
{withWarnings && result.feedback.warning && (
<Typography variant="caption">{result.feedback.warning}</Typography>
)}
</Box>
</Stack>
);
@@ -0,0 +1,59 @@
import * as React from 'react';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { Box, Stack, TextField } from '@mui/material';
import { PasswordStrength } from './password-strength';
export default {
title: 'Wallet / Password Strength',
component: PasswordStrength,
} as ComponentMeta<typeof PasswordStrength>;
const Template: ComponentStory<typeof PasswordStrength> = (args) => {
const [password, setPassword] = React.useState(args.password);
return (
<Stack alignContent="center">
<TextField value={password} onChange={(e) => setPassword(e.target.value)} sx={{ mb: 0.5 }} />
{!!password.length && <PasswordStrength {...args} password={password} />}
</Stack>
);
};
export const VeryStrong = Template.bind({});
VeryStrong.args = { password: 'fedgklnrf34£', withWarnings: true, handleIsSafePassword: () => undefined };
export const Strong = Template.bind({});
Strong.args = { password: '"56%abc123?@', withWarnings: true, handleIsSafePassword: () => undefined };
export const Average = Template.bind({});
Average.args = { password: '"abc123?', withWarnings: true, handleIsSafePassword: () => undefined };
export const Weak = Template.bind({});
Weak.args = { password: 'abc123?', withWarnings: true, handleIsSafePassword: () => undefined };
export const VeryWeak = Template.bind({});
VeryWeak.args = {
password: 'abc123',
withWarnings: true,
handleIsSafePassword: () => undefined,
};
export const WithName = Template.bind({});
WithName.args = {
password: 'fred',
withWarnings: true,
handleIsSafePassword: () => undefined,
};
export const WithSequence = Template.bind({});
WithSequence.args = {
password: '121212',
withWarnings: true,
handleIsSafePassword: () => undefined,
};
export const Default = Template.bind({});
Default.args = {
password: 'abc123',
withWarnings: true,
handleIsSafePassword: () => undefined,
};
@@ -9,9 +9,8 @@ import { Subtitle, Title, PasswordStrength } from '../components';
export const ConnectPassword = () => {
const [confirmedPassword, setConfirmedPassword] = useState<string>('');
const [isStrongPassword, setIsStrongPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isSafePassword, setIsSafePassword] = useState(false);
const { mnemonic, password, setPassword, resetState } = useContext(AuthContext);
const navigate = useNavigate();
@@ -49,7 +48,7 @@ export const ConnectPassword = () => {
label="Password"
autoFocus
/>
<PasswordStrength password={password} onChange={(isStrong) => setIsStrongPassword(isStrong)} />
<PasswordStrength password={password} handleIsSafePassword={setIsSafePassword} withWarnings />
</>
<PasswordInput
password={confirmedPassword}
@@ -59,7 +58,7 @@ export const ConnectPassword = () => {
<Button
size="large"
variant="contained"
disabled={password !== confirmedPassword || password.length === 0 || !isStrongPassword || isLoading}
disabled={password !== confirmedPassword || password.length === 0 || isLoading || !isSafePassword}
onClick={storePassword}
>
{isLoading ? <CircularProgress size={25} /> : 'Create password'}
@@ -10,8 +10,8 @@ import { Subtitle, Title, PasswordStrength } from '../components';
export const CreatePassword = () => {
const { password, setPassword, resetState, mnemonic } = useContext(AuthContext);
const [confirmedPassword, setConfirmedPassword] = useState<string>('');
const [isStrongPassword, setIsStrongPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isSafePassword, setIsSafePassword] = useState(false);
const navigate = useNavigate();
@@ -48,7 +48,7 @@ export const CreatePassword = () => {
label="Password"
autoFocus
/>
<PasswordStrength password={password} onChange={(isStrong) => setIsStrongPassword(isStrong)} />
<PasswordStrength password={password} handleIsSafePassword={setIsSafePassword} withWarnings />
</>
<PasswordInput
password={confirmedPassword}
@@ -58,7 +58,7 @@ export const CreatePassword = () => {
<Button
size="large"
variant="contained"
disabled={password !== confirmedPassword || password.length === 0 || !isStrongPassword || isLoading}
disabled={password !== confirmedPassword || password.length === 0 || isLoading || !isSafePassword}
onClick={storePassword}
>
Next
+2 -1
View File
@@ -17,6 +17,7 @@ import { AppContext, urls } from 'src/context/main';
import { isGateway, isMixnode, TBondGatewayArgs, TBondMixNodeArgs, TBondMoreArgs } from 'src/types';
import { BondedGateway } from 'src/components/Bonding/BondedGateway';
import { RedeemRewardsModal } from 'src/components/Bonding/modals/RedeemRewardsModal';
import { Console } from 'src/utils/console';
import { BondingContextProvider, useBondingContext } from '../../context';
import { getMixnodeStakeSaturation } from '../../requests';
@@ -104,7 +105,7 @@ const Bonding = () => {
}
return { isOverSaturated: false, saturationPercentage: undefined };
} catch (e) {
console.error('Error fetching the saturation, error:', e);
Console.error('Error fetching the saturation, error:', e);
return { isOverSaturated: false, saturationPercentage: undefined };
}
};
@@ -7,6 +7,7 @@ import { CalculateArgs, Inputs } from 'src/components/RewardsPlayground/Inputs';
import { TBondedMixnode } from 'src/context';
import { useSnackbar } from 'notistack';
import { LoadingModal } from 'src/components/Modals/LoadingModal';
import { Console } from 'src/utils/console';
import { computeEstimate, computeStakeSaturation, handleCalculatePeriodRewards } from './utils';
export type DefaultInputValues = {
@@ -99,7 +100,7 @@ export const ApyPlayground = ({ bondedNode }: { bondedNode: TBondedMixnode }) =>
setStakeSaturation(computedStakeSaturation);
setResults(estimationResult);
} catch (e) {
console.log(e);
Console.log(e);
}
};
@@ -56,7 +56,7 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode
const { nextInterval } = await getIntervalAsDate();
setIntervalTime(nextInterval);
} catch {
console.log('cant retrieve next interval');
Console.log('cant retrieve next interval');
}
};
+1 -5
View File
@@ -59,9 +59,5 @@ export const computeMixnodeRewardEstimation = async (args: {
totalDelegation: number;
profitMarginPercent: string;
intervalOperatingCost: { denom: 'unym'; amount: string };
}) => {
console.log(args);
return invokeWrapper<RewardEstimationResponse>('compute_mixnode_reward_estimation', args);
};
}) => invokeWrapper<RewardEstimationResponse>('compute_mixnode_reward_estimation', args);
export const getMixnodeUptime = async (mixId: number) => invokeWrapper<number>('get_mixnode_uptime', { mixId });
+10
View File
@@ -5338,6 +5338,11 @@
dependencies:
"@types/yargs-parser" "*"
"@types/zxcvbn@^4.4.1":
version "4.4.1"
resolved "https://registry.yarnpkg.com/@types/zxcvbn/-/zxcvbn-4.4.1.tgz#46e42cbdcee681b22181478feaf4af2bc4c1abd2"
integrity sha512-3NoqvZC2W5gAC5DZbTpCeJ251vGQmgcWIHQJGq2J240HY6ErQ9aWKkwfoKJlHLx+A83WPNTZ9+3cd2ILxbvr1w==
"@typescript-eslint/eslint-plugin@^5.13.0", "@typescript-eslint/eslint-plugin@^5.7.0":
version "5.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.49.0.tgz#d0b4556f0792194bf0c2fb297897efa321492389"
@@ -19750,3 +19755,8 @@ zwitch@^2.0.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7"
integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==
zxcvbn@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/zxcvbn/-/zxcvbn-4.4.2.tgz#28ec17cf09743edcab056ddd8b1b06262cc73c30"
integrity sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ==