Update
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,6 @@ import {
|
||||
} from "@material-tailwind/react";
|
||||
|
||||
import { useToast } from "@/components/ToastProvider.jsx";
|
||||
import { platformSettingsData } from "@/data";
|
||||
import { AVATAR_COLORS } from "@/data/avatarColors.js";
|
||||
import { useMaintenance } from "@/components/MaintenanceProvider.jsx";
|
||||
import { useAuth } from "@/components/AuthProvider.jsx";
|
||||
@@ -69,6 +68,47 @@ const mapUser = (item) => ({
|
||||
securityPhraseDownloadedAt: item?.securityPhraseDownloadedAt || null
|
||||
});
|
||||
|
||||
const normalizeServer = (item) => {
|
||||
if (!item || typeof item !== "object") {
|
||||
return null;
|
||||
}
|
||||
const id = Number(item.id ?? item.serverId ?? item.server_id);
|
||||
if (!Number.isFinite(id)) {
|
||||
return null;
|
||||
}
|
||||
const name = item.name || item.serverName || item.server_name || `Server ${id}`;
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
url: item.url || item.serverUrl || item.server_url || ""
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeAssignmentsList = (list, servers = []) => {
|
||||
const serverMap = new Map(servers.map((server) => [server.id, server]));
|
||||
if (!Array.isArray(list)) {
|
||||
return [];
|
||||
}
|
||||
return list
|
||||
.map((entry) => {
|
||||
const serverId = Number(entry?.serverId ?? entry?.server_id);
|
||||
if (!Number.isFinite(serverId)) {
|
||||
return null;
|
||||
}
|
||||
const groupId = Number(entry?.groupId ?? entry?.group_id);
|
||||
const serverFallback = serverMap.get(serverId);
|
||||
return {
|
||||
serverId,
|
||||
serverName: entry?.serverName || entry?.server_name || serverFallback?.name || "",
|
||||
serverUrl: entry?.serverUrl || entry?.server_url || serverFallback?.url || "",
|
||||
groupId: Number.isFinite(groupId) ? groupId : null,
|
||||
groupName: entry?.groupName || entry?.group_name || "",
|
||||
useGlobalGroup: Boolean(entry?.useGlobalGroup ?? entry?.use_global_group)
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
const extractPrimaryGroupId = (user) => {
|
||||
if (!user || !Array.isArray(user.groups) || user.groups.length === 0) {
|
||||
return null;
|
||||
@@ -123,6 +163,11 @@ export function UserDetails() {
|
||||
const [securityPhraseLoading, setSecurityPhraseLoading] = useState(false);
|
||||
const [securityPhraseError, setSecurityPhraseError] = useState("");
|
||||
const [renewingSecurityPhrase, setRenewingSecurityPhrase] = useState(false);
|
||||
const [serverAssignments, setServerAssignments] = useState([]);
|
||||
const [availableServers, setAvailableServers] = useState([]);
|
||||
const [assignmentsError, setAssignmentsError] = useState("");
|
||||
const [savingAssignments, setSavingAssignments] = useState(false);
|
||||
const initialAssignmentsRef = useRef([]);
|
||||
|
||||
const maintenanceActive = Boolean(maintenanceMeta?.active);
|
||||
const maintenanceMessage = maintenanceMeta?.message;
|
||||
@@ -172,11 +217,22 @@ export function UserDetails() {
|
||||
if (!item.id) {
|
||||
throw new Error("USER_NOT_FOUND");
|
||||
}
|
||||
const normalizedServers = Array.isArray(response.data?.servers)
|
||||
? response.data.servers.map(normalizeServer).filter(Boolean)
|
||||
: [];
|
||||
setAvailableServers(normalizedServers);
|
||||
const normalizedAssignments = normalizeAssignmentsList(
|
||||
response.data?.item?.serverAssignments ?? response.data?.serverAssignments ?? [],
|
||||
normalizedServers
|
||||
);
|
||||
initialAssignmentsRef.current = normalizedAssignments;
|
||||
setServerAssignments(normalizedAssignments);
|
||||
setAssignmentsError("");
|
||||
setUser(item);
|
||||
setSecurityPhraseWords([]);
|
||||
setSecurityPhraseDownloadedAt(item.securityPhraseDownloadedAt || null);
|
||||
setSecurityPhraseError("");
|
||||
const initialValues = buildInitialFormValues(item);
|
||||
const initialValues = buildInitialFormValues(item);
|
||||
initialFormValuesRef.current = { ...initialValues };
|
||||
setFormValues(initialValues);
|
||||
setSaveError("");
|
||||
@@ -195,6 +251,9 @@ export function UserDetails() {
|
||||
setUser(null);
|
||||
initialFormValuesRef.current = buildInitialFormValues(null);
|
||||
setFormValues(buildInitialFormValues(null));
|
||||
setServerAssignments([]);
|
||||
setAvailableServers([]);
|
||||
setAssignmentsError("");
|
||||
setError(message);
|
||||
showToast({
|
||||
variant: "error",
|
||||
@@ -341,6 +400,208 @@ export function UserDetails() {
|
||||
}));
|
||||
}, [canEditUsers]);
|
||||
|
||||
const assignmentsChanged = useMemo(() => {
|
||||
const serialize = (list) =>
|
||||
JSON.stringify(
|
||||
list
|
||||
.map((entry) => ({
|
||||
serverId: Number(entry.serverId) || null,
|
||||
groupId: entry.useGlobalGroup ? null : (Number(entry.groupId) || null),
|
||||
useGlobalGroup: Boolean(entry.useGlobalGroup)
|
||||
}))
|
||||
.sort((a, b) => (a.serverId || 0) - (b.serverId || 0))
|
||||
);
|
||||
return serialize(serverAssignments) !== serialize(initialAssignmentsRef.current);
|
||||
}, [serverAssignments]);
|
||||
|
||||
const usedServerIds = useMemo(
|
||||
() =>
|
||||
new Set(
|
||||
serverAssignments
|
||||
.map((entry) => Number(entry.serverId))
|
||||
.filter((value) => Number.isFinite(value) && value > 0)
|
||||
),
|
||||
[serverAssignments]
|
||||
);
|
||||
|
||||
const getServerOptionsForRow = useCallback(
|
||||
(currentServerId) => {
|
||||
return availableServers.filter(
|
||||
(server) => server.id === currentServerId || !usedServerIds.has(server.id)
|
||||
);
|
||||
},
|
||||
[availableServers, usedServerIds]
|
||||
);
|
||||
|
||||
const handleAddAssignment = useCallback(() => {
|
||||
if (!canEditUsers || isSuperuserUser) return;
|
||||
const usedServerIds = new Set(
|
||||
serverAssignments
|
||||
.map((entry) => Number(entry.serverId))
|
||||
.filter((value) => Number.isFinite(value) && value > 0)
|
||||
);
|
||||
const nextServer = availableServers.find((server) => !usedServerIds.has(server.id));
|
||||
if (!nextServer) {
|
||||
setAssignmentsError("Alle verfügbaren Server sind bereits zugeordnet.");
|
||||
return;
|
||||
}
|
||||
setAssignmentsError("");
|
||||
setServerAssignments((prev) => [
|
||||
...prev,
|
||||
{
|
||||
serverId: nextServer.id,
|
||||
serverName: nextServer.name ?? "",
|
||||
serverUrl: nextServer.url ?? "",
|
||||
groupId: null,
|
||||
groupName: "",
|
||||
useGlobalGroup: true
|
||||
}
|
||||
]);
|
||||
}, [canEditUsers, serverAssignments, availableServers]);
|
||||
|
||||
const handleAssignmentServerChange = useCallback(
|
||||
(index, value) => {
|
||||
if (!canEditUsers || isSuperuserUser) return;
|
||||
const numeric = Number(value);
|
||||
const server = availableServers.find((item) => item.id === numeric);
|
||||
setServerAssignments((prev) =>
|
||||
prev.map((entry, idx) =>
|
||||
idx === index
|
||||
? {
|
||||
...entry,
|
||||
serverId: Number.isFinite(numeric) ? numeric : null,
|
||||
serverName: server?.name || entry.serverName,
|
||||
serverUrl: server?.url || entry.serverUrl
|
||||
}
|
||||
: entry
|
||||
)
|
||||
);
|
||||
},
|
||||
[availableServers, canEditUsers]
|
||||
);
|
||||
|
||||
const handleAssignmentGroupChange = useCallback(
|
||||
(index, value) => {
|
||||
if (!canEditUsers || isSuperuserUser) return;
|
||||
const numeric = Number(value);
|
||||
setServerAssignments((prev) =>
|
||||
prev.map((entry, idx) =>
|
||||
idx === index
|
||||
? {
|
||||
...entry,
|
||||
groupId: Number.isFinite(numeric) ? numeric : null,
|
||||
useGlobalGroup: false
|
||||
}
|
||||
: entry
|
||||
)
|
||||
);
|
||||
},
|
||||
[canEditUsers]
|
||||
);
|
||||
|
||||
const handleAssignmentUseGlobalToggle = useCallback(
|
||||
(index, checked) => {
|
||||
if (!canEditUsers || isSuperuserUser) return;
|
||||
setServerAssignments((prev) =>
|
||||
prev.map((entry, idx) =>
|
||||
idx === index
|
||||
? {
|
||||
...entry,
|
||||
useGlobalGroup: Boolean(checked),
|
||||
groupId: checked ? null : entry.groupId
|
||||
}
|
||||
: entry
|
||||
)
|
||||
);
|
||||
},
|
||||
[canEditUsers]
|
||||
);
|
||||
|
||||
const handleRemoveAssignment = useCallback(
|
||||
(index) => {
|
||||
if (!canEditUsers || isSuperuserUser) return;
|
||||
setServerAssignments((prev) => prev.filter((_, idx) => idx !== index));
|
||||
},
|
||||
[canEditUsers, isSuperuserUser]
|
||||
);
|
||||
|
||||
const handleSaveAssignments = useCallback(async () => {
|
||||
if (!canEditUsers || !numericUserId || !assignmentsChanged || isSuperuserUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedPayload = [];
|
||||
const seenServers = new Set();
|
||||
for (const assignment of serverAssignments) {
|
||||
const serverId = Number(assignment.serverId);
|
||||
if (!Number.isFinite(serverId) || serverId <= 0) {
|
||||
setAssignmentsError("Bitte wähle für jede Zuordnung einen Server aus.");
|
||||
return;
|
||||
}
|
||||
if (seenServers.has(serverId)) {
|
||||
setAssignmentsError("Jeder Server kann nur einmal zugeordnet werden.");
|
||||
return;
|
||||
}
|
||||
seenServers.add(serverId);
|
||||
const useGlobalGroup = Boolean(assignment.useGlobalGroup);
|
||||
const groupId = Number(assignment.groupId);
|
||||
if (!useGlobalGroup && (!Number.isFinite(groupId) || groupId <= 0)) {
|
||||
setAssignmentsError("Bitte wähle eine Rechtegruppe oder aktiviere die globale Gruppe.");
|
||||
return;
|
||||
}
|
||||
normalizedPayload.push({
|
||||
serverId,
|
||||
groupId: useGlobalGroup ? null : groupId,
|
||||
useGlobalGroup
|
||||
});
|
||||
}
|
||||
|
||||
setAssignmentsError("");
|
||||
setSavingAssignments(true);
|
||||
|
||||
try {
|
||||
const response = await axios.put(
|
||||
`/api/users/${numericUserId}/server-assignments`,
|
||||
{ assignments: normalizedPayload }
|
||||
);
|
||||
const normalizedAssignments = normalizeAssignmentsList(
|
||||
response.data?.items ?? [],
|
||||
availableServers
|
||||
);
|
||||
initialAssignmentsRef.current = normalizedAssignments;
|
||||
setServerAssignments(normalizedAssignments);
|
||||
showToast({
|
||||
variant: "success",
|
||||
title: "Serverzuordnungen gespeichert",
|
||||
description: "Die Zuordnungen wurden erfolgreich aktualisiert."
|
||||
});
|
||||
} catch (err) {
|
||||
const serverError = err.response?.data?.error;
|
||||
let message = "Die Serverzuordnungen konnten nicht gespeichert werden.";
|
||||
if (serverError === "USER_NOT_FOUND") {
|
||||
message = "Der Benutzer wurde nicht gefunden.";
|
||||
} else if (serverError === "INVALID_USER_ID") {
|
||||
message = "Die Benutzer-ID ist ungültig.";
|
||||
} else if (serverError === "USER_SUPERUSER_PROTECTED") {
|
||||
message = "Für den Superuser können keine Server-Zuordnungen gesetzt werden.";
|
||||
} else if (serverError === "SERVER_NOT_FOUND") {
|
||||
message = "Ein ausgewählter Server existiert nicht mehr.";
|
||||
} else if (serverError === "GROUP_NOT_FOUND") {
|
||||
message = "Eine ausgewählte Rechtegruppe existiert nicht mehr.";
|
||||
} else if (serverError === "INSUFFICIENT_PERMISSIONS") {
|
||||
message = "Keine Berechtigung zum Ändern der Zuordnungen.";
|
||||
}
|
||||
setAssignmentsError(message);
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: "Speichern fehlgeschlagen",
|
||||
description: message
|
||||
});
|
||||
} finally {
|
||||
setSavingAssignments(false);
|
||||
}
|
||||
}, [canEditUsers, numericUserId, serverAssignments, availableServers, assignmentsChanged, showToast]);
|
||||
|
||||
const handleSaveUser = useCallback(async () => {
|
||||
if (!canEditUsers || !user || !hasChanges) {
|
||||
return;
|
||||
@@ -775,30 +1036,171 @@ export function UserDetails() {
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="h6" color="blue-gray" className="mb-3">
|
||||
Platform Settings
|
||||
Server-Zuordnungen
|
||||
</Typography>
|
||||
<div className="flex flex-col gap-12">
|
||||
{platformSettingsData.map(({ title, options }) => (
|
||||
<div key={title}>
|
||||
<Typography className="mb-4 block text-xs font-semibold uppercase text-blue-gray-500">
|
||||
{title}
|
||||
</Typography>
|
||||
<div className="flex flex-col gap-6">
|
||||
{options.map(({ checked, label }) => (
|
||||
<Switch
|
||||
key={label}
|
||||
id={label}
|
||||
label={label}
|
||||
defaultChecked={checked}
|
||||
labelProps={{
|
||||
className: "text-sm font-normal text-blue-gray-500",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{isSuperuserUser ? (
|
||||
<div className="mb-4 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
|
||||
Für den Superuser gelten immer alle Rechte und es können keine server-spezifischen Zuordnungen gesetzt werden.
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Typography className="mb-4 text-sm text-blue-gray-500">
|
||||
Ohne Zuordnungen hat der Benutzer Zugriff auf alle Server mit seinen globalen Rechten.
|
||||
Lege hier Server-spezifische Gruppen fest oder markiere den Server explizit zur Nutzung der globalen Gruppe.
|
||||
</Typography>
|
||||
{assignmentsError && (
|
||||
<div className="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
|
||||
{assignmentsError}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-4">
|
||||
{serverAssignments.length === 0 ? (
|
||||
<div className="rounded-lg border border-blue-gray-100 bg-blue-gray-50/60 px-4 py-3 text-sm text-blue-gray-600">
|
||||
Keine Server zugeordnet. Die globalen Gruppen gelten für alle Server.
|
||||
</div>
|
||||
) : (
|
||||
serverAssignments.map((assignment, index) => {
|
||||
const serverOptions = getServerOptionsForRow(assignment.serverId);
|
||||
const groupSelectValue =
|
||||
assignment.useGlobalGroup || !assignment.groupId
|
||||
? ""
|
||||
: String(assignment.groupId);
|
||||
return (
|
||||
<div
|
||||
key={`assignment-${assignment.serverId ?? index}-${index}`}
|
||||
className="rounded-lg border border-blue-gray-100 bg-white px-4 py-3 shadow-sm"
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<Typography className="mb-2 block text-xs font-semibold uppercase text-blue-gray-500">
|
||||
Server
|
||||
</Typography>
|
||||
<Select
|
||||
label="Server auswählen"
|
||||
value={
|
||||
assignment.serverId
|
||||
? String(assignment.serverId)
|
||||
: ""
|
||||
}
|
||||
onChange={(value) =>
|
||||
handleAssignmentServerChange(index, value)
|
||||
}
|
||||
disabled={
|
||||
maintenanceLocked ||
|
||||
savingAssignments ||
|
||||
!canEditUsers
|
||||
}
|
||||
variant="outlined"
|
||||
>
|
||||
{serverOptions.map((server) => (
|
||||
<Option key={server.id} value={String(server.id)}>
|
||||
{server.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Typography className="mb-2 block text-xs font-semibold uppercase text-blue-gray-500">
|
||||
Rechtegruppe
|
||||
</Typography>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Switch
|
||||
id={`assignment-global-${index}`}
|
||||
label="Globale Gruppe verwenden"
|
||||
checked={assignment.useGlobalGroup}
|
||||
onChange={({ target }) =>
|
||||
handleAssignmentUseGlobalToggle(
|
||||
index,
|
||||
target?.checked
|
||||
)
|
||||
}
|
||||
disabled={
|
||||
maintenanceLocked ||
|
||||
savingAssignments ||
|
||||
!canEditUsers
|
||||
}
|
||||
labelProps={{
|
||||
className: "text-sm font-normal text-blue-gray-600"
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
label="Server-Gruppe wählen"
|
||||
value={groupSelectValue}
|
||||
onChange={(value) =>
|
||||
handleAssignmentGroupChange(index, value)
|
||||
}
|
||||
disabled={
|
||||
maintenanceLocked ||
|
||||
savingAssignments ||
|
||||
!canEditUsers ||
|
||||
assignment.useGlobalGroup
|
||||
}
|
||||
variant="outlined"
|
||||
className="max-w-xl"
|
||||
>
|
||||
<Option value="">Globale Gruppe</Option>
|
||||
{availableGroups.map((group) => (
|
||||
<Option key={group.id} value={String(group.id)}>
|
||||
{group.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex justify-end">
|
||||
<Button
|
||||
variant="text"
|
||||
color="red"
|
||||
className="normal-case"
|
||||
onClick={() => handleRemoveAssignment(index)}
|
||||
disabled={
|
||||
maintenanceLocked ||
|
||||
savingAssignments ||
|
||||
!canEditUsers
|
||||
}
|
||||
>
|
||||
Entfernen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap items-center gap-3">
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="blue"
|
||||
className="normal-case"
|
||||
onClick={handleAddAssignment}
|
||||
disabled={
|
||||
maintenanceLocked ||
|
||||
savingAssignments ||
|
||||
!canEditUsers ||
|
||||
availableServers.length === 0 ||
|
||||
usedServerIds.size >= availableServers.length
|
||||
}
|
||||
>
|
||||
Server hinzufügen
|
||||
</Button>
|
||||
<Button
|
||||
color="green"
|
||||
className="normal-case"
|
||||
onClick={handleSaveAssignments}
|
||||
disabled={
|
||||
maintenanceLocked ||
|
||||
savingAssignments ||
|
||||
!canEditUsers ||
|
||||
!assignmentsChanged
|
||||
}
|
||||
>
|
||||
{savingAssignments ? "Speichert ..." : "Zuordnungen speichern"}
|
||||
</Button>
|
||||
{savingAssignments && <Spinner className="h-4 w-4" />}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -78,8 +78,14 @@ const LEVEL_PRIORITY = {
|
||||
full: 2
|
||||
};
|
||||
|
||||
const normalizePermissionLevel = (key, value) =>
|
||||
key === "maintenance-server-delete" && value !== "full" ? "none" : value;
|
||||
const isDependencySatisfied = (value, requirement) => {
|
||||
const req = requirement || "!=none";
|
||||
if (req === "!=none") return value !== "none";
|
||||
if (req.startsWith("!=")) return value !== req.substring(2);
|
||||
if (req.startsWith("=")) return value === req.substring(1);
|
||||
if (!req) return true;
|
||||
return value === req;
|
||||
};
|
||||
|
||||
export function UserGroupDetail() {
|
||||
const { groupId } = useParams();
|
||||
@@ -177,22 +183,28 @@ export function UserGroupDetail() {
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeGroup = (group, groupIdx = 0) => {
|
||||
const groupItems = Array.isArray(group.items)
|
||||
? group.items.map((item, itemIdx) => normalizeItem(item, itemIdx)).filter(Boolean)
|
||||
: [];
|
||||
const childGroups = Array.isArray(group.groups)
|
||||
? group.groups.map((child, childIdx) => normalizeGroup(child, childIdx))
|
||||
: [];
|
||||
return {
|
||||
...group,
|
||||
sortOrder: typeof group.sortOrder === "number" ? group.sortOrder : groupIdx,
|
||||
items: groupItems,
|
||||
groups: childGroups
|
||||
};
|
||||
};
|
||||
|
||||
const normalizedSections = rawSections.map((section, sectionIndex) => {
|
||||
const sectionItems = Array.isArray(section.items)
|
||||
? section.items.map((item, itemIdx) => normalizeItem(item, itemIdx)).filter(Boolean)
|
||||
: [];
|
||||
|
||||
const sectionGroups = Array.isArray(section.groups)
|
||||
? section.groups.map((group, groupIdx) => {
|
||||
const groupItems = Array.isArray(group.items)
|
||||
? group.items.map((item, itemIdx) => normalizeItem(item, itemIdx)).filter(Boolean)
|
||||
: [];
|
||||
return {
|
||||
...group,
|
||||
sortOrder: typeof group.sortOrder === "number" ? group.sortOrder : groupIdx,
|
||||
items: groupItems
|
||||
};
|
||||
})
|
||||
? section.groups.map((group, groupIdx) => normalizeGroup(group, groupIdx))
|
||||
: [];
|
||||
|
||||
return {
|
||||
@@ -515,16 +527,52 @@ export function UserGroupDetail() {
|
||||
if (!permissionKey || !canAdjustPermissions || isSuperuserGroup) {
|
||||
return;
|
||||
}
|
||||
const nextValue = normalizePermissionLevel(permissionKey, value);
|
||||
setPermissionSelection((prev) => {
|
||||
const shouldResetDelete =
|
||||
permissionKey === "maintenance-server-manage" && nextValue !== "full";
|
||||
const shouldResetEdit =
|
||||
permissionKey === "maintenance-server-manage" && nextValue === "none";
|
||||
const shouldResetDeleteFromEdit =
|
||||
permissionKey === "maintenance-server-edit" && nextValue !== "full";
|
||||
const getValueWithDefaults = (key, selectionMap) =>
|
||||
selectionMap[key] ?? permissionDefaults[key] ?? "none";
|
||||
|
||||
if (!shouldResetDelete && !shouldResetEdit && !shouldResetDeleteFromEdit && prev[permissionKey] === nextValue) {
|
||||
const collectAllItems = () => {
|
||||
const items = [];
|
||||
const walkGroups = (groups) => {
|
||||
(groups || []).forEach((group) => {
|
||||
(group.items || []).forEach((item) => items.push(item));
|
||||
if (group.groups) {
|
||||
walkGroups(group.groups);
|
||||
}
|
||||
});
|
||||
};
|
||||
permissionSections.forEach((section) => {
|
||||
(section.items || []).forEach((item) => items.push(item));
|
||||
walkGroups(section.groups);
|
||||
});
|
||||
return items;
|
||||
};
|
||||
|
||||
const allItems = collectAllItems();
|
||||
const itemMap = new Map(allItems.map((item) => [item.key, item]));
|
||||
|
||||
const clampToAllowed = (itemKey, desiredLevel) => {
|
||||
const item = itemMap.get(itemKey);
|
||||
if (!item) return desiredLevel;
|
||||
const allowed = Array.isArray(item.availableLevels) && item.availableLevels.length
|
||||
? item.availableLevels
|
||||
: LEVEL_OPTIONS.map((opt) => opt.value);
|
||||
if (allowed.includes(desiredLevel)) {
|
||||
return desiredLevel;
|
||||
}
|
||||
// Fallback: wähle niedrigstes erlaubtes Level
|
||||
return allowed.reduce((lowest, candidate) => {
|
||||
return LEVEL_PRIORITY[candidate] < LEVEL_PRIORITY[lowest] ? candidate : lowest;
|
||||
}, allowed[0] || "none");
|
||||
};
|
||||
|
||||
const nextValue = clampToAllowed(permissionKey, value);
|
||||
|
||||
|
||||
setPermissionSelection((prev) => {
|
||||
const currentValue = getValueWithDefaults(permissionKey, prev);
|
||||
const isDowngrade = LEVEL_PRIORITY[nextValue] < LEVEL_PRIORITY[currentValue];
|
||||
|
||||
if (prev[permissionKey] === nextValue) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
@@ -533,19 +581,31 @@ export function UserGroupDetail() {
|
||||
[permissionKey]: nextValue
|
||||
};
|
||||
|
||||
if (shouldResetDelete) {
|
||||
nextState["maintenance-server-delete"] = "none";
|
||||
}
|
||||
if (shouldResetEdit) {
|
||||
nextState["maintenance-server-edit"] = "none";
|
||||
}
|
||||
if (shouldResetDeleteFromEdit) {
|
||||
nextState["maintenance-server-delete"] = "none";
|
||||
// Kaskade: Nur bei Downgrade des auslösenden Rechts abhängige Rechte herabstufen.
|
||||
if (isDowngrade) {
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
allItems.forEach((item) => {
|
||||
const dependencies = Array.isArray(item.dependencies) ? item.dependencies : [];
|
||||
if (!dependencies.length) return;
|
||||
const satisfied = dependencies.every((dep) =>
|
||||
isDependencySatisfied(getValueWithDefaults(dep.key, nextState), dep.requiredLevel)
|
||||
);
|
||||
if (!satisfied) {
|
||||
const cascadedValue = clampToAllowed(item.key, nextValue);
|
||||
if (getValueWithDefaults(item.key, nextState) !== cascadedValue) {
|
||||
nextState[item.key] = cascadedValue;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return nextState;
|
||||
});
|
||||
}, [canAdjustPermissions, isSuperuserGroup]);
|
||||
}, [canAdjustPermissions, isSuperuserGroup, permissionDefaults, permissionSections]);
|
||||
|
||||
const getPermissionValue = useCallback(
|
||||
(permissionKey) => {
|
||||
@@ -559,7 +619,7 @@ export function UserGroupDetail() {
|
||||
value = permissionDefaults[permissionKey];
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return normalizePermissionLevel(permissionKey, value);
|
||||
return value;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
@@ -572,12 +632,6 @@ export function UserGroupDetail() {
|
||||
),
|
||||
[]
|
||||
);
|
||||
const radioCheckedIcon = useMemo(
|
||||
() => (
|
||||
<span className="mx-auto block h-2.5 w-2.5 rounded-full border border-blue-gray-700 bg-gray-900" />
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const isPermissionRowVisible = useCallback(
|
||||
(row) => {
|
||||
@@ -617,9 +671,7 @@ export function UserGroupDetail() {
|
||||
|
||||
const rowName = `permission-${ownerKey}-${row.key}`;
|
||||
const currentValue = getPermissionValue(row.key) ?? "none";
|
||||
const displayValue = normalizePermissionLevel(row.key, currentValue);
|
||||
const serverManageLevel = getPermissionValue("maintenance-server-manage") ?? "none";
|
||||
const serverEditLevel = getPermissionValue("maintenance-server-edit") ?? "none";
|
||||
const displayValue = currentValue;
|
||||
const { addTopBorder = true } = options;
|
||||
|
||||
const rowClasses = [
|
||||
@@ -632,11 +684,7 @@ export function UserGroupDetail() {
|
||||
const baseLevels = Array.isArray(row.availableLevels) && row.availableLevels.length
|
||||
? row.availableLevels
|
||||
: LEVEL_OPTIONS.map((option) => option.value);
|
||||
const availableLevels = new Set(
|
||||
row.key === "maintenance-server-delete"
|
||||
? baseLevels.filter((level) => level === "full" || level === "none")
|
||||
: baseLevels
|
||||
);
|
||||
const availableLevels = new Set(baseLevels);
|
||||
const levelOptions = LEVEL_OPTIONS;
|
||||
|
||||
return (
|
||||
@@ -648,22 +696,12 @@ export function UserGroupDetail() {
|
||||
{levelOptions.map((option) => {
|
||||
const optionId = `${rowName}-${option.value}`;
|
||||
const checked = displayValue === option.value;
|
||||
const requiresManageFull =
|
||||
row.key === "maintenance-server-delete" &&
|
||||
option.value === "full" &&
|
||||
(LEVEL_PRIORITY[serverManageLevel] ?? 0) < LEVEL_PRIORITY.full;
|
||||
const requiresEditFull =
|
||||
row.key === "maintenance-server-delete" &&
|
||||
option.value === "full" &&
|
||||
(LEVEL_PRIORITY[serverEditLevel] ?? 0) < LEVEL_PRIORITY.full;
|
||||
const disabled =
|
||||
!availableLevels.has(option.value) ||
|
||||
permissionsLoading ||
|
||||
savingGroup ||
|
||||
!canAdjustPermissions ||
|
||||
isSuperuserGroup ||
|
||||
requiresManageFull ||
|
||||
requiresEditFull;
|
||||
isSuperuserGroup;
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
@@ -689,7 +727,6 @@ export function UserGroupDetail() {
|
||||
className: "p-0"
|
||||
}}
|
||||
icon={radioIcon}
|
||||
checkedIcon={radioCheckedIcon}
|
||||
/>
|
||||
</ListItemPrefix>
|
||||
<Typography
|
||||
@@ -708,6 +745,30 @@ export function UserGroupDetail() {
|
||||
);
|
||||
};
|
||||
|
||||
const groupHasVisibleRows = (grp) => {
|
||||
if (!grp) {
|
||||
return false;
|
||||
}
|
||||
const groupItems = Array.isArray(grp.items) ? grp.items : [];
|
||||
const childGroups = Array.isArray(grp.groups) ? grp.groups : [];
|
||||
if (groupItems.some(isPermissionRowVisible)) {
|
||||
return true;
|
||||
}
|
||||
return childGroups.some(groupHasVisibleRows);
|
||||
};
|
||||
|
||||
const sectionHasVisibleRows = (section) => {
|
||||
if (!section) {
|
||||
return false;
|
||||
}
|
||||
const sectionItems = Array.isArray(section.items) ? section.items : [];
|
||||
const sectionGroups = Array.isArray(section.groups) ? section.groups : [];
|
||||
if (sectionItems.some(isPermissionRowVisible)) {
|
||||
return true;
|
||||
}
|
||||
return sectionGroups.some(groupHasVisibleRows);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-12 flex flex-col gap-12">
|
||||
@@ -725,9 +786,6 @@ export function UserGroupDetail() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative h-72 w-full overflow-hidden rounded-xl bg-[url('/img/background-image.png')] bg-cover\tbg-center">
|
||||
<div className="absolute inset-0 h-full w-full bg-gray-900/75" />
|
||||
</div>
|
||||
<Card className="mx-3 -mt-16 mb-6 lg:mx-4 border border-blue-gray-100">
|
||||
<CardBody className="p-4">
|
||||
<div className="mb-10 flex flex-wrap items-center justify-between gap-6">
|
||||
@@ -893,7 +951,7 @@ export function UserGroupDetail() {
|
||||
Die Superuser-Gruppe besitzt automatisch Vollzugriff auf alle Bereiche. Berechtigungen können hier nicht angepasst werden.
|
||||
</div>
|
||||
) : !canViewPermissionSettings ? null : (
|
||||
<div className="rounded-xl border border-blue-gray-100 bg-white shadow-sm">
|
||||
<>
|
||||
{permissionsLoading && (
|
||||
<div className="border-b border-blue-gray-100 px-4 py-4">
|
||||
<div className="flex items-center gap-3 text-sm text-blue-gray-500">
|
||||
@@ -913,53 +971,85 @@ export function UserGroupDetail() {
|
||||
Für diese Gruppe sind keine Berechtigungen definiert.
|
||||
</div>
|
||||
) : (
|
||||
permissionSections.map((section, sectionIndex) => {
|
||||
const sectionItems = Array.isArray(section.items) ? section.items : [];
|
||||
const sectionGroups = Array.isArray(section.groups) ? section.groups : [];
|
||||
(() => {
|
||||
const renderedSections = permissionSections
|
||||
.map((section, sectionIndex) => {
|
||||
const sectionItems = Array.isArray(section.items) ? section.items : [];
|
||||
const sectionGroups = Array.isArray(section.groups) ? section.groups : [];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={section.key || sectionIndex}
|
||||
className={sectionIndex === 0 ? "" : "border-t border-blue-gray-100"}
|
||||
>
|
||||
<div className="border-b border-blue-gray-100 px-4 py-4">
|
||||
<Typography variant="h6" className="text-lg font-semibold text-blue-gray-700">
|
||||
{section.title}
|
||||
</Typography>
|
||||
</div>
|
||||
{sectionItems.map((row, rowIndex) =>
|
||||
renderPermissionRow(row, section.key || sectionIndex, {
|
||||
addTopBorder: rowIndex !== 0
|
||||
})
|
||||
)}
|
||||
{sectionGroups.map((group, groupIdx) => {
|
||||
const groupItems = Array.isArray(group.items) ? group.items : [];
|
||||
const hasVisibleRows = groupItems.some(isPermissionRowVisible);
|
||||
if (!sectionHasVisibleRows(section)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!hasVisibleRows) {
|
||||
const renderGroup = (currentGroup, path = `${section.key || sectionIndex}-group`, depth = 0) => {
|
||||
if (!groupHasVisibleRows(currentGroup)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const groupItems = Array.isArray(currentGroup.items) ? currentGroup.items : [];
|
||||
const childGroups = Array.isArray(currentGroup.groups) ? currentGroup.groups : [];
|
||||
|
||||
return (
|
||||
<div key={group.key || `${section.key || sectionIndex}-${groupIdx}`} className="border-t border-blue-gray-100">
|
||||
<div className="border-b border-blue-gray-100 px-4 py-3">
|
||||
<Typography className="text-sm font-semibold uppercase tracking-wide text-blue-gray-600">
|
||||
{group.title}
|
||||
<div
|
||||
key={currentGroup.key || path}
|
||||
className=""
|
||||
>
|
||||
<div className="border-b border-blue-gray-100 bg-blue-gray-50/80 px-4 py-3">
|
||||
<Typography className={`${depth > 0 ? "ml-2" : ""} text-xs font-semibold uppercase tracking-wide text-blue-gray-600`}>
|
||||
{currentGroup.title}
|
||||
</Typography>
|
||||
</div>
|
||||
{groupItems.map((row, rowIndex) =>
|
||||
renderPermissionRow(row, group.key || `${section.key || sectionIndex}-${groupIdx}`, {
|
||||
addTopBorder: rowIndex !== 0
|
||||
})
|
||||
)}
|
||||
<div className="divide-y divide-blue-gray-100">
|
||||
{groupItems.map((row) =>
|
||||
renderPermissionRow(row, `${path}-${row.key}`, {
|
||||
addTopBorder: false
|
||||
})
|
||||
)}
|
||||
{childGroups.map((childGroup, childIdx) =>
|
||||
renderGroup(childGroup, `${path}-${childGroup.key || childIdx}`, depth + 1)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={section.key || sectionIndex}
|
||||
className="overflow-hidden rounded-xl border border-blue-gray-100 bg-white shadow-sm"
|
||||
>
|
||||
<div className="border-b border-blue-gray-100 bg-blue-gray-50/70 px-4 py-3">
|
||||
<Typography variant="h6" className="text-lg font-semibold text-blue-gray-700">
|
||||
{section.title}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="divide-y divide-blue-gray-100">
|
||||
{sectionItems.map((row) =>
|
||||
renderPermissionRow(row, section.key || sectionIndex, {
|
||||
addTopBorder: false
|
||||
})
|
||||
)}
|
||||
{sectionGroups.map((group, groupIdx) =>
|
||||
renderGroup(group, `${section.key || sectionIndex}-${groupIdx}`, 0)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (renderedSections.length === 0) {
|
||||
return (
|
||||
<div className="border-b border-blue-gray-100 px-4 py-4 text-sm text-blue-gray-500">
|
||||
Für diese Gruppe sind keine Berechtigungen definiert.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="flex flex-col gap-4">{renderedSections}</div>;
|
||||
})()
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user