diff --git a/backend/src/services/permission.service.ts b/backend/src/services/permission.service.ts index 67e7cb7..a453089 100644 --- a/backend/src/services/permission.service.ts +++ b/backend/src/services/permission.service.ts @@ -144,20 +144,30 @@ class PermissionService { // ── Admin methods ── async getMatrix(): Promise { - const [fgResult, pResult, gpResult] = await Promise.all([ + const [fgResult, pResult, gpResult, userGroupsResult] = await Promise.all([ pool.query('SELECT id, label, sort_order, maintenance FROM feature_groups ORDER BY sort_order'), pool.query('SELECT id, feature_group_id, label, description, sort_order FROM permissions ORDER BY feature_group_id, sort_order'), pool.query('SELECT authentik_group, permission_id FROM group_permissions'), + // Also include all dashboard_ groups from users table + pool.query(`SELECT DISTINCT g AS group_name FROM users, unnest(authentik_groups) AS g WHERE g LIKE 'dashboard_%' AND g != 'dashboard_admin'`), ]); const grants: Record = {}; const groupSet = new Set(); + + // Add groups from group_permissions for (const row of gpResult.rows) { groupSet.add(row.authentik_group); if (!grants[row.authentik_group]) grants[row.authentik_group] = []; grants[row.authentik_group].push(row.permission_id); } + // Also add groups from users table (they may have no permissions yet) + for (const row of userGroupsResult.rows) { + groupSet.add(row.group_name); + if (!grants[row.group_name]) grants[row.group_name] = []; + } + const maintenance: Record = {}; for (const row of fgResult.rows) { maintenance[row.id] = row.maintenance; @@ -180,13 +190,14 @@ class PermissionService { } async getUnknownGroups(): Promise { - // Groups from users table that are not yet in the permission matrix + // Groups from users table that have zero permissions assigned + // (they appear in the matrix but admin should be notified) const result = await pool.query(` SELECT DISTINCT g AS group_name FROM users, unnest(authentik_groups) AS g WHERE g LIKE 'dashboard_%' - AND g NOT IN (SELECT DISTINCT authentik_group FROM group_permissions) AND g != 'dashboard_admin' + AND g NOT IN (SELECT DISTINCT authentik_group FROM group_permissions) ORDER BY group_name `); return result.rows.map((r: any) => r.group_name); diff --git a/frontend/src/components/admin/PermissionMatrixTab.tsx b/frontend/src/components/admin/PermissionMatrixTab.tsx index 759c2d3..797a30d 100644 --- a/frontend/src/components/admin/PermissionMatrixTab.tsx +++ b/frontend/src/components/admin/PermissionMatrixTab.tsx @@ -10,6 +10,11 @@ import { Chip, CircularProgress, Collapse, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, FormControlLabel, IconButton, Switch, @@ -109,6 +114,7 @@ function PermissionMatrixTab() { const [expandedGroups, setExpandedGroups] = useState>({}); const [showDepEditor, setShowDepEditor] = useState(false); + const [deleteGroupConfirm, setDeleteGroupConfirm] = useState(null); const toggleGroup = (groupId: string) => { setExpandedGroups(prev => ({ ...prev, [groupId]: !prev[groupId] })); @@ -137,16 +143,6 @@ function PermissionMatrixTab() { onError: () => showError('Fehler beim Speichern der Berechtigungen'), }); - const addGroupMutation = useMutation({ - mutationFn: (groupName: string) => permissionsApi.setGroupPermissions(groupName, []), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['admin-permission-matrix'] }); - queryClient.invalidateQueries({ queryKey: ['admin-unknown-groups'] }); - showSuccess('Gruppe hinzugefügt'); - }, - onError: () => showError('Fehler beim Hinzufügen der Gruppe'), - }); - const deleteGroupMutation = useMutation({ mutationFn: (groupName: string) => permissionsApi.deleteGroup(groupName), onSuccess: () => { @@ -290,18 +286,10 @@ function PermissionMatrixTab() { {/* Unknown Groups Alert */} {unknownGroups && unknownGroups.length > 0 && ( - - - Folgende Gruppen wurden in der Benutzertabelle gefunden, sind aber noch nicht in der Berechtigungsmatrix: + + + Folgende Gruppen haben noch keine Berechtigungen zugewiesen: {unknownGroups.join(', ')} - - {unknownGroups.map(g => ( - - ))} - )} @@ -339,7 +327,7 @@ function PermissionMatrixTab() { g === 'dashboard_admin')]} + allGroups={nonAdminGroups} allPermissions={permissions} onSave={(config) => depConfigMutation.mutate(config)} isSaving={depConfigMutation.isPending} @@ -376,11 +364,8 @@ function PermissionMatrixTab() { {g.replace('dashboard_', '')} - { - if (window.confirm(`Gruppe "${g}" und alle zugehörigen Berechtigungen wirklich entfernen?`)) { - deleteGroupMutation.mutate(g); - } - }} sx={{ opacity: 0.4, '&:hover': { opacity: 1, color: 'error.main' } }}> + setDeleteGroupConfirm(g)} + sx={{ opacity: 0.4, '&:hover': { opacity: 1, color: 'error.main' } }}> @@ -471,6 +456,33 @@ function PermissionMatrixTab() { + + {/* Delete Group Confirmation Dialog */} + setDeleteGroupConfirm(null)}> + Gruppe entfernen + + + Soll die Gruppe "{deleteGroupConfirm}" und alle zugehörigen Berechtigungen + wirklich entfernt werden? Diese Aktion kann nicht rückgängig gemacht werden. + + + + + + + ); } @@ -487,8 +499,28 @@ interface DependencyEditorProps { } function DependencyEditor({ groupHierarchy, permissionDeps, allGroups, allPermissions, onSave, isSaving }: DependencyEditorProps) { - const [editHierarchy, setEditHierarchy] = useState>(() => ({ ...groupHierarchy })); - const [editDeps, setEditDeps] = useState>(() => ({ ...permissionDeps })); + const groupSet = useMemo(() => new Set(allGroups), [allGroups]); + const permIdSet = useMemo(() => new Set(allPermissions.map(p => p.id)), [allPermissions]); + + // Filter saved config to only include groups/permissions that actually exist + const [editHierarchy, setEditHierarchy] = useState>(() => { + const filtered: Record = {}; + for (const [g, inheritors] of Object.entries(groupHierarchy)) { + if (groupSet.has(g)) { + filtered[g] = inheritors.filter(i => groupSet.has(i)); + } + } + return filtered; + }); + const [editDeps, setEditDeps] = useState>(() => { + const filtered: Record = {}; + for (const [p, deps] of Object.entries(permissionDeps)) { + if (permIdSet.has(p)) { + filtered[p] = deps.filter(d => permIdSet.has(d)); + } + } + return filtered; + }); const nonAdminGroups = allGroups.filter(g => g !== 'dashboard_admin'); const permOptions = allPermissions.map(p => p.id);