Skip to content

Commit

Permalink
test 2
Browse files Browse the repository at this point in the history
  • Loading branch information
OKendigelyan committed Feb 20, 2025
1 parent 7dd3dd3 commit 8ac2b67
Showing 1 changed file with 76 additions and 148 deletions.
224 changes: 76 additions & 148 deletions packages/components/src/hooks/usePasswordValidation.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,22 @@
import { Box, Collapse, Flex, List, ListItem, Text } from "@chakra-ui/react";
import { CheckIcon, CloseIcon } from "@chakra-ui/icons";
import { Flex, List, ListIcon, ListItem, Text } from "@chakra-ui/react";
import { useEffect, useState } from "react";
import { useFormContext } from "react-hook-form";
import { type ZodIssue, z } from "zod";
import { z } from "zod";
import zxcvbn from "zxcvbn";

export const DEFAULT_MIN_LENGTH = 12;
const DEFAULT_SCORE = 0;
const DEFAULT_COLOR = "gray.100";
const DEFAULT_PASSWORD_FIELD_NAME = "password";
const PASSWORD_REQUIREMENTS_COUNT = 4;

// Types
type ValidationPath = "minLength" | "uppercase" | "number" | "special" | "simple";
type ValidationPath = "minLength" | "uppercase" | "number" | "special" | "simplicity";

type Requirement = {
message: string;
path: ValidationPath;
passed: boolean;
};

type PasswordStrengthBarProps = {
score: number;
color: string;
errors: ZodIssue[];
hasRequiredError: boolean;
requirements: Requirement[];
};

type UsePasswordValidationProps = {
Expand All @@ -31,143 +25,89 @@ type UsePasswordValidationProps = {
minLength?: number;
};

const REQUIREMENTS: Requirement[] = [
const getPasswordSchema = (minLength: number) =>
z
.string()
.refine(value => value.length >= minLength, {
path: ["minLength"],
})
.refine(value => /[A-Z]/.test(value), {
path: ["uppercase"],
})
.refine(value => /\d/.test(value), {
path: ["number"],
})
.refine(value => /[!@#$%^&*(),.?":{}|<>]/.test(value), {
path: ["special"],
})
.refine(
value => {
const score = zxcvbn(value).score;

return score > 3;
},
{
path: ["simplicity"],
}
);

const PasswordStrengthBar = ({ requirements }: PasswordStrengthBarProps) => (
<Flex flexDirection="column" gap="8px" marginTop="12px">
<List>
{requirements.map(({ message, path, passed }) => (
<ListItem
key={path}
alignItems="center"
display="flex"
data-testid={`${path}-${passed ? "passed" : "failed"}`}
>
<ListIcon
as={passed ? CheckIcon : CloseIcon}
boxSize="12px"
color={passed ? "green.500" : "red.300"}
/>
<Text color={passed ? "green.500" : "red.300"} fontSize="sm">
{message}
</Text>
</ListItem>
))}
</List>
</Flex>
);

const DEFAULT_REQUIREMENTS: Requirement[] = [
{
message: `Password must be at least ${DEFAULT_MIN_LENGTH} characters long`,
path: "minLength",
passed: false,
},
{
message: "Password must contain at least one uppercase letter",
path: "uppercase",
passed: false,
},
{
message: "Password must contain at least one number",
path: "number",
passed: false,
},
{
message: "Password must contain at least one special character",
path: "special",
passed: false,
},
{
message: "Avoid common passwords and simple patterns",
path: "simple",
message: "Avoid common passwords, simple patterns and repeated characters",
path: "simplicity",
passed: false,
},
];

const RoundStatusDot = ({ background }: { background: string }) => (
<Box
display="inline-block"
width="8px"
height="8px"
marginRight="5px"
background={background}
borderRadius="100%"
/>
);

const getPasswordSchema = (minLength: number) =>
z
.string()
.refine(value => value.length >= minLength, {
message: `Password must be at least ${minLength} characters long`,
path: ["minLength"],
})
.refine(value => /[A-Z]/.test(value), {
message: "Password must contain at least one uppercase letter",
path: ["uppercase"],
})
.refine(value => /\d/.test(value), {
message: "Password must contain at least one number",
path: ["number"],
})
.refine(value => /[!@#$%^&*(),.?":{}|<>]/.test(value), {
message: "Password must contain at least one special character",
path: ["special"],
});

const PasswordStrengthBar = ({
score,
color,
errors,
hasRequiredError,
}: PasswordStrengthBarProps) => {
console.log(errors);

const isPasswordStrong = !errors.length && score === 4;
const shouldShowRequirements = !!errors.length && !hasRequiredError;

const checkRequirement = (path: ValidationPath) =>
!errors.some(error => error.path.includes(path));

const colors = [color, "red.500", "yellow.500", "green.500"];

const getSectionColor = (index: number) => {
switch (score) {
case 1:
case 2:
return index === 0 ? colors[1] : colors[0];
case 3:
return index <= 1 ? colors[2] : colors[0];
case 4:
return colors[3];
default:
return colors[0];
}
};

return (
<Flex flexDirection="column" gap="8px" marginTop="12px">
<Flex gap="4px" height="6px">
{Array.from({ length: 3 }).map((_, index) => (
<Box
key={index}
flex="1"
background={getSectionColor(index)}
borderRadius="8px"
transition="background-color 0.2s ease"
/>
))}
</Flex>

{isPasswordStrong && (
<Text lineHeight="normal" data-testid="password-strength-text" size="sm">
Your password is strong
</Text>
)}

<Collapse animateOpacity in={shouldShowRequirements}>
<List>
{REQUIREMENTS.map(({ message, path }) => (
<ListItem
key={path}
alignItems="center"
display="flex"
data-testid={`${path}-${checkRequirement(path) ? "passed" : "failed"}`}
>
<RoundStatusDot background={checkRequirement(path) ? "green.500" : "red.300"} />
<Text color={checkRequirement(path) ? "green.500" : "red.300"} fontSize="sm">
{message}
</Text>
</ListItem>
))}
</List>
</Collapse>
</Flex>
);
};

export const usePasswordValidation = ({
color = DEFAULT_COLOR,
inputName = DEFAULT_PASSWORD_FIELD_NAME,
minLength = DEFAULT_MIN_LENGTH,
inputName = "password",
}: UsePasswordValidationProps = {}) => {
const [passwordScore, setPasswordScore] = useState(DEFAULT_SCORE);
const [passwordErrors, setPasswordErrors] = useState<ZodIssue[]>([
{
message: "Avoid common passwords and simple patterns",
path: ["simple"],
} as ZodIssue,
]);
const [requirements, setRequirements] = useState<Requirement[]>(DEFAULT_REQUIREMENTS);

const {
formState: { errors, isDirty },
Expand All @@ -178,45 +118,33 @@ export const usePasswordValidation = ({

useEffect(() => {
if (hasRequiredError || !isDirty) {
setPasswordScore(DEFAULT_SCORE);
setRequirements(DEFAULT_REQUIREMENTS);
}
}, [isDirty, hasRequiredError]);

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

try {
getPasswordSchema(minLength).parse(value);
setPasswordErrors([]);
setRequirements(requirements.map(requirement => ({ ...requirement, passed: true })));
} catch (e) {
if (e instanceof z.ZodError) {
schemaErrors = e.errors.length;
setPasswordErrors(e.errors);
const errorPaths = new Set(e.errors.map(error => error.path[0]));
setRequirements(prev =>
prev.map(requirement => ({
...requirement,
passed: !errorPaths.has(requirement.path),
}))
);

return false;
}
}

const requirementsMeetingPercentage = (PASSWORD_REQUIREMENTS_COUNT - schemaErrors) / 4;
setPasswordScore(Math.ceil(result.score * requirementsMeetingPercentage));

if (result.score < 4) {
return false;
}

return true;
};

return {
validatePasswordStrength,
PasswordStrengthBar: (
<PasswordStrengthBar
color={color}
errors={passwordErrors}
hasRequiredError={hasRequiredError}
score={passwordScore}
/>
),
PasswordStrengthBar: <PasswordStrengthBar requirements={requirements} />,
};
};

0 comments on commit 8ac2b67

Please sign in to comment.