diff --git a/src/lib/auth.tsx b/src/lib/auth.tsx index b7dce4e..adde904 100644 --- a/src/lib/auth.tsx +++ b/src/lib/auth.tsx @@ -40,6 +40,10 @@ function normalizeRoleValue(value: unknown): string { return String(value || '').trim().toUpperCase().replace(/\s+/g, '_'); } +function isJobSeekerRole(roleKey: string): boolean { + return normalizeRoleValue(roleKey) === 'JOB_SEEKER'; +} + function getStoredPreferredRole(emailHint?: string): string | null { if (typeof window === 'undefined') return null; const keys = ['nxtgauge_signup_profile_v1', 'nxtgauge_auth_user', 'nxtgauge_user']; @@ -65,10 +69,13 @@ function getStoredPreferredRole(emailHint?: string): string | null { function resolveActiveRole(rawBackendRole: unknown, emailHint?: string): string { const backendRole = normalizeRoleValue(rawBackendRole); - if (backendRole) return backendRole; const preferredRole = getStoredPreferredRole(emailHint); + if (backendRole && preferredRole && isJobSeekerRole(backendRole) && !isJobSeekerRole(preferredRole)) { + return preferredRole; + } + if (backendRole) return backendRole; if (preferredRole) return preferredRole; - return preferredRole || 'JOB_SEEKER'; + return ''; } async function fetchSession(): Promise { @@ -144,14 +151,16 @@ export function useAuth() { export function RequireAuth(props: ParentProps<{ fallback?: string }>) { const navigate = useNavigate(); const auth = useAuth(); + const fallback = props.fallback || '/login'; - if (auth.isLoading()) { - return
Loading...
; - } + createEffect(() => { + if (typeof window === 'undefined') return; + if (!auth.isAuthenticated()) { + navigate(fallback, { replace: true }); + } + }); if (!auth.isAuthenticated()) { - const fallback = props.fallback || '/login'; - navigate(fallback, { replace: true }); return null; } diff --git a/src/routes/dashboard.tsx b/src/routes/dashboard.tsx index 0215e90..959fadb 100644 --- a/src/routes/dashboard.tsx +++ b/src/routes/dashboard.tsx @@ -9,7 +9,7 @@ import { onMount, } from "solid-js"; import { useNavigate } from "@solidjs/router"; -import { useAuth, RequireAuth } from "~/lib/auth"; +import { useAuth, RequireAuth, getToken } from "~/lib/auth"; import DashboardDesignPreview from "~/components/admin/DashboardDesignPreview"; import DashboardShell from "~/components/DashboardShell"; import ProfilePage from "~/components/dashboard/ProfilePage"; @@ -317,6 +317,25 @@ function getNameFromStorage(): string { return "User"; } +function getEmailFromStorage(): string { + if (typeof window === "undefined") return ""; + const keys = ["nxtgauge_signup_profile_v1", "nxtgauge_auth_user", "nxtgauge_user"]; + for (const key of keys) { + try { + const raw = window.localStorage.getItem(key) || window.sessionStorage.getItem(key); + if (!raw) continue; + const parsed = JSON.parse(raw); + const email = String(parsed?.email || parsed?.user?.email || "") + .trim() + .toLowerCase(); + if (email) return email; + } catch { + // ignore invalid payload + } + } + return ""; +} + const EXPLORE_ROLES = [ { key: "PHOTOGRAPHER", name: "Photographer" }, { key: "MAKEUP_ARTIST", name: "Makeup Artist" }, @@ -369,6 +388,14 @@ function asStringArray(value: unknown): string[] { return value.map((item) => String(item || "").trim()).filter(Boolean); } +function firstNonJobSeekerRole(roleKeys: string[]): RoleKey | null { + for (const roleKey of roleKeys) { + const normalized = normalizeRole(roleKey); + if (normalized !== "JOB_SEEKER") return normalized; + } + return null; +} + function getInitialRoleFromStorage(): RoleKey { if (typeof window === "undefined") return "JOB_SEEKER"; const keys = ["nxtgauge_signup_profile_v1", "nxtgauge_auth_user", "nxtgauge_user"]; @@ -589,6 +616,7 @@ export default function RuntimeDashboardPage() { const [userName, setUserName] = createSignal("User"); const [userId, setUserId] = createSignal(""); const [urlRoleLocked, setUrlRoleLocked] = createSignal(false); + const [roleReconcileAttempted, setRoleReconcileAttempted] = createSignal(false); onMount(() => { setHydrated(true); @@ -615,10 +643,101 @@ export default function RuntimeDashboardPage() { if (u && !urlRoleLocked()) { if (u.full_name && userName() === "User") setUserName(u.full_name); if (u.id && !userId()) setUserId(u.id); - if (u.active_role) setRole(resolveRoleForDashboard(u.active_role)); + if (u.active_role) { + const authRole = resolveRoleForDashboard(u.active_role); + const preferredRole = getInitialRoleFromStorage(); + if (authRole === "JOB_SEEKER" && preferredRole !== "JOB_SEEKER") { + setRole(preferredRole); + } else { + setRole(authRole); + } + } } }); + createEffect(() => { + if (urlRoleLocked() || roleReconcileAttempted()) return; + if (role() !== "JOB_SEEKER") { + setRoleReconcileAttempted(true); + return; + } + + setRoleReconcileAttempted(true); + void (async () => { + try { + const authEmail = String(auth.user()?.email || "") + .trim() + .toLowerCase(); + const email = authEmail || getEmailFromStorage(); + if (!email) return; + + const checkRes = await fetch("/api/auth/check-email", { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + credentials: "include", + body: JSON.stringify({ email }), + }); + const checkPayload = await checkRes.json().catch(() => ({})); + if (!checkRes.ok || !checkPayload?.exists) return; + + const discoveredRoles = asStringArray(checkPayload?.roles).map((item) => + normalizeRole(String(item)) + ); + const discoveredActive = normalizeRole( + String(checkPayload?.active_role || checkPayload?.role || "") + ); + const targetRole = + discoveredActive !== "JOB_SEEKER" + ? discoveredActive + : firstNonJobSeekerRole(discoveredRoles); + if (!targetRole || targetRole === "JOB_SEEKER") return; + + const token = getToken(); + if (token) { + const switchRes = await fetch("/api/auth/switch-role", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + Authorization: `Bearer ${token}`, + }, + credentials: "include", + body: JSON.stringify({ role_key: targetRole }), + }); + const switchPayload = await switchRes.json().catch(() => ({})); + const switchedToken = String(switchPayload?.access_token || "").trim(); + if (switchRes.ok && switchedToken && typeof window !== "undefined") { + window.sessionStorage.setItem("nxtgauge_access_token", switchedToken); + window.sessionStorage.setItem("nxtgauge_frontend_access_token", switchedToken); + } + } + + if (typeof window !== "undefined") { + const storageKeys = ["nxtgauge_signup_profile_v1", "nxtgauge_auth_user", "nxtgauge_user"]; + for (const key of storageKeys) { + const raw = window.localStorage.getItem(key); + if (!raw) continue; + try { + const parsed = JSON.parse(raw); + parsed.active_role = targetRole; + parsed.selectedProfessionalRole = targetRole; + parsed.role = String(targetRole).toLowerCase(); + parsed.roleKey = String(targetRole).toLowerCase(); + window.localStorage.setItem(key, JSON.stringify(parsed)); + } catch { + // ignore malformed storage payloads + } + } + } + + setRole(targetRole); + navigate(`/dashboard?role=${encodeURIComponent(targetRole)}`, { replace: true }); + } catch { + // best-effort reconciliation only + } + })(); + }); + const [bundle] = createResource(() => role(), loadRoleBundle); const activeSidebarKey = createMemo(() => normalizeSidebarKey(activeSidebar())); diff --git a/src/routes/dashboard/index.tsx b/src/routes/dashboard/index.tsx index 72674ef..33ddb0f 100644 --- a/src/routes/dashboard/index.tsx +++ b/src/routes/dashboard/index.tsx @@ -1 +1,3 @@ -export { default } from "../dashboard"; +import RuntimeDashboardPage from "../dashboard"; + +export default RuntimeDashboardPage; diff --git a/src/routes/login.tsx b/src/routes/login.tsx index f8c4641..2c74bd4 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -15,6 +15,32 @@ function normalizeRoleValue(value: unknown): string { .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"]; @@ -47,7 +73,7 @@ function resolveActiveRole(rawBackendRole: unknown, emailHint?: string): string if (backendRole) return backendRole; const preferredRole = getStoredPreferredRole(emailHint); if (preferredRole) return preferredRole; - return preferredRole || "JOB_SEEKER"; + return ""; } function makeCaptcha() { @@ -160,9 +186,17 @@ export default function LoginRoute() { const fullName = String(user?.full_name || user?.fullName || "").trim(); const [firstName, ...rest] = fullName.split(" "); const lastName = rest.join(" "); - // Trust the passed-in role — don't re-resolve from storage (which can be stale) - const normalizedRole = normalizeRoleValue(user?.active_role || user?.role || "JOB_SEEKER"); - const storedRole = normalizedRole.toLowerCase(); + // 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 || "", @@ -175,7 +209,7 @@ export default function LoginRoute() { roleKey: storedRole, role: storedRole, active_role: normalizedRole, - selectedProfessionalRole: normalizedRole, + selectedProfessionalRole: selectedRoleForStorage, user, }; if (typeof window !== "undefined") { @@ -226,36 +260,106 @@ export default function LoginRoute() { } const accessToken = String(data?.access_token || "").trim(); - if (typeof window !== "undefined" && accessToken) { - window.sessionStorage.setItem("nxtgauge_access_token", accessToken); - window.sessionStorage.setItem("nxtgauge_frontend_access_token", accessToken); - } - const user_roles = data?.user?.roles || []; - const selectedRole = getStoredPreferredRole(data?.user?.email || email().trim().toLowerCase()); - const backendActiveRole = data?.user?.active_role; - - // Use selected role from storage if available, otherwise use backend role - // If backend returns JOB_SEEKER but user has professional roles, pick the first professional role - let resolvedActiveRole = backendActiveRole; - if (normalizeRoleValue(backendActiveRole) === "JOB_SEEKER" && user_roles.length > 0) { - // Find first non-JOB_SEEKER role from backend's roles array - for (const role of user_roles) { - if (normalizeRoleValue(role) !== "JOB_SEEKER") { - resolvedActiveRole = role; - break; + 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. } } - if (!resolvedActiveRole) { - resolvedActiveRole = selectedRole || backendActiveRole || "JOB_SEEKER"; + + // 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 normalizedEmail = email().trim().toLowerCase(); - const finalRole = normalizeRoleValue(resolvedActiveRole); const userPayload = { id: String(data?.user?.id || ""), - email: String(data?.user?.email || normalizedEmail), - full_name: String(data?.user?.full_name || ""), + 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), }; @@ -263,7 +367,7 @@ export default function LoginRoute() { if (auth.refreshUser) { auth.refreshUser(userPayload); } - navigate(`/dashboard?role=${encodeURIComponent(normalizeRoleValue(resolvedActiveRole))}`, { replace: true }); + navigate(`/dashboard?role=${encodeURIComponent(finalRole)}`, { replace: true }); } catch { setError("Network error during login. Please try again."); } finally {