/** * useAccessRules Hook * * Hook for managing RBAC access rules for a role. * Supports both system admin (template roles) and feature admin (instance roles). */ import { useState, useCallback } from 'react'; import api from '../api'; // ============================================================================= // TYPES // ============================================================================= export type RuleContext = 'DATA' | 'UI' | 'RESOURCE'; export type AccessLevel = 'n' | 'm' | 'g' | 'a' | null; // ============================================================================= // ACCESS LEVEL LABELS // ============================================================================= export const ACCESS_LEVEL_OPTIONS: { value: 'n' | 'm' | 'g' | 'a'; label: string; color: string }[] = [ { value: 'n', label: 'Keine', color: '#e53e3e' }, { value: 'm', label: 'Eigene', color: '#d69e2e' }, { value: 'g', label: 'Gruppe', color: '#3182ce' }, { value: 'a', label: 'Alle', color: '#38a169' }, ]; export const getAccessLevelLabel = (level: AccessLevel | null): string => { if (!level) return '-'; const option = ACCESS_LEVEL_OPTIONS.find(opt => opt.value === level); return option?.label || level; }; export const getAccessLevelColor = (level: AccessLevel | null): string => { if (!level) return '#718096'; const option = ACCESS_LEVEL_OPTIONS.find(opt => opt.value === level); return option?.color || '#718096'; }; export interface AccessRule { id: string; roleId: string; context: RuleContext; item: string | null; view: boolean; read: AccessLevel; create: AccessLevel; update: AccessLevel; delete: AccessLevel; } export interface AccessRuleCreate { context: RuleContext; item?: string | null; view?: boolean; read?: AccessLevel; create?: AccessLevel; update?: AccessLevel; delete?: AccessLevel; } interface GroupedRules { DATA: AccessRule[]; UI: AccessRule[]; RESOURCE: AccessRule[]; } interface SaveResult { success: boolean; error?: string; } // ============================================================================= // HOOK // ============================================================================= export function useAccessRules(roleId: string, apiBasePath: string = '/api/rbac', mandateId?: string) { const [rules, setRules] = useState([]); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); // Determine if this is a feature-instance API path const isInstanceApi = apiBasePath.includes('/instance-roles/'); // Build headers with optional mandate ID const getHeaders = useCallback(() => { const headers: Record = {}; if (mandateId) { headers['X-Mandate-Id'] = mandateId; } return headers; }, [mandateId]); /** * Fetch all rules for the role */ const fetchRules = useCallback(async (): Promise => { setLoading(true); setError(null); try { // Different endpoint structure for instance roles vs system roles const endpoint = isInstanceApi ? `${apiBasePath}/rules` : `${apiBasePath}/rules/by-role/${roleId}`; const response = await api.get(endpoint, { headers: getHeaders() }); const fetchedRules = response.data?.items || response.data || []; setRules(fetchedRules); return fetchedRules; } catch (err: any) { const errorMsg = err.response?.data?.detail || err.message || 'Fehler beim Laden der Regeln'; setError(errorMsg); console.error('Error fetching rules:', err); return []; } finally { setLoading(false); } }, [roleId, apiBasePath, isInstanceApi, getHeaders]); /** * Save all rules for the role */ const saveRules = useCallback(async (rulesToSave: AccessRule[]): Promise => { setSaving(true); setError(null); try { const headers = getHeaders(); // Different endpoint structure for instance roles vs system roles const rulesEndpoint = isInstanceApi ? `${apiBasePath}/rules` : `${apiBasePath}/rules/by-role/${roleId}`; // Get current rules from server const currentResponse = await api.get(rulesEndpoint, { headers }); const currentRules: AccessRule[] = currentResponse.data?.items || currentResponse.data || []; const currentRuleIds = new Set(currentRules.map(r => r.id)); // Determine changes const newRules = rulesToSave.filter(r => r.id.startsWith('temp-')); const existingRules = rulesToSave.filter(r => !r.id.startsWith('temp-')); const deletedRuleIds = [...currentRuleIds].filter( id => !existingRules.some(r => r.id === id) ); // Delete removed rules for (const deletedId of deletedRuleIds) { const deleteEndpoint = isInstanceApi ? `${apiBasePath}/rules/${deletedId}` : `${apiBasePath}/rules/${deletedId}`; await api.delete(deleteEndpoint, { headers }); } // Create new rules for (const rule of newRules) { const createEndpoint = isInstanceApi ? `${apiBasePath}/rules` : `${apiBasePath}/rules`; await api.post(createEndpoint, { roleId, context: rule.context, item: rule.item, view: rule.view, read: rule.read, create: rule.create, update: rule.update, delete: rule.delete, }, { headers }); } // Update existing rules for (const rule of existingRules) { const original = currentRules.find(r => r.id === rule.id); if (original && JSON.stringify(original) !== JSON.stringify(rule)) { const updateEndpoint = isInstanceApi ? `${apiBasePath}/rules/${rule.id}` : `${apiBasePath}/rules/${rule.id}`; await api.put(updateEndpoint, { view: rule.view, read: rule.read, create: rule.create, update: rule.update, delete: rule.delete, }, { headers }); } } // Refresh rules await fetchRules(); return { success: true }; } catch (err: any) { const errorMsg = err.response?.data?.detail || err.message || 'Fehler beim Speichern'; setError(errorMsg); console.error('Error saving rules:', err); return { success: false, error: errorMsg }; } finally { setSaving(false); } }, [roleId, apiBasePath, isInstanceApi, fetchRules, getHeaders]); /** * Get rules grouped by context */ const getGroupedRules = useCallback((): GroupedRules => { return { DATA: rules.filter(r => r.context === 'DATA'), UI: rules.filter(r => r.context === 'UI'), RESOURCE: rules.filter(r => r.context === 'RESOURCE'), }; }, [rules]); /** * Update a rule locally (not saved until saveRules is called) */ const updateRuleLocally = useCallback((ruleId: string, updates: Partial) => { setRules(prev => prev.map(r => r.id === ruleId ? { ...r, ...updates } : r )); }, []); /** * Add a rule locally (not saved until saveRules is called) */ const addRuleLocally = useCallback((rule: AccessRule) => { setRules(prev => [...prev, rule]); }, []); /** * Remove a rule locally (not saved until saveRules is called) */ const removeRuleLocally = useCallback((ruleId: string) => { setRules(prev => prev.filter(r => r.id !== ruleId)); }, []); return { rules, loading, saving, error, fetchRules, saveRules, getGroupedRules, updateRuleLocally, addRuleLocally, removeRuleLocally, }; } export default useAccessRules;