feat(auth): add stepped auth and password recovery flows

This commit is contained in:
2026-05-03 17:10:02 +03:30
parent 9b1cd772fb
commit 380b794ab1
19 changed files with 1857 additions and 687 deletions

View File

@@ -5,7 +5,8 @@ import {
getUserProfile,
updateUserProfile,
updateProfilePicture,
removeProfilePicture
removeProfilePicture,
changePassword,
} from "../api/users"
import { Button } from "../components/ui/button"
import { Camera, Edit2, Trash2, User as UserIcon, UploadCloud, X, Check } from "lucide-react"
@@ -14,6 +15,7 @@ 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"
export interface UserProfile {
id?: string;
@@ -36,6 +38,7 @@ export default function Profile() {
const { t, lang } = useTranslation()
const isFa = lang === 'fa'
const passwordCopy = t.profile.password
const toPersianNum = (num: string | number | undefined | null) => {
if (num === null || num === undefined) return num
@@ -59,6 +62,7 @@ export default function Profile() {
// Modals & Editing state
const [isEditing, setIsEditing] = useState(false)
const [isPicModalOpen, setIsPicModalOpen] = useState(false)
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false)
const [isSaving, setIsSaving] = useState(false)
// Form states
@@ -66,6 +70,11 @@ export default function Profile() {
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [dragActive, setDragActive] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
const [passwordForm, setPasswordForm] = useState({
currentPassword: "",
newPassword: "",
confirmPassword: "",
})
const fetchProfile = async () => {
try {
@@ -162,6 +171,44 @@ export default function Profile() {
}
}
const resetPasswordForm = () => {
setPasswordForm({
currentPassword: "",
newPassword: "",
confirmPassword: "",
})
}
const handleChangePassword = async (event: React.FormEvent) => {
event.preventDefault()
if (!passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) {
toast.error(t.login.toasts.fillAll)
return
}
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
toast.error(t.login.passwordMismatch)
return
}
setIsSaving(true)
try {
await changePassword(
passwordForm.currentPassword,
passwordForm.newPassword,
passwordForm.confirmPassword,
)
resetPasswordForm()
setIsPasswordModalOpen(false)
toast.success(passwordCopy.toasts.success)
} catch (error) {
toast.error(error instanceof Error ? error.message : passwordCopy.toasts.error)
} finally {
setIsSaving(false)
}
}
// Drag & Drop Handlers
const handleDrag = (e: React.DragEvent) => {
e.preventDefault()
@@ -233,10 +280,14 @@ export default function Profile() {
</h2>
{!isEditing && (
<Button onClick={handleEditClick} className="flex items-center gap-2">
<Edit2 className="h-4 w-4" />
{t.profile?.editInfo || 'Edit Profile'}
</Button>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={() => setIsPasswordModalOpen(true)}>
{passwordCopy.trigger}
</Button>
<Button onClick={handleEditClick}>
<Edit2 className="h-4 w-4" />
</Button>
</div>
)}
</div>
@@ -446,6 +497,62 @@ export default function Profile() {
</Modal>
)}
{isPasswordModalOpen && (
<Modal
isOpen={isPasswordModalOpen}
isFa={isFa}
onClose={() => {
if (isSaving) return
setIsPasswordModalOpen(false)
resetPasswordForm()
}}
title={passwordCopy.title}
description={passwordCopy.description}
maxWidth="max-w-md"
>
<form onSubmit={handleChangePassword} className="grid gap-4">
<AuthPasswordField
id="current-password"
value={passwordForm.currentPassword}
onChange={(value) => setPasswordForm((current) => ({ ...current, currentPassword: value }))}
placeholder={passwordCopy.currentPassword}
disabled={isSaving}
/>
<AuthPasswordField
id="new-password"
value={passwordForm.newPassword}
onChange={(value) => setPasswordForm((current) => ({ ...current, newPassword: value }))}
placeholder={passwordCopy.newPassword}
disabled={isSaving}
/>
<AuthPasswordField
id="confirm-password"
value={passwordForm.confirmPassword}
onChange={(value) => setPasswordForm((current) => ({ ...current, confirmPassword: value }))}
placeholder={passwordCopy.confirmPassword}
disabled={isSaving}
/>
<div className="flex flex-col-reverse gap-3 mt-3 sm:flex-row sm:justify-end">
<Button
type="button"
variant="outline"
onClick={() => {
setIsPasswordModalOpen(false)
resetPasswordForm()
}}
disabled={isSaving}
>
{t.actions?.cancel || "Cancel"}
</Button>
<Button type="submit" disabled={isSaving}>
{isSaving ? passwordCopy.saving : passwordCopy.submit}
</Button>
</div>
</form>
</Modal>
)}
</div>
</>
)