feat(accounts): Implement password strength validation and live feedback

This commit is contained in:
ghost2023
2026-05-28 15:40:11 +03:00
parent edfd885b4c
commit 4262de5424

View File

@@ -1,11 +1,12 @@
import { useState } from "react";
import { useState, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { ArrowRight, Eye, EyeOff, LockKeyhole, Loader2 } from "lucide-react";
import { ArrowRight, Check, Eye, EyeOff, LockKeyhole, Loader2, X } from "lucide-react";
import useAuth from "@/hooks/useAuth";
import AuthLayout from "@/components/auth/AuthLayout";
import { cn } from "@/lib/utils";
import {
Button,
Input,
@@ -15,10 +16,24 @@ import {
FieldGroup,
} from "@edr/ui-common";
const passwordRequirements = [
{ label: "At least 8 characters", test: (v: string) => v.length >= 8 },
{ label: "One uppercase letter", test: (v: string) => /[A-Z]/.test(v) },
{ label: "One lowercase letter", test: (v: string) => /[a-z]/.test(v) },
{ label: "One number", test: (v: string) => /\d/.test(v) },
{ label: "One special character", test: (v: string) => /[^A-Za-z0-9]/.test(v) },
] as const;
const passwordSchema = z
.object({
password: z.string().min(8, "Password must be at least 8 characters"),
confirmPassword: z.string().min(8, "Confirm password is required"),
password: z
.string()
.min(8, "Password must be at least 8 characters")
.regex(/[A-Z]/, "Password must include an uppercase letter")
.regex(/[a-z]/, "Password must include a lowercase letter")
.regex(/\d/, "Password must include a number")
.regex(/[^A-Za-z0-9]/, "Password must include a special character"),
confirmPassword: z.string().min(1, "Please confirm your password"),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords do not match",
@@ -38,12 +53,22 @@ export default function SetPasswordPage() {
const {
register,
handleSubmit,
watch,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(passwordSchema),
defaultValues: { password: "", confirmPassword: "" },
});
const password = watch("password");
const requirements = useMemo(
() => passwordRequirements.map((r) => ({ ...r, met: r.test(password || "") })),
[password],
);
const allMet = requirements.every((r) => r.met);
const onSubmit = async (data: FormData) => {
setError(null);
setLoading(true);
@@ -122,6 +147,27 @@ export default function SetPasswordPage() {
<FieldError errors={[errors.password]} />
</Field>
{password && (
<ul className="space-y-1.5">
{requirements.map((req) => (
<li
key={req.label}
className={cn(
"flex items-center gap-2 text-sm",
req.met ? "text-emerald-600" : "text-muted-foreground",
)}
>
{req.met ? (
<Check className="size-4 shrink-0 text-emerald-500" />
) : (
<X className="size-4 shrink-0 text-muted-foreground/50" />
)}
{req.label}
</li>
))}
</ul>
)}
<Field data-invalid={Boolean(errors.confirmPassword)}>
<FieldLabel>Confirm Password</FieldLabel>
<div className="relative">
@@ -149,7 +195,7 @@ export default function SetPasswordPage() {
<Button
type="submit"
disabled={loading}
disabled={loading || !allMet}
size="lg"
className="w-full"
>