|
|
|
|
@@ -1,6 +1,8 @@
|
|
|
|
|
import React, { useState, useCallback } from 'react';
|
|
|
|
|
import React, { useState, useCallback, useMemo } from 'react';
|
|
|
|
|
import {
|
|
|
|
|
Alert,
|
|
|
|
|
Box,
|
|
|
|
|
Button,
|
|
|
|
|
Card,
|
|
|
|
|
CardContent,
|
|
|
|
|
Checkbox,
|
|
|
|
|
@@ -19,12 +21,141 @@ import {
|
|
|
|
|
Tooltip,
|
|
|
|
|
Typography,
|
|
|
|
|
} from '@mui/material';
|
|
|
|
|
import { ExpandMore, ExpandLess } from '@mui/icons-material';
|
|
|
|
|
import { ExpandMore, ExpandLess, Add as AddIcon } 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';
|
|
|
|
|
|
|
|
|
|
// ── Permission dependency map ──
|
|
|
|
|
// Each permission lists its prerequisites (must also be granted).
|
|
|
|
|
const PERMISSION_DEPS: Record<string, string[]> = {
|
|
|
|
|
// kalender
|
|
|
|
|
'kalender:create': ['kalender:view'],
|
|
|
|
|
'kalender:cancel': ['kalender:view'],
|
|
|
|
|
'kalender:mark_attendance': ['kalender:view'],
|
|
|
|
|
'kalender:create_bookings': ['kalender:view'],
|
|
|
|
|
'kalender:edit_bookings': ['kalender:view', 'kalender:create_bookings'],
|
|
|
|
|
'kalender:cancel_own_bookings': ['kalender:view'],
|
|
|
|
|
'kalender:delete_bookings': ['kalender:view', 'kalender:edit_bookings'],
|
|
|
|
|
'kalender:manage_categories': ['kalender:view'],
|
|
|
|
|
'kalender:view_reports': ['kalender:view'],
|
|
|
|
|
'kalender:widget_events': ['kalender:view'],
|
|
|
|
|
'kalender:widget_bookings': ['kalender:view'],
|
|
|
|
|
'kalender:widget_quick_add': ['kalender:view', 'kalender:create'],
|
|
|
|
|
// fahrzeuge
|
|
|
|
|
'fahrzeuge:create': ['fahrzeuge:view'],
|
|
|
|
|
'fahrzeuge:change_status': ['fahrzeuge:view'],
|
|
|
|
|
'fahrzeuge:manage_maintenance': ['fahrzeuge:view'],
|
|
|
|
|
'fahrzeuge:delete': ['fahrzeuge:view', 'fahrzeuge:create'],
|
|
|
|
|
'fahrzeuge:widget': ['fahrzeuge:view'],
|
|
|
|
|
// einsaetze
|
|
|
|
|
'einsaetze:view_reports': ['einsaetze:view'],
|
|
|
|
|
'einsaetze:create': ['einsaetze:view'],
|
|
|
|
|
'einsaetze:delete': ['einsaetze:view', 'einsaetze:create'],
|
|
|
|
|
'einsaetze:manage_personnel': ['einsaetze:view'],
|
|
|
|
|
// ausruestung
|
|
|
|
|
'ausruestung:create': ['ausruestung:view'],
|
|
|
|
|
'ausruestung:manage_maintenance': ['ausruestung:view'],
|
|
|
|
|
'ausruestung:delete': ['ausruestung:view', 'ausruestung:create'],
|
|
|
|
|
'ausruestung:widget': ['ausruestung:view'],
|
|
|
|
|
// mitglieder
|
|
|
|
|
'mitglieder:view_all': ['mitglieder:view_own'],
|
|
|
|
|
'mitglieder:edit': ['mitglieder:view_own', 'mitglieder:view_all'],
|
|
|
|
|
'mitglieder:create_profile': ['mitglieder:view_own', 'mitglieder:view_all', 'mitglieder:edit'],
|
|
|
|
|
// atemschutz
|
|
|
|
|
'atemschutz:create': ['atemschutz:view'],
|
|
|
|
|
'atemschutz:delete': ['atemschutz:view', 'atemschutz:create'],
|
|
|
|
|
'atemschutz:widget': ['atemschutz:view'],
|
|
|
|
|
// wissen
|
|
|
|
|
'wissen:widget_recent': ['wissen:view'],
|
|
|
|
|
'wissen:widget_search': ['wissen:view'],
|
|
|
|
|
// admin
|
|
|
|
|
'admin:write': ['admin:view'],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ── Group hierarchy ──
|
|
|
|
|
// When a permission is granted to a group, it is also auto-granted to all listed groups.
|
|
|
|
|
const GROUP_HIERARCHY: Record<string, string[]> = {
|
|
|
|
|
'dashboard_mitglied': ['dashboard_chargen', 'dashboard_atemschutz', 'dashboard_moderator', 'dashboard_zeugmeister', 'dashboard_fahrmeister', 'dashboard_gruppenfuehrer', 'dashboard_kommando'],
|
|
|
|
|
'dashboard_chargen': ['dashboard_gruppenfuehrer', 'dashboard_kommando'],
|
|
|
|
|
'dashboard_atemschutz': ['dashboard_kommando'],
|
|
|
|
|
'dashboard_moderator': ['dashboard_kommando'],
|
|
|
|
|
'dashboard_zeugmeister': ['dashboard_kommando'],
|
|
|
|
|
'dashboard_fahrmeister': ['dashboard_kommando'],
|
|
|
|
|
'dashboard_gruppenfuehrer': ['dashboard_kommando'],
|
|
|
|
|
'dashboard_kommando': [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Build reverse hierarchy: for each group, which groups propagate DOWN to it
|
|
|
|
|
// i.e. if group X lists Y in its hierarchy, then removing from Y should remove from X
|
|
|
|
|
function buildReverseHierarchy(): Record<string, string[]> {
|
|
|
|
|
const reverse: Record<string, string[]> = {};
|
|
|
|
|
for (const [group, inheritors] of Object.entries(GROUP_HIERARCHY)) {
|
|
|
|
|
for (const inheritor of inheritors) {
|
|
|
|
|
if (!reverse[inheritor]) reverse[inheritor] = [];
|
|
|
|
|
reverse[inheritor].push(group);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return reverse;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const REVERSE_HIERARCHY = buildReverseHierarchy();
|
|
|
|
|
|
|
|
|
|
// ── Dependency helpers ──
|
|
|
|
|
|
|
|
|
|
/** Recursively collect all prerequisite permissions for `permId`. */
|
|
|
|
|
function collectAllDeps(permId: string, visited = new Set<string>()): Set<string> {
|
|
|
|
|
if (visited.has(permId)) return visited;
|
|
|
|
|
visited.add(permId);
|
|
|
|
|
const deps = PERMISSION_DEPS[permId] || [];
|
|
|
|
|
for (const dep of deps) {
|
|
|
|
|
collectAllDeps(dep, visited);
|
|
|
|
|
}
|
|
|
|
|
return visited;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Build a reverse dependency map: for each permission, which permissions depend on it. */
|
|
|
|
|
function buildReverseDeps(): Record<string, string[]> {
|
|
|
|
|
const rev: Record<string, string[]> = {};
|
|
|
|
|
for (const [perm, deps] of Object.entries(PERMISSION_DEPS)) {
|
|
|
|
|
for (const dep of deps) {
|
|
|
|
|
if (!rev[dep]) rev[dep] = [];
|
|
|
|
|
rev[dep].push(perm);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return rev;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const REVERSE_DEPS = buildReverseDeps();
|
|
|
|
|
|
|
|
|
|
/** Recursively collect all permissions that depend on `permId`. */
|
|
|
|
|
function collectAllDependents(permId: string, visited = new Set<string>()): Set<string> {
|
|
|
|
|
if (visited.has(permId)) return visited;
|
|
|
|
|
visited.add(permId);
|
|
|
|
|
const dependents = REVERSE_DEPS[permId] || [];
|
|
|
|
|
for (const dep of dependents) {
|
|
|
|
|
collectAllDependents(dep, visited);
|
|
|
|
|
}
|
|
|
|
|
return visited;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Add a permission with all its deps to a set. */
|
|
|
|
|
function addPermWithDeps(current: Set<string>, permId: string): Set<string> {
|
|
|
|
|
const allNeeded = collectAllDeps(permId);
|
|
|
|
|
for (const p of allNeeded) current.add(p);
|
|
|
|
|
return current;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Remove a permission and all its dependents from a set. */
|
|
|
|
|
function removePermWithDependents(current: Set<string>, permId: string): Set<string> {
|
|
|
|
|
const allToRemove = collectAllDependents(permId);
|
|
|
|
|
for (const p of allToRemove) current.delete(p);
|
|
|
|
|
return current;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Component ──
|
|
|
|
|
|
|
|
|
|
function PermissionMatrixTab() {
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
const { showSuccess, showError } = useNotification();
|
|
|
|
|
@@ -34,14 +165,18 @@ function PermissionMatrixTab() {
|
|
|
|
|
queryFn: permissionsApi.getMatrix,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Track which feature groups are expanded
|
|
|
|
|
const { data: unknownGroups } = useQuery<string[]>({
|
|
|
|
|
queryKey: ['admin-unknown-groups'],
|
|
|
|
|
queryFn: permissionsApi.getUnknownGroups,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
|
|
|
|
|
|
|
|
|
const toggleGroup = (groupId: string) => {
|
|
|
|
|
setExpandedGroups(prev => ({ ...prev, [groupId]: !prev[groupId] }));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ── Maintenance toggle mutation ──
|
|
|
|
|
// ── Maintenance toggle ──
|
|
|
|
|
const maintenanceMutation = useMutation({
|
|
|
|
|
mutationFn: ({ featureGroup, active }: { featureGroup: string; active: boolean }) =>
|
|
|
|
|
permissionsApi.setMaintenanceFlag(featureGroup, active),
|
|
|
|
|
@@ -53,10 +188,10 @@ function PermissionMatrixTab() {
|
|
|
|
|
onError: () => showError('Fehler beim Aktualisieren des Wartungsmodus'),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── Permission toggle mutation (saves full group permissions) ──
|
|
|
|
|
// ── Permission save (saves full group permissions) ──
|
|
|
|
|
const permissionMutation = useMutation({
|
|
|
|
|
mutationFn: ({ group, permissions }: { group: string; permissions: string[] }) =>
|
|
|
|
|
permissionsApi.setGroupPermissions(group, permissions),
|
|
|
|
|
mutationFn: (updates: { group: string; permissions: string[] }[]) =>
|
|
|
|
|
Promise.all(updates.map(u => permissionsApi.setGroupPermissions(u.group, u.permissions))),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['admin-permission-matrix'] });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['my-permissions'] });
|
|
|
|
|
@@ -65,40 +200,185 @@ function PermissionMatrixTab() {
|
|
|
|
|
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 });
|
|
|
|
|
// ── Add unknown group ──
|
|
|
|
|
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');
|
|
|
|
|
},
|
|
|
|
|
[permissionMutation]
|
|
|
|
|
onError: () => showError('Fehler beim Hinzufügen der Gruppe'),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── Compute affected groups when toggling a permission ──
|
|
|
|
|
const computeUpdates = useCallback(
|
|
|
|
|
(
|
|
|
|
|
group: string,
|
|
|
|
|
permId: string,
|
|
|
|
|
grants: Record<string, string[]>,
|
|
|
|
|
allGroups: string[],
|
|
|
|
|
): { group: string; permissions: string[] }[] => {
|
|
|
|
|
const currentPerms = new Set(grants[group] || []);
|
|
|
|
|
const isAdding = !currentPerms.has(permId);
|
|
|
|
|
const updates: { group: string; permissions: string[] }[] = [];
|
|
|
|
|
|
|
|
|
|
if (isAdding) {
|
|
|
|
|
// Add perm + deps to this group
|
|
|
|
|
const newPerms = new Set(currentPerms);
|
|
|
|
|
addPermWithDeps(newPerms, permId);
|
|
|
|
|
updates.push({ group, permissions: Array.from(newPerms) });
|
|
|
|
|
|
|
|
|
|
// Hierarchy: also add to all groups that should inherit upward
|
|
|
|
|
const inheritors = GROUP_HIERARCHY[group] || [];
|
|
|
|
|
for (const inheritor of inheritors) {
|
|
|
|
|
if (!allGroups.includes(inheritor) || inheritor === 'dashboard_admin') continue;
|
|
|
|
|
const inhPerms = new Set(grants[inheritor] || []);
|
|
|
|
|
const beforeSize = inhPerms.size;
|
|
|
|
|
addPermWithDeps(inhPerms, permId);
|
|
|
|
|
if (inhPerms.size !== beforeSize || !inhPerms.has(permId)) {
|
|
|
|
|
// Only push if something changed
|
|
|
|
|
inhPerms.add(permId); // ensure the perm itself is there
|
|
|
|
|
// Re-add deps just in case
|
|
|
|
|
const allNeeded = collectAllDeps(permId);
|
|
|
|
|
for (const p of allNeeded) inhPerms.add(p);
|
|
|
|
|
updates.push({ group: inheritor, permissions: Array.from(inhPerms) });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Deduplicate: if a group already in updates, skip
|
|
|
|
|
const seen = new Set<string>();
|
|
|
|
|
return updates.filter(u => {
|
|
|
|
|
if (seen.has(u.group)) return false;
|
|
|
|
|
seen.add(u.group);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// Remove perm + dependents from this group
|
|
|
|
|
const newPerms = new Set(currentPerms);
|
|
|
|
|
removePermWithDependents(newPerms, permId);
|
|
|
|
|
updates.push({ group, permissions: Array.from(newPerms) });
|
|
|
|
|
|
|
|
|
|
// Hierarchy: also remove from all groups that are LOWER (reverse hierarchy)
|
|
|
|
|
// i.e. groups for which this group appears in their hierarchy list
|
|
|
|
|
const lowerGroups = REVERSE_HIERARCHY[group] || [];
|
|
|
|
|
for (const lower of lowerGroups) {
|
|
|
|
|
if (!allGroups.includes(lower) || lower === 'dashboard_admin') continue;
|
|
|
|
|
const lowerPerms = new Set(grants[lower] || []);
|
|
|
|
|
const beforeSize = lowerPerms.size;
|
|
|
|
|
const hadPerm = lowerPerms.has(permId);
|
|
|
|
|
removePermWithDependents(lowerPerms, permId);
|
|
|
|
|
if (lowerPerms.size !== beforeSize || hadPerm) {
|
|
|
|
|
updates.push({ group: lower, permissions: Array.from(lowerPerms) });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const seen = new Set<string>();
|
|
|
|
|
return updates.filter(u => {
|
|
|
|
|
if (seen.has(u.group)) return false;
|
|
|
|
|
seen.add(u.group);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handlePermissionToggle = useCallback(
|
|
|
|
|
(group: string, permId: string, grants: Record<string, string[]>, allGroups: string[]) => {
|
|
|
|
|
const updates = computeUpdates(group, permId, grants, allGroups);
|
|
|
|
|
if (updates.length > 0) {
|
|
|
|
|
permissionMutation.mutate(updates);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[computeUpdates, permissionMutation],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleSelectAllForGroup = useCallback(
|
|
|
|
|
(
|
|
|
|
|
authentikGroup: string,
|
|
|
|
|
featureGroupId: string,
|
|
|
|
|
permissions: Permission[],
|
|
|
|
|
currentGrants: Record<string, string[]>,
|
|
|
|
|
selectAll: boolean
|
|
|
|
|
allPermissions: Permission[],
|
|
|
|
|
grants: Record<string, string[]>,
|
|
|
|
|
allGroups: string[],
|
|
|
|
|
selectAll: boolean,
|
|
|
|
|
) => {
|
|
|
|
|
const fgPermIds = permissions
|
|
|
|
|
const fgPermIds = allPermissions
|
|
|
|
|
.filter(p => p.feature_group_id === featureGroupId)
|
|
|
|
|
.map(p => p.id);
|
|
|
|
|
const current = currentGrants[authentikGroup] || [];
|
|
|
|
|
let newPerms: string[];
|
|
|
|
|
|
|
|
|
|
// Build combined updates across all affected groups
|
|
|
|
|
const allUpdates = new Map<string, Set<string>>();
|
|
|
|
|
|
|
|
|
|
// Initialize with current grants for potentially affected groups
|
|
|
|
|
const initGroup = (g: string) => {
|
|
|
|
|
if (!allUpdates.has(g)) {
|
|
|
|
|
allUpdates.set(g, new Set(grants[g] || []));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
initGroup(authentikGroup);
|
|
|
|
|
|
|
|
|
|
if (selectAll) {
|
|
|
|
|
const permSet = new Set([...current, ...fgPermIds]);
|
|
|
|
|
newPerms = Array.from(permSet);
|
|
|
|
|
// Add all feature group perms with deps
|
|
|
|
|
for (const permId of fgPermIds) {
|
|
|
|
|
addPermWithDeps(allUpdates.get(authentikGroup)!, permId);
|
|
|
|
|
// Hierarchy upward
|
|
|
|
|
const inheritors = GROUP_HIERARCHY[authentikGroup] || [];
|
|
|
|
|
for (const inheritor of inheritors) {
|
|
|
|
|
if (!allGroups.includes(inheritor) || inheritor === 'dashboard_admin') continue;
|
|
|
|
|
initGroup(inheritor);
|
|
|
|
|
addPermWithDeps(allUpdates.get(inheritor)!, permId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
const removeSet = new Set(fgPermIds);
|
|
|
|
|
newPerms = current.filter(p => !removeSet.has(p));
|
|
|
|
|
// Remove all feature group perms with dependents
|
|
|
|
|
for (const permId of fgPermIds) {
|
|
|
|
|
removePermWithDependents(allUpdates.get(authentikGroup)!, permId);
|
|
|
|
|
// Hierarchy downward
|
|
|
|
|
const lowerGroups = REVERSE_HIERARCHY[authentikGroup] || [];
|
|
|
|
|
for (const lower of lowerGroups) {
|
|
|
|
|
if (!allGroups.includes(lower) || lower === 'dashboard_admin') continue;
|
|
|
|
|
initGroup(lower);
|
|
|
|
|
removePermWithDependents(allUpdates.get(lower)!, permId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build final update list, only groups that actually changed
|
|
|
|
|
const updates: { group: string; permissions: string[] }[] = [];
|
|
|
|
|
for (const [g, perms] of allUpdates) {
|
|
|
|
|
const original = new Set(grants[g] || []);
|
|
|
|
|
const newArr = Array.from(perms);
|
|
|
|
|
if (newArr.length !== original.size || newArr.some(p => !original.has(p))) {
|
|
|
|
|
updates.push({ group: g, permissions: newArr });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (updates.length > 0) {
|
|
|
|
|
permissionMutation.mutate(updates);
|
|
|
|
|
}
|
|
|
|
|
permissionMutation.mutate({ group: authentikGroup, permissions: newPerms });
|
|
|
|
|
},
|
|
|
|
|
[permissionMutation]
|
|
|
|
|
[permissionMutation],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// ── All known permission IDs for dependency tooltip ──
|
|
|
|
|
const allPermissionIds = useMemo(
|
|
|
|
|
() => (matrix ? new Set(matrix.permissions.map(p => p.id)) : new Set<string>()),
|
|
|
|
|
[matrix],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const getDepTooltip = useCallback(
|
|
|
|
|
(permId: string): string => {
|
|
|
|
|
const deps = PERMISSION_DEPS[permId];
|
|
|
|
|
if (!deps || deps.length === 0) return '';
|
|
|
|
|
const labels = deps
|
|
|
|
|
.filter(d => allPermissionIds.has(d))
|
|
|
|
|
.map(d => {
|
|
|
|
|
const p = matrix?.permissions.find(p => p.id === d);
|
|
|
|
|
return p ? p.label : d;
|
|
|
|
|
});
|
|
|
|
|
return labels.length > 0 ? `Benötigt: ${labels.join(', ')}` : '';
|
|
|
|
|
},
|
|
|
|
|
[allPermissionIds, matrix],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (isLoading || !matrix) {
|
|
|
|
|
@@ -114,6 +394,29 @@ function PermissionMatrixTab() {
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
|
|
|
|
{/* Unknown Groups Alert */}
|
|
|
|
|
{unknownGroups && unknownGroups.length > 0 && (
|
|
|
|
|
<Alert severity="warning" sx={{ alignItems: 'center' }}>
|
|
|
|
|
<Typography variant="body2" sx={{ mb: 1 }}>
|
|
|
|
|
Folgende Gruppen wurden in der Benutzertabelle gefunden, sind aber noch nicht in der Berechtigungsmatrix:
|
|
|
|
|
</Typography>
|
|
|
|
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
|
|
|
|
{unknownGroups.map(g => (
|
|
|
|
|
<Button
|
|
|
|
|
key={g}
|
|
|
|
|
variant="outlined"
|
|
|
|
|
size="small"
|
|
|
|
|
startIcon={<AddIcon />}
|
|
|
|
|
onClick={() => addGroupMutation.mutate(g)}
|
|
|
|
|
disabled={addGroupMutation.isPending}
|
|
|
|
|
>
|
|
|
|
|
{g} hinzufügen
|
|
|
|
|
</Button>
|
|
|
|
|
))}
|
|
|
|
|
</Box>
|
|
|
|
|
</Alert>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Section 1: Maintenance Toggles */}
|
|
|
|
|
<Card>
|
|
|
|
|
<CardContent>
|
|
|
|
|
@@ -162,13 +465,22 @@ function PermissionMatrixTab() {
|
|
|
|
|
<Table size="small" stickyHeader>
|
|
|
|
|
<TableHead>
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableCell sx={{ minWidth: 250, fontWeight: 'bold', position: 'sticky', left: 0, zIndex: 3, bgcolor: 'background.paper' }}>
|
|
|
|
|
<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
|
|
|
|
|
admin
|
|
|
|
|
</TableCell>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
{nonAdminGroups.map(g => (
|
|
|
|
|
@@ -215,7 +527,7 @@ function PermissionMatrixTab() {
|
|
|
|
|
{/* Per-group: select all / deselect all */}
|
|
|
|
|
{nonAdminGroups.map(g => {
|
|
|
|
|
const groupGrants = grants[g] || [];
|
|
|
|
|
const allGranted = fgPerms.every((p: Permission) => groupGrants.includes(p.id));
|
|
|
|
|
const allGranted = fgPerms.length > 0 && fgPerms.every((p: Permission) => groupGrants.includes(p.id));
|
|
|
|
|
const someGranted = fgPerms.some((p: Permission) => groupGrants.includes(p.id));
|
|
|
|
|
return (
|
|
|
|
|
<TableCell key={g} align="center">
|
|
|
|
|
@@ -223,7 +535,7 @@ function PermissionMatrixTab() {
|
|
|
|
|
checked={allGranted}
|
|
|
|
|
indeterminate={someGranted && !allGranted}
|
|
|
|
|
onChange={() =>
|
|
|
|
|
handleSelectAllForGroup(g, fg.id, permissions, grants, !allGranted)
|
|
|
|
|
handleSelectAllForGroup(g, fg.id, permissions, grants, groups, !allGranted)
|
|
|
|
|
}
|
|
|
|
|
disabled={permissionMutation.isPending}
|
|
|
|
|
size="small"
|
|
|
|
|
@@ -239,41 +551,47 @@ function PermissionMatrixTab() {
|
|
|
|
|
<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>
|
|
|
|
|
))}
|
|
|
|
|
{fgPerms.map((perm: Permission) => {
|
|
|
|
|
const depTooltip = getDepTooltip(perm.id);
|
|
|
|
|
const tooltipText = [perm.description, depTooltip].filter(Boolean).join('\n');
|
|
|
|
|
return (
|
|
|
|
|
<TableRow key={perm.id} hover>
|
|
|
|
|
<TableCell
|
|
|
|
|
sx={{
|
|
|
|
|
pl: 6,
|
|
|
|
|
minWidth: 250,
|
|
|
|
|
position: 'sticky',
|
|
|
|
|
left: 0,
|
|
|
|
|
zIndex: 1,
|
|
|
|
|
bgcolor: 'background.paper',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Tooltip title={tooltipText || ''} 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, groups)
|
|
|
|
|
}
|
|
|
|
|
disabled={permissionMutation.isPending}
|
|
|
|
|
size="small"
|
|
|
|
|
/>
|
|
|
|
|
</TableCell>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</TableRow>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</TableBody>
|
|
|
|
|
</Table>
|
|
|
|
|
</Collapse>
|
|
|
|
|
|