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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user