From 812fc5ef04f8d18e16d516219a8ab9ccc3d5102d Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Sivakumar Date: Tue, 14 Jul 2026 22:56:52 +0530 Subject: [PATCH] fix(admin-auth): point forgot-password flow at endpoints that actually exist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reset-password flow called /api/auth/internal/forgot-password/request-code and /verify-code with a challengeId-based two-step contract — neither route exists anywhere in the backend, so every reset attempt failed with "Failed to send reset code." The real backend only exposes /api/auth/forgot-password (sends a one-time code, always 200 to avoid account enumeration) and /api/auth/reset-password (consumes the code + new password, no challengeId involved). Rewrites both request/verify handlers to match that contract. Co-Authored-By: Claude Sonnet 5 --- src/routes/login.tsx | 71 ++++++++++++++------------------------------ 1 file changed, 22 insertions(+), 49 deletions(-) diff --git a/src/routes/login.tsx b/src/routes/login.tsx index fa03e11..92b96de 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -6,21 +6,6 @@ import { hasAdminSession, setAdminSession } from '~/lib/admin-session'; type AuthMode = 'login' | 'reset'; type ResetStep = 'request' | 'verify'; -function pickChallengeId(payload: any): string { - const direct = String(payload?.challengeId || '').trim(); - if (direct) return direct; - const nested = String(payload?.data?.challengeId || '').trim(); - if (nested) return nested; - return String(payload?.challenge_id || '').trim(); -} - -function pickMaskedEmail(payload: any, fallback: string): string { - const direct = String(payload?.maskedEmail || '').trim(); - if (direct) return direct; - const nested = String(payload?.data?.maskedEmail || '').trim(); - return nested || fallback; -} - /* Matches public website input exactly */ const inputCls = 'h-11 w-full rounded-xl border border-[#E5E7EB] bg-white px-4 text-sm text-[#111827] outline-none transition placeholder:text-[#9CA3AF] focus:border-[#FF5E13] focus:ring-2 focus:ring-[#FFE6D9]'; @@ -38,7 +23,6 @@ export default function LoginPage() { const [resetCode, setResetCode] = createSignal(''); const [newPassword, setNewPassword] = createSignal(''); const [confirmPassword, setConfirmPassword] = createSignal(''); - const [challengeId, setChallengeId] = createSignal(''); const [maskedEmail, setMaskedEmail] = createSignal(''); const [isSubmitting, setIsSubmitting] = createSignal(false); const [error, setError] = createSignal(''); @@ -46,7 +30,7 @@ export default function LoginPage() { const canSubmitLogin = createMemo(() => email().trim().length > 0 && password().trim().length > 0); const canSubmitResetRequest = createMemo(() => email().trim().length > 0 && newPassword().trim().length > 0 && confirmPassword().trim().length > 0); - const canSubmitResetVerify = createMemo(() => challengeId().trim().length > 0 && resetCode().trim().length === 6); + const canSubmitResetVerify = createMemo(() => resetCode().trim().length === 6); const clearMessages = () => { setError(''); setInfo(''); }; @@ -55,7 +39,7 @@ export default function LoginPage() { setMode(next); if (next === 'login') { setResetStep('request'); - setResetCode(''); setChallengeId(''); setMaskedEmail(''); + setResetCode(''); setMaskedEmail(''); setNewPassword(''); setConfirmPassword(''); } }; @@ -105,30 +89,23 @@ export default function LoginPage() { if (!canSubmitResetRequest()) { setError('All fields are required.'); return; } if (newPassword() !== confirmPassword()) { setError('Passwords do not match.'); return; } const trimmedEmail = email().trim().toLowerCase(); - const pw = newPassword().trim(); - const reqBody = { email: trimmedEmail, newPassword: pw, new_password: pw, password: pw }; setIsSubmitting(true); try { - let payload: any = {}; let status = 500; - for (const { url, body } of [ - { url: '/api/auth/internal/forgot-password/request-code', body: JSON.stringify(reqBody) }, - { url: '/api/auth/internal/forgot-password/request-code', body: JSON.stringify(reqBody) }, - { url: '/api/auth/internal/forgot-password/request-code', body: JSON.stringify({ data: reqBody }) }, - { url: '/api/auth/internal/forgot-password/request-code', body: JSON.stringify({ data: reqBody }) }, - ]) { - const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body }); - status = r.status; payload = await r.json().catch(() => ({})); - if (r.ok) break; - } - const cId = pickChallengeId(payload); - if (!cId) { - const fallback = status === 502 ? 'Service unavailable (502). Please retry.' : 'Failed to send reset code.'; + const r = await fetch('/api/auth/forgot-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify({ email: trimmedEmail }), + }); + if (!r.ok) { + const payload = await r.json().catch(() => ({})); + const fallback = r.status === 502 ? 'Service unavailable (502). Please retry.' : 'Failed to send reset code.'; throw new Error(String(payload?.message || payload?.error || fallback).trim()); } - setChallengeId(cId); - setMaskedEmail(pickMaskedEmail(payload, trimmedEmail)); + // The backend intentionally responds 200 whether or not the email exists, + // to avoid account enumeration — there's no challengeId in this API. + setMaskedEmail(trimmedEmail); setResetStep('verify'); - setInfo('Reset code sent to your email.'); + setInfo('If that email has an account, a reset code has been sent.'); } catch (e: any) { setError(String(e?.message || 'Failed to send reset code.')); } finally { setIsSubmitting(false); } }; @@ -137,20 +114,16 @@ export default function LoginPage() { clearMessages(); if (!canSubmitResetVerify()) { setError('A valid 6-digit code is required.'); return; } if (newPassword().trim() !== confirmPassword().trim()) { setError('Passwords do not match.'); return; } - const pw = newPassword().trim(); - const cId = challengeId().trim(); - const code = resetCode().trim(); - const body = JSON.stringify({ challengeId: cId, challenge_id: cId, code, otp: code, verificationCode: code, verification_code: code, newPassword: pw, new_password: pw, password: pw }); setIsSubmitting(true); try { - let payload: any = {}; let status = 500; let success = false; - for (const url of ['/api/auth/internal/forgot-password/verify-code', '/api/auth/internal/forgot-password/verify-code']) { - const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body }); - status = r.status; payload = await r.json().catch(() => ({})); - if (r.ok) { success = true; break; } - } - if (!success) { - const fallback = status === 502 ? 'Service unavailable (502). Please retry.' : 'Password reset failed.'; + const r = await fetch('/api/auth/reset-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify({ code: resetCode().trim(), new_password: newPassword().trim() }), + }); + const payload = await r.json().catch(() => ({})); + if (!r.ok) { + const fallback = r.status === 502 ? 'Service unavailable (502). Please retry.' : 'Password reset failed.'; throw new Error(String(payload?.message || payload?.error || fallback).trim()); } setPassword(''); switchMode('login');