104 lines
4.4 KiB
TypeScript
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
|