frontend_nyla/src/hooks/usePermissions.ts
2026-01-25 03:01:07 +01:00

255 lines
8 KiB
TypeScript

import { useState, useCallback, useRef } from 'react';
import { useApiRequest } from './useApi';
import {
fetchPermissions as fetchPermissionsApi,
fetchAllPermissions as fetchAllPermissionsApi,
type PermissionLevel,
type UserPermissions,
type PermissionContext
} from '../api/permissionApi';
// Re-export types for backward compatibility
export type { PermissionLevel, UserPermissions, PermissionContext };
// Permission cache interface
interface PermissionCache {
[key: string]: UserPermissions;
}
// Operation type for permission checks
export type PermissionOperation = 'read' | 'create' | 'update' | 'delete';
// Default permission (no access)
const DEFAULT_NO_ACCESS: UserPermissions = {
view: false,
read: 'n' as PermissionLevel,
create: 'n' as PermissionLevel,
update: 'n' as PermissionLevel,
delete: 'n' as PermissionLevel,
};
/**
* Hook for managing RBAC permissions
* Provides centralized permission checking with caching
*
* Optimized to fetch all UI permissions in a single API call on first use,
* then serve subsequent requests from cache.
*/
export const usePermissions = () => {
const [loading, setLoading] = useState(false);
const cacheRef = useRef<PermissionCache>({});
const bulkLoadPromiseRef = useRef<Promise<void> | null>(null);
const bulkLoadedContextsRef = useRef<Set<string>>(new Set());
const pendingRequests = useRef<Map<string, Promise<UserPermissions>>>(new Map());
const { request } = useApiRequest();
/**
* Generate a cache key for a permission check
*/
const getPermissionKey = (context: PermissionContext, item?: string): string => {
return `${context}:${item || 'null'}`;
};
/**
* Load all permissions for a context (UI or RESOURCE) in bulk
* This is called once per context and caches all permissions
*/
const loadBulkPermissions = useCallback(async (context: 'UI' | 'RESOURCE'): Promise<void> => {
// Skip if already loaded for this context
if (bulkLoadedContextsRef.current.has(context)) {
return;
}
// Check if there's already a pending bulk load
if (bulkLoadPromiseRef.current) {
await bulkLoadPromiseRef.current;
return;
}
// Create the bulk load promise
bulkLoadPromiseRef.current = (async () => {
setLoading(true);
try {
console.log(`🔐 usePermissions: Bulk loading all ${context} permissions...`);
const response = await fetchAllPermissionsApi(request, context);
// Cache all permissions from the response
const contextKey = context.toLowerCase() as 'ui' | 'resource';
const permissions = response[contextKey] || {};
const newCache: PermissionCache = { ...cacheRef.current };
let count = 0;
for (const [item, perm] of Object.entries(permissions)) {
const key = getPermissionKey(context, item);
newCache[key] = perm;
count++;
}
cacheRef.current = newCache;
bulkLoadedContextsRef.current.add(context);
console.log(`✅ usePermissions: Bulk loaded ${count} ${context} permissions`);
} catch (error: any) {
console.error(`❌ usePermissions: Error bulk loading ${context} permissions:`, error);
// Don't mark as loaded on error - allow retry
} finally {
bulkLoadPromiseRef.current = null;
setLoading(false);
}
})();
await bulkLoadPromiseRef.current;
}, [request]);
/**
* Fetch individual permission (used for DATA context and fallback)
*/
const fetchIndividualPermission = useCallback(async (
context: PermissionContext,
item?: string
): Promise<UserPermissions> => {
const key = getPermissionKey(context, item);
// Check cache first
if (cacheRef.current[key]) {
return cacheRef.current[key];
}
// Check if there's already a pending request for this key
if (pendingRequests.current.has(key)) {
return pendingRequests.current.get(key)!;
}
// Fetch individual permission
const requestPromise = (async () => {
setLoading(true);
try {
const permissions = await fetchPermissionsApi(request, context, item);
// Update cache
cacheRef.current = { ...cacheRef.current, [key]: permissions };
return permissions;
} catch (error: any) {
console.error('Error checking permissions:', error);
// Return cached value if available, otherwise default (no access)
return cacheRef.current[key] || DEFAULT_NO_ACCESS;
} finally {
pendingRequests.current.delete(key);
setLoading(false);
}
})();
// Store pending request to prevent duplicates
pendingRequests.current.set(key, requestPromise);
return requestPromise;
}, [request]);
/**
* Check permissions for a given context and item
* Returns full UserPermissions object
*
* For UI/RESOURCE contexts: Uses bulk-loaded cache, falls back to individual fetch
* For DATA context: Fetches individually (as items are dynamic)
*/
const checkPermission = useCallback(async (
context: PermissionContext,
item?: string
): Promise<UserPermissions> => {
const key = getPermissionKey(context, item);
// For UI and RESOURCE contexts, try bulk loading first
if (context === 'UI' || context === 'RESOURCE') {
// Ensure bulk permissions are loaded
await loadBulkPermissions(context);
// Check cache after bulk load
if (cacheRef.current[key]) {
return cacheRef.current[key];
}
// Check for global permission (_global key) - grants access to all items in this context
const globalKey = getPermissionKey(context, '_global');
if (cacheRef.current[globalKey]) {
console.log(`✅ usePermissions: ${context}:${item} using global permission`);
// Cache the global permission for this specific item too
cacheRef.current[key] = cacheRef.current[globalKey];
return cacheRef.current[globalKey];
}
// If not in bulk cache and no global permission, fall back to individual fetch
// (item may not have explicit rule, but backend will calculate effective permissions)
console.log(`⚠️ usePermissions: ${context}:${item} not in bulk cache, fetching individually`);
return fetchIndividualPermission(context, item);
}
// For DATA context, fetch individually
return fetchIndividualPermission(context, item);
}, [loadBulkPermissions, fetchIndividualPermission]);
/**
* Check if user has permission for a specific operation
* Returns true if user has any level of permission (not 'n')
*/
const hasPermission = useCallback(async (
context: PermissionContext,
item: string,
operation?: PermissionOperation
): Promise<boolean> => {
const permissions = await checkPermission(context, item);
if (!permissions.view) {
return false;
}
if (context === 'DATA' && operation) {
const level = permissions[operation];
return level !== 'n';
}
return true;
}, [checkPermission]);
/**
* Check if user can view a resource
* Returns true if permissions.view is true
*/
const canView = useCallback(async (
context: PermissionContext,
item: string
): Promise<boolean> => {
const permissions = await checkPermission(context, item);
return permissions.view === true;
}, [checkPermission]);
/**
* Preload all permissions for UI context
* Call this early in the app lifecycle to warm the cache
*/
const preloadUiPermissions = useCallback(async (): Promise<void> => {
await loadBulkPermissions('UI');
}, [loadBulkPermissions]);
/**
* Clear the permission cache
* Useful when user permissions change or after logout
*/
const clearCache = useCallback(() => {
cacheRef.current = {};
bulkLoadedContextsRef.current.clear();
pendingRequests.current.clear();
}, []);
return {
checkPermission,
hasPermission,
canView,
preloadUiPermissions,
loading,
clearCache,
};
};