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

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

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,