From 335c572dfc950ff45f60c59ae04e471af5c8eb2e Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Wed, 26 Aug 2026 09:27:23 +0200 Subject: [PATCH] feat: RBAC Phase 5 - Rollenverwaltung im Frontend Neue /settings/roles-Seite (Aktiv/Historie, Karten-Grid) zum Anlegen, Bearbeiten, Deaktivieren und Reaktivieren von Rollen mit einer Rechte-Matrix (nach Bereich gruppierte Checkboxen, Gruppen-Toggle, Live-Zaehler), passend zum Backend aus Phase 1-3. - AuthContext liest Rolle/Rechte jetzt ueber GET /auth/me statt aus dem JWT (das seit Phase 3 keinen role-Claim mehr traegt); neue hasPermission()-Hilfsfunktion ersetzt String-Vergleiche gegen "ADMIN". - Sidebar zeigt Verwaltungs-Eintraege jetzt einzeln nach Berechtigung (statt Alles-oder-Nichts fuer ADMIN), inkl. neuem "Rollen & Rechte"- Eintrag. - Settings-Layout blockt nur noch, wenn gar keine Verwaltungs- Berechtigung vorhanden ist, statt stur auf die ADMIN-Rolle zu pruefen. - Benutzerseite: Rollen-Auswahl laedt jetzt aktive Rollen von GET /roles statt einer festen 5-Werte-Liste; sendet roleId statt role an POST/PATCH /users. - Totes jwt.ts (Rolle wurde bislang aus dem Token dekodiert) entfernt. Live im Browser getestet (Login, Sidebar, Rollen anlegen/bearbeiten/ deaktivieren/reaktivieren, Benutzer-Dialog): dabei einen echten a11y- Bug gefunden und behoben - Gruppen-Checkboxen fuer mehrwortige Bereichsnamen ("Wiederkehrende Aufgaben", "Rollen & Rechte") hatten keinen zugaenglichen Namen, weil die HTML-id ein Leerzeichen enthielt; jetzt ueber einen Slug erzeugt. Verifiziert: tsc, lint, build, alle Unit-Tests (14/14) gruen. Co-Authored-By: Claude Sonnet 5 --- .../src/app/(app)/settings/layout.tsx | 15 +- .../settings/roles/create-role-dialog.tsx | 140 ++++++++++++++ .../(app)/settings/roles/edit-role-dialog.tsx | 110 +++++++++++ .../src/app/(app)/settings/roles/page.tsx | 174 ++++++++++++++++++ .../settings/roles/permission-matrix.tsx | 115 ++++++++++++ .../settings/users/create-user-dialog.tsx | 39 ++-- .../app/(app)/settings/users/page.spec.tsx | 4 +- .../src/app/(app)/settings/users/page.tsx | 2 +- ApiCore-Frontend/src/app/(app)/sidebar.tsx | 33 +++- .../src/components/ui/checkbox.tsx | 23 +++ ApiCore-Frontend/src/lib/auth/api.ts | 20 ++ .../src/lib/auth/auth-context.tsx | 30 +-- ApiCore-Frontend/src/lib/auth/jwt.spec.ts | 24 --- ApiCore-Frontend/src/lib/auth/jwt.ts | 17 -- ApiCore-Frontend/src/lib/roles/api.ts | 37 ++++ ApiCore-Frontend/src/lib/roles/types.ts | 37 ++++ ApiCore-Frontend/src/lib/users/types.ts | 13 +- 17 files changed, 753 insertions(+), 80 deletions(-) create mode 100644 ApiCore-Frontend/src/app/(app)/settings/roles/create-role-dialog.tsx create mode 100644 ApiCore-Frontend/src/app/(app)/settings/roles/edit-role-dialog.tsx create mode 100644 ApiCore-Frontend/src/app/(app)/settings/roles/page.tsx create mode 100644 ApiCore-Frontend/src/app/(app)/settings/roles/permission-matrix.tsx create mode 100644 ApiCore-Frontend/src/components/ui/checkbox.tsx delete mode 100644 ApiCore-Frontend/src/lib/auth/jwt.spec.ts delete mode 100644 ApiCore-Frontend/src/lib/auth/jwt.ts create mode 100644 ApiCore-Frontend/src/lib/roles/api.ts create mode 100644 ApiCore-Frontend/src/lib/roles/types.ts diff --git a/ApiCore-Frontend/src/app/(app)/settings/layout.tsx b/ApiCore-Frontend/src/app/(app)/settings/layout.tsx index 85a0ee3..672e872 100644 --- a/ApiCore-Frontend/src/app/(app)/settings/layout.tsx +++ b/ApiCore-Frontend/src/app/(app)/settings/layout.tsx @@ -3,10 +3,23 @@ import { ReactNode } from "react"; import { useAuth } from "@/lib/auth/auth-context"; +const SETTINGS_AREA_PERMISSIONS = [ + "users:read", + "roles:read", + "audit:read", + "webhooks:read", + "automation:read", + "task-templates:read", + "import:execute", +]; + export default function SettingsLayout({ children }: { children: ReactNode }) { const { user } = useAuth(); + const hasSettingsAccess = SETTINGS_AREA_PERMISSIONS.some((permission) => + user?.permissions.includes(permission), + ); - if (user?.role !== "ADMIN") { + if (!hasSettingsAccess) { return (

diff --git a/ApiCore-Frontend/src/app/(app)/settings/roles/create-role-dialog.tsx b/ApiCore-Frontend/src/app/(app)/settings/roles/create-role-dialog.tsx new file mode 100644 index 0000000..6b69f03 --- /dev/null +++ b/ApiCore-Frontend/src/app/(app)/settings/roles/create-role-dialog.tsx @@ -0,0 +1,140 @@ +"use client"; + +import { FormEvent, useState } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { createRole } from "@/lib/roles/api"; +import { fetchPermissionCatalog } from "@/lib/roles/api"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { PermissionMatrix } from "./permission-matrix"; + +function normalizeKey(value: string): string { + return value + .toUpperCase() + .replace(/[^A-Z0-9]+/g, "_") + .replace(/^_+|_+$/g, ""); +} + +export function CreateRoleDialog() { + const queryClient = useQueryClient(); + const [open, setOpen] = useState(false); + const [key, setKey] = useState(""); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [selected, setSelected] = useState>(new Set()); + const [error, setError] = useState(null); + + const { data: catalog } = useQuery({ + queryKey: ["permission-catalog"], + queryFn: fetchPermissionCatalog, + enabled: open, + }); + + function reset() { + setKey(""); + setName(""); + setDescription(""); + setSelected(new Set()); + setError(null); + } + + const mutation = useMutation({ + mutationFn: createRole, + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: ["roles"] }); + setOpen(false); + reset(); + }, + onError: () => setError("Rolle konnte nicht angelegt werden."), + }); + + function handleSubmit(event: FormEvent) { + event.preventDefault(); + setError(null); + mutation.mutate({ + key, + name, + description: description || undefined, + permissionKeys: Array.from(selected), + }); + } + + return ( +

{ + setOpen(next); + if (!next) reset(); + }} + > + + + + Neue Rolle + +
+
+ + { + setName(event.target.value); + setKey((current) => + current === "" || current === normalizeKey(name) + ? normalizeKey(event.target.value) + : current, + ); + }} + /> +
+
+ + setKey(normalizeKey(event.target.value))} + /> +
+
+ +