mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
feat(accounts): Implement password strength validation and live feedback
This commit is contained in:
@@ -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"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user