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({}); const bulkLoadPromiseRef = useRef | null>(null); const bulkLoadedContextsRef = useRef>(new Set()); const pendingRequests = useRef>>(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 => { // 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 => { 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 => { 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 => { 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 => { 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 => { 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, }; };