feat(auth): enforce password policy in reset and change flows

This commit is contained in:
2026-05-03 20:02:14 +03:30
parent de74db2703
commit 040ee4b1f7
6 changed files with 70 additions and 22 deletions

View File

@@ -60,6 +60,9 @@ export const en = {
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",

View File

@@ -59,6 +59,9 @@ export const fa = {
newPasswordPlaceholder: "رمز عبور جدید",
confirmPasswordPlaceholder: "تکرار رمز عبور",
passwordMismatch: "رمز عبور و تکرار آن یکسان نیستند.",
passwordRequirements:
"رمز عبور باید حداقل 8 کاراکتر باشد و حداقل یک حرف کوچک، یک حرف بزرگ، یک عدد و یک نماد داشته باشد.",
passwordReuse: "رمز عبور جدید نباید با رمز عبور قبلی یکسان باشد.",
welcome: (title: string = "Qlockifiy") => `به ${title} خوش آمدید`,
enterPassword: "رمز عبور خود را وارد کنید",
verifyNumber: "تایید شماره موبایل",

View File

@@ -16,6 +16,7 @@ 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;
@@ -192,6 +193,17 @@ export default function Profile() {
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(

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,