rights system
This commit is contained in:
294
frontend/src/components/admin/PermissionMatrixTab.tsx
Normal file
294
frontend/src/components/admin/PermissionMatrixTab.tsx
Normal file
@@ -0,0 +1,294 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Checkbox,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Collapse,
|
||||
FormControlLabel,
|
||||
IconButton,
|
||||
Switch,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { ExpandMore, ExpandLess } from '@mui/icons-material';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { permissionsApi } from '../../services/permissions';
|
||||
import { useNotification } from '../../contexts/NotificationContext';
|
||||
import type { PermissionMatrix, FeatureGroup, Permission } from '../../types/permissions.types';
|
||||
|
||||
function PermissionMatrixTab() {
|
||||
const queryClient = useQueryClient();
|
||||
const { showSuccess, showError } = useNotification();
|
||||
|
||||
const { data: matrix, isLoading } = useQuery<PermissionMatrix>({
|
||||
queryKey: ['admin-permission-matrix'],
|
||||
queryFn: permissionsApi.getMatrix,
|
||||
});
|
||||
|
||||
// Track which feature groups are expanded
|
||||
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
||||
|
||||
const toggleGroup = (groupId: string) => {
|
||||
setExpandedGroups(prev => ({ ...prev, [groupId]: !prev[groupId] }));
|
||||
};
|
||||
|
||||
// ── Maintenance toggle mutation ──
|
||||
const maintenanceMutation = useMutation({
|
||||
mutationFn: ({ featureGroup, active }: { featureGroup: string; active: boolean }) =>
|
||||
permissionsApi.setMaintenanceFlag(featureGroup, active),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-permission-matrix'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['my-permissions'] });
|
||||
showSuccess('Wartungsmodus aktualisiert');
|
||||
},
|
||||
onError: () => showError('Fehler beim Aktualisieren des Wartungsmodus'),
|
||||
});
|
||||
|
||||
// ── Permission toggle mutation (saves full group permissions) ──
|
||||
const permissionMutation = useMutation({
|
||||
mutationFn: ({ group, permissions }: { group: string; permissions: string[] }) =>
|
||||
permissionsApi.setGroupPermissions(group, permissions),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-permission-matrix'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['my-permissions'] });
|
||||
showSuccess('Berechtigungen gespeichert');
|
||||
},
|
||||
onError: () => showError('Fehler beim Speichern der Berechtigungen'),
|
||||
});
|
||||
|
||||
const handlePermissionToggle = useCallback(
|
||||
(group: string, permId: string, currentGrants: Record<string, string[]>) => {
|
||||
const current = currentGrants[group] || [];
|
||||
const newPerms = current.includes(permId)
|
||||
? current.filter(p => p !== permId)
|
||||
: [...current, permId];
|
||||
permissionMutation.mutate({ group, permissions: newPerms });
|
||||
},
|
||||
[permissionMutation]
|
||||
);
|
||||
|
||||
const handleSelectAllForGroup = useCallback(
|
||||
(
|
||||
authentikGroup: string,
|
||||
featureGroupId: string,
|
||||
permissions: Permission[],
|
||||
currentGrants: Record<string, string[]>,
|
||||
selectAll: boolean
|
||||
) => {
|
||||
const fgPermIds = permissions
|
||||
.filter(p => p.feature_group_id === featureGroupId)
|
||||
.map(p => p.id);
|
||||
const current = currentGrants[authentikGroup] || [];
|
||||
let newPerms: string[];
|
||||
if (selectAll) {
|
||||
const permSet = new Set([...current, ...fgPermIds]);
|
||||
newPerms = Array.from(permSet);
|
||||
} else {
|
||||
const removeSet = new Set(fgPermIds);
|
||||
newPerms = current.filter(p => !removeSet.has(p));
|
||||
}
|
||||
permissionMutation.mutate({ group: authentikGroup, permissions: newPerms });
|
||||
},
|
||||
[permissionMutation]
|
||||
);
|
||||
|
||||
if (isLoading || !matrix) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const { featureGroups, permissions, groups, grants, maintenance } = matrix;
|
||||
const nonAdminGroups = groups.filter(g => g !== 'dashboard_admin');
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
{/* Section 1: Maintenance Toggles */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Wartungsmodus
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
Wenn aktiviert, wird die Funktion für alle Benutzer ausser Administratoren ausgeblendet.
|
||||
</Typography>
|
||||
{featureGroups.map((fg: FeatureGroup) => (
|
||||
<Box key={fg.id} sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 0.5 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={maintenance[fg.id] ?? false}
|
||||
onChange={() =>
|
||||
maintenanceMutation.mutate({
|
||||
featureGroup: fg.id,
|
||||
active: !(maintenance[fg.id] ?? false),
|
||||
})
|
||||
}
|
||||
disabled={maintenanceMutation.isPending}
|
||||
/>
|
||||
}
|
||||
label={fg.label}
|
||||
/>
|
||||
{maintenance[fg.id] && (
|
||||
<Chip label="Wartungsmodus" color="warning" size="small" />
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Section 2: Permission Matrix */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Berechtigungsmatrix
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
Berechtigungen pro Authentik-Gruppe zuweisen. Die Gruppe "dashboard_admin" hat immer vollen Zugriff.
|
||||
</Typography>
|
||||
|
||||
<TableContainer sx={{ maxHeight: '70vh' }}>
|
||||
<Table size="small" stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ minWidth: 250, fontWeight: 'bold', position: 'sticky', left: 0, zIndex: 3, bgcolor: 'background.paper' }}>
|
||||
Berechtigung
|
||||
</TableCell>
|
||||
{/* dashboard_admin column */}
|
||||
<Tooltip title="Admin hat immer vollen Zugriff" placement="top">
|
||||
<TableCell align="center" sx={{ minWidth: 120, fontWeight: 'bold', opacity: 0.5 }}>
|
||||
dashboard_admin
|
||||
</TableCell>
|
||||
</Tooltip>
|
||||
{nonAdminGroups.map(g => (
|
||||
<TableCell key={g} align="center" sx={{ minWidth: 120, fontWeight: 'bold' }}>
|
||||
{g.replace('dashboard_', '')}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{featureGroups.map((fg: FeatureGroup) => {
|
||||
const fgPerms = permissions.filter((p: Permission) => p.feature_group_id === fg.id);
|
||||
const isExpanded = expandedGroups[fg.id] !== false; // default expanded
|
||||
|
||||
return (
|
||||
<React.Fragment key={fg.id}>
|
||||
{/* Feature group header row */}
|
||||
<TableRow sx={{ bgcolor: 'action.hover' }}>
|
||||
<TableCell
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
position: 'sticky',
|
||||
left: 0,
|
||||
zIndex: 2,
|
||||
bgcolor: 'action.hover',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => toggleGroup(fg.id)}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<IconButton size="small">
|
||||
{isExpanded ? <ExpandLess fontSize="small" /> : <ExpandMore fontSize="small" />}
|
||||
</IconButton>
|
||||
{fg.label}
|
||||
{maintenance[fg.id] && (
|
||||
<Chip label="Wartung" color="warning" size="small" />
|
||||
)}
|
||||
</Box>
|
||||
</TableCell>
|
||||
{/* Admin: all checked */}
|
||||
<TableCell align="center">
|
||||
<Checkbox checked disabled sx={{ opacity: 0.3 }} />
|
||||
</TableCell>
|
||||
{/* Per-group: select all / deselect all */}
|
||||
{nonAdminGroups.map(g => {
|
||||
const groupGrants = grants[g] || [];
|
||||
const allGranted = fgPerms.every((p: Permission) => groupGrants.includes(p.id));
|
||||
const someGranted = fgPerms.some((p: Permission) => groupGrants.includes(p.id));
|
||||
return (
|
||||
<TableCell key={g} align="center">
|
||||
<Checkbox
|
||||
checked={allGranted}
|
||||
indeterminate={someGranted && !allGranted}
|
||||
onChange={() =>
|
||||
handleSelectAllForGroup(g, fg.id, permissions, grants, !allGranted)
|
||||
}
|
||||
disabled={permissionMutation.isPending}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
|
||||
{/* Individual permission rows */}
|
||||
<TableRow>
|
||||
<TableCell colSpan={2 + nonAdminGroups.length} sx={{ p: 0 }}>
|
||||
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
||||
<Table size="small">
|
||||
<TableBody>
|
||||
{fgPerms.map((perm: Permission) => (
|
||||
<TableRow key={perm.id} hover>
|
||||
<TableCell
|
||||
sx={{
|
||||
pl: 6,
|
||||
minWidth: 250,
|
||||
position: 'sticky',
|
||||
left: 0,
|
||||
zIndex: 1,
|
||||
bgcolor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
<Tooltip title={perm.description || ''} placement="right">
|
||||
<span>{perm.label}</span>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
{/* Admin: always checked */}
|
||||
<TableCell align="center" sx={{ minWidth: 120 }}>
|
||||
<Checkbox checked disabled sx={{ opacity: 0.3 }} />
|
||||
</TableCell>
|
||||
{nonAdminGroups.map(g => {
|
||||
const isGranted = (grants[g] || []).includes(perm.id);
|
||||
return (
|
||||
<TableCell key={g} align="center" sx={{ minWidth: 120 }}>
|
||||
<Checkbox
|
||||
checked={isGranted}
|
||||
onChange={() => handlePermissionToggle(g, perm.id, grants)}
|
||||
disabled={permissionMutation.isPending}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default PermissionMatrixTab;
|
||||
Reference in New Issue
Block a user