255 lines
8 KiB
TypeScript
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,
|
|
};
|
|
};
|
|
|