import { apiClient } from './httpClient'; import { Role, Claim } from '../types'; import { ROLE_CLAIM_DEFINITIONS } from '../auth/rbac'; export const LOCAL_STORAGE_SYSTEM_ROLES_KEY = 'pfm_partner_cache_system_roles'; export const LOCAL_STORAGE_ROLE_CLAIMS_PREFIX = 'pfm_partner_cache_role_claims_'; export const LOCAL_STORAGE_SYSTEM_CLAIMS_KEY = 'pfm_partner_cache_system_claims'; export const LOCAL_STORAGE_RBAC_VERSION_KEY = 'pfm_partner_rbac_cache_version'; export const CURRENT_RBAC_CACHE_VERSION = 'v2.2-partner'; export function ensureFreshRbacCache(): void { if (typeof window === 'undefined') return; try { const version = localStorage.getItem(LOCAL_STORAGE_RBAC_VERSION_KEY); if (version !== CURRENT_RBAC_CACHE_VERSION) { const keysToRemove: string[] = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && (key.startsWith(LOCAL_STORAGE_ROLE_CLAIMS_PREFIX) || key === 'pfm_user_claims')) { keysToRemove.push(key); } } keysToRemove.forEach((k) => localStorage.removeItem(k)); inMemoryRoleClaims.clear(); localStorage.setItem(LOCAL_STORAGE_RBAC_VERSION_KEY, CURRENT_RBAC_CACHE_VERSION); } } catch {} } let inMemorySystemRoles: Role[] | null = null; const inMemoryRoleClaims = new Map(); let inMemorySystemClaims: Claim[] | null = null; const inMemoryUserRoles = new Map(); const inMemoryUserClaims = new Map(); const USER_RBAC_CACHE_TTL_MS = 5 * 60 * 1000; ensureFreshRbacCache(); // PartnerUI RBAC only functions for Partner roles export const DEFAULT_ROLE_CLAIMS: Record = { partner: [ { claimId: 50, claimType: 'dashboard', claimValue: 'view, export' }, { claimId: 51, claimType: 'projects_milestones', claimValue: 'view, add, export' }, { claimId: 52, claimType: 'grant_financials', claimValue: 'view, add, export' }, { claimId: 53, claimType: 'evidence_risks', claimValue: 'view, add, export' }, { claimId: 54, claimType: 'beneficiaries', claimValue: 'view, add, export' }, { claimId: 55, claimType: 'change_management', claimValue: 'view, add, export' }, { claimId: 56, claimType: 'site_visits', claimValue: 'view, export' }, { claimId: 57, claimType: 'compliance', claimValue: 'view, add, export' }, { claimId: 58, claimType: 'settings_notifications', claimValue: 'view, add' }, ], }; export function normalizeClaim(c: any): Claim { const claimType = c.claimType || c.type || c.name || ''; const claimValue = c.claimValue || c.value || 'all'; const claimId = c.claimId ?? c.id ?? undefined; return { ...c, type: claimType, value: claimValue, claimType, claimValue, claimId, }; } export function getCachedSystemRoles(): Role[] | null { if (inMemorySystemRoles && inMemorySystemRoles.length > 0) { return inMemorySystemRoles; } if (typeof window !== 'undefined') { try { const raw = localStorage.getItem(LOCAL_STORAGE_SYSTEM_ROLES_KEY); if (raw) { const parsed = JSON.parse(raw); if (Array.isArray(parsed) && parsed.length > 0) { inMemorySystemRoles = parsed; return parsed; } } } catch {} } return null; } export function setCachedSystemRoles(roles: Role[]): void { inMemorySystemRoles = roles; if (typeof window !== 'undefined') { try { localStorage.setItem(LOCAL_STORAGE_SYSTEM_ROLES_KEY, JSON.stringify(roles)); } catch {} } } export function getCachedRoleClaims(roleKey: string): Claim[] | null { const normalizedKey = roleKey.toLowerCase().trim(); if (inMemoryRoleClaims.has(normalizedKey)) { return inMemoryRoleClaims.get(normalizedKey)!; } if (typeof window !== 'undefined') { try { const raw = localStorage.getItem(`${LOCAL_STORAGE_ROLE_CLAIMS_PREFIX}${normalizedKey}`); if (raw) { const parsed = JSON.parse(raw); if (Array.isArray(parsed)) { const normalized = parsed.map(normalizeClaim); inMemoryRoleClaims.set(normalizedKey, normalized); return normalized; } } } catch {} } return null; } export function setCachedRoleClaims(roleKey: string, claims: Claim[]): void { const normalizedKey = roleKey.toLowerCase().trim(); const normalized = claims.map(normalizeClaim); inMemoryRoleClaims.set(normalizedKey, normalized); if (typeof window !== 'undefined') { try { localStorage.setItem( `${LOCAL_STORAGE_ROLE_CLAIMS_PREFIX}${normalizedKey}`, JSON.stringify(normalized) ); } catch {} } } export async function fetchAndCacheSystemRoles(forceRefresh = false): Promise { if (!forceRefresh) { const cached = getCachedSystemRoles(); if (cached && cached.length > 0) return cached; } try { const res = await apiClient.get('/v1/api/users/system/roles'); if (res.data && Array.isArray(res.data) && res.data.length > 0) { setCachedSystemRoles(res.data); return res.data; } } catch {} const cached = getCachedSystemRoles(); if (cached && cached.length > 0) return cached; const defaults: Role[] = [{ id: '6', name: 'Partner' }]; setCachedSystemRoles(defaults); return defaults; } const GUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; export async function fetchAndCacheRoleClaims( roleId: string, roleName?: string, forceRefresh = false ): Promise { const roleIdKey = roleId.toLowerCase().trim(); const roleNameKey = (roleName || '').toLowerCase().trim(); if (!forceRefresh) { const cachedById = getCachedRoleClaims(roleIdKey); if (cachedById && cachedById.length > 0) return cachedById; if (roleNameKey) { const cachedByName = getCachedRoleClaims(roleNameKey); if (cachedByName && cachedByName.length > 0) return cachedByName; } } let effectiveGuid = GUID_REGEX.test(roleId.trim()) ? roleId.trim() : null; if (!effectiveGuid) { const cachedRoles = getCachedSystemRoles(); if (cachedRoles && cachedRoles.length > 0) { const match = cachedRoles.find( (r) => (r.name && r.name.toLowerCase() === roleNameKey) || (r.name && r.name.toLowerCase() === roleIdKey) || String(r.id).toLowerCase() === roleIdKey ); if (match && match.id && GUID_REGEX.test(String(match.id).trim())) { effectiveGuid = String(match.id).trim(); } } } if (effectiveGuid) { try { const res = await apiClient.get(`/v1/api/users/system/roles/${effectiveGuid}/claims`); if (res.data && Array.isArray(res.data) && res.data.length > 0) { const normalized = res.data.map(normalizeClaim); setCachedRoleClaims(roleIdKey, normalized); if (roleNameKey) setCachedRoleClaims(roleNameKey, normalized); if (effectiveGuid.toLowerCase() !== roleIdKey) { setCachedRoleClaims(effectiveGuid.toLowerCase(), normalized); } return normalized; } } catch {} } const cachedById = getCachedRoleClaims(roleIdKey); if (cachedById && cachedById.length > 0) return cachedById; if (roleNameKey) { const cachedByName = getCachedRoleClaims(roleNameKey); if (cachedByName && cachedByName.length > 0) return cachedByName; } const fallback = (roleNameKey && DEFAULT_ROLE_CLAIMS[roleNameKey]) || DEFAULT_ROLE_CLAIMS[roleIdKey] || []; const normalized = fallback.map(normalizeClaim); setCachedRoleClaims(roleIdKey, normalized); if (roleNameKey) setCachedRoleClaims(roleNameKey, normalized); return normalized; } export function clearCachedUserRbac(userId?: string): void { if (userId) { const key = userId.toLowerCase().trim(); inMemoryUserRoles.delete(key); inMemoryUserClaims.delete(key); if (typeof window !== 'undefined') { try { localStorage.removeItem(`pfm_partner_cache_user_roles_${key}`); localStorage.removeItem(`pfm_partner_cache_user_claims_${key}`); } catch {} } } else { inMemoryUserRoles.clear(); inMemoryUserClaims.clear(); } } export async function getUserRolesFromApi(userId: string, forceRefresh = false): Promise { const key = userId.toLowerCase().trim(); if (!forceRefresh) { const cached = inMemoryUserRoles.get(key); if (cached && Date.now() - cached.timestamp < USER_RBAC_CACHE_TTL_MS) { return cached.roles; } if (typeof window !== 'undefined') { try { const raw = localStorage.getItem(`pfm_partner_cache_user_roles_${key}`); if (raw) { const parsed = JSON.parse(raw); if (Array.isArray(parsed) && parsed.length > 0) { inMemoryUserRoles.set(key, { roles: parsed, timestamp: Date.now() }); return parsed; } } } catch {} } } try { const res = await apiClient.get(`/v1/api/users/${userId}/roles`); const roles = res.data || []; if (roles.length > 0) { inMemoryUserRoles.set(key, { roles, timestamp: Date.now() }); if (typeof window !== 'undefined') { try { localStorage.setItem(`pfm_partner_cache_user_roles_${key}`, JSON.stringify(roles)); } catch {} } } return roles; } catch { return inMemoryUserRoles.get(key)?.roles || []; } } export async function getUserClaimsFromApi(userId: string, forceRefresh = false): Promise { const key = userId.toLowerCase().trim(); if (!forceRefresh) { const cached = inMemoryUserClaims.get(key); if (cached && Date.now() - cached.timestamp < USER_RBAC_CACHE_TTL_MS) { return cached.claims; } if (typeof window !== 'undefined') { try { const raw = localStorage.getItem(`pfm_partner_cache_user_claims_${key}`); if (raw) { const parsed = JSON.parse(raw); if (Array.isArray(parsed) && parsed.length > 0) { const normalized = parsed.map(normalizeClaim); inMemoryUserClaims.set(key, { claims: normalized, timestamp: Date.now() }); return normalized; } } } catch {} } } try { const res = await apiClient.get(`/v1/api/users/${userId}/claims`); const claims = (res.data || []).map(normalizeClaim); if (claims.length > 0) { inMemoryUserClaims.set(key, { claims, timestamp: Date.now() }); if (typeof window !== 'undefined') { try { localStorage.setItem(`pfm_partner_cache_user_claims_${key}`, JSON.stringify(claims)); } catch {} } } return claims; } catch { return inMemoryUserClaims.get(key)?.claims || []; } } export async function resolveUserRolesAndClaims( userSub: string, initialClaims: Record = {}, currentProfile: { role?: string; tenant?: string; tenant_id?: number } = {} ): Promise<{ roles: string[]; claims: Record; primaryRole: string; tenant?: string; tenant_id?: number; }> { ensureFreshRbacCache(); const mergedClaims: Record = { ...initialClaims }; const resolvedRoles: string[] = []; let userRoleObjects: Role[] = []; let primaryRole = currentProfile.role || (initialClaims.role as string) || ''; let tenant = currentProfile.tenant || (initialClaims.tenant as string) || undefined; let tenant_id = currentProfile.tenant_id ?? (initialClaims.tenant_id ? Number(initialClaims.tenant_id) : undefined); const [rolesResult, claimsResult] = await Promise.allSettled([ userSub ? getUserRolesFromApi(userSub) : Promise.resolve([]), userSub ? getUserClaimsFromApi(userSub) : Promise.resolve([]), ]); if (rolesResult.status === 'fulfilled' && Array.isArray(rolesResult.value) && rolesResult.value.length > 0) { userRoleObjects = rolesResult.value; rolesResult.value.forEach((r) => { const rName = r.name || r.Name; if (rName && !resolvedRoles.includes(rName)) { resolvedRoles.push(rName); } }); if (resolvedRoles.length > 0) { const partnerMatch = resolvedRoles.find((r) => r.toLowerCase().trim() === 'partner'); primaryRole = partnerMatch || resolvedRoles[0]; mergedClaims.roles = resolvedRoles; } } if (resolvedRoles.length === 0 && primaryRole) { resolvedRoles.push(primaryRole); mergedClaims.roles = resolvedRoles; } const allRoleClaims: Claim[] = []; const roleClaimsPromises = resolvedRoles .filter((roleName) => roleName.toLowerCase().trim() === 'partner') .map(async (roleName) => { const matchedRoleObj = userRoleObjects.find( (r) => (r.name || r.Name || '').toLowerCase() === roleName.toLowerCase() ); const roleIdToPass = matchedRoleObj?.id ? String(matchedRoleObj.id) : roleName; return fetchAndCacheRoleClaims(roleIdToPass, roleName); }); const roleClaimsResults = await Promise.allSettled(roleClaimsPromises); roleClaimsResults.forEach((res) => { if (res.status === 'fulfilled' && Array.isArray(res.value)) { res.value.forEach((rc) => { const norm = normalizeClaim(rc); allRoleClaims.push(norm); if (norm.claimType) { mergedClaims[norm.claimType] = norm.claimValue || 'view, add, export'; } }); } }); if (allRoleClaims.length > 0) { mergedClaims.role_claims = allRoleClaims; } if (claimsResult.status === 'fulfilled' && Array.isArray(claimsResult.value) && claimsResult.value.length > 0) { claimsResult.value.forEach((c) => { const norm = normalizeClaim(c); const type = (norm.claimType || '').toLowerCase().trim(); const val = norm.claimValue; if (type === 'tenant' && val) { tenant = String(val); mergedClaims.tenant = String(val); } else if ((type === 'tenant_id' || type === 'tenantid') && val !== undefined && val !== null) { const parsed = Number(val); if (!isNaN(parsed) && isFinite(parsed)) { tenant_id = parsed; mergedClaims.tenant_id = parsed; } } if (norm.claimType) { mergedClaims[norm.claimType] = norm.claimValue; } }); mergedClaims.user_claims = claimsResult.value.map(normalizeClaim); } return { roles: resolvedRoles, claims: mergedClaims, primaryRole, tenant, tenant_id, }; } export async function preloadAndCacheAllRoleClaims(): Promise { const roles = await fetchAndCacheSystemRoles(); await Promise.allSettled( roles.map((role) => fetchAndCacheRoleClaims(role.id || role.Id || '', role.name || role.Name || '')) ); } export async function getSystemClaims(): Promise { if (inMemorySystemClaims && inMemorySystemClaims.length > 0) { return inMemorySystemClaims; } const claimsMap = new Map(); ROLE_CLAIM_DEFINITIONS.forEach((def, index) => { claimsMap.set(def.claim, { claimId: index + 1, claimType: def.claim, claimValue: 'view, add, export', type: def.claim, value: 'view, add, export', }); }); const allClaims = Array.from(claimsMap.values()); inMemorySystemClaims = allClaims; return allClaims; }