From 4262de5424bb8ad24331ecea959ce64ffa95aee4 Mon Sep 17 00:00:00 2001 From: ghost2023 Date: Thu, 28 May 2026 15:40:11 +0300 Subject: [PATCH] feat(accounts): Implement password strength validation and live feedback --- .../src/pages/accounts/SetPasswordPage.tsx | 56 +++++++++++++++++-- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/apps/edr-freight-web/portal/src/pages/accounts/SetPasswordPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/SetPasswordPage.tsx index c8f2a7184..44ae45afa 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/SetPasswordPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/SetPasswordPage.tsx @@ -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({ 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() { + {password && ( +
    + {requirements.map((req) => ( +
  • + {req.met ? ( + + ) : ( + + )} + {req.label} +
  • + ))} +
+ )} + Confirm Password
@@ -149,7 +195,7 @@ export default function SetPasswordPage() {