From 040ee4b1f7b77d4d36ad8bd38cd953437ff827b7 Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Sun, 3 May 2026 20:02:14 +0330 Subject: [PATCH] feat(auth): enforce password policy in reset and change flows --- src/locales/en.ts | 13 +++++--- src/locales/fa.ts | 13 +++++--- src/pages/Profile.tsx | 32 +++++++++++++------ src/pages/auth/ForgotPasswordPasswordPage.tsx | 8 ++++- src/pages/auth/SignupPasswordPage.tsx | 8 ++++- src/pages/auth/utils.ts | 18 +++++++++++ 6 files changed, 70 insertions(+), 22 deletions(-) diff --git a/src/locales/en.ts b/src/locales/en.ts index 19b81ab..c8eba9d 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -56,11 +56,14 @@ export const en = { continueToResetPassword: "Continue to reset password", resetPasswordTitle: "Choose a new password", resetPasswordDescription: "Set a new password for your account and confirm it.", - resetPasswordCta: "Reset password", - newPasswordPlaceholder: "New password", - confirmPasswordPlaceholder: "Confirm password", - passwordMismatch: "The password confirmation does not match.", - enterPassword: "Enter your password", + resetPasswordCta: "Reset password", + newPasswordPlaceholder: "New password", + confirmPasswordPlaceholder: "Confirm password", + passwordMismatch: "The password confirmation does not match.", + passwordRequirements: + "Password must be at least 8 characters and include at least one lowercase letter, one uppercase letter, one digit, and one symbol.", + passwordReuse: "New password must be different from your previous password.", + enterPassword: "Enter your password", verifyNumber: "Verify your number", enterMobileDesc: "Enter your mobile number to continue", signInDesc: "Sign in using your account password", diff --git a/src/locales/fa.ts b/src/locales/fa.ts index dcd92ec..a04521b 100644 --- a/src/locales/fa.ts +++ b/src/locales/fa.ts @@ -55,11 +55,14 @@ export const fa = { continueToResetPassword: "ادامه برای تعیین رمز جدید", resetPasswordTitle: "انتخاب رمز عبور جدید", resetPasswordDescription: "رمز عبور جدید خود را وارد کنید و آن را تایید کنید.", - resetPasswordCta: "تغییر رمز عبور", - newPasswordPlaceholder: "رمز عبور جدید", - confirmPasswordPlaceholder: "تکرار رمز عبور", - passwordMismatch: "رمز عبور و تکرار آن یکسان نیستند.", - welcome: (title: string = "Qlockifiy") => `به ${title} خوش آمدید`, + resetPasswordCta: "تغییر رمز عبور", + newPasswordPlaceholder: "رمز عبور جدید", + confirmPasswordPlaceholder: "تکرار رمز عبور", + passwordMismatch: "رمز عبور و تکرار آن یکسان نیستند.", + passwordRequirements: + "رمز عبور باید حداقل 8 کاراکتر باشد و حداقل یک حرف کوچک، یک حرف بزرگ، یک عدد و یک نماد داشته باشد.", + passwordReuse: "رمز عبور جدید نباید با رمز عبور قبلی یکسان باشد.", + welcome: (title: string = "Qlockifiy") => `به ${title} خوش آمدید`, enterPassword: "رمز عبور خود را وارد کنید", verifyNumber: "تایید شماره موبایل", enterMobileDesc: "برای ادامه، شماره موبایل خود را وارد کنید", diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 9d8341d..2eb3733 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -12,10 +12,11 @@ import { Button } from "../components/ui/button" import { Camera, Edit2, Trash2, User as UserIcon, UploadCloud, X, Check } from "lucide-react" import JalaliDatePicker from "../components/ui/JalaliDatePicker" import { toast } from "sonner" -import { Modal } from "../components/Modal" -import { Input } from "../components/ui/input" -import { TextAreaInput } from "../components/ui/TextAreaInput" -import { AuthPasswordField } from "./auth/AuthPasswordField" +import { Modal } from "../components/Modal" +import { Input } from "../components/ui/input" +import { TextAreaInput } from "../components/ui/TextAreaInput" +import { AuthPasswordField } from "./auth/AuthPasswordField" +import { getPasswordValidationMessage } from "./auth/utils" export interface UserProfile { id?: string; @@ -187,12 +188,23 @@ export default function Profile() { return } - if (passwordForm.newPassword !== passwordForm.confirmPassword) { - toast.error(t.login.passwordMismatch) - return - } - - setIsSaving(true) + if (passwordForm.newPassword !== passwordForm.confirmPassword) { + toast.error(t.login.passwordMismatch) + return + } + + const passwordValidationMessage = getPasswordValidationMessage(passwordForm.newPassword, t.login) + if (passwordValidationMessage) { + toast.error(passwordValidationMessage) + return + } + + if (passwordForm.currentPassword === passwordForm.newPassword) { + toast.error(t.login.passwordReuse) + return + } + + setIsSaving(true) try { await changePassword( passwordForm.currentPassword, diff --git a/src/pages/auth/ForgotPasswordPasswordPage.tsx b/src/pages/auth/ForgotPasswordPasswordPage.tsx index 4b0e1e0..0ac487a 100644 --- a/src/pages/auth/ForgotPasswordPasswordPage.tsx +++ b/src/pages/auth/ForgotPasswordPasswordPage.tsx @@ -9,7 +9,7 @@ import { useAuthFlow } from "../../context/AuthFlowContext" import { useTranslation } from "../../hooks/useTranslation" import { AuthPanel } from "./AuthPanel" import { AuthPasswordField } from "./AuthPasswordField" -import { getApiErrorMessage } from "./utils" +import { getApiErrorMessage, getPasswordValidationMessage } from "./utils" export function ForgotPasswordPasswordPage() { const navigate = useNavigate() @@ -40,6 +40,12 @@ export function ForgotPasswordPasswordPage() { return } + const passwordValidationMessage = getPasswordValidationMessage(password, t.login) + if (passwordValidationMessage) { + toast.error(passwordValidationMessage) + return + } + setLoading(true) try { await resetPasswordWithOtp(state.forgotPassword.mobile, state.forgotPassword.code, password, confirmation) diff --git a/src/pages/auth/SignupPasswordPage.tsx b/src/pages/auth/SignupPasswordPage.tsx index 4517d21..b460425 100644 --- a/src/pages/auth/SignupPasswordPage.tsx +++ b/src/pages/auth/SignupPasswordPage.tsx @@ -9,7 +9,7 @@ import { useAuthFlow } from "../../context/AuthFlowContext" import { useTranslation } from "../../hooks/useTranslation" import { AuthPanel } from "./AuthPanel" import { AuthPasswordField } from "./AuthPasswordField" -import { completeAuthentication, getApiErrorMessage } from "./utils" +import { completeAuthentication, getApiErrorMessage, getPasswordValidationMessage } from "./utils" export function SignupPasswordPage() { const navigate = useNavigate() @@ -40,6 +40,12 @@ export function SignupPasswordPage() { return } + const passwordValidationMessage = getPasswordValidationMessage(password, t.login) + if (passwordValidationMessage) { + toast.error(passwordValidationMessage) + return + } + setLoading(true) try { const data = await registerWithOtp(state.signup.mobile, state.signup.code, password, confirmation) diff --git a/src/pages/auth/utils.ts b/src/pages/auth/utils.ts index 32e2b02..876c576 100644 --- a/src/pages/auth/utils.ts +++ b/src/pages/auth/utils.ts @@ -29,6 +29,24 @@ export const getApiErrorMessage = (error: unknown, fallbackMessage: string) => { return fallbackMessage } +export const getPasswordValidationMessage = ( + password: string, + copy: { + passwordRequirements: string + }, +) => { + const hasLowercase = /[a-z]/.test(password) + const hasUppercase = /[A-Z]/.test(password) + const hasDigit = /\d/.test(password) + const hasSymbol = /[^A-Za-z0-9]/.test(password) + + if (password.length < 8 || !hasLowercase || !hasUppercase || !hasDigit || !hasSymbol) { + return copy.passwordRequirements + } + + return null +} + export const handleThrottleError = ({ error, cooldownKey,