444 lines
15 KiB
TypeScript
444 lines
15 KiB
TypeScript
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<string, Claim[]>();
|
|
let inMemorySystemClaims: Claim[] | null = null;
|
|
const inMemoryUserRoles = new Map<string, { roles: Role[]; timestamp: number }>();
|
|
const inMemoryUserClaims = new Map<string, { claims: Claim[]; timestamp: number }>();
|
|
const USER_RBAC_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
|
|
ensureFreshRbacCache();
|
|
|
|
// PartnerUI RBAC only functions for Partner roles
|
|
export const DEFAULT_ROLE_CLAIMS: Record<string, Claim[]> = {
|
|
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<Role[]> {
|
|
if (!forceRefresh) {
|
|
const cached = getCachedSystemRoles();
|
|
if (cached && cached.length > 0) return cached;
|
|
}
|
|
|
|
try {
|
|
const res = await apiClient.get<Role[]>('/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<Claim[]> {
|
|
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<Claim[]>(`/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<Role[]> {
|
|
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<Role[]>(`/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<Claim[]> {
|
|
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<Claim[]>(`/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<string, any> = {},
|
|
currentProfile: { role?: string; tenant?: string; tenant_id?: number } = {}
|
|
): Promise<{
|
|
roles: string[];
|
|
claims: Record<string, any>;
|
|
primaryRole: string;
|
|
tenant?: string;
|
|
tenant_id?: number;
|
|
}> {
|
|
ensureFreshRbacCache();
|
|
const mergedClaims: Record<string, any> = { ...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<void> {
|
|
const roles = await fetchAndCacheSystemRoles();
|
|
await Promise.allSettled(
|
|
roles.map((role) => fetchAndCacheRoleClaims(role.id || role.Id || '', role.name || role.Name || ''))
|
|
);
|
|
}
|
|
|
|
export async function getSystemClaims(): Promise<Claim[]> {
|
|
if (inMemorySystemClaims && inMemorySystemClaims.length > 0) {
|
|
return inMemorySystemClaims;
|
|
}
|
|
|
|
const claimsMap = new Map<string, Claim>();
|
|
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;
|
|
}
|
|
|