From b6004241e7cb72504e8d51ef127168318f92e076 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sat, 7 Feb 2026 20:54:17 +0000 Subject: [PATCH] validation --- client/src/Hooks/usePasswordForm.ts | 13 +++++++ client/src/Pages/Account/TabPassword.tsx | 46 ++++++++++++++++++++++-- client/src/Pages/Account/TabProfile.tsx | 2 +- client/src/Validation/password.ts | 25 +++++++++++++ client/src/Validation/patterns.ts | 4 +++ client/src/Validation/profile.ts | 7 ++-- client/src/Validation/register.ts | 4 +-- client/src/locales/en.json | 22 ++++++++++++ 8 files changed, 113 insertions(+), 10 deletions(-) create mode 100644 client/src/Hooks/usePasswordForm.ts create mode 100644 client/src/Validation/password.ts create mode 100644 client/src/Validation/patterns.ts diff --git a/client/src/Hooks/usePasswordForm.ts b/client/src/Hooks/usePasswordForm.ts new file mode 100644 index 000000000..d38f73abf --- /dev/null +++ b/client/src/Hooks/usePasswordForm.ts @@ -0,0 +1,13 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { passwordSchema, type PasswordFormData } from "@/Validation/password"; + +export const usePasswordForm = () => { + return { + resolver: zodResolver(passwordSchema), + defaults: { + currentPassword: "", + newPassword: "", + confirm: "", + } as PasswordFormData, + }; +}; diff --git a/client/src/Pages/Account/TabPassword.tsx b/client/src/Pages/Account/TabPassword.tsx index 0acecd35a..cd07410ac 100644 --- a/client/src/Pages/Account/TabPassword.tsx +++ b/client/src/Pages/Account/TabPassword.tsx @@ -1,5 +1,47 @@ -import { Box } from "@mui/material"; +import { Stack } from "@mui/material"; +import { ConfigBox } from "@/Components/v2/design-elements"; +import { TextField } from "@/Components/v2/inputs"; + +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material"; export const TabPassword = () => { - return {/* TODO: Implement TabPassword */}; + const { t } = useTranslation(); + const theme = useTheme(); + + return ( + + + } + /> + + + + + } + /> + + ); }; diff --git a/client/src/Pages/Account/TabProfile.tsx b/client/src/Pages/Account/TabProfile.tsx index 6b05e0082..8ea8a50a0 100644 --- a/client/src/Pages/Account/TabProfile.tsx +++ b/client/src/Pages/Account/TabProfile.tsx @@ -1,4 +1,4 @@ -import { Stack, Box } from "@mui/material"; +import { Stack } from "@mui/material"; import { ConfigBox } from "@/Components/v2/design-elements"; import { TextField, Button } from "@/Components/v2/inputs"; import { ImageUpload } from "@/Components/v2/inputs"; diff --git a/client/src/Validation/password.ts b/client/src/Validation/password.ts new file mode 100644 index 000000000..c287fcb25 --- /dev/null +++ b/client/src/Validation/password.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; +import { specialCharPattern } from "@/Validation/patterns"; + +export const passwordSchema = z + .object({ + currentPassword: z.string().min(1, "Current password is required"), + newPassword: z + .string() + .min(1, "New password is required") + .min(8, "Password must be at least 8 characters") + .refine((val) => /[A-Z]/.test(val), "Password must contain an uppercase letter") + .refine((val) => /[a-z]/.test(val), "Password must contain a lowercase letter") + .refine((val) => /\d/.test(val), "Password must contain a number") + .refine( + (val) => specialCharPattern.test(val), + "Password must contain a special character" + ), + confirm: z.string().min(1, "Please confirm your new password"), + }) + .refine((data) => data.newPassword === data.confirm, { + message: "Passwords do not match", + path: ["confirm"], + }); + +export type PasswordFormData = z.infer; diff --git a/client/src/Validation/patterns.ts b/client/src/Validation/patterns.ts new file mode 100644 index 000000000..d1e20fa73 --- /dev/null +++ b/client/src/Validation/patterns.ts @@ -0,0 +1,4 @@ +export const specialCharPattern = /[!?@#$%^&*()\-_=+[\]{};:'",.<>~`|\\/]/; + +// Unicode-aware name pattern (allows letters, marks, apostrophes, parentheses, hyphens, periods, spaces) +export const namePattern = /^[\p{L}\p{M}''()\-\. ]+$/u; diff --git a/client/src/Validation/profile.ts b/client/src/Validation/profile.ts index 56301c31f..eca0ddf5c 100644 --- a/client/src/Validation/profile.ts +++ b/client/src/Validation/profile.ts @@ -1,18 +1,17 @@ import { z } from "zod"; - -const NAME_PATTERN = /^[A-Za-z'-]+$/; +import { namePattern } from "@/Validation/patterns"; export const profileSchema = z.object({ firstName: z .string() .min(1, "First name is required") .max(50, "First name must be 50 characters or less") - .regex(NAME_PATTERN, "First name can only contain letters, hyphens, and apostrophes"), + .regex(namePattern, "First name contains invalid characters"), lastName: z .string() .min(1, "Last name is required") .max(50, "Last name must be 50 characters or less") - .regex(NAME_PATTERN, "Last name can only contain letters, hyphens, and apostrophes"), + .regex(namePattern, "Last name contains invalid characters"), profileImage: z.instanceof(File).optional().nullable(), deleteProfileImage: z.boolean().optional(), }); diff --git a/client/src/Validation/register.ts b/client/src/Validation/register.ts index 001dfa56a..d76c5f37e 100644 --- a/client/src/Validation/register.ts +++ b/client/src/Validation/register.ts @@ -1,7 +1,5 @@ import { z } from "zod"; - -const namePattern = /^[\p{L}\p{M}''()\-\. ]+$/u; -const specialCharPattern = /[!?@#$%^&*()\-_=+[\]{};:'",.<>~`|\\/]/; +import { specialCharPattern, namePattern } from "@/Validation/patterns"; export const registerSchema = z .object({ diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 21a8e8e0a..451c59e17 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -523,6 +523,28 @@ "title": "Profile photo", "description": "Upload a profile picture" }, + "currentPassword": { + "title": "Current password", + "description": "Enter your current password to verify your identity", + "option": { + "label": "Current password", + "placeholder": "Enter current password" + } + }, + "newPassword": { + "title": "New password", + "description": "Choose a strong password with at least 8 characters", + "option": { + "newPassword": { + "label": "New password", + "placeholder": "Enter new password" + }, + "confirm": { + "label": "Confirm password", + "placeholder": "Confirm new password" + } + } + }, "deleteAccount": { "title": "Delete account", "description": "This action is permanent and cannot be undone"