import { A, useNavigate } from "@solidjs/router"; import { createMemo, createSignal, For, Show } from "solid-js"; import { useAuth } from "~/lib/auth"; import PublicBackground from "~/components/PublicBackground"; import PublicHeader from "~/components/PublicHeader"; import CaptchaCanvas from "~/components/CaptchaCanvas"; import { isValidEmail } from "~/lib/form-validation"; type RoleKey = "company" | "job_seeker" | "professional" | "customer"; function normalizeRoleValue(value: unknown): string { return String(value || "") .trim() .toUpperCase() .replace(/\s+/g, "_"); } function extractRoleKey(value: unknown): string { const normalized = normalizeRoleValue(value); if (normalized && normalized !== "[OBJECT_OBJECT]") return normalized; if (!value || typeof value !== "object") return normalized; const maybe = value as Record; return normalizeRoleValue( maybe.key ?? maybe.role_key ?? maybe.roleKey ?? maybe.name ?? maybe.role ?? maybe.id ); } function normalizeRoleKeysList(value: unknown): string[] { if (!Array.isArray(value)) return []; return value.map((item) => extractRoleKey(item)).filter(Boolean); } function isJobSeekerRole(roleKey: string): boolean { return normalizeRoleValue(roleKey) === "JOB_SEEKER"; } function firstNonJobSeekerRole(roleKeys: string[]): string | null { for (const roleKey of roleKeys) { if (!isJobSeekerRole(roleKey)) return roleKey; } return null; } function getStoredPreferredRole(emailHint?: string): string | null { if (typeof window === "undefined") return null; const keys = ["nxtgauge_signup_profile_v1", "nxtgauge_auth_user", "nxtgauge_user"]; for (const key of keys) { const raw = window.localStorage.getItem(key); if (!raw) continue; try { const parsed = JSON.parse(raw) as Record; const storedEmail = String(parsed?.email || "") .trim() .toLowerCase(); if (emailHint && storedEmail && storedEmail !== emailHint.trim().toLowerCase()) continue; const selectedProfessionalRole = normalizeRoleValue(parsed?.selectedProfessionalRole); if (selectedProfessionalRole) return selectedProfessionalRole; const activeRole = normalizeRoleValue(parsed?.active_role || parsed?.role); if (activeRole) return activeRole; } catch { // Ignore malformed local storage payloads. } } return null; } function resolveActiveRole(rawBackendRole: unknown, emailHint?: string): string { const backendRole = normalizeRoleValue(rawBackendRole); if (backendRole) return backendRole; const preferredRole = getStoredPreferredRole(emailHint); if (preferredRole) return preferredRole; return ""; } function makeCaptcha() { const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; return Array.from({ length: 6 }, () => chars[Math.floor(Math.random() * chars.length)]).join(""); } function PasswordVisibilityIcon(props: { visible: boolean }) { if (props.visible) { return ( ); } return ( ); } export default function LoginRoute() { const navigate = useNavigate(); const auth = useAuth(); const [email, setEmail] = createSignal(""); const [password, setPassword] = createSignal(""); const [otp, setOtp] = createSignal(["", "", "", "", "", ""]); const [showVerify, setShowVerify] = createSignal(false); const [showPassword, setShowPassword] = createSignal(false); const [captcha, setCaptcha] = createSignal(makeCaptcha()); const [captchaInput, setCaptchaInput] = createSignal(""); const [error, setError] = createSignal(""); const [submitting, setSubmitting] = createSignal(false); const [roleGuess, setRoleGuess] = createSignal("job_seeker"); const [roleHint, setRoleHint] = createSignal(""); const [checkingRole, setCheckingRole] = createSignal(false); const otpCode = createMemo(() => otp().join("")); const formatRoleLabel = (value: string): string => String(value || "") .trim() .replace(/[_\s]+/g, " ") .toLowerCase() .replace(/\b\w/g, (ch) => ch.toUpperCase()); const lookupRoleByEmail = async (emailValue: string) => { const normalized = emailValue.trim().toLowerCase(); if (!normalized || !isValidEmail(normalized)) { setRoleHint(""); return; } setCheckingRole(true); try { const response = await fetch("/api/auth/check-email", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, credentials: "include", body: JSON.stringify({ email: normalized }), }); const payload = await response.json().catch(() => ({})); if (!response.ok || !payload?.exists) { setRoleHint(""); return; } const detectedRole = normalizeRoleValue( payload?.active_role || payload?.role || payload?.roles?.[0] ); if (!detectedRole) { const fallbackRole = normalizeRoleValue(getStoredPreferredRole(normalized)); setRoleHint(fallbackRole ? `Role: ${formatRoleLabel(fallbackRole)}` : "Role: Not assigned"); return; } setRoleHint(`Role: ${formatRoleLabel(detectedRole)}`); const roleLower = detectedRole.toLowerCase(); if ( roleLower === "company" || roleLower === "customer" || roleLower === "job_seeker" || roleLower === "professional" ) { setRoleGuess(roleLower as RoleKey); } } catch { setRoleHint(""); } finally { setCheckingRole(false); } }; const setOtpDigit = (index: number, value: string) => { const clean = value.replace(/\D/g, "").slice(0, 1); setOtp((prev) => { const next = prev.slice(); next[index] = clean; return next; }); // Defer focus until after SolidJS reactive flush so the next input exists in DOM queueMicrotask(() => { if (clean && index < 5) { const nextEl = document.querySelector(`#login-otp-${index + 1}`); if (nextEl) nextEl.focus(); } }); }; const saveUser = (user: any) => { const fullName = String(user?.full_name || user?.fullName || "").trim(); const [firstName, ...rest] = fullName.split(" "); const lastName = rest.join(" "); // Trust the passed-in role and never force a JOB_SEEKER fallback. const preferredRole = getStoredPreferredRole(String(user?.email || email())); const normalizedRole = normalizeRoleValue( user?.active_role || user?.role || preferredRole || "" ); const storedRole = normalizedRole ? normalizedRole.toLowerCase() : ""; const selectedRoleForStorage = isJobSeekerRole(normalizedRole) && preferredRole && !isJobSeekerRole(preferredRole) ? preferredRole : normalizedRole; const payload = { firstName: firstName || "", lastName: lastName || "", fullName: fullName || "", name: fullName || "", displayName: fullName || "", email: String(user?.email || email()) .trim() .toLowerCase(), roleKey: storedRole, role: storedRole, active_role: normalizedRole, selectedProfessionalRole: selectedRoleForStorage, user, }; if (typeof window !== "undefined") { window.localStorage.setItem("nxtgauge_auth_user", JSON.stringify(payload)); window.localStorage.setItem("nxtgauge_user", JSON.stringify(payload)); window.localStorage.setItem("nxtgauge_signup_profile_v1", JSON.stringify(payload)); } }; const login = async () => { if (submitting()) return; setError(""); if (!isValidEmail(email())) { setError("Enter a valid email address."); return; } if (!password().trim()) { setError("Password is required."); return; } // DEV bypass: skip CAPTCHA validation in development const isDev = typeof import.meta !== 'undefined' && import.meta.env?.DEV; if (!isDev && (!captchaInput().trim() || captchaInput().trim().toUpperCase() !== captcha().toUpperCase())) { setError("Captcha does not match. Please try again."); setCaptcha(makeCaptcha()); setCaptchaInput(""); return; } setSubmitting(true); try { const res = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, credentials: "include", body: JSON.stringify({ email: email().trim().toLowerCase(), password: password(), }), }); const data = await res.json().catch(() => ({})); if (!res.ok) { const code = String(data?.code || "").toUpperCase(); if (code === "EMAIL_NOT_VERIFIED") { setShowVerify(true); setError("Email not verified. Enter OTP sent to your inbox."); return; } setError(String(data?.error || data?.message || "Invalid login credentials.")); return; } const accessToken = String(data?.access_token || "").trim(); const normalizedEmail = email().trim().toLowerCase(); const userEmail = String(data?.user?.email || normalizedEmail).trim().toLowerCase(); const availableRoleKeys = normalizeRoleKeysList(data?.user?.roles || data?.roles); const backendActiveRoleKey = extractRoleKey( data?.user?.active_role || data?.active_role || data?.role || data?.role_code ) || firstNonJobSeekerRole(availableRoleKeys) || ""; const preferredRoleKey = getStoredPreferredRole(userEmail); let discoveredRoleKeys = [...availableRoleKeys]; let discoveredActiveRole = backendActiveRoleKey; if (discoveredRoleKeys.length === 0 || isJobSeekerRole(discoveredActiveRole)) { try { const checkRes = await fetch("/api/auth/check-email", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, credentials: "include", body: JSON.stringify({ email: userEmail }), }); const checkPayload = await checkRes.json().catch(() => ({})); if (checkRes.ok && checkPayload?.exists) { const checkRoles = normalizeRoleKeysList(checkPayload?.roles); if (checkRoles.length > 0) { discoveredRoleKeys = Array.from(new Set([...discoveredRoleKeys, ...checkRoles])); } const checkActive = extractRoleKey(checkPayload?.active_role || checkPayload?.role); if (checkActive) { discoveredActiveRole = checkActive; } } } catch { // Ignore check-email failures; continue with login payload roles. } } // Choose the role we *want* to activate: // 1) Prefer the stored role selection (from signup/switch services) when it isn't JOB_SEEKER. // If backend doesn't return roles, still attempt to activate it via switch-role. // 2) Otherwise, if backend returns JOB_SEEKER but other roles exist, pick the first non-JOB_SEEKER role. let requestedRoleKey: string | null = null; if ( preferredRoleKey && !isJobSeekerRole(preferredRoleKey) && (discoveredRoleKeys.length === 0 || discoveredRoleKeys.includes(preferredRoleKey)) ) { requestedRoleKey = preferredRoleKey; } else if (isJobSeekerRole(discoveredActiveRole)) { requestedRoleKey = firstNonJobSeekerRole(discoveredRoleKeys); } let finalAccessToken = accessToken; let desiredRoleKey = discoveredActiveRole; if (finalAccessToken && requestedRoleKey && requestedRoleKey !== discoveredActiveRole) { try { const switchRes = await fetch("/api/auth/switch-role", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", Authorization: `Bearer ${finalAccessToken}`, }, credentials: "include", body: JSON.stringify({ role_key: requestedRoleKey }), }); const switchPayload = await switchRes.json().catch(() => ({})); const switchedToken = String(switchPayload?.access_token || "").trim(); if (switchRes.ok && switchedToken) { finalAccessToken = switchedToken; desiredRoleKey = requestedRoleKey; } } catch { // Ignore switch failures; fall back to backend active role token. } } if (typeof window !== "undefined" && finalAccessToken) { window.sessionStorage.setItem("nxtgauge_access_token", finalAccessToken); window.sessionStorage.setItem("nxtgauge_frontend_access_token", finalAccessToken); } const finalRole = normalizeRoleValue( desiredRoleKey || backendActiveRoleKey || preferredRoleKey || firstNonJobSeekerRole(discoveredRoleKeys) || "" ); if (!finalRole) { setError("No active role is assigned to this account. Please contact support."); return; } const userPayload = { id: String(data?.user?.id || ""), email: userEmail, full_name: String(data?.user?.full_name || data?.user?.name || data?.name || ""), active_role: finalRole, email_verified: Boolean(data?.user?.email_verified ?? true), }; saveUser({ ...userPayload }); if (auth.refreshUser) { auth.refreshUser(userPayload); } navigate(`/dashboard?role=${encodeURIComponent(finalRole)}`, { replace: true }); } catch { setError("Network error during login. Please try again."); } finally { setSubmitting(false); } }; const resendOtp = async () => { if (submitting()) return; setError(""); setSubmitting(true); try { const res = await fetch("/api/auth/resend-otp", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, credentials: "include", body: JSON.stringify({ email: email().trim().toLowerCase() }), }); const data = await res.json().catch(() => ({})); if (!res.ok) { setError(String(data?.error || data?.message || "Unable to resend OTP.")); } } finally { setSubmitting(false); } }; const verifyThenLogin = async () => { if (submitting()) return; setError(""); if (otpCode().length !== 6) { setError("Enter a valid 6-digit OTP."); return; } setSubmitting(true); try { const verifyRes = await fetch("/api/auth/verify-email", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, credentials: "include", body: JSON.stringify({ otp: otpCode() }), }); const verifyData = await verifyRes.json().catch(() => ({})); if (!verifyRes.ok) { setError(String(verifyData?.error || verifyData?.message || "OTP verification failed.")); return; } await login(); } finally { setSubmitting(false); } }; if (typeof window !== 'undefined') { window.__captchaCode = captcha(); (window as any).__loginVerifyOtp = verifyThenLogin; (window as any).__setLoginOtp = setOtp; (window as any).__loginOtp = otp; } return (
Public Workspace

Public Workspace

Welcome Back To Nxtgauge

Sign in to manage your profile, portfolio, and verification in one place.

Sign In

{ const value = e.currentTarget.value; setEmail(value); void lookupRoleByEmail(value); }} onBlur={(e) => { void lookupRoleByEmail(e.currentTarget.value); }} placeholder="Enter your email" />

{email().trim() && isValidEmail(email()) ? "✓ Valid email format" : "• Enter a valid email format"}

{checkingRole() ? "Checking account role..." : `• ${roleHint()}`}

setPassword(e.currentTarget.value)} placeholder="Enter your password" />
setCaptchaInput(e.currentTarget.value)} placeholder="Enter captcha" />
index)}> {(index) => ( setOtpDigit(index, e.currentTarget.value)} /> )}

{error()}

); }