/** * useAccessRules Hook * * Hook for managing RBAC AccessRules for a specific role. * Provides CRUD operations for access rules. */ import { useState, useCallback } from 'react'; import api from '../api'; // ============================================================================= // TYPES // ============================================================================= export type AccessLevel = 'n' | 'm' | 'g' | 'a'; export type RuleContext = 'DATA' | 'UI' | 'RESOURCE'; export interface AccessRule { id: string; roleId: string; context: RuleContext; item: string | null; view: boolean; read: AccessLevel | null; create: AccessLevel | null; update: AccessLevel | null; delete: AccessLevel | null; } export interface AccessRuleCreate { context: RuleContext; item: string | null; view?: boolean; read?: AccessLevel | null; create?: AccessLevel | null; update?: AccessLevel | null; delete?: AccessLevel | null; } export interface AccessRuleUpdate { view?: boolean; read?: AccessLevel | null; create?: AccessLevel | null; update?: AccessLevel | null; delete?: AccessLevel | null; } // Grouped rules by context export interface GroupedRules { DATA: AccessRule[]; UI: AccessRule[]; RESOURCE: AccessRule[]; } // ============================================================================= // ACCESS LEVEL LABELS // ============================================================================= export const ACCESS_LEVEL_OPTIONS: { value: AccessLevel; 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'; }; // ============================================================================= // HOOK // ============================================================================= export function useAccessRules(roleId: string | null) { const [rules, setRules] = useState([]); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); /** * Fetch all rules for the role */ const fetchRules = useCallback(async (): Promise => { if (!roleId) { setRules([]); return []; } setLoading(true); setError(null); try { const response = await api.get(`/api/rbac/roles/${roleId}/rules`); const fetchedRules = Array.isArray(response.data) ? response.data : response.data.rules || []; setRules(fetchedRules); return fetchedRules; } catch (err: any) { const errorMessage = err.response?.data?.detail || err.message || 'Failed to fetch access rules'; setError(errorMessage); setRules([]); return []; } finally { setLoading(false); } }, [roleId]); /** * Save all rules for the role (bulk update) */ const saveRules = useCallback(async (newRules: AccessRule[]): Promise<{ success: boolean; error?: string }> => { if (!roleId) { return { success: false, error: 'No role selected' }; } setSaving(true); setError(null); try { await api.put(`/api/rbac/roles/${roleId}/rules`, { rules: newRules }); setRules(newRules); return { success: true }; } catch (err: any) { const errorMessage = err.response?.data?.detail || err.message || 'Failed to save access rules'; setError(errorMessage); return { success: false, error: errorMessage }; } finally { setSaving(false); } }, [roleId]); /** * Create a new rule */ const createRule = useCallback(async (ruleData: AccessRuleCreate): Promise<{ success: boolean; data?: AccessRule; error?: string }> => { if (!roleId) { return { success: false, error: 'No role selected' }; } setSaving(true); setError(null); try { const response = await api.post(`/api/rbac/roles/${roleId}/rules`, ruleData); const newRule = response.data; setRules(prev => [...prev, newRule]); return { success: true, data: newRule }; } catch (err: any) { const errorMessage = err.response?.data?.detail || err.message || 'Failed to create access rule'; setError(errorMessage); return { success: false, error: errorMessage }; } finally { setSaving(false); } }, [roleId]); /** * Update an existing rule */ const updateRule = useCallback(async (ruleId: string, updates: AccessRuleUpdate): Promise<{ success: boolean; error?: string }> => { setSaving(true); setError(null); try { const response = await api.patch(`/api/rbac/rules/${ruleId}`, updates); setRules(prev => prev.map(r => r.id === ruleId ? { ...r, ...response.data } : r)); return { success: true }; } catch (err: any) { const errorMessage = err.response?.data?.detail || err.message || 'Failed to update access rule'; setError(errorMessage); return { success: false, error: errorMessage }; } finally { setSaving(false); } }, []); /** * Delete a rule */ const deleteRule = useCallback(async (ruleId: string): Promise<{ success: boolean; error?: string }> => { setSaving(true); setError(null); try { await api.delete(`/api/rbac/rules/${ruleId}`); setRules(prev => prev.filter(r => r.id !== ruleId)); return { success: true }; } catch (err: any) { const errorMessage = err.response?.data?.detail || err.message || 'Failed to delete access rule'; setError(errorMessage); return { success: false, error: errorMessage }; } finally { setSaving(false); } }, []); /** * 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 rule locally (for optimistic updates) */ const updateRuleLocally = useCallback((ruleId: string, updates: Partial) => { setRules(prev => prev.map(r => r.id === ruleId ? { ...r, ...updates } : r)); }, []); /** * Add rule locally (for optimistic updates) */ const addRuleLocally = useCallback((rule: AccessRule) => { setRules(prev => [...prev, rule]); }, []); /** * Remove rule locally (for optimistic updates) */ const removeRuleLocally = useCallback((ruleId: string) => { setRules(prev => prev.filter(r => r.id !== ruleId)); }, []); return { rules, loading, saving, error, fetchRules, saveRules, createRule, updateRule, deleteRule, getGroupedRules, updateRuleLocally, addRuleLocally, removeRuleLocally, }; } export default useAccessRules;