diff --git a/src/routes/login.tsx b/src/routes/login.tsx index 04f143c..ea4c539 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -24,6 +24,11 @@ function pickMaskedEmail(payload: any, fallback: string): string { 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'); @@ -45,7 +50,6 @@ export default function LoginPage() { newPassword().trim().length > 0 && confirmPassword().trim().length > 0, ); - const canSubmitResetVerify = createMemo( () => challengeId().trim().length > 0 && resetCode().trim().length === 6, ); @@ -53,10 +57,7 @@ export default function LoginPage() { () => email().trim().length > 0 && password().trim().length > 0, ); - const clearMessages = () => { - setError(''); - setInfo(''); - }; + const clearMessages = () => { setError(''); setInfo(''); }; const resetPasswordFlow = () => { setResetStep('request'); @@ -70,86 +71,51 @@ export default function LoginPage() { const switchMode = (nextMode: AuthMode) => { clearMessages(); setMode(nextMode); - if (nextMode === 'login') { - resetPasswordFlow(); - } + if (nextMode === 'login') resetPasswordFlow(); }; onMount(() => { - if (hasAdminSession()) { - navigate('/admin', { replace: true }); - } + if (hasAdminSession()) navigate('/admin', { replace: true }); }); const completeAdminLogin = () => { setAdminSession(); const params = new URLSearchParams(window.location.search); const from = params.get('from'); - const nextPath = from && from.startsWith('/admin') ? from : '/admin'; - navigate(nextPath, { replace: true }); + navigate(from && from.startsWith('/admin') ? from : '/admin', { replace: true }); }; const directSignIn = async () => { clearMessages(); - if (!canSubmitLoginCredentials()) { - setError('Email and password are required.'); - return; - } - + if (!canSubmitLoginCredentials()) { setError('Email and password are required.'); return; } setIsSubmitting(true); try { - const loginPayload = { - email: email().trim().toLowerCase(), - password: password(), - loginTarget: 'admin', - }; - - const attempts: string[] = [ - '/api/gateway/users/auth/internal/login', - '/api/gateway/auth/internal/login', - ]; - + 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', - }, + 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 (response.ok) { success = true; break; } } - if (!success) { - const fallback = status === 502 - ? 'Auth service is temporarily unavailable (502). Please retry in 1-2 minutes.' - : 'Sign in failed.'; + 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 are not allowed on management login. Please use the external user login.'); - } - + if (isExternalIdentity(payload)) throw new Error('External users cannot use this portal.'); const accessToken = String(payload?.access_token || payload?.accessToken || '').trim(); - if (accessToken && typeof sessionStorage !== 'undefined') { - sessionStorage.setItem('nxtgauge_admin_access_token', accessToken); - } - + if (accessToken) sessionStorage.setItem('nxtgauge_admin_access_token', accessToken); completeAdminLogin(); - } catch (nextError: any) { - setError(String(nextError?.message || 'Sign in failed.')); + } catch (e: any) { + setError(String(e?.message || 'Sign in failed.')); } finally { setIsSubmitting(false); } @@ -157,77 +123,39 @@ export default function LoginPage() { 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; - } - + 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: Array<{ url: string; body: string }> = [ - { - 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 }), - }, + 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, - }); + 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 fallbackMessage = - status === 502 - ? 'Verification service is temporarily unavailable (502). Please retry in 1-2 minutes.' - : 'Failed to send reset code.'; - throw new Error(String(payload?.message || payload?.error || fallbackMessage).trim()); + 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 CODE: ${debugCode}]` : 'Reset code sent to your email.'); - } catch (nextError: any) { - setError(String(nextError?.message || 'Failed to send reset code.')); + 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); } @@ -235,228 +163,238 @@ export default function LoginPage() { const verifyResetCode = async () => { clearMessages(); - if (!canSubmitResetVerify()) { - setError('A valid 6-digit verification code is required.'); - return; - } - - if (newPassword().trim() !== confirmPassword().trim()) { - setError('Passwords do not match.'); - return; - } - + 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, + challengeId: resolvedChallengeId, challenge_id: resolvedChallengeId, + code: resolvedCode, otp: resolvedCode, verificationCode: resolvedCode, verification_code: resolvedCode, + newPassword: resolvedPassword, new_password: resolvedPassword, password: resolvedPassword, }; - - const attempts: string[] = [ - '/api/gateway/users/auth/internal/forgot-password/verify-code', - '/api/gateway/auth/internal/forgot-password/verify-code', - ]; - + 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), - }); + 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 (response.ok) { success = true; break; } } - if (!success) { - const fallbackMessage = - status === 502 - ? 'Verification service is temporarily unavailable (502). Please retry in 1-2 minutes.' - : 'Password reset failed.'; - throw new Error(String(payload?.message || payload?.error || fallbackMessage).trim()); + 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 (nextError: any) { - setError(String(nextError?.message || 'Password reset failed.')); + } catch (e: any) { + setError(String(e?.message || 'Password reset failed.')); } finally { setIsSubmitting(false); } }; return ( -
-
- -
-
-

{mode() === 'login' ? 'Sign In' : 'Reset Password'}

-

{mode() === 'login' ? 'Internal team access only.' : 'Use your internal email to reset access.'}

+ {/* Card */} +
-
-
-
- - { - setEmail(event.currentTarget.value); - clearMessages(); - }} - placeholder="Enter your email" - class="h-11 w-full rounded-xl border border-[#cfd4e3] bg-white px-3.5 text-[15px] text-[#101228] outline-none transition focus:border-[#fd6216] focus:ring-2 focus:ring-[#ffd8c3]" - /> -
+
+

+ {mode() === 'login' ? 'Sign in' : 'Reset password'} +

+

+ {mode() === 'login' ? 'Internal team access only.' : 'Enter your email and set a new password.'} +

+
- -
-
- - -
- { - setPassword(event.currentTarget.value); - clearMessages(); - }} - placeholder="Enter your password" - class="h-11 w-full rounded-xl border border-[#cfd4e3] bg-white px-3.5 text-[15px] text-[#101228] outline-none transition focus:border-[#fd6216] focus:ring-2 focus:ring-[#ffd8c3]" - /> -
-
+
- -
-
- - { - setNewPassword(event.currentTarget.value); - clearMessages(); - }} - placeholder="Minimum 8 characters" - class="h-11 w-full rounded-xl border border-[#cfd4e3] bg-white px-3.5 text-[15px] text-[#101228] outline-none transition focus:border-[#fd6216] focus:ring-2 focus:ring-[#ffd8c3]" - /> -
-
- - { - setConfirmPassword(event.currentTarget.value); - clearMessages(); - }} - placeholder="Confirm new password" - class="h-11 w-full rounded-xl border border-[#cfd4e3] bg-white px-3.5 text-[15px] text-[#101228] outline-none transition focus:border-[#fd6216] focus:ring-2 focus:ring-[#ffd8c3]" - /> -
- -
- - { - setResetCode(event.currentTarget.value.replace(/\D/g, '').slice(0, 6)); - clearMessages(); - }} - placeholder="000000" - class="h-11 w-full rounded-xl border border-[#cfd4e3] bg-white px-3.5 text-center text-[18px] tracking-[0.2em] text-[#101228] outline-none transition focus:border-[#fd6216] focus:ring-2 focus:ring-[#ffd8c3]" - /> -

- Code sent to {maskedEmail() || email()}. -

-
-
-
-
+ {/* Email */} +
+ + { setEmail(e.currentTarget.value); clearMessages(); }} + placeholder="you@nxtgauge.com" + class={inputCls} + autocomplete="email" + />
+ {/* Login: Password */} + +
+
+ + +
+ { setPassword(e.currentTarget.value); clearMessages(); }} + placeholder="••••••••" + class={inputCls} + autocomplete="current-password" + /> +
+
+ + {/* Reset: New + Confirm + Code */} + +
+ + { setNewPassword(e.currentTarget.value); clearMessages(); }} + placeholder="Minimum 8 characters" + class={inputCls} + /> +
+
+ + { setConfirmPassword(e.currentTarget.value); clearMessages(); }} + placeholder="Repeat new password" + class={inputCls} + /> +
+ +
+ + { setResetCode(e.currentTarget.value.replace(/\D/g, '').slice(0, 6)); clearMessages(); }} + placeholder="000000" + class={`${inputCls} text-center text-lg tracking-[0.3em]`} + /> +

+ Code sent to {maskedEmail() || email()} +

+
+
+
+ + {/* Error / Info */} + +

{error()}

+
+ +

{info()}

+
+ + {/* Submit button */} -
- -
- - {isSubmitting() ? 'Resetting Password...' : 'Verify & Reset Password'} + {isSubmitting() ? 'Resetting…' : 'Verify & reset password'} - )}> + }> +
- - {info() ?

{info()}

: null} - {error() ?

{error()}

: null} +
-
+ +

+ © {new Date().getFullYear()} Nxtgauge. Internal use only. +

+
-
+ ); }