mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 09:58:12 +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 { useNavigate } from "react-router-dom";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "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 useAuth from "@/hooks/useAuth";
|
||||||
import AuthLayout from "@/components/auth/AuthLayout";
|
import AuthLayout from "@/components/auth/AuthLayout";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Input,
|
Input,
|
||||||
@@ -15,10 +16,24 @@ import {
|
|||||||
FieldGroup,
|
FieldGroup,
|
||||||
} from "@edr/ui-common";
|
} 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
|
const passwordSchema = z
|
||||||
.object({
|
.object({
|
||||||
password: z.string().min(8, "Password must be at least 8 characters"),
|
password: z
|
||||||
confirmPassword: z.string().min(8, "Confirm password is required"),
|
.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, {
|
.refine((data) => data.password === data.confirmPassword, {
|
||||||
message: "Passwords do not match",
|
message: "Passwords do not match",
|
||||||
@@ -38,12 +53,22 @@ export default function SetPasswordPage() {
|
|||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
watch,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: zodResolver(passwordSchema),
|
resolver: zodResolver(passwordSchema),
|
||||||
defaultValues: { password: "", confirmPassword: "" },
|
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) => {
|
const onSubmit = async (data: FormData) => {
|
||||||
setError(null);
|
setError(null);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -122,6 +147,27 @@ export default function SetPasswordPage() {
|
|||||||
<FieldError errors={[errors.password]} />
|
<FieldError errors={[errors.password]} />
|
||||||
</Field>
|
</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)}>
|
<Field data-invalid={Boolean(errors.confirmPassword)}>
|
||||||
<FieldLabel>Confirm Password</FieldLabel>
|
<FieldLabel>Confirm Password</FieldLabel>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -149,7 +195,7 @@ export default function SetPasswordPage() {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading || !allMet}
|
||||||
size="lg"
|
size="lg"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user