Files
planet/frontend/src/pages/ForgotPassword/ForgotPassword.tsx
linkong 5bdb55f3f1
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / delivery (push) Has been cancelled
release / images (push) Has been cancelled
release: bump version to 0.74.0
2026-06-30 13:52:52 +08:00

104 lines
4.4 KiB
TypeScript

import axios from 'axios'
import { useEffect, useState, type FormEvent } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { AuthButton, AuthField, AuthForm, AuthInput, AuthLinks, AuthNotice, AuthShell, BackToLogin } from '../Auth/AuthShell'
import { describeApiError } from '../../i18n/api-errors'
const API_URL = import.meta.env.VITE_API_URL || '/api/v1'
function ForgotPassword() {
const { t } = useTranslation()
const [step, setStep] = useState<'request' | 'reset'>('request')
const [email, setEmail] = useState('')
const [code, setCode] = useState('')
const [newPassword, setNewPassword] = useState('')
const [loading, setLoading] = useState(false)
const [cooldown, setCooldown] = useState(0)
const [feedback, setFeedback] = useState<{ tone: 'error' | 'success' | 'info'; text: string } | null>(null)
useEffect(() => {
if (cooldown <= 0) return
const timer = window.setTimeout(() => setCooldown((value) => value - 1), 1000)
return () => window.clearTimeout(timer)
}, [cooldown])
const onRequest = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
setLoading(true)
setFeedback(null)
try {
await axios.post(`${API_URL}/auth/forgot-password`, { email })
setStep('reset')
setCooldown(60)
setFeedback({ tone: 'success', text: t('auth.recoveryCodeSent') })
} catch (error) {
setFeedback({ tone: 'error', text: describeApiError(error, t('common.operationFailed')) })
} finally {
setLoading(false)
}
}
const onReset = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
setLoading(true)
setFeedback(null)
try {
await axios.post(`${API_URL}/auth/reset-password`, { email, code, new_password: newPassword })
setStep('request')
setCode('')
setNewPassword('')
setFeedback({ tone: 'success', text: t('auth.passwordResetSuccess') })
} catch (error) {
setFeedback({ tone: 'error', text: describeApiError(error, t('common.operationFailed')) })
} finally {
setLoading(false)
}
}
const onResend = async () => {
if (!email || cooldown > 0) return
try {
await axios.post(`${API_URL}/auth/forgot-password`, { email })
setCooldown(60)
setFeedback({ tone: 'success', text: t('auth.resendSuccess') })
} catch (error) {
setFeedback({ tone: 'error', text: describeApiError(error, t('common.operationFailed')) })
}
}
return (
<AuthShell eyebrow={t('auth.accountRecovery')} title={t('auth.forgotPasswordTitle')} description={t('auth.forgotPasswordDescription')}>
{step === 'request' ? (
<AuthForm onSubmit={onRequest}>
{feedback ? <AuthNotice tone={feedback.tone}>{feedback.text}</AuthNotice> : null}
<AuthField label={t('auth.email')}>
<AuthInput value={email} onChange={(event) => setEmail(event.target.value)} type="email" autoComplete="email" required />
</AuthField>
<AuthButton type="submit" loading={loading}>{t('auth.sendCode')}</AuthButton>
<AuthLinks><BackToLogin /></AuthLinks>
</AuthForm>
) : (
<AuthForm onSubmit={onReset}>
{feedback ? <AuthNotice tone={feedback.tone}>{feedback.text}</AuthNotice> : null}
<AuthNotice><Trans i18nKey="auth.codeSent" values={{ email }} components={{ strong: <strong /> }} /></AuthNotice>
<AuthField label={t('auth.code')}>
<AuthInput value={code} onChange={(event) => setCode(event.target.value)} maxLength={6} inputMode="numeric" required />
</AuthField>
<AuthField label={t('auth.newPassword')}>
<AuthInput value={newPassword} onChange={(event) => setNewPassword(event.target.value)} type="password" minLength={8} autoComplete="new-password" required />
</AuthField>
<AuthButton type="submit" loading={loading}>{t('auth.resetPassword')}</AuthButton>
<AuthLinks>
<button className="auth-link auth-link--button" type="button" onClick={() => setStep('request')}>{t('auth.updateEmail')}</button>
<button className="auth-link auth-link--button" type="button" disabled={cooldown > 0} onClick={onResend}>
{cooldown > 0 ? t('auth.resendCountdown', { seconds: cooldown }) : t('auth.resend')}
</button>
</AuthLinks>
</AuthForm>
)}
</AuthShell>
)
}
export default ForgotPassword