rights system

This commit is contained in:
Matthias Hochmeister
2026-03-23 10:07:53 +01:00
parent f976f36cbc
commit 2bb22850f4
35 changed files with 1565 additions and 282 deletions

View 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 &quot;dashboard_admin&quot; 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;