import { apiClient } from './httpClient'; import { Role, Claim } from '../types'; import { ROLE_CLAIM_DEFINITIONS } from '../auth/rbac'; export const LOCAL_STORAGE_SYSTEM_ROLES_KEY = 'pfm_cache_system_roles'; export const LOCAL_STORAGE_ROLE_CLAIMS_PREFIX = 'pfm_cache_role_claims_'; export const LOCAL_STORAGE_SYSTEM_CLAIMS_KEY = 'pfm_cache_system_claims'; // In-memory runtime caches let inMemorySystemRoles: Role[] | null = null; const inMemoryRoleClaims = new Map(); let inMemorySystemClaims: Claim[] | null = null; // Standard role claim defaults matching PFM Identity Server specification export const DEFAULT_ROLE_CLAIMS: Record = { contributor: [ { claimId: 30, claimType: 'projects_milestones', claimValue: 'view, edit, update' }, { claimId: 31, claimType: 'grant_financials', claimValue: 'view, edit, update' }, { claimId: 32, claimType: 'evidence_risks', claimValue: 'view, edit, update' }, { claimId: 33, claimType: 'beneficiaries', claimValue: 'view, edit, update' }, { claimId: 34, claimType: 'change_management', claimValue: 'view, edit, update' }, { claimId: 35, claimType: 'site_visits', claimValue: 'view, edit, update' }, { claimId: 36, claimType: 'settings_invites', claimValue: 'view, edit, update' }, ], supervisor: [ { claimId: 20, claimType: 'dashboard', claimValue: 'view, add, export' }, { claimId: 21, claimType: 'projects_milestones', claimValue: 'view, add, export' }, { claimId: 22, claimType: 'grant_financials', claimValue: 'view, add, export' }, { claimId: 23, claimType: 'evidence_risks', claimValue: 'view, add, export' }, { claimId: 24, claimType: 'beneficiaries', claimValue: 'view, add, export' }, { claimId: 25, claimType: 'change_management', claimValue: 'view, add, export' }, { claimId: 26, claimType: 'site_visits', claimValue: 'view, add, export' }, { claimId: 27, claimType: 'settings_invites', claimValue: 'view, add, export' }, { claimId: 28, claimType: 'settings_users', claimValue: 'view, add, export' }, ], manager: [ { claimId: 10, claimType: 'dashboard', claimValue: 'all' }, { claimId: 11, claimType: 'projects_milestones', claimValue: 'all' }, { claimId: 12, claimType: 'grant_financials', claimValue: 'all' }, { claimId: 13, claimType: 'evidence_risks', claimValue: 'all' }, { claimId: 14, claimType: 'beneficiaries', claimValue: 'all' }, { claimId: 15, claimType: 'change_management', claimValue: 'all' }, { claimId: 16, claimType: 'site_visits', claimValue: 'all' }, { claimId: 17, claimType: 'settings_invites', claimValue: 'all' }, { claimId: 18, claimType: 'settings_users', claimValue: 'all' }, { claimId: 19, claimType: 'settings_auditlog', claimValue: 'all' }, { claimId: 29, claimType: 'settings_notifications', claimValue: 'all' }, { claimId: 37, claimType: 'tenant_self_service', claimValue: 'all' }, ], executive: [ { claimId: 1, claimType: 'dashboard', claimValue: 'all' }, { claimId: 2, claimType: 'governance', claimValue: 'all' }, { claimId: 3, claimType: 'projects_milestones', claimValue: 'all' }, { claimId: 4, claimType: 'grant_financials', claimValue: 'all' }, { claimId: 5, claimType: 'evidence_risks', claimValue: 'all' }, { claimId: 6, claimType: 'tenant_self_service', claimValue: 'all' }, ], admin: [ { claimId: 99, claimType: 'unrestricted', claimValue: 'all' }, ], }; /** * Normalizes claim objects to contain both claimType/claimValue and type/value */ 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, }; } /** * Retrieve cached system roles from memory or localStorage. */ 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; } /** * Store system roles into memory and localStorage. */ export function setCachedSystemRoles(roles: Role[]): void { inMemorySystemRoles = roles; if (typeof window !== 'undefined') { try { localStorage.setItem(LOCAL_STORAGE_SYSTEM_ROLES_KEY, JSON.stringify(roles)); } catch {} } } /** * Retrieve cached role claims for a specific role ID or role name. */ 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; } /** * Store role claims into memory and localStorage. */ 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 {} } } /** * Fetches system roles from backend API, caching the result in memory and localStorage. */ export async function fetchAndCacheSystemRoles(): Promise { 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 (err) { // If request fails, fall back to cached roles } const cached = getCachedSystemRoles(); if (cached && cached.length > 0) return cached; // Fallback defaults if offline / bootstrapping const defaults: Role[] = [ { id: '1', name: 'Admin' }, { id: '2', name: 'Executive' }, { id: '3', name: 'Supervisor' }, { id: '4', name: 'Manager' }, { id: '5', name: 'Contributor' }, ]; setCachedSystemRoles(defaults); return defaults; } /** * Fetches role claims for a role from the API, caching them under role ID and role name. */ export async function fetchAndCacheRoleClaims(roleId: string, roleName?: string): Promise { const roleIdKey = roleId.toLowerCase().trim(); const roleNameKey = (roleName || '').toLowerCase().trim(); try { const res = await apiClient.get(`/v1/api/users/system/roles/${roleId}/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); return normalized; } } catch (err) { // Network or server error, proceed to cache / defaults fallback } // Check cache by role ID const cachedById = getCachedRoleClaims(roleIdKey); if (cachedById && cachedById.length > 0) return cachedById; // Check cache by role name if (roleNameKey) { const cachedByName = getCachedRoleClaims(roleNameKey); if (cachedByName && cachedByName.length > 0) return cachedByName; } // Fallback from predefined matrix 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; } /** * Direct API calls for user roles and user claims */ export async function getUserRolesFromApi(userId: string): Promise { try { const res = await apiClient.get(`/v1/api/users/${userId}/roles`); return res.data || []; } catch { return []; } } export async function getUserClaimsFromApi(userId: string): Promise { try { const res = await apiClient.get(`/v1/api/users/${userId}/claims`); return res.data || []; } catch { return []; } } /** * Resolves user roles, role claims, and user claims (including tenant and tenant_id) * and caches system roles and role claims as a persistent reference. */ 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; }> { const mergedClaims: Record = { ...initialClaims }; const resolvedRoles: string[] = []; 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); // 1. Get user's roles from backend API (Requirement 2) if (userSub) { try { const userRoles = await getUserRolesFromApi(userSub); if (Array.isArray(userRoles) && userRoles.length > 0) { userRoles.forEach((r) => { if (r.name && !resolvedRoles.includes(r.name)) { resolvedRoles.push(r.name); } }); if (resolvedRoles.length > 0) { primaryRole = resolvedRoles[0]; } } } catch {} } // Fallback to initial role if API returned empty if (resolvedRoles.length === 0 && primaryRole) { resolvedRoles.push(primaryRole); } // 2. For each role, retrieve role claims using the endpoint (Requirement 2 & 7) const allRoleClaims: Claim[] = []; for (const roleName of resolvedRoles) { try { const roleClaims = await fetchAndCacheRoleClaims(roleName, roleName); if (Array.isArray(roleClaims)) { roleClaims.forEach((rc) => { const norm = normalizeClaim(rc); allRoleClaims.push(norm); if (norm.claimType) { mergedClaims[norm.claimType] = norm.claimValue || 'all'; } }); } } catch {} } if (allRoleClaims.length > 0) { mergedClaims.role_claims = allRoleClaims; } // 3. Retrieve user claims from endpoint (Requirement 3) if (userSub) { try { const userClaims = await getUserClaimsFromApi(userSub); if (Array.isArray(userClaims) && userClaims.length > 0) { userClaims.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 = userClaims.map(normalizeClaim); } } catch {} } // 4. Background preload all system roles & role claims for reference (Requirement 7) preloadAndCacheAllRoleClaims().catch(() => {}); return { roles: resolvedRoles, claims: mergedClaims, primaryRole, tenant, tenant_id, }; } /** * Preloads and caches all system roles and their respective role claims. */ export async function preloadAndCacheAllRoleClaims(): Promise { const roles = await fetchAndCacheSystemRoles(); for (const role of roles) { await fetchAndCacheRoleClaims(role.id, role.name); } } /** * Retrieves all system claims in the identity management system. * Any claim is assignable to an authorized user (with admin being a special case). */ export async function getSystemClaims(): Promise { if (inMemorySystemClaims && inMemorySystemClaims.length > 0) { return inMemorySystemClaims; } if (typeof window !== 'undefined') { try { const raw = localStorage.getItem(LOCAL_STORAGE_SYSTEM_CLAIMS_KEY); if (raw) { const parsed = JSON.parse(raw); if (Array.isArray(parsed) && parsed.length > 0) { inMemorySystemClaims = parsed; return parsed; } } } catch {} } const claimsMap = new Map(); // 1. Try system claims endpoint directly try { const res = await apiClient.get('/v1/api/users/system/claims'); if (res.data && Array.isArray(res.data)) { res.data.forEach((c) => { const norm = normalizeClaim(c); if (norm.claimType) claimsMap.set(norm.claimType, norm); }); } } catch {} // 2. Aggregate role claims from all system roles try { const roles = await fetchAndCacheSystemRoles(); for (const r of roles) { const claims = await fetchAndCacheRoleClaims(r.id, r.name); claims.forEach((c) => { const norm = normalizeClaim(c); if (norm.claimType && !claimsMap.has(norm.claimType)) { claimsMap.set(norm.claimType, norm); } }); } } catch {} // 3. Ensure all standard defined claims from RBAC specification exist ROLE_CLAIM_DEFINITIONS.forEach((def, index) => { if (!claimsMap.has(def.claim)) { claimsMap.set(def.claim, { claimId: index + 1, claimType: def.claim, claimValue: 'all', type: def.claim, value: 'all', }); } }); const allClaims = Array.from(claimsMap.values()); inMemorySystemClaims = allClaims; if (typeof window !== 'undefined') { try { localStorage.setItem(LOCAL_STORAGE_SYSTEM_CLAIMS_KEY, JSON.stringify(allClaims)); } catch {} } return allClaims; }