import {useState} from 'react'; import {Link, useLocation} from 'react-router-dom'; import {useTranslation} from 'react-i18next'; import {useForm} from 'react-hook-form'; import {zodResolver} from '@hookform/resolvers/zod'; import {Loader2} from 'lucide-react'; import {Button} from '@/components/ui/button'; import {Input} from '@/components/ui/input'; import AuthLayout from '../components/AuthLayout.jsx'; import FeedbackAlert from '../components/FeedbackAlert.jsx'; import FormField from '../components/FormField.jsx'; import NewPasswordFields from '../components/NewPasswordFields.jsx'; import {confirmEmailSchema} from '../utils/validation.js'; import {clientFetch} from '../utils/api.js'; const post = (path, body) => clientFetch(`${import.meta.env.VITE_API_URL}${path}`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body), }); const failureMessage = (t, response) => { if (response.status === 429) { const minutes = Math.ceil(Number(response.headers.get('Retry-After') ?? 900) / 60); return t('RESET_PASSWORD.ERROR_TOO_MANY', {minutes}); } return t('RESET_PASSWORD.ERROR_GENERIC'); }; /** * Both ends of "forgot password". Without a fragment: ask for the mail. With the fragment of the mail link: choose * the new password. The token is in the fragment, so no server log sees it, and it is sent only on the click: a mail * scanner that opens the link must not use it up. */ export default function ResetPasswordPage() { const token = useLocation().hash.slice(1); // A new link in the same tab only changes the fragment: key restarts the state. return ; } function ResetPassword({token}) { const {t} = useTranslation(); const [state, setState] = useState(token ? 'idle' : 'request'); return ( {state === 'idle' && ( setState('done')} onInvalid={() => setState('invalid')}/> )} {state === 'done' && <> {t('RESET_PASSWORD.TO_LOGIN')} } {state === 'invalid' && } {(state === 'request' || state === 'invalid') && } ); } function RequestForm() { const {t} = useTranslation(); const [email, setEmail] = useState(''); const [isLoading, setIsLoading] = useState(false); const [feedback, setFeedback] = useState({message: '', isError: false}); const send = async (event) => { event.preventDefault(); setIsLoading(true); setFeedback({message: '', isError: false}); try { const response = await post('/api/auth/password-reset', {email}); setFeedback(response.ok ? {message: t('RESET_PASSWORD.SENT'), isError: false} : {message: failureMessage(t, response), isError: true}); } catch (error) { console.error('Password reset API error:', error); setFeedback({message: t('INFO.ERROR.NETWORK_FAILURE'), isError: true}); } finally { setIsLoading(false); } }; return (
setEmail(e.target.value)}/> ); } function NewPasswordForm({token, onDone, onInvalid}) { const {t} = useTranslation(); const [error, setError] = useState(''); const {register, handleSubmit, formState: {errors, isSubmitting}} = useForm({ resolver: zodResolver(confirmEmailSchema), defaultValues: {password: '', passwordConfirmation: ''}, }); const submit = async ({password}) => { setError(''); try { const response = await post('/api/auth/password-reset/confirm', {token, password}); if (response.ok) { onDone(); } else if (response.status === 410 || response.status === 400) { onInvalid(); } else { setError(failureMessage(t, response)); } } catch (err) { console.error('Password reset confirm error:', err); setError(t('INFO.ERROR.NETWORK_FAILURE')); } }; return (

{t('RESET_PASSWORD.INTRO')}

); }