From 159b051ac824a485c6abf227d32fbdbca2c71ca9 Mon Sep 17 00:00:00 2001 From: Tracewebstudio Dev Date: Tue, 21 Apr 2026 21:50:57 +0200 Subject: [PATCH] fix: resolve role resolution priority - backend roles override localStorage - login.tsx: pass role via URL param to dashboard instead of relying on localStorage - dashboard.tsx: add urlRoleLocked signal to prevent auth effects from overriding URL-passed role - auth.tsx: trust passed-in role over re-reading from localStorage in saveUser --- src/lib/auth.tsx | 24 +++++++++++++++----- src/routes/dashboard.tsx | 19 +++++++++++----- src/routes/login.tsx | 48 ++++++++++++++++++++++++++-------------- 3 files changed, 63 insertions(+), 28 deletions(-) diff --git a/src/lib/auth.tsx b/src/lib/auth.tsx index db7db7a..b7dce4e 100644 --- a/src/lib/auth.tsx +++ b/src/lib/auth.tsx @@ -1,4 +1,4 @@ -import { createContext, createResource, createSignal, useContext, type ParentProps, type Accessor, type Setter } from 'solid-js'; +import { createContext, createEffect, createResource, createSignal, useContext, type ParentProps, type Accessor, type Setter } from 'solid-js'; import { useNavigate } from '@solidjs/router'; const API = '/api/gateway'; @@ -17,6 +17,7 @@ type AuthState = { isLoading: Accessor; logout: () => void; setUser: Setter; + refreshUser: (userData?: AuthUser | null) => void; }; const AuthContext = createContext(); @@ -99,14 +100,25 @@ async function fetchSession(): Promise { export function AuthProvider(props: ParentProps) { const [user, setUser] = createSignal(null); - const [session] = createResource(fetchSession); + const [session, { refetch: refetchSession }] = createResource(fetchSession); const isLoading = () => session.loading; const isAuthenticated = () => !!user() || !!getToken() || (!!session() && !session.error); - if (session()) { - setUser(session() as AuthUser | null); - } + createEffect(() => { + const sess = session(); + if (sess) { + setUser(sess); + } + }); + + const refreshUser = (userData?: AuthUser | null) => { + if (userData) { + setUser(userData); + } else { + refetchSession(); + } + }; const logout = () => { clearAuthStorage(); @@ -117,7 +129,7 @@ export function AuthProvider(props: ParentProps) { }; return ( - + {props.children} ); diff --git a/src/routes/dashboard.tsx b/src/routes/dashboard.tsx index 84a0cb8..0215e90 100644 --- a/src/routes/dashboard.tsx +++ b/src/routes/dashboard.tsx @@ -578,6 +578,7 @@ function mergeSidebar( return merged; } +// FIX_APPLIED_V13_ROLE_FROM_URL_PROTECTION export default function RuntimeDashboardPage() { const navigate = useNavigate(); const auth = useAuth(); @@ -587,23 +588,31 @@ export default function RuntimeDashboardPage() { const [activeTab, setActiveTab] = createSignal("overview"); const [userName, setUserName] = createSignal("User"); const [userId, setUserId] = createSignal(""); + const [urlRoleLocked, setUrlRoleLocked] = createSignal(false); onMount(() => { setHydrated(true); - const storedRole = getInitialRoleFromStorage(); - setRole(storedRole); + const urlParams = new URLSearchParams(window.location.search); + const roleParam = urlParams.get("role"); + if (roleParam) { + setUrlRoleLocked(true); + setRole(normalizeRole(roleParam)); + } else { + const storedRole = getInitialRoleFromStorage(); + setRole(storedRole); + } setUserName(getNameFromStorage()); if (auth.user()) { const u = auth.user()!; if (u.full_name) setUserName(u.full_name); if (u.id) setUserId(u.id); - if (u.active_role) setRole(resolveRoleForDashboard(u.active_role)); } }); createEffect(() => { const u = auth.user(); - if (u) { + // If role was explicitly set from URL, never let auth.user() override it + 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)); @@ -619,7 +628,7 @@ export default function RuntimeDashboardPage() { createEffect(() => { const runtimeRole = bundle()?.role; - if (runtimeRole && runtimeRole !== role()) setRole(runtimeRole); + if (runtimeRole && runtimeRole !== role() && !urlRoleLocked()) setRole(runtimeRole); }); createEffect(() => { diff --git a/src/routes/login.tsx b/src/routes/login.tsx index 84f2599..f8c4641 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -160,12 +160,9 @@ export default function LoginRoute() { const fullName = String(user?.full_name || user?.fullName || "").trim(); const [firstName, ...rest] = fullName.split(" "); const lastName = rest.join(" "); - const normalizedRole = resolveActiveRole( - user?.active_role || user?.role || roleGuess(), - String(user?.email || email()) - ); - const storedRole = normalizedRole ? normalizedRole.toLowerCase() : roleGuess(); - const selectedProfessionalRole = getStoredPreferredRole(String(user?.email || email())); + // 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(); const payload = { firstName: firstName || "", lastName: lastName || "", @@ -177,8 +174,8 @@ export default function LoginRoute() { .toLowerCase(), roleKey: storedRole, role: storedRole, - active_role: normalizedRole || "JOB_SEEKER", - selectedProfessionalRole: selectedProfessionalRole || null, + active_role: normalizedRole, + selectedProfessionalRole: normalizedRole, user, }; if (typeof window !== "undefined") { @@ -233,23 +230,40 @@ export default function LoginRoute() { window.sessionStorage.setItem("nxtgauge_access_token", accessToken); window.sessionStorage.setItem("nxtgauge_frontend_access_token", accessToken); } - const resolvedActiveRole = resolveActiveRole( - data?.user?.active_role || data?.user?.role, - data?.user?.email || email().trim().toLowerCase() - ); + 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; + } + } + } + if (!resolvedActiveRole) { + resolvedActiveRole = selectedRole || backendActiveRole || "JOB_SEEKER"; + } + 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 || ""), - active_role: resolvedActiveRole, + active_role: finalRole, email_verified: Boolean(data?.user?.email_verified ?? true), }; - saveUser({ ...(data?.user || {}), ...userPayload }); - if (auth.setUser) { - auth.setUser(userPayload); + saveUser({ ...userPayload }); + if (auth.refreshUser) { + auth.refreshUser(userPayload); } - navigate("/dashboard", { replace: true }); + navigate(`/dashboard?role=${encodeURIComponent(normalizeRoleValue(resolvedActiveRole))}`, { replace: true }); } catch { setError("Network error during login. Please try again."); } finally {