248 lines
7.2 KiB
TypeScript
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;
|