130 lines
4.1 KiB
TypeScript
130 lines
4.1 KiB
TypeScript
import { useMemo, useState } from 'react'
|
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { useForm } from 'react-hook-form'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
|
|
import { z } from 'zod'
|
|
import { AuthLayout } from '@/components/layout/AuthLayout'
|
|
import { Button } from '@/components/ui/Button'
|
|
import { TextField } from '@/components/ui/fields'
|
|
import { useConfirmPasswordReset, useRequestPasswordReset } from '@/hooks/useAuth'
|
|
import { describeApiError } from '@/lib/apiErrors'
|
|
|
|
interface ResetValues {
|
|
email: string
|
|
password: string
|
|
confirm: string
|
|
}
|
|
|
|
export function ResetPasswordPage() {
|
|
const { t } = useTranslation()
|
|
const [searchParams] = useSearchParams()
|
|
const token = searchParams.get('token') ?? ''
|
|
const requestReset = useRequestPasswordReset()
|
|
const confirmReset = useConfirmPasswordReset()
|
|
const navigate = useNavigate()
|
|
const [sent, setSent] = useState(false)
|
|
const [apiError, setApiError] = useState<string | null>(null)
|
|
const schema = useMemo(
|
|
() =>
|
|
token
|
|
? z
|
|
.object({
|
|
email: z.string(),
|
|
password: z.string().min(12, t('auth.passwordHint')),
|
|
confirm: z.string().min(1, t('common.requiredField')),
|
|
})
|
|
.refine((values) => values.password === values.confirm, {
|
|
path: ['confirm'],
|
|
message: t('auth.passwordMismatch'),
|
|
})
|
|
: z.object({
|
|
email: z.string().min(1, t('common.requiredField')).email(t('auth.invalidEmail')),
|
|
password: z.string(),
|
|
confirm: z.string(),
|
|
}),
|
|
[t, token],
|
|
)
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
formState: { errors },
|
|
} = useForm<ResetValues>({
|
|
resolver: zodResolver(schema),
|
|
defaultValues: { email: '', password: '', confirm: '' },
|
|
})
|
|
const pending = requestReset.isPending || confirmReset.isPending
|
|
const submit = handleSubmit((values) => {
|
|
setApiError(null)
|
|
if (!token) {
|
|
requestReset.mutate(values.email, {
|
|
onSuccess: () => setSent(true),
|
|
onError: (error) => setApiError(describeApiError(error, t)),
|
|
})
|
|
return
|
|
}
|
|
confirmReset.mutate(
|
|
{ token, password: values.password },
|
|
{
|
|
onSuccess: () => navigate('/accounts', { replace: true }),
|
|
onError: (error) => setApiError(describeApiError(error, t)),
|
|
},
|
|
)
|
|
})
|
|
|
|
return (
|
|
<AuthLayout
|
|
title={t('auth.resetTitle')}
|
|
subtitle={t('auth.resetSubtitle')}
|
|
context={t('authShell.reset')}
|
|
>
|
|
{apiError ? (
|
|
<div className="banner banner-danger" role="alert">
|
|
{apiError}
|
|
</div>
|
|
) : null}
|
|
{sent ? (
|
|
<div className="banner banner-success" role="status">
|
|
{t('auth.resetSent')}
|
|
</div>
|
|
) : (
|
|
<form onSubmit={submit} noValidate className="form-grid">
|
|
{token ? (
|
|
<>
|
|
<TextField
|
|
label={t('auth.password')}
|
|
type="password"
|
|
autoComplete="new-password"
|
|
hint={t('auth.passwordHint')}
|
|
error={errors.password?.message}
|
|
{...register('password')}
|
|
/>
|
|
<TextField
|
|
label={t('auth.passwordConfirm')}
|
|
type="password"
|
|
autoComplete="new-password"
|
|
error={errors.confirm?.message}
|
|
{...register('confirm')}
|
|
/>
|
|
</>
|
|
) : (
|
|
<TextField
|
|
label={t('auth.email')}
|
|
type="email"
|
|
autoComplete="email"
|
|
error={errors.email?.message}
|
|
{...register('email')}
|
|
/>
|
|
)}
|
|
<Button type="submit" disabled={pending}>
|
|
{token ? t('auth.resetSubmit') : t('auth.resetSend')}
|
|
</Button>
|
|
</form>
|
|
)}
|
|
<p className="auth-alt">
|
|
<Link to="/login">{t('auth.backToLogin')}</Link>
|
|
</p>
|
|
</AuthLayout>
|
|
)
|
|
}
|