feat(auth): add stepped auth and password recovery flows
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user