Fix forgot-password flow: UI never let users enter the emailed code
All checks were successful
build-and-release / build (push) Successful in 2m14s

The backend emails a plain 6-digit reset code (crates/email/templates/
password-reset.html just renders {{reset_code}}, no link) and expects
POST /api/auth/reset-password with {code, new_password}. This page was
built for a different, unused link-based flow instead: it only read a
`token` from the URL query string and posted it as `token`, a field
name the backend's ResetPasswordPayload doesn't even have. Users had
no way to type the code in at all, so submitting a request just left
them stuck back on the request screen.

Now: requesting a reset code moves straight to a "set new password"
step with a 6-digit code input, and the code is submitted under the
correct `code` field name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar Sivakumar 2026-07-23 22:25:31 +05:30
parent 7a0799f1d1
commit 83b5072011

View file

@ -27,7 +27,7 @@ export default function ForgotPasswordRoute() {
const navigate = useNavigate();
const [search] = useSearchParams();
const [email, setEmail] = createSignal('');
const [token, setToken] = createSignal(search.token || '');
const [code, setCode] = createSignal(typeof search.code === 'string' ? search.code : '');
const [newPassword, setNewPassword] = createSignal('');
const [confirmPassword, setConfirmPassword] = createSignal('');
const [showPassword, setShowPassword] = createSignal(false);
@ -35,7 +35,7 @@ export default function ForgotPasswordRoute() {
const [error, setError] = createSignal('');
const [success, setSuccess] = createSignal('');
const [submitting, setSubmitting] = createSignal(false);
const [step, setStep] = createSignal<'request' | 'reset'>(token() ? 'reset' : 'request');
const [step, setStep] = createSignal<'request' | 'reset'>('request');
const passwordsMatch = createMemo(() => newPassword() === confirmPassword() && newPassword().length >= 8);
@ -54,7 +54,8 @@ export default function ForgotPasswordRoute() {
body: JSON.stringify({ email: email().trim().toLowerCase() }),
});
if (res.ok) {
setSuccess('If an account exists with this email, a reset link has been sent.');
setSuccess('If an account exists with this email, a 6-digit reset code has been sent.');
setStep('reset');
} else {
setError('Unable to process request. Please try again.');
}
@ -68,8 +69,8 @@ export default function ForgotPasswordRoute() {
const resetPassword = async () => {
setError('');
setSuccess('');
if (!token()) {
setError('Reset token is missing. Use the link from your email.');
if (!code().trim()) {
setError('Enter the reset code from your email.');
return;
}
if (newPassword().length < 8) {
@ -86,7 +87,7 @@ export default function ForgotPasswordRoute() {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: token(),
code: code().trim(),
new_password: newPassword(),
}),
});
@ -95,7 +96,7 @@ export default function ForgotPasswordRoute() {
setSuccess('Password reset successfully. You can now login.');
setTimeout(() => navigate('/login', { replace: true }), 2000);
} else {
setError(String(data?.message || data?.error || 'Reset failed. Token may be expired.'));
setError(String(data?.message || data?.error || 'Reset failed. Code may be invalid or expired.'));
}
} catch {
setError('Network error. Please check your connection.');
@ -124,7 +125,21 @@ export default function ForgotPasswordRoute() {
<Show when={step() === 'request'} fallback={
<>
<h2 class="title">Set New Password</h2>
<p class="subtitle">Enter your new password below.</p>
<p class="subtitle">Enter the 6-digit code sent to your email and choose a new password.</p>
<div class="field">
<label class="label" for="reset-code">RESET CODE</label>
<input
id="reset-code"
type="text"
inputmode="numeric"
maxlength="6"
class="input"
value={code()}
onInput={(e) => setCode(e.currentTarget.value.replace(/\D/g, '').slice(0, 6))}
placeholder="Enter 6-digit code"
/>
</div>
<div class="field">
<label class="label" for="new-password">NEW PASSWORD</label>
@ -157,12 +172,24 @@ export default function ForgotPasswordRoute() {
</button>
<div class="auth-footer-row">
<p class="note">
Didn't get a code?{' '}
<button
type="button"
class="auth-forgot-link"
style={{ background: 'none', border: 'none', padding: 0, cursor: 'pointer' }}
onClick={() => void requestReset()}
disabled={submitting()}
>
Resend code
</button>
</p>
<p class="note"><A href="/login">Back to Sign In</A></p>
</div>
</>
}>
<h2 class="title">Forgot Password</h2>
<p class="subtitle">Enter your email to receive a password reset link.</p>
<p class="subtitle">Enter your email to receive a 6-digit password reset code.</p>
<div class="field">
<label class="label" for="reset-email">EMAIL ADDRESS</label>
@ -173,7 +200,7 @@ export default function ForgotPasswordRoute() {
</div>
<button class="auth-submit-btn" type="button" onClick={() => void requestReset()} disabled={submitting()}>
{submitting() ? 'Sending...' : 'Send Reset Link'}
{submitting() ? 'Sending...' : 'Send Reset Code'}
</button>
<div class="auth-footer-row">