256 lines
7.6 KiB
TypeScript
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;
|