From 25d35197c1b00c447c5d29524487a88a47b0431c Mon Sep 17 00:00:00 2001 From: israel Date: Fri, 13 Mar 2026 17:48:03 +0100 Subject: [PATCH 1/3] fix: enforce consistent password validation across all flows (#784) Password rules (8+ chars, lowercase, uppercase, number, special char) were only fully enforced on sign-up. Add Member, Edit Member, Change Password, and Set Password forms had weaker or no validation. - Create shared password schema (lib/shared/schemas/password.ts) - Add usePasswordValidation hook for consistent UI display - Update all 5 frontend forms to use shared schema + ValidationCheckList - Add backend validation in set_member_password and update_user_password - Add missing specialChar translation key for changePassword requirements --- .../account/components/account-form.tsx | 142 ++++++++++----- .../components/member-add-dialog.tsx | 51 ++---- .../components/member-edit-dialog.tsx | 22 ++- .../app/hooks/use-password-validation.ts | 38 ++++ .../platform/app/routes/_auth/sign-up.tsx | 39 ++--- .../convex/users/set_member_password.ts | 7 + .../convex/users/update_user_password.ts | 7 + .../lib/shared/schemas/password.test.ts | 163 ++++++++++++++++++ .../platform/lib/shared/schemas/password.ts | 74 ++++++++ services/platform/messages/en.json | 3 +- 10 files changed, 433 insertions(+), 113 deletions(-) create mode 100644 services/platform/app/hooks/use-password-validation.ts create mode 100644 services/platform/lib/shared/schemas/password.test.ts create mode 100644 services/platform/lib/shared/schemas/password.ts diff --git a/services/platform/app/features/settings/account/components/account-form.tsx b/services/platform/app/features/settings/account/components/account-form.tsx index d4a6d20e33..b9b70a3dc4 100644 --- a/services/platform/app/features/settings/account/components/account-form.tsx +++ b/services/platform/app/features/settings/account/components/account-form.tsx @@ -1,15 +1,22 @@ 'use client'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useMemo } from 'react'; import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { ValidationCheckList } from '@/app/components/ui/feedback/validation-check-item'; import { Form } from '@/app/components/ui/forms/form'; +import { FormSection } from '@/app/components/ui/forms/form-section'; import { Input } from '@/app/components/ui/forms/input'; import { NarrowContainer } from '@/app/components/ui/layout/layout'; import { PageSection } from '@/app/components/ui/layout/page-section'; import { Button } from '@/app/components/ui/primitives/button'; import { useHasCredentialAccount } from '@/app/features/auth/hooks/queries'; +import { usePasswordValidation } from '@/app/hooks/use-password-validation'; import { useToast } from '@/app/hooks/use-toast'; import { useT } from '@/lib/i18n/client'; +import { createPasswordSchema } from '@/lib/shared/schemas/password'; import { useUpdatePassword } from '../hooks/mutations'; @@ -91,6 +98,31 @@ function ChangePasswordForm({ tCommon: ReturnType['t']; tToast: ReturnType['t']; }) { + const changePasswordSchema = useMemo( + () => + z + .object({ + currentPassword: z + .string() + .min(1, tAuth('changePassword.validation.currentRequired')), + newPassword: createPasswordSchema({ + minLength: tAuth('validation.passwordMinLength'), + lowercase: tAuth('validation.passwordLowercase'), + uppercase: tAuth('validation.passwordUppercase'), + number: tAuth('validation.passwordNumber'), + specialChar: tAuth('validation.passwordSpecial'), + }), + confirmPassword: z + .string() + .min(1, tAuth('changePassword.validation.confirmRequired')), + }) + .refine((data) => data.newPassword === data.confirmPassword, { + message: tAuth('changePassword.validation.mismatch'), + path: ['confirmPassword'], + }), + [tAuth], + ); + const { register, handleSubmit, @@ -98,6 +130,8 @@ function ChangePasswordForm({ reset, watch, } = useForm({ + resolver: zodResolver(changePasswordSchema), + mode: 'onChange', defaultValues: { currentPassword: '', newPassword: '', @@ -106,6 +140,7 @@ function ChangePasswordForm({ }); const newPassword = watch('newPassword'); + const passwordValidationItems = usePasswordValidation(newPassword); const onSubmit = async (data: ChangePasswordFormData) => { try { @@ -137,26 +172,26 @@ function ChangePasswordForm({ placeholder={tAuth('changePassword.placeholder.current')} disabled={isSubmitting} errorMessage={errors.currentPassword?.message} - {...register('currentPassword', { - required: tAuth('changePassword.validation.currentRequired'), - })} + {...register('currentPassword')} /> - + + + {newPassword && ( + + )} + - value === newPassword || - tAuth('changePassword.validation.mismatch'), - })} + {...register('confirmPassword')} />