From 7f5bbb65d53c6e27c611d82951cca2cd6681cd88 Mon Sep 17 00:00:00 2001 From: patrick Date: Fri, 4 Sep 2026 22:25:32 +0200 Subject: [PATCH] Rollen-differenzierte Verwaltungs-UI (Material-/Leitungsverantwortliche) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: Frontend prüfte nur istAdmin, Materialverantwortliche und Leitungs- verantwortliche sahen exakt dieselbe UI wie Mitarbeiter - obwohl das Backend längst granular unterscheidet (require_roles je Endpunkt). - AuthContext gibt volle Rollenliste + istVerantwortlich/istMaterial- verantwortlich/istLeitungsverantwortlich weiter. - /admin-Route für alle drei Rollen offen, AdminPage filtert Tabs nach Rolle (Material/Leitung: Objekte-Pflege, Kontrollverantwortung, Änderungslog; nur Administration: Stammdaten/Benutzer/Eskalation/ Zuständigkeit). - ObjektSection blendet Anlegen/Duplizieren/Kopplung für Nicht-Admins aus (waren admin-only im Backend, hätten 403 geworfen). - Promise.allSettled statt Promise.all beim Laden, damit ein admin-only Endpunkt (z.B. /benutzer) nicht die ganze Seite für niedrigere Rollen blockiert. - Backend: GET /benutzer jetzt auch für Material-/Leitungsverantwortliche lesbar (für Kontrollverantwortung-Auswahl im Frontend nötig), Schreiben bleibt admin-only. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV --- DEVLOG.md | 24 ++++++ backend/app/api/v1/endpoints/benutzer.py | 6 +- backend/tests/test_benutzer_admin.py | 37 +++++++++ frontend/src/App.tsx | 7 +- frontend/src/auth/AuthContext.tsx | 15 ++++ frontend/src/components/AppShell.tsx | 6 +- frontend/src/pages/AdminPage.tsx | 94 ++++++++++++++-------- frontend/src/pages/admin/ObjektSection.tsx | 42 +++++++--- 8 files changed, 180 insertions(+), 51 deletions(-) diff --git a/DEVLOG.md b/DEVLOG.md index cd2ab7b..959ec11 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -3433,3 +3433,27 @@ Keine Commits in dieser Session. - ergebnisse/material_bereinigung_log.md | 17 ++++++++++------- --- +## 2026-09-04 22:07 – 22:08 (1m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +- cf97b2e Materialstamm: Nachkontrolle bestätigt sauber, 6 Namen getrimmt + +### Geänderte Dateien +- DEVLOG.md | 13 +++++++++++++ +- ergebnisse/material_bereinigung_log.md | 8 ++++++++ + +--- +## 2026-09-04 22:20 – 22:25 (4m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 13 +++++++++++++ +- ergebnisse/material_bereinigung_log.md | 8 ++++++++ + +--- diff --git a/backend/app/api/v1/endpoints/benutzer.py b/backend/app/api/v1/endpoints/benutzer.py index b955838..f7508ba 100644 --- a/backend/app/api/v1/endpoints/benutzer.py +++ b/backend/app/api/v1/endpoints/benutzer.py @@ -11,10 +11,14 @@ from app.schemas.benutzer import BenutzerCreate, BenutzerRead, BenutzerUpdate router = APIRouter() _admin_only = require_roles(RolleTyp.administration) +# Lesend auch für Material-/Leitungsverantwortliche (Rollen-Vorgabe): brauchen +# die Liste u.a. für Kontrollverantwortung/Zuständigkeit-Auswahl im Frontend - +# Anlegen/Ändern bleibt admin-only. +_lesend = require_roles(RolleTyp.administration, RolleTyp.materialverantwortlicher, RolleTyp.leitungsverantwortlicher) @router.get("/benutzer", response_model=list[BenutzerRead]) -async def liste_benutzer(db: AsyncSession = Depends(get_db), _=Depends(_admin_only)) -> list[BenutzerRead]: +async def liste_benutzer(db: AsyncSession = Depends(get_db), _=Depends(_lesend)) -> list[BenutzerRead]: result = await db.execute(select(Benutzer)) return [BenutzerRead.from_orm_benutzer(b) for b in result.scalars().all()] diff --git a/backend/tests/test_benutzer_admin.py b/backend/tests/test_benutzer_admin.py index f268bca..77ccb90 100644 --- a/backend/tests/test_benutzer_admin.py +++ b/backend/tests/test_benutzer_admin.py @@ -54,3 +54,40 @@ async def test_admin_can_update_rollen(client, admin_user, mitarbeiter_user): ) assert response.status_code == 200 assert response.json()["rollen"] == ["leitungsverantwortlicher"] + + +@pytest.mark.asyncio +async def test_materialverantwortlicher_darf_benutzer_lesen_nicht_anlegen( + client, materialverantwortlicher_user +): + """Rollen-Vorgabe: Material-/Leitungsverantwortliche brauchen die Liste + lesend (z.B. Kontrollverantwortung-Auswahl im Frontend), aber kein + Schreibrecht - anders als Administration.""" + token = await login(client, "materialverantwortlicher1") + gelesen = await client.get("/api/v1/benutzer", headers=auth_header(token)) + assert gelesen.status_code == 200 + + angelegt = await client.post( + "/api/v1/benutzer", + json={"name": "X", "login": "x1", "passwort": "sicheres-passwort", "rollen": ["mitarbeiter"]}, + headers=auth_header(token), + ) + assert angelegt.status_code == 403 + + +@pytest.mark.asyncio +async def test_leitungsverantwortlicher_darf_benutzer_lesen(client, db_session): + from app.core.security import hash_password + from app.models.auth import Benutzer, BenutzerRolle, RolleTyp + + benutzer = Benutzer( + name="Test Leitung", login="leitung1", passwort_hash=hash_password("test-passwort-123"), aktiv=True + ) + db_session.add(benutzer) + await db_session.flush() + db_session.add(BenutzerRolle(benutzer_id=benutzer.id, rolle=RolleTyp.leitungsverantwortlicher)) + await db_session.flush() + + token = await login(client, "leitung1") + response = await client.get("/api/v1/benutzer", headers=auth_header(token)) + assert response.status_code == 200 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3ec3082..cd5345f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,11 +17,14 @@ function GeschuetzteRoute({ children }: { children: ReactNode }) { } function AdminRoute({ children }: { children: ReactNode }) { - const { eingeloggt, istAdmin } = useAuth(); + const { eingeloggt, istVerantwortlich } = useAuth(); if (!eingeloggt) { return ; } - if (!istAdmin) { + // Nicht mehr admin-exklusiv: Materialverantwortliche/Leitungsverantwortliche + // dürfen rein, sehen dort aber nur die für sie freigeschalteten Tabs + // (AdminPage filtert selbst nach Rolle, Backend erzwingt es ohnehin je Endpunkt). + if (!istVerantwortlich) { return ; } return {children}; diff --git a/frontend/src/auth/AuthContext.tsx b/frontend/src/auth/AuthContext.tsx index f84400b..676b592 100644 --- a/frontend/src/auth/AuthContext.tsx +++ b/frontend/src/auth/AuthContext.tsx @@ -5,7 +5,15 @@ import type { Me } from "../api/types"; interface AuthContextValue { eingeloggt: boolean; + rollen: string[]; istAdmin: boolean; + // Rollen-Vorgabe: Materialverantwortliche/Leitungsverantwortliche sahen bisher + // exakt dieselbe UI wie Mitarbeiter (nur istAdmin wurde geprüft), obwohl das + // Backend längst granular unterscheidet (require_roles je Endpunkt). Diese + // beiden Flags spiegeln die Backend-Rollenprüfung ins Frontend. + istMaterialverantwortlich: boolean; + istLeitungsverantwortlich: boolean; + istVerantwortlich: boolean; login: (username: string, password: string) => Promise; logout: () => void; } @@ -29,7 +37,14 @@ export function AuthProvider({ children }: { children: ReactNode }) { const value = useMemo( () => ({ eingeloggt, + rollen, istAdmin: rollen.includes("administration"), + istMaterialverantwortlich: rollen.includes("materialverantwortlicher"), + istLeitungsverantwortlich: rollen.includes("leitungsverantwortlicher"), + istVerantwortlich: + rollen.includes("administration") || + rollen.includes("materialverantwortlicher") || + rollen.includes("leitungsverantwortlicher"), login: async (username: string, password: string) => { const token = await apiLogin(username, password); setAuthToken(token); diff --git a/frontend/src/components/AppShell.tsx b/frontend/src/components/AppShell.tsx index 75ce031..5f9a6b9 100644 --- a/frontend/src/components/AppShell.tsx +++ b/frontend/src/components/AppShell.tsx @@ -5,7 +5,7 @@ import { useAuth } from "../auth/AuthContext"; /** Gemeinsamer Rahmen (Kopfzeile + Navigation) für alle eingeloggten Screens. */ export function AppShell({ children }: { children: ReactNode }) { - const { istAdmin, logout } = useAuth(); + const { istAdmin, istVerantwortlich, logout } = useAuth(); const navigate = useNavigate(); function handleLogout() { @@ -23,9 +23,9 @@ export function AppShell({ children }: { children: ReactNode }) { (isActive ? "active" : "")}> Objekte - {istAdmin && ( + {istVerantwortlich && ( (isActive ? "active" : "")}> - Administration + {istAdmin ? "Administration" : "Verwaltung"} )} )} + + )} +
{objekte.length > 0 && ( -
    +
      {objekte.map((o) => (
    • @@ -235,12 +253,16 @@ export function ObjektSection({ objekte, vorlagen, standorte, materialien, onGea > {ausgeklapptId === o.id ? "Positionen ausblenden" : "Ablauf/Charge/SN pflegen"} - - + {!nurPflegen && ( + + )} + {!nurPflegen && ( + + )}
{kopplungBearbeitenId === o.id && (