195 lines
5.4 KiB
TypeScript
195 lines
5.4 KiB
TypeScript
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<PermissionCache>({});
|
|
const cacheRef = useRef<PermissionCache>({});
|
|
const [loading, setLoading] = useState(false);
|
|
const pendingRequests = useRef<Map<string, Promise<UserPermissions>>>(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<any>,
|
|
maxRetries = 3,
|
|
baseDelay = 1000
|
|
): Promise<any> => {
|
|
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<UserPermissions> => {
|
|
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<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;
|
|
}, [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,
|
|
};
|
|
};
|
|
|