import { useState, useEffect, useCallback, useRef } from 'react'; import { useApiRequest } from './useApi'; import { fetchPermissions as fetchPermissionsApi, 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'; /** * Hook for managing RBAC permissions * Provides centralized permission checking with caching */ export const usePermissions = () => { const [cache, setCache] = useState({}); const cacheRef = useRef({}); const [loading, setLoading] = useState(false); const pendingRequests = useRef>>(new Map()); const { request } = useApiRequest(); // Keep cacheRef in sync with cache state useEffect(() => { cacheRef.current = cache; }, [cache]); /** * Generate a cache key for a permission check */ const getPermissionKey = (context: PermissionContext, item?: string): string => { return `${context}:${item || 'null'}`; }; /** * Retry function with exponential backoff for 429 errors */ const retryWithBackoff = async ( fn: () => Promise, maxRetries = 3, baseDelay = 1000 ): Promise => { for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await fn(); } catch (error: any) { if (error.response?.status === 429 && attempt < maxRetries - 1) { const delay = baseDelay * Math.pow(2, attempt); await new Promise(resolve => setTimeout(resolve, delay)); continue; } throw error; } } }; /** * Check permissions for a given context and item * Returns full UserPermissions object * Checks cache first, then fetches from backend if not cached */ const checkPermission = useCallback(async ( context: PermissionContext, item?: string ): Promise => { const key = getPermissionKey(context, item); // Check cache first using ref to avoid stale closures 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)!; } // Create new request const requestPromise = (async () => { setLoading(true); try { // Use retry logic for 429 errors // Note: We wrap the API call in retry logic since useApiRequest doesn't handle 429 retries const permissions = await retryWithBackoff(async () => { try { return await fetchPermissionsApi(request, context, item); } catch (error: any) { // If useApiRequest throws, we need to check if it's a 429 // For now, we'll let the retry logic handle it throw error; } }); // Update cache after fetching from backend setCache(prev => { const newCache = { ...prev, [key]: permissions }; cacheRef.current = newCache; return newCache; }); return permissions; } catch (error: any) { // Only log non-429 errors to avoid spam if (error.response?.status !== 429) { console.error('Error checking permissions:', error); } // Return cached value if available, otherwise default (no access) const cached = cacheRef.current[key]; if (cached) { return cached; } return { view: false, read: 'n' as PermissionLevel, create: 'n' as PermissionLevel, update: 'n' as PermissionLevel, delete: 'n' as PermissionLevel, }; } finally { pendingRequests.current.delete(key); setLoading(false); } })(); // Store pending request to prevent duplicates pendingRequests.current.set(key, requestPromise); return requestPromise; }, [request]); /** * 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; }, [checkPermission]); /** * Clear the permission cache * Useful when user permissions change or after logout */ const clearCache = useCallback(() => { setCache({}); cacheRef.current = {}; pendingRequests.current.clear(); }, []); return { checkPermission, hasPermission, canView, loading, clearCache, }; };