Fix forgot-password flow: UI never let users enter the emailed code
All checks were successful
build-and-release / build (push) Successful in 2m14s
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:
parent
7a0799f1d1
commit
83b5072011
1 changed files with 37 additions and 10 deletions
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue