frontend_nyla/src/hooks/useAccessRules.tsx
2026-04-11 00:07:30 +02:00

256 lines
7.6 KiB
TypeScript

/**
* 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 function _getAccessLevelOptions(t: (key: string) => string): { value: 'n' | 'm' | 'g' | 'a'; label: string; color: string }[] {
return [
{ value: 'n', label: t('Keine'), color: '#e53e3e' },
{ value: 'm', label: t('Eigene'), color: '#d69e2e' },
{ value: 'g', label: t('Gruppe'), color: '#3182ce' },
{ value: 'a', label: t('Alle'), color: '#38a169' },
];
}
const _accessLevelColors: Record<string, string> = {
n: '#e53e3e',
m: '#d69e2e',
g: '#3182ce',
a: '#38a169',
};
export const getAccessLevelColor = (level: AccessLevel | null): string => {
if (!level) return '#718096';
return _accessLevelColors[level] || '#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<AccessRule[]>([]);
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(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<string, string> = {};
if (mandateId) {
headers['X-Mandate-Id'] = mandateId;
}
return headers;
}, [mandateId]);
/**
* Fetch all rules for the role
*/
const fetchRules = useCallback(async (): Promise<AccessRule[]> => {
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<SaveResult> => {
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<AccessRule>) => {
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;