2026-03-19 03:36:46 +01:00
|
|
|
|
import { useNavigate } from '@solidjs/router';
|
2026-03-24 02:36:40 +01:00
|
|
|
|
import { Show, createMemo, createSignal, onMount } from 'solid-js';
|
2026-03-20 22:37:17 +01:00
|
|
|
|
import { isExternalIdentity, pickManagementLoginError } from '~/lib/admin-auth';
|
2026-03-19 03:36:46 +01:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-24 15:57:28 +01:00
|
|
|
|
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';
|
|
|
|
|
|
|
2026-03-19 03:36:46 +01:00
|
|
|
|
export default function LoginPage() {
|
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
const [mode, setMode] = createSignal<AuthMode>('login');
|
|
|
|
|
|
const [resetStep, setResetStep] = createSignal<ResetStep>('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,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-03-24 15:57:28 +01:00
|
|
|
|
const clearMessages = () => { setError(''); setInfo(''); };
|
2026-03-19 03:36:46 +01:00
|
|
|
|
|
|
|
|
|
|
const resetPasswordFlow = () => {
|
|
|
|
|
|
setResetStep('request');
|
|
|
|
|
|
setResetCode('');
|
|
|
|
|
|
setChallengeId('');
|
|
|
|
|
|
setMaskedEmail('');
|
|
|
|
|
|
setNewPassword('');
|
|
|
|
|
|
setConfirmPassword('');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const switchMode = (nextMode: AuthMode) => {
|
|
|
|
|
|
clearMessages();
|
|
|
|
|
|
setMode(nextMode);
|
2026-03-24 15:57:28 +01:00
|
|
|
|
if (nextMode === 'login') resetPasswordFlow();
|
2026-03-19 03:36:46 +01:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
onMount(() => {
|
2026-03-24 15:57:28 +01:00
|
|
|
|
if (hasAdminSession()) navigate('/admin', { replace: true });
|
2026-03-19 03:36:46 +01:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const completeAdminLogin = () => {
|
|
|
|
|
|
setAdminSession();
|
|
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
|
|
|
const from = params.get('from');
|
2026-03-24 15:57:28 +01:00
|
|
|
|
navigate(from && from.startsWith('/admin') ? from : '/admin', { replace: true });
|
2026-03-19 03:36:46 +01:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const directSignIn = async () => {
|
|
|
|
|
|
clearMessages();
|
2026-03-24 15:57:28 +01:00
|
|
|
|
if (!canSubmitLoginCredentials()) { setError('Email and password are required.'); return; }
|
2026-03-19 03:36:46 +01:00
|
|
|
|
setIsSubmitting(true);
|
|
|
|
|
|
try {
|
2026-03-24 15:57:28 +01:00
|
|
|
|
const loginPayload = { email: email().trim().toLowerCase(), password: password(), loginTarget: 'admin' };
|
|
|
|
|
|
const attempts = ['/api/gateway/users/auth/internal/login', '/api/gateway/auth/internal/login'];
|
2026-03-20 22:37:17 +01:00
|
|
|
|
let payload: any = {};
|
|
|
|
|
|
let status = 500;
|
|
|
|
|
|
let success = false;
|
|
|
|
|
|
for (const url of attempts) {
|
|
|
|
|
|
const response = await fetch(url, {
|
|
|
|
|
|
method: 'POST',
|
2026-03-24 15:57:28 +01:00
|
|
|
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'x-portal-target': 'admin' },
|
2026-03-20 22:37:17 +01:00
|
|
|
|
credentials: 'include',
|
|
|
|
|
|
body: JSON.stringify(loginPayload),
|
|
|
|
|
|
});
|
|
|
|
|
|
status = response.status;
|
|
|
|
|
|
payload = await response.json().catch(() => ({}));
|
2026-03-24 15:57:28 +01:00
|
|
|
|
if (response.ok) { success = true; break; }
|
2026-03-20 22:37:17 +01:00
|
|
|
|
}
|
|
|
|
|
|
if (!success) {
|
2026-03-24 15:57:28 +01:00
|
|
|
|
const fallback = status === 502 ? 'Auth service unavailable (502). Please retry in 1–2 minutes.' : 'Sign in failed.';
|
2026-03-20 22:37:17 +01:00
|
|
|
|
throw new Error(pickManagementLoginError(payload) || fallback);
|
|
|
|
|
|
}
|
2026-03-24 15:57:28 +01:00
|
|
|
|
if (isExternalIdentity(payload)) throw new Error('External users cannot use this portal.');
|
2026-03-23 21:13:42 +01:00
|
|
|
|
const accessToken = String(payload?.access_token || payload?.accessToken || '').trim();
|
2026-03-24 15:57:28 +01:00
|
|
|
|
if (accessToken) sessionStorage.setItem('nxtgauge_admin_access_token', accessToken);
|
2026-03-19 03:36:46 +01:00
|
|
|
|
completeAdminLogin();
|
2026-03-24 15:57:28 +01:00
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
setError(String(e?.message || 'Sign in failed.'));
|
2026-03-19 03:36:46 +01:00
|
|
|
|
} finally {
|
|
|
|
|
|
setIsSubmitting(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const requestResetCode = async () => {
|
|
|
|
|
|
clearMessages();
|
2026-03-24 15:57:28 +01:00
|
|
|
|
if (!canSubmitResetRequest()) { setError('Email, new password, and confirm password are required.'); return; }
|
|
|
|
|
|
if (newPassword() !== confirmPassword()) { setError('Passwords do not match.'); return; }
|
2026-03-19 03:36:46 +01:00
|
|
|
|
const trimmedEmail = email().trim().toLowerCase();
|
|
|
|
|
|
const resolvedPassword = newPassword().trim();
|
2026-03-24 15:57:28 +01:00
|
|
|
|
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 }) },
|
2026-03-19 03:36:46 +01:00
|
|
|
|
];
|
|
|
|
|
|
setIsSubmitting(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
let payload: any = {};
|
|
|
|
|
|
let status = 500;
|
|
|
|
|
|
for (const attempt of attempts) {
|
2026-03-24 15:57:28 +01:00
|
|
|
|
const response = await fetch(attempt.url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: attempt.body });
|
2026-03-19 03:36:46 +01:00
|
|
|
|
status = response.status;
|
|
|
|
|
|
payload = await response.json().catch(() => ({}));
|
|
|
|
|
|
if (response.ok) break;
|
|
|
|
|
|
}
|
|
|
|
|
|
const nextChallengeId = pickChallengeId(payload);
|
|
|
|
|
|
if (!nextChallengeId) {
|
2026-03-24 15:57:28 +01:00
|
|
|
|
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());
|
2026-03-19 03:36:46 +01:00
|
|
|
|
}
|
|
|
|
|
|
setChallengeId(nextChallengeId);
|
|
|
|
|
|
setMaskedEmail(pickMaskedEmail(payload, trimmedEmail));
|
|
|
|
|
|
setResetStep('verify');
|
|
|
|
|
|
const debugCode = String(payload?.debugCode || payload?.data?.debugCode || '').trim();
|
2026-03-24 15:57:28 +01:00
|
|
|
|
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.'));
|
2026-03-19 03:36:46 +01:00
|
|
|
|
} finally {
|
|
|
|
|
|
setIsSubmitting(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const verifyResetCode = async () => {
|
|
|
|
|
|
clearMessages();
|
2026-03-24 15:57:28 +01:00
|
|
|
|
if (!canSubmitResetVerify()) { setError('A valid 6-digit code is required.'); return; }
|
|
|
|
|
|
if (newPassword().trim() !== confirmPassword().trim()) { setError('Passwords do not match.'); return; }
|
2026-03-19 03:36:46 +01:00
|
|
|
|
const resolvedPassword = newPassword().trim();
|
|
|
|
|
|
const resolvedChallengeId = challengeId().trim();
|
|
|
|
|
|
const resolvedCode = resetCode().trim();
|
|
|
|
|
|
const verifyPayload = {
|
2026-03-24 15:57:28 +01:00
|
|
|
|
challengeId: resolvedChallengeId, challenge_id: resolvedChallengeId,
|
|
|
|
|
|
code: resolvedCode, otp: resolvedCode, verificationCode: resolvedCode, verification_code: resolvedCode,
|
|
|
|
|
|
newPassword: resolvedPassword, new_password: resolvedPassword, password: resolvedPassword,
|
2026-03-19 03:36:46 +01:00
|
|
|
|
};
|
2026-03-24 15:57:28 +01:00
|
|
|
|
const attempts = ['/api/gateway/users/auth/internal/forgot-password/verify-code', '/api/gateway/auth/internal/forgot-password/verify-code'];
|
2026-03-19 03:36:46 +01:00
|
|
|
|
setIsSubmitting(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
let payload: any = {};
|
|
|
|
|
|
let status = 500;
|
|
|
|
|
|
let success = false;
|
|
|
|
|
|
for (const url of attempts) {
|
2026-03-24 15:57:28 +01:00
|
|
|
|
const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(verifyPayload) });
|
2026-03-19 03:36:46 +01:00
|
|
|
|
status = response.status;
|
|
|
|
|
|
payload = await response.json().catch(() => ({}));
|
2026-03-24 15:57:28 +01:00
|
|
|
|
if (response.ok) { success = true; break; }
|
2026-03-19 03:36:46 +01:00
|
|
|
|
}
|
|
|
|
|
|
if (!success) {
|
2026-03-24 15:57:28 +01:00
|
|
|
|
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());
|
2026-03-19 03:36:46 +01:00
|
|
|
|
}
|
|
|
|
|
|
setPassword('');
|
|
|
|
|
|
switchMode('login');
|
|
|
|
|
|
setInfo('Password reset successful. Please sign in with your new password.');
|
2026-03-24 15:57:28 +01:00
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
setError(String(e?.message || 'Password reset failed.'));
|
2026-03-19 03:36:46 +01:00
|
|
|
|
} finally {
|
|
|
|
|
|
setIsSubmitting(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-03-24 15:57:28 +01:00
|
|
|
|
<div class="flex min-h-screen w-full overflow-hidden">
|
|
|
|
|
|
|
|
|
|
|
|
{/* ── Left brand panel ── */}
|
|
|
|
|
|
<div class="relative hidden flex-col justify-between overflow-hidden bg-[#0a1d37] p-10 lg:flex lg:w-[44%] xl:w-[42%]">
|
|
|
|
|
|
{/* Subtle radial glow */}
|
|
|
|
|
|
<div class="pointer-events-none absolute -top-32 -left-32 h-96 w-96 rounded-full bg-[#fd6216]/10 blur-3xl" />
|
|
|
|
|
|
<div class="pointer-events-none absolute bottom-0 right-0 h-72 w-72 rounded-full bg-[#fd6216]/5 blur-2xl" />
|
|
|
|
|
|
|
|
|
|
|
|
{/* Logo */}
|
|
|
|
|
|
<div class="relative z-10">
|
|
|
|
|
|
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="h-10 w-auto brightness-0 invert" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Headline */}
|
|
|
|
|
|
<div class="relative z-10 space-y-6">
|
|
|
|
|
|
<span class="inline-flex items-center gap-2 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-[11px] font-bold uppercase tracking-widest text-orange-300">
|
|
|
|
|
|
<span class="h-1.5 w-1.5 rounded-full bg-orange-400" />
|
|
|
|
|
|
Internal Admin Portal
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<h1 class="text-4xl font-extrabold leading-tight text-white xl:text-[44px]">
|
|
|
|
|
|
Manage everything<br />from one place.
|
|
|
|
|
|
</h1>
|
|
|
|
|
|
<p class="text-[15px] leading-relaxed text-slate-400">
|
|
|
|
|
|
Roles, approvals, users, dashboards — all controlled from a single secure control panel.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Feature list */}
|
|
|
|
|
|
<ul class="space-y-3 text-sm text-slate-300">
|
|
|
|
|
|
{(['Role & permission management', 'Approval workflow control', 'User & company oversight', 'Dashboard configuration'] as const).map((item) => (
|
|
|
|
|
|
<li class="flex items-center gap-3">
|
|
|
|
|
|
<span class="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-[#fd6216]/20 text-[#fd6216]">
|
|
|
|
|
|
<svg width="10" height="8" viewBox="0 0 10 8" fill="none"><path d="M1 4l2.5 2.5L9 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
{item}
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Bottom tag */}
|
|
|
|
|
|
<div class="relative z-10 rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-[13px] text-slate-400">
|
|
|
|
|
|
🔒 Secured with internal access policies. Authorised personnel only.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* ── Right form panel ── */}
|
|
|
|
|
|
<div class="flex flex-1 items-center justify-center bg-[#f5f6fa] px-4 py-12 sm:px-8">
|
|
|
|
|
|
<div class="w-full max-w-[420px]">
|
|
|
|
|
|
|
|
|
|
|
|
{/* Mobile logo */}
|
|
|
|
|
|
<div class="mb-8 flex justify-center lg:hidden">
|
|
|
|
|
|
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="h-9 w-auto" />
|
2026-03-24 02:36:40 +01:00
|
|
|
|
</div>
|
2026-03-19 03:36:46 +01:00
|
|
|
|
|
2026-03-24 15:57:28 +01:00
|
|
|
|
{/* Card */}
|
|
|
|
|
|
<div class="rounded-2xl border border-gray-200 bg-white p-8 shadow-sm">
|
|
|
|
|
|
|
|
|
|
|
|
<div class="mb-7">
|
|
|
|
|
|
<h2 class="text-2xl font-bold text-gray-900">
|
|
|
|
|
|
{mode() === 'login' ? 'Sign in' : 'Reset password'}
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<p class="mt-1 text-sm text-gray-500">
|
|
|
|
|
|
{mode() === 'login' ? 'Internal team access only.' : 'Enter your email and set a new password.'}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="space-y-4">
|
|
|
|
|
|
|
|
|
|
|
|
{/* Email */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class={labelCls}>Email</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="email"
|
|
|
|
|
|
value={email()}
|
|
|
|
|
|
onInput={(e) => { setEmail(e.currentTarget.value); clearMessages(); }}
|
|
|
|
|
|
placeholder="you@nxtgauge.com"
|
|
|
|
|
|
class={inputCls}
|
|
|
|
|
|
autocomplete="email"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-03-24 02:36:40 +01:00
|
|
|
|
|
2026-03-24 15:57:28 +01:00
|
|
|
|
{/* Login: Password */}
|
|
|
|
|
|
<Show when={mode() === 'login'}>
|
2026-03-24 02:36:40 +01:00
|
|
|
|
<div>
|
2026-03-24 15:57:28 +01:00
|
|
|
|
<div class="mb-1.5 flex items-center justify-between">
|
|
|
|
|
|
<label class={labelCls} style="margin-bottom:0">Password</label>
|
|
|
|
|
|
<button type="button" class="text-xs font-semibold text-[#fd6216] hover:underline" onClick={() => switchMode('reset')}>
|
|
|
|
|
|
Forgot password?
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-03-19 03:36:46 +01:00
|
|
|
|
<input
|
2026-03-24 15:57:28 +01:00
|
|
|
|
type="password"
|
|
|
|
|
|
value={password()}
|
|
|
|
|
|
onInput={(e) => { setPassword(e.currentTarget.value); clearMessages(); }}
|
|
|
|
|
|
placeholder="••••••••"
|
|
|
|
|
|
class={inputCls}
|
|
|
|
|
|
autocomplete="current-password"
|
2026-03-19 03:36:46 +01:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-03-24 15:57:28 +01:00
|
|
|
|
</Show>
|
2026-03-19 03:36:46 +01:00
|
|
|
|
|
2026-03-24 15:57:28 +01:00
|
|
|
|
{/* Reset: New + Confirm + Code */}
|
|
|
|
|
|
<Show when={mode() === 'reset'}>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class={labelCls}>New password</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="password"
|
|
|
|
|
|
value={newPassword()}
|
|
|
|
|
|
onInput={(e) => { setNewPassword(e.currentTarget.value); clearMessages(); }}
|
|
|
|
|
|
placeholder="Minimum 8 characters"
|
|
|
|
|
|
class={inputCls}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class={labelCls}>Confirm password</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="password"
|
|
|
|
|
|
value={confirmPassword()}
|
|
|
|
|
|
onInput={(e) => { setConfirmPassword(e.currentTarget.value); clearMessages(); }}
|
|
|
|
|
|
placeholder="Repeat new password"
|
|
|
|
|
|
class={inputCls}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Show when={resetStep() === 'verify'}>
|
2026-03-24 02:36:40 +01:00
|
|
|
|
<div>
|
2026-03-24 15:57:28 +01:00
|
|
|
|
<label class={labelCls}>Verification code</label>
|
2026-03-19 03:36:46 +01:00
|
|
|
|
<input
|
2026-03-24 15:57:28 +01:00
|
|
|
|
type="text"
|
|
|
|
|
|
inputMode="numeric"
|
|
|
|
|
|
maxLength={6}
|
|
|
|
|
|
value={resetCode()}
|
|
|
|
|
|
onInput={(e) => { setResetCode(e.currentTarget.value.replace(/\D/g, '').slice(0, 6)); clearMessages(); }}
|
|
|
|
|
|
placeholder="000000"
|
|
|
|
|
|
class={`${inputCls} text-center text-lg tracking-[0.3em]`}
|
2026-03-19 03:36:46 +01:00
|
|
|
|
/>
|
2026-03-24 15:57:28 +01:00
|
|
|
|
<p class="mt-2 rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs text-emerald-700">
|
|
|
|
|
|
Code sent to <span class="font-semibold">{maskedEmail() || email()}</span>
|
|
|
|
|
|
</p>
|
2026-03-19 03:36:46 +01:00
|
|
|
|
</div>
|
2026-03-24 02:36:40 +01:00
|
|
|
|
</Show>
|
2026-03-24 15:57:28 +01:00
|
|
|
|
</Show>
|
2026-03-24 02:36:40 +01:00
|
|
|
|
|
2026-03-24 15:57:28 +01:00
|
|
|
|
{/* Error / Info */}
|
|
|
|
|
|
<Show when={error()}>
|
|
|
|
|
|
<p class="rounded-lg border border-red-200 bg-red-50 px-3 py-2.5 text-sm text-red-600">{error()}</p>
|
|
|
|
|
|
</Show>
|
|
|
|
|
|
<Show when={info()}>
|
|
|
|
|
|
<p class="rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2.5 text-sm text-emerald-700">{info()}</p>
|
|
|
|
|
|
</Show>
|
2026-03-24 02:36:40 +01:00
|
|
|
|
|
2026-03-24 15:57:28 +01:00
|
|
|
|
{/* Submit button */}
|
2026-03-24 02:36:40 +01:00
|
|
|
|
<Show when={mode() === 'login'}>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
2026-03-24 15:57:28 +01:00
|
|
|
|
class="mt-1 h-11 w-full rounded-lg bg-[#0a1d37] text-sm font-semibold text-white transition hover:bg-[#0f2a4e] disabled:cursor-not-allowed disabled:opacity-50"
|
|
|
|
|
|
disabled={isSubmitting()}
|
2026-03-24 02:36:40 +01:00
|
|
|
|
onClick={directSignIn}
|
|
|
|
|
|
>
|
2026-03-24 15:57:28 +01:00
|
|
|
|
{isSubmitting() ? 'Signing in…' : 'Sign in'}
|
2026-03-24 02:36:40 +01:00
|
|
|
|
</button>
|
|
|
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
|
|
<Show when={mode() === 'reset'}>
|
2026-03-24 15:57:28 +01:00
|
|
|
|
<Show when={resetStep() === 'request'} fallback={
|
2026-03-24 02:36:40 +01:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
2026-03-24 15:57:28 +01:00
|
|
|
|
class="mt-1 h-11 w-full rounded-lg bg-[#0a1d37] text-sm font-semibold text-white transition hover:bg-[#0f2a4e] disabled:cursor-not-allowed disabled:opacity-50"
|
2026-03-24 02:36:40 +01:00
|
|
|
|
disabled={isSubmitting() || !canSubmitResetVerify()}
|
|
|
|
|
|
onClick={verifyResetCode}
|
|
|
|
|
|
>
|
2026-03-24 15:57:28 +01:00
|
|
|
|
{isSubmitting() ? 'Resetting…' : 'Verify & reset password'}
|
2026-03-24 02:36:40 +01:00
|
|
|
|
</button>
|
2026-03-24 15:57:28 +01:00
|
|
|
|
}>
|
2026-03-24 02:36:40 +01:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
2026-03-24 15:57:28 +01:00
|
|
|
|
class="mt-1 h-11 w-full rounded-lg bg-[#0a1d37] text-sm font-semibold text-white transition hover:bg-[#0f2a4e] disabled:cursor-not-allowed disabled:opacity-50"
|
2026-03-24 02:36:40 +01:00
|
|
|
|
disabled={isSubmitting() || !canSubmitResetRequest()}
|
|
|
|
|
|
onClick={requestResetCode}
|
|
|
|
|
|
>
|
2026-03-24 15:57:28 +01:00
|
|
|
|
{isSubmitting() ? 'Sending code…' : 'Send reset code'}
|
2026-03-24 02:36:40 +01:00
|
|
|
|
</button>
|
|
|
|
|
|
</Show>
|
2026-03-24 15:57:28 +01:00
|
|
|
|
<button type="button" class="w-full text-center text-sm font-medium text-gray-500 hover:text-gray-700" onClick={() => switchMode('login')}>
|
|
|
|
|
|
← Back to sign in
|
|
|
|
|
|
</button>
|
2026-03-24 02:36:40 +01:00
|
|
|
|
</Show>
|
|
|
|
|
|
|
2026-03-24 15:57:28 +01:00
|
|
|
|
</div>
|
2026-03-24 02:36:40 +01:00
|
|
|
|
</div>
|
2026-03-24 15:57:28 +01:00
|
|
|
|
|
|
|
|
|
|
<p class="mt-6 text-center text-xs text-gray-400">
|
|
|
|
|
|
© {new Date().getFullYear()} Nxtgauge. Internal use only.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
2026-03-19 03:36:46 +01:00
|
|
|
|
</div>
|
2026-03-24 15:57:28 +01:00
|
|
|
|
</div>
|
2026-03-19 03:36:46 +01:00
|
|
|
|
);
|
|
|
|
|
|
}
|