import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react'; import { UserManager, WebStorageStateStore, User as OidcUser } from 'oidc-client-ts'; import { Navigate, useLocation } from 'react-router-dom'; import { getSecuritySettings, loadRuntimeConfig, onConfigChanged } from '../config'; import { parseJwtClaims } from './jwt'; import { BrandLogo } from '../components/ui/BrandLogo'; import { syncUserAccount, isUserSyncedInBrowser, isActivationPending, getActivationPendingReason, setActivationPending, clearUserSyncInBrowser, UserSyncResult, } from '../services/userSyncService'; import { AccountActivationModal } from '../components/auth/AccountActivationModal'; import { NonPartnerAccessDeniedModal } from '../components/auth/NonPartnerAccessDeniedModal'; import { SessionExpiredModal } from '../components/auth/SessionExpiredModal'; import { encodeHashIdSync } from '../services/toolsService'; import { onSessionExpired, triggerSessionExpired, clearSessionAndRedirectToLanding, cancelSessionExpiration, dismissSubordinateModals, } from '../services/sessionExpiration'; import { resolveUserRolesAndClaims, clearCachedUserRbac } from '../services/roleClaimsCache'; export interface AuthUserProfile { id: string; sub: string; name: string; email: string; role: string; organisationName?: string; tenant?: string; tenant_id?: number; avatarUrl?: string; } export type UserClaims = Record; interface AuthContextType { user: AuthUserProfile | null; sub: string; claims: UserClaims; tenantId: number | null; hashedTenantId: string | null; tenantName: string | null; accessToken: string | null; roles: string[]; isPartner: boolean; canManageTenants: boolean; isAuthenticated: boolean; isLoading: boolean; isSynced: boolean; isPendingActivation: boolean; activationError: string | null; isSessionExpired: boolean; triggerSessionExpired: (reason?: string) => void; login: () => Promise; logout: () => Promise; syncUserSession: (force?: boolean) => Promise; authError: string | null; } export const AuthContext = createContext(undefined); export const LOCAL_STORAGE_TOKEN_KEY = 'pfm_access_token'; export const LOCAL_STORAGE_USER_KEY = 'pfm_auth_user'; export const LOCAL_STORAGE_SUB_KEY = 'pfm_user_sub'; export const LOCAL_STORAGE_CLAIMS_KEY = 'pfm_user_claims'; export function extractUserRoles( claims?: UserClaims | null, user?: AuthUserProfile | null, token?: string | null ): string[] { const rolesSet = new Set(); const processRoles = (raw: any) => { if (!raw) return; if (Array.isArray(raw)) { raw.forEach((r) => { if (typeof r === 'string' && r.trim()) rolesSet.add(r.trim()); }); } else if (typeof raw === 'string' && raw.trim()) { if (raw.includes(',')) { raw.split(',').forEach((r) => { if (r.trim()) rolesSet.add(r.trim()); }); } else { rolesSet.add(raw.trim()); } } }; if (claims) { processRoles(claims.roles); processRoles(claims.role); processRoles(claims['roles']); processRoles(claims['role']); processRoles(claims['http://schemas.microsoft.com/ws/2008/06/identity/claims/role']); processRoles(claims['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role']); processRoles(claims['realm_access']?.roles); processRoles(claims['resource_access']?.roles); } if (token) { try { const tokenClaims = parseJwtClaims(token); processRoles(tokenClaims.roles); processRoles(tokenClaims.role); processRoles(tokenClaims['roles']); processRoles(tokenClaims['role']); processRoles(tokenClaims['http://schemas.microsoft.com/ws/2008/06/identity/claims/role']); processRoles(tokenClaims['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role']); } catch {} } if (user?.role && typeof user.role === 'string') { processRoles(user.role); } if ((user as any)?.roles) { processRoles((user as any).roles); } return Array.from(rolesSet); } /** * Checks whether the user has the 'Partner' role. * On PartnerUI, ONLY users with the 'Partner' role are permitted. */ export function hasPartnerRole(roles: string[]): boolean { return Array.isArray(roles) && roles.some((r) => r.toLowerCase().trim() === 'partner'); } export function canManageTenants(_tenantId?: number | string | null, _roles?: string[]): boolean { // PartnerUI does not permit tenant administration return false; } export function extractTenantId(claims?: UserClaims | null, token?: string | null): number | null { if (claims) { const raw = claims.tenant_id ?? (claims as any).tenantId ?? claims['tenant_id'] ?? claims['http://schemas.microsoft.com/ws/2008/06/identity/claims/tenantid']; if (raw !== undefined && raw !== null && raw !== '') { const parsed = Number(raw); if (!isNaN(parsed) && isFinite(parsed)) return parsed; } } if (token) { try { const tokenClaims = parseJwtClaims(token); const raw = tokenClaims.tenant_id ?? tokenClaims.tenantId ?? tokenClaims['tenant_id']; if (raw !== undefined && raw !== null && raw !== '') { const parsed = Number(raw); if (!isNaN(parsed) && isFinite(parsed)) return parsed; } } catch {} } if (typeof window !== 'undefined') { try { const storedClaimsRaw = localStorage.getItem(LOCAL_STORAGE_CLAIMS_KEY); if (storedClaimsRaw) { const parsedClaims = JSON.parse(storedClaimsRaw); const raw = parsedClaims.tenant_id ?? parsedClaims.tenantId ?? parsedClaims['tenant_id']; if (raw !== undefined && raw !== null && raw !== '') { const parsed = Number(raw); if (!isNaN(parsed) && isFinite(parsed)) return parsed; } } const storedToken = localStorage.getItem(LOCAL_STORAGE_TOKEN_KEY); if (storedToken) { const tokenClaims = parseJwtClaims(storedToken); const raw = tokenClaims.tenant_id ?? tokenClaims.tenantId ?? tokenClaims['tenant_id']; if (raw !== undefined && raw !== null && raw !== '') { const parsed = Number(raw); if (!isNaN(parsed) && isFinite(parsed)) return parsed; } } const storedUser = localStorage.getItem(LOCAL_STORAGE_USER_KEY); if (storedUser) { const parsedUser = JSON.parse(storedUser); const raw = parsedUser.tenant_id ?? parsedUser.tenantId; if (raw !== undefined && raw !== null && raw !== '') { const parsed = Number(raw); if (!isNaN(parsed) && isFinite(parsed)) return parsed; } } } catch {} } return null; } export function extractTenantClaimValues( claims?: UserClaims | null, token?: string | null ): { tenantId: number | string | null; tenant: string | null; } { let tenantId: number | string | null = null; let tenant: string | null = null; if (claims) { const rawId = claims.tenant_id ?? (claims as any).tenantId ?? claims['tenant_id'] ?? claims['http://schemas.microsoft.com/ws/2008/06/identity/claims/tenantid']; if (rawId !== undefined && rawId !== null && rawId !== '') { const parsed = Number(rawId); tenantId = !isNaN(parsed) && isFinite(parsed) ? parsed : String(rawId).trim(); } const rawTenant = claims.tenant ?? claims['tenant'] ?? claims['http://schemas.microsoft.com/ws/2008/06/identity/claims/tenant'] ?? claims.tenant_name ?? (claims as any).tenantName ?? claims.org; if (rawTenant !== undefined && rawTenant !== null && rawTenant !== '') { if (typeof rawTenant === 'string') { tenant = rawTenant.trim(); } else if (typeof rawTenant === 'object' && rawTenant.name) { tenant = String(rawTenant.name).trim(); } } } if (token && (!tenantId || !tenant)) { try { const tokenClaims = parseJwtClaims(token); if (!tenantId) { const rawId = tokenClaims.tenant_id ?? tokenClaims.tenantId ?? tokenClaims['tenant_id'] ?? tokenClaims['http://schemas.microsoft.com/ws/2008/06/identity/claims/tenantid']; if (rawId !== undefined && rawId !== null && rawId !== '') { const parsed = Number(rawId); tenantId = !isNaN(parsed) && isFinite(parsed) ? parsed : String(rawId).trim(); } } if (!tenant) { const rawTenant = tokenClaims.tenant ?? tokenClaims['tenant'] ?? tokenClaims['http://schemas.microsoft.com/ws/2008/06/identity/claims/tenant'] ?? tokenClaims.tenant_name ?? (tokenClaims as any).tenantName ?? tokenClaims.org; if (rawTenant !== undefined && rawTenant !== null && rawTenant !== '') { if (typeof rawTenant === 'string') { tenant = rawTenant.trim(); } else if (typeof rawTenant === 'object' && rawTenant.name) { tenant = String(rawTenant.name).trim(); } } } } catch {} } return { tenantId, tenant }; } export function getCurrentUserTenantId(): number | null { return extractTenantId( getCurrentUserClaims(), typeof window !== 'undefined' ? localStorage.getItem(LOCAL_STORAGE_TOKEN_KEY) : null ); } export function getCurrentUserHashedTenantId(): string | null { const tid = getCurrentUserTenantId(); if (tid === null || tid === undefined) return null; return encodeHashIdSync(tid); } export function getCurrentUserSub(): string { if (typeof window !== 'undefined') { return localStorage.getItem(LOCAL_STORAGE_SUB_KEY) || ''; } return ''; } export function getCurrentUserClaims(): UserClaims { if (typeof window !== 'undefined') { const raw = localStorage.getItem(LOCAL_STORAGE_CLAIMS_KEY); if (raw) { try { return JSON.parse(raw); } catch { return {}; } } } return {}; } let userManagerInstance: UserManager | null = null; export function resetUserManager(): void { userManagerInstance = null; } onConfigChanged(() => { resetUserManager(); }); export function getUserManager(): UserManager { if (!userManagerInstance && typeof window !== 'undefined') { const sec = getSecuritySettings(); const origin = window.location.origin; userManagerInstance = new UserManager({ authority: sec.Authority, client_id: sec.ClientId, client_secret: sec.ClientSecret, redirect_uri: `${origin}/dashboard`, post_logout_redirect_uri: `${origin}/`, response_type: 'code', scope: sec.Scope || 'openid profile roles email address pfm-api-dev', extraQueryParams: { audience: sec.Audience, }, userStore: new WebStorageStateStore({ store: window.localStorage }), automaticSilentRenew: false, }); } return userManagerInstance!; } export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const [user, setUser] = useState(null); const [sub, setSub] = useState(''); const [claims, setClaims] = useState({}); const [tenantId, setTenantId] = useState(() => { return extractTenantId( getCurrentUserClaims(), typeof window !== 'undefined' ? localStorage.getItem(LOCAL_STORAGE_TOKEN_KEY) : null ); }); const [tenantName, setTenantName] = useState(() => { const claims = getCurrentUserClaims(); const token = typeof window !== 'undefined' ? localStorage.getItem(LOCAL_STORAGE_TOKEN_KEY) : null; const { tenant } = extractTenantClaimValues(claims, token); return tenant; }); const [accessToken, setAccessToken] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isSynced, setIsSynced] = useState(() => isUserSyncedInBrowser()); const [isPendingActivation, setIsPendingActivation] = useState(() => isActivationPending()); const [activationError, setActivationError] = useState(() => getActivationPendingReason()); const [authError, setAuthError] = useState(null); const [isSessionExpired, setIsSessionExpired] = useState(false); const [sessionExpiredReason, setSessionExpiredReason] = useState(null); const hashedTenantId = tenantId !== null && tenantId !== undefined ? encodeHashIdSync(tenantId) : null; useEffect(() => { const unsubscribe = onSessionExpired((reason) => { if (isPendingActivation || isActivationPending()) { return; } const token = typeof window !== 'undefined' ? localStorage.getItem(LOCAL_STORAGE_TOKEN_KEY) : null; if (token || accessToken) { setSessionExpiredReason(reason || 'Session expired or token invalidated (HTTP 401)'); setIsSessionExpired(true); } }); return () => { unsubscribe(); }; }, [accessToken, isPendingActivation]); useEffect(() => { if (isPendingActivation) { setIsSessionExpired(false); setSessionExpiredReason(null); cancelSessionExpiration(); dismissSubordinateModals(); } }, [isPendingActivation]); useEffect(() => { if (isSessionExpired) { dismissSubordinateModals(); } }, [isSessionExpired]); const handleSessionExpiredDismiss = () => { setIsSessionExpired(false); setSessionExpiredReason(null); clearSessionAndRedirectToLanding(); }; useEffect(() => { const initAuth = async () => { try { await loadRuntimeConfig(); const urlParams = new URLSearchParams(window.location.search); if (urlParams.has('code') && urlParams.has('state')) { const mgr = getUserManager(); const oidcUser: OidcUser | undefined = await mgr.signinCallback(); if (oidcUser && oidcUser.access_token) { const token = oidcUser.access_token; setAccessToken(token); localStorage.setItem(LOCAL_STORAGE_TOKEN_KEY, token); const decodedTokenClaims = parseJwtClaims(token); const mergedClaims: UserClaims = { ...decodedTokenClaims, ...oidcUser.profile, }; const userSub = (oidcUser.profile?.sub || mergedClaims.sub || '') as string; setSub(userSub); setClaims(mergedClaims); localStorage.setItem(LOCAL_STORAGE_SUB_KEY, userSub); localStorage.setItem(LOCAL_STORAGE_CLAIMS_KEY, JSON.stringify(mergedClaims)); const tenantClaimValues = extractTenantClaimValues(mergedClaims, token); let tenantClaim = (mergedClaims.tenant as string) || (oidcUser.profile?.tenant as string) || tenantClaimValues.tenant || undefined; let parsedTenantId = extractTenantId(mergedClaims, token); setTenantId(parsedTenantId); if (tenantClaim) { setTenantName(tenantClaim); } const initialRoles = extractUserRoles(mergedClaims, null, token); const initialPrimaryRole = initialRoles.find((r) => r.toLowerCase().trim() === 'partner') || initialRoles[0] || (oidcUser.profile.role as string) || (mergedClaims.role as string) || ''; const profile: AuthUserProfile = { id: userSub || 'user-1', sub: userSub, name: oidcUser.profile.name || oidcUser.profile.preferred_username || (mergedClaims.name as string) || 'Partner User', email: oidcUser.profile.email || (mergedClaims.email as string) || '', role: initialPrimaryRole, organisationName: (mergedClaims.organisation as string) || (mergedClaims.org as string) || undefined, tenant: tenantClaim, tenant_id: parsedTenantId ?? undefined, }; setUser(profile); localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(profile)); window.history.replaceState({}, document.title, window.location.pathname); // Step 1: Sync user account FIRST (/v1/api/integration/sync) before calling any ActiveUserFilter-protected endpoints try { const syncResult = await syncUserAccount(userSub, token, profile, true); if (syncResult.isUnauthorized) { await logout(); window.location.replace('/'); return; } // Step 2: Resolve user roles & claims from /v1/api/users/{userSub}/roles and /claims try { const resolved = await resolveUserRolesAndClaims(userSub, mergedClaims, profile); if (resolved.primaryRole) { profile.role = resolved.primaryRole; } if (resolved.tenant) { profile.tenant = resolved.tenant; tenantClaim = resolved.tenant; setTenantName(resolved.tenant); } if (resolved.tenant_id !== undefined) { profile.tenant_id = resolved.tenant_id; parsedTenantId = resolved.tenant_id; setTenantId(resolved.tenant_id); } setUser({ ...profile }); setClaims({ ...resolved.claims }); localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(profile)); localStorage.setItem(LOCAL_STORAGE_CLAIMS_KEY, JSON.stringify(resolved.claims)); } catch {} if (syncResult.isActivationPending) { setIsPendingActivation(true); setIsSynced(false); setActivationError(syncResult.errorMessage || null); setIsLoading(false); return; } setUser({ ...profile }); localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(profile)); setActivationPending(false); setIsPendingActivation(false); setIsSynced(true); setActivationError(null); } catch (syncErr: any) { if (syncErr?.response?.status === 401) { await logout(); window.location.replace('/'); return; } setIsPendingActivation(true); setIsSynced(false); setActivationError(syncErr?.message || 'Account activation pending'); } setIsLoading(false); return; } } const storedToken = localStorage.getItem(LOCAL_STORAGE_TOKEN_KEY); const storedUser = localStorage.getItem(LOCAL_STORAGE_USER_KEY); let storedSub = localStorage.getItem(LOCAL_STORAGE_SUB_KEY); const storedClaimsRaw = localStorage.getItem(LOCAL_STORAGE_CLAIMS_KEY); if (storedToken) { setAccessToken(storedToken); let parsedClaims: UserClaims = {}; if (storedClaimsRaw) { try { parsedClaims = JSON.parse(storedClaimsRaw); } catch {} } else { parsedClaims = parseJwtClaims(storedToken); localStorage.setItem(LOCAL_STORAGE_CLAIMS_KEY, JSON.stringify(parsedClaims)); } setClaims(parsedClaims); if (!storedSub && parsedClaims.sub) { storedSub = String(parsedClaims.sub); localStorage.setItem(LOCAL_STORAGE_SUB_KEY, storedSub); } if (storedSub) { setSub(storedSub); } let storedTenantId = extractTenantId(parsedClaims, storedToken); setTenantId(storedTenantId); const tenantClaimValues = extractTenantClaimValues(parsedClaims, storedToken); let storedTenantClaim = (parsedClaims.tenant as string) || tenantClaimValues.tenant || undefined; let resolvedUser: AuthUserProfile | null = null; if (storedUser) { try { const parsedUser = JSON.parse(storedUser); if (!parsedUser.tenant && parsedClaims.tenant) { parsedUser.tenant = parsedClaims.tenant; } if (parsedUser.tenant_id === undefined && storedTenantId !== null) { parsedUser.tenant_id = storedTenantId; } setUser(parsedUser); resolvedUser = parsedUser; } catch {} } if (storedTenantClaim) { setTenantName(storedTenantClaim); } const hasInitialPartnerRole = hasPartnerRole(extractUserRoles(parsedClaims, resolvedUser, storedToken)); const hadPendingActivation = isActivationPending(); // If the user already has the Partner role and is not pending activation, unblock UI immediately and verify in background if (hasInitialPartnerRole && !hadPendingActivation) { setIsLoading(false); } const verifyPersistedSession = async () => { try { // Step 1: Sync user account FIRST (/v1/api/integration/sync) const syncResult = await syncUserAccount( storedSub || '', storedToken, resolvedUser || undefined, hadPendingActivation ); if (syncResult.isUnauthorized) { await logout(); return; } // Step 2: Resolve user roles & claims try { const resolved = await resolveUserRolesAndClaims( storedSub || '', parsedClaims, resolvedUser || {} ); if (resolvedUser) { if (resolved.primaryRole) resolvedUser.role = resolved.primaryRole; if (resolved.tenant) { resolvedUser.tenant = resolved.tenant; storedTenantClaim = resolved.tenant; setTenantName(resolved.tenant); } if (resolved.tenant_id !== undefined) { resolvedUser.tenant_id = resolved.tenant_id; storedTenantId = resolved.tenant_id; setTenantId(resolved.tenant_id); } setUser({ ...resolvedUser }); localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(resolvedUser)); } setClaims({ ...resolved.claims }); localStorage.setItem(LOCAL_STORAGE_CLAIMS_KEY, JSON.stringify(resolved.claims)); } catch {} if (syncResult.isActivationPending) { setIsPendingActivation(true); setIsSynced(false); setActivationError(syncResult.errorMessage || null); return; } if (resolvedUser) { setUser({ ...resolvedUser }); localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(resolvedUser)); } setActivationPending(false); setIsPendingActivation(false); setIsSynced(true); setActivationError(null); } catch (syncErr: any) { if (syncErr?.response?.status === 401) { await logout(); return; } setIsPendingActivation(true); setIsSynced(false); setActivationError(syncErr?.message || 'Account activation pending'); } finally { setIsLoading(false); } }; if (hasInitialPartnerRole && !hadPendingActivation) { verifyPersistedSession(); } else { await verifyPersistedSession(); } return; } } catch (err: unknown) { console.warn('OIDC initialization notice:', err); setAuthError(err instanceof Error ? err.message : 'Auth initialization notice'); } finally { setIsLoading(false); } }; initAuth(); }, []); const syncUserSession = async (force = true): Promise => { try { await loadRuntimeConfig(); if (sub) { clearCachedUserRbac(sub); } // Step 1: Sync user account FIRST (/v1/api/integration/sync) const result = await syncUserAccount(sub, accessToken, user || undefined, force); if (result.isUnauthorized) { await logout(); return result; } if (result.isActivationPending) { setIsPendingActivation(true); setIsSynced(false); setActivationError(result.errorMessage || null); return result; } // Step 2: Refresh user roles & claims from backend let currentTenantId: number | string | null = tenantId; let currentTenantClaim: string | undefined = (claims.tenant as string) || user?.tenant || undefined; try { const resolved = await resolveUserRolesAndClaims(sub, claims, user || {}); const updatedProfile = user ? { ...user } : null; if (updatedProfile && resolved.primaryRole) { updatedProfile.role = resolved.primaryRole; } if (resolved.tenant) { currentTenantClaim = resolved.tenant; setTenantName(resolved.tenant); if (updatedProfile) updatedProfile.tenant = resolved.tenant; } if (resolved.tenant_id !== undefined) { currentTenantId = resolved.tenant_id; setTenantId(resolved.tenant_id); if (updatedProfile) updatedProfile.tenant_id = resolved.tenant_id; } if (updatedProfile) { setUser(updatedProfile); localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(updatedProfile)); } setClaims({ ...resolved.claims }); localStorage.setItem(LOCAL_STORAGE_CLAIMS_KEY, JSON.stringify(resolved.claims)); } catch {} // Partner user verified successfully setActivationPending(false); setIsPendingActivation(false); setIsSynced(true); setActivationError(null); return result; } catch (err: any) { if (err?.response?.status === 401) { await logout(); return { success: false, isUnauthorized: true }; } const msg = err?.message || 'Sync failed'; setIsPendingActivation(true); setIsSynced(false); setActivationError(msg); return { success: false, isActivationPending: true, errorMessage: msg }; } }; const login = async () => { try { setAuthError(null); await loadRuntimeConfig(); resetUserManager(); const mgr = getUserManager(); await mgr.signinRedirect(); } catch (err: unknown) { console.warn('OIDC Redirect encountered:', err); if (typeof window !== 'undefined' && window.self !== window.top) { const errorMsg = 'Identity Provider redirect was restricted inside this preview frame. To complete single sign-on authentication, open the application in a new browser tab.'; setAuthError(errorMsg); } else { setAuthError(err instanceof Error ? err.message : 'Identity Provider redirect error'); } } }; const logout = async () => { try { localStorage.removeItem(LOCAL_STORAGE_TOKEN_KEY); localStorage.removeItem(LOCAL_STORAGE_USER_KEY); localStorage.removeItem(LOCAL_STORAGE_SUB_KEY); localStorage.removeItem(LOCAL_STORAGE_CLAIMS_KEY); clearUserSyncInBrowser(); setUser(null); setSub(''); setClaims({}); setTenantId(null); setAccessToken(null); setIsSynced(false); setIsPendingActivation(false); setActivationError(null); try { const mgr = getUserManager(); await mgr.signoutRedirect(); } catch { // Local state cleared } } catch (err) { console.error('Logout error:', err); } }; const roles = React.useMemo(() => { return extractUserRoles(claims, user, accessToken); }, [claims, user, accessToken]); const isPartner = React.useMemo(() => { return hasPartnerRole(roles); }, [roles]); return ( {children} ); }; export const useAuth = () => { const context = useContext(AuthContext); if (!context) { throw new Error('useAuth must be used within an AuthProvider'); } return context; }; /** * Route Guard for PartnerUI: * 1. Redirects unauthenticated users to '/' * 2. Strictly enforces that ONLY users with the 'Partner' role are permitted on PartnerUI. * Any other role (Admin, Executive, Supervisor, Manager, Contributor, etc.) is blocked with NonPartnerAccessDeniedModal. * 3. Renders AccountActivationModal if the partner account is pending administrator activation. */ export const ProtectedRoute: React.FC<{ children: ReactNode }> = ({ children }) => { const { isAuthenticated, isLoading, isPendingActivation, isPartner, roles, user, claims, sub, logout, syncUserSession, activationError, } = useAuth(); const location = useLocation(); if (isLoading) { return (

Verifying Partner Portal Session...

); } if (!isAuthenticated) { return ; } // Requirement 2: RBAC on PartnerUI only functions for Partner roles, no other roles are permitted on this app. if (!isPartner && !hasPartnerRole(roles)) { return ( ); } if (isPendingActivation) { return ( ); } return <>{children}; };