import { useNavigate } from '@solidjs/router'; import { Show, createMemo, createSignal, onMount } from 'solid-js'; import { isExternalIdentity, pickManagementLoginError } from '~/lib/admin-auth'; 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; const snake = String(payload?.challenge_id || '').trim(); if (snake) return snake; return ''; } function pickMaskedEmail(payload: any, fallback: string): string { const direct = String(payload?.maskedEmail || '').trim(); if (direct) return direct; const nested = String(payload?.data?.maskedEmail || '').trim(); if (nested) return nested; return fallback; } const inputCls = 'h-11 w-full rounded-lg border border-gray-200 bg-gray-50 px-3.5 text-sm text-gray-900 outline-none transition placeholder:text-gray-400 focus:border-[#0a1d37] focus:bg-white focus:ring-2 focus:ring-[#0a1d37]/10'; const labelCls = 'mb-1.5 block text-xs font-semibold uppercase tracking-wider text-gray-500'; export default function LoginPage() { const navigate = useNavigate(); const [mode, setMode] = createSignal('login'); const [resetStep, setResetStep] = createSignal('request'); const [email, setEmail] = createSignal(''); const [password, setPassword] = createSignal(''); 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(''); const [info, setInfo] = createSignal(''); 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 canSubmitLoginCredentials = createMemo( () => email().trim().length > 0 && password().trim().length > 0, ); const clearMessages = () => { setError(''); setInfo(''); }; const resetPasswordFlow = () => { setResetStep('request'); setResetCode(''); setChallengeId(''); setMaskedEmail(''); setNewPassword(''); setConfirmPassword(''); }; const switchMode = (nextMode: AuthMode) => { clearMessages(); setMode(nextMode); if (nextMode === 'login') resetPasswordFlow(); }; onMount(() => { if (hasAdminSession()) navigate('/admin', { replace: true }); }); const completeAdminLogin = () => { setAdminSession(); const params = new URLSearchParams(window.location.search); const from = params.get('from'); navigate(from && from.startsWith('/admin') ? from : '/admin', { replace: true }); }; const directSignIn = async () => { clearMessages(); if (!canSubmitLoginCredentials()) { setError('Email and password are required.'); return; } setIsSubmitting(true); try { const loginPayload = { email: email().trim().toLowerCase(), password: password(), loginTarget: 'admin' }; const attempts = ['/api/gateway/users/auth/internal/login', '/api/gateway/auth/internal/login']; let payload: any = {}; let status = 500; let success = false; for (const url of attempts) { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'x-portal-target': 'admin' }, credentials: 'include', body: JSON.stringify(loginPayload), }); status = response.status; payload = await response.json().catch(() => ({})); if (response.ok) { success = true; break; } } if (!success) { const fallback = status === 502 ? 'Auth service unavailable (502). Please retry in 1–2 minutes.' : 'Sign in failed.'; throw new Error(pickManagementLoginError(payload) || fallback); } if (isExternalIdentity(payload)) throw new Error('External users cannot use this portal.'); const accessToken = String(payload?.access_token || payload?.accessToken || '').trim(); if (accessToken) sessionStorage.setItem('nxtgauge_admin_access_token', accessToken); completeAdminLogin(); } catch (e: any) { setError(String(e?.message || 'Sign in failed.')); } finally { setIsSubmitting(false); } }; const requestResetCode = async () => { clearMessages(); if (!canSubmitResetRequest()) { setError('Email, new password, and confirm password are required.'); return; } if (newPassword() !== confirmPassword()) { setError('Passwords do not match.'); return; } const trimmedEmail = email().trim().toLowerCase(); const resolvedPassword = newPassword().trim(); const requestPayload = { email: trimmedEmail, newPassword: resolvedPassword, new_password: resolvedPassword, password: resolvedPassword }; const attempts = [ { url: '/api/gateway/users/auth/internal/forgot-password/request-code', body: JSON.stringify(requestPayload) }, { url: '/api/gateway/auth/internal/forgot-password/request-code', body: JSON.stringify(requestPayload) }, { url: '/api/gateway/users/auth/internal/forgot-password/request-code', body: JSON.stringify({ data: requestPayload }) }, { url: '/api/gateway/auth/internal/forgot-password/request-code', body: JSON.stringify({ data: requestPayload }) }, ]; setIsSubmitting(true); try { let payload: any = {}; let status = 500; for (const attempt of attempts) { const response = await fetch(attempt.url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: attempt.body }); status = response.status; payload = await response.json().catch(() => ({})); if (response.ok) break; } const nextChallengeId = pickChallengeId(payload); if (!nextChallengeId) { const fallback = status === 502 ? 'Verification service unavailable (502). Please retry in 1–2 minutes.' : 'Failed to send reset code.'; throw new Error(String(payload?.message || payload?.error || fallback).trim()); } setChallengeId(nextChallengeId); setMaskedEmail(pickMaskedEmail(payload, trimmedEmail)); setResetStep('verify'); const debugCode = String(payload?.debugCode || payload?.data?.debugCode || '').trim(); setInfo(debugCode ? `Reset code sent. [DEV: ${debugCode}]` : 'Reset code sent to your email.'); } catch (e: any) { setError(String(e?.message || 'Failed to send reset code.')); } finally { setIsSubmitting(false); } }; const verifyResetCode = async () => { clearMessages(); if (!canSubmitResetVerify()) { setError('A valid 6-digit code is required.'); return; } if (newPassword().trim() !== confirmPassword().trim()) { setError('Passwords do not match.'); return; } const resolvedPassword = newPassword().trim(); const resolvedChallengeId = challengeId().trim(); const resolvedCode = resetCode().trim(); const verifyPayload = { challengeId: resolvedChallengeId, challenge_id: resolvedChallengeId, code: resolvedCode, otp: resolvedCode, verificationCode: resolvedCode, verification_code: resolvedCode, newPassword: resolvedPassword, new_password: resolvedPassword, password: resolvedPassword, }; const attempts = ['/api/gateway/users/auth/internal/forgot-password/verify-code', '/api/gateway/auth/internal/forgot-password/verify-code']; setIsSubmitting(true); try { let payload: any = {}; let status = 500; let success = false; for (const url of attempts) { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(verifyPayload) }); status = response.status; payload = await response.json().catch(() => ({})); if (response.ok) { success = true; break; } } if (!success) { const fallback = status === 502 ? 'Verification service unavailable (502). Please retry in 1–2 minutes.' : 'Password reset failed.'; throw new Error(String(payload?.message || payload?.error || fallback).trim()); } setPassword(''); switchMode('login'); setInfo('Password reset successful. Please sign in with your new password.'); } catch (e: any) { setError(String(e?.message || 'Password reset failed.')); } finally { setIsSubmitting(false); } }; return (
{/* ── Left brand panel ── */}