Skip to content

Commit

Permalink
Add password requirements check
Browse files Browse the repository at this point in the history
  • Loading branch information
OKendigelyan committed Oct 3, 2024
1 parent 4169de6 commit 234f586
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ const request: SignPayloadRequestOutput = {
};

const account = mockMnemonicAccount(1);
const password = "Qwerty123123!23vcxz";

let store: UmamiStore;

Expand Down
29 changes: 17 additions & 12 deletions apps/web/src/components/PasswordInput/PasswordInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ export const PasswordInput = <T extends FieldValues, U extends Path<T>>({
validate,
...rest
}: PasswordInputProps<T, U>) => {
const form = useFormContext<T>();
const {
register,
formState: { errors },
} = useFormContext<T>();
} = form;
const [showPassword, setShowPassword] = useState<boolean>(false);
const { validatePasswordStrength, PasswordStrengthBar } = usePasswordValidation({
inputName,
Expand All @@ -69,6 +70,18 @@ export const PasswordInput = <T extends FieldValues, U extends Path<T>>({
}
};

const registerProps = register(inputName, {
required,
minLength:
minLength && required
? {
value: minLength,
message: `Your password must be at least ${minLength} characters long`,
}
: undefined,
validate: handleValidate,
});

return (
<FormControl isInvalid={!!error}>
<FormLabel>{label}</FormLabel>
Expand All @@ -78,17 +91,7 @@ export const PasswordInput = <T extends FieldValues, U extends Path<T>>({
autoComplete="off"
placeholder={placeholder}
type={showPassword ? "text" : "password"}
{...register(inputName, {
required,
minLength:
minLength && required
? {
value: minLength,
message: `Your password must be at least ${minLength} characters long`,
}
: undefined,
validate: handleValidate,
})}
{...registerProps}
{...rest}
/>
<InputRightElement>
Expand Down Expand Up @@ -117,3 +120,5 @@ export const PasswordInput = <T extends FieldValues, U extends Path<T>>({
</FormControl>
);
};

export default PasswordInput;
2 changes: 2 additions & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
]
},
"dependencies": {
"@chakra-ui/icons": "^2.1.1",
"@chakra-ui/react": "^2.8.2",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
Expand All @@ -83,6 +84,7 @@
"react-dom": "^18.3.1",
"react-hook-form": "^7.52.2",
"react-remove-scroll": "^2.6.0",
"zod": "^3.23.8",
"zxcvbn": "^4.4.2"
}
}
65 changes: 33 additions & 32 deletions packages/components/src/hooks/usePasswordValidation.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Box, Flex, Text } from "@chakra-ui/react";
import { useEffect, useState } from "react";
import { useFormContext } from "react-hook-form";
import { z } from "zod";
import zxcvbn from "zxcvbn";

const DEFAULT_SCORE = 0;
Expand All @@ -9,19 +10,26 @@ const DEFAULT_COLOR = "gray.100";
type PasswordStrengthBarProps = {
score: number;
color: string;
inputName: string;
hasError: boolean;
};

type UsePasswordValidationProps = {
color?: string;
inputName?: string;
};

const PasswordStrengthBar = ({ score, color, inputName }: PasswordStrengthBarProps) => {
const form = useFormContext();

const PASSWORD_REQUIREMENTS_COUNT = 4;
const passwordSchema = z
.string()
.min(12, { message: "Password must be at least 12 characters long" })
.regex(/[A-Z]/, { message: "Password must contain at least one uppercase letter" })
.regex(/\d/, { message: "Password must contain at least one number" })
.regex(/[!@#$%^&*(),.?":{}|<>]/, {
message: "Password must contain at least one special character",
});

const PasswordStrengthBar = ({ score, color, hasError }: PasswordStrengthBarProps) => {
const colors = [color, "red.500", "yellow.500", "green.500"];
const passwordError = form.formState.errors[inputName];

const getColor = (index: number) => {
switch (score) {
Expand All @@ -37,29 +45,10 @@ const PasswordStrengthBar = ({ score, color, inputName }: PasswordStrengthBarPro
}
};

const getText = () => {
switch (score) {
case 1:
case 2:
return "Weak";
case 3:
return "Medium";
case 4:
return "Strong";
default:
return;
}
};

const text = getText();
const showPasswordStrengthText = !hasError && score === 4;

return (
<Flex
flexDirection="column"
gap="8px"
marginTop="12px"
data-testid={`password-strength-${text}`}
>
<Flex flexDirection="column" gap="8px" marginTop="12px">
<Flex gap="4px" height="6px">
{Array.from({ length: 3 }).map((_, index) => (
<Box
Expand All @@ -71,9 +60,9 @@ const PasswordStrengthBar = ({ score, color, inputName }: PasswordStrengthBarPro
/>
))}
</Flex>
{!passwordError && text && (
{showPasswordStrengthText && (
<Text lineHeight="normal" data-testid="password-strength-text" size="sm">
Your password is {text}
Your password is strong
</Text>
)}
</Flex>
Expand All @@ -97,19 +86,31 @@ export const usePasswordValidation = ({

const validatePasswordStrength = (value: string) => {
const result = zxcvbn(value);

setPasswordScore(result.score);
let schemaErrors = 0;

try {
passwordSchema.parse(value);
} catch (e) {
if (e instanceof z.ZodError) {
schemaErrors = e.errors.length;
return e.errors[0].message;
}
} finally {
const requirementsMeetingPercentage = (PASSWORD_REQUIREMENTS_COUNT - schemaErrors) / 4;
setPasswordScore(Math.ceil(result.score * requirementsMeetingPercentage));
}

if (result.score < 4) {
return result.feedback.suggestions.at(-1);
return result.feedback.suggestions.at(-1) ?? "Keep on, make the password more complex!";
}

return true;
};

return {
validatePasswordStrength,
PasswordStrengthBar: (
<PasswordStrengthBar color={color} inputName={inputName} score={passwordScore} />
<PasswordStrengthBar color={color} hasError={!!passwordError} score={passwordScore} />
),
};
};
13 changes: 11 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 234f586

Please sign in to comment.