ui-nyla/src/hooks/useAccessRules.ts
2026-01-21 00:32:52 +01:00

248 lines
7.2 KiB
TypeScript

/**
* 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<AccessRule[]>([]);
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
/**
* Fetch all rules for the role
*/
const fetchRules = useCallback(async (): Promise<AccessRule[]> => {
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<AccessRule>) => {
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;