feat(auth): Anmeldung, Rollen und Benutzerverwaltung (#21)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

OIDC mit Discovery, PKCE, state und nonce; lokaler Login mit argon2 als
Notfallzugang; Rechte in einer Tabelle, Prüfung ausschließlich serverseitig,
Rolle und Aktiv-Status pro Anfrage aus der Datenbank.

Ein unabhängiger Review fand eine offene Weiterleitung: die Präfix-Prüfung des
Rücksprungziels ließ /<TAB>/fremd.example durch, woraus der Browser
//fremd.example macht. Das Ziel wird jetzt aufgelöst und die Herkunft
verglichen. Dazu acht weitere Befunde, alle behoben.

274 Tests.

Closes #7

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #21.
This commit is contained in:
2026-09-25 01:35:02 +00:00
co-authored by Claude Opus 5
parent 7a0f875f55
commit 40e95dbe5f
43 changed files with 3426 additions and 56 deletions
+60 -2
View File
@@ -9,11 +9,12 @@ als Unterpunkte mit Aufgabe und Dauer.
|---|---|---|
| #005 | M0 – Gerüst, Prozess, Design-Token | 1 h 25 min |
| #006 | M1 – Datenmodell | 1 h 10 min |
| | **Summe** | **2 h 35 min** |
| #007 | M2 – Auth, Rollen, Benutzer | 1 h 35 min |
| | **Summe** | **4 h 10 min** |
| Meilenstein | Tatsächlich | Status |
|---|---|---|
| v0.1.0 | 2 h 35 min | in Arbeit |
| v0.1.0 | 4 h 10 min | in Arbeit |
---
@@ -165,3 +166,60 @@ als Unterpunkte mit Aufgabe und Dauer.
- #007 Auth, Rollen, Benutzer: `openid-client` mit Discovery, `jose`-Sitzungscookie, lokaler
Login mit argon2, `RECHTE`-Tabelle, Login-Seite mit Rücksprung auf die ursprüngliche URL.
---
## 2026-09-25 · Sitzung 3 · 01:55–03:35
**Issue:** #007 (M2)
**Version:** 0.1.0
### Erledigt
- OIDC mit `openid-client` 6.8.8: Discovery, PKCE, `state`, `nonce`; API vorher gegen die
Typdefinitionen im Paket geprüft statt aus dem Gedächtnis geschrieben
- `jose`-Sitzungscookie, lokaler Login mit argon2, Rechtetabelle, JIT-Provisionierung
- Anmeldeseite, geschützter Bereich, Benutzerverwaltung, rollenabhängige Navigation
- Sub-Agent hat `rollen.ts` und `sitzung.ts` samt 85 Tests parallel gebaut (rund 6 min)
- Unabhängiger Review-Agent: ein Blocker, acht weitere Befunde, alle abgearbeitet
- **274 Tests** grün (vorher 106)
### Entscheidungen
- **Rücksprungziel wird aufgelöst, nicht mit Präfixen verglichen.** Die Präfix-Prüfung war eine
offene Weiterleitung: `/<TAB>/fremd.example` besteht `startsWith("/")`, ist kein `//` und kein
`/\` — Browser entfernen den Tabulator aber beim Verarbeiten der URL, übrig bleibt
`//fremd.example`. Das Auflösen gegen eine Wegwerf-Basis macht genau das, was der Browser tut,
und der Vergleich der Herkunft erschlägt alle Formen auf einmal.
- **Alle Umleitungen über `APP_URL`.** `nextUrl.origin` lieferte im Test `localhost` statt
`127.0.0.1`; hinter einem Reverse Proxy wäre es der interne Host. Neues Modul
`src/lib/adressen.ts`, die Herkunft der Anfrage ist nur noch Notnagel.
- **Ohne `OIDC_GROUP_*` bleibt die Rolle, wie sie ist.** Sonst wäre jede OIDC-Anmeldung eine
stille Herabstufung auf `LESER` — bei einem einzigen Administrator ohne Rückweg.
- **Verknüpfung über die Adresse nur bei `email_verified`.** Sonst wäre ein Anbieter mit frei
wählbaren Adressen der Weg, den lokalen Notfall-Administrator zu übernehmen.
- **Der letzte aktive Administrator ist geschützt**, gegen Herabstufung wie gegen Abschalten.
- **Kein `overflow-x: hidden` auf `body`.** Es sieht aus wie eine Lösung, verdeckt aber nur: zu
breiter Inhalt wird abgeschnitten statt erreichbar, und die Prüfung im Screenshot-Skript kann
ihn nicht mehr finden. Layouts müssen bei 390 px von sich aus passen.
- **`ROLLEN_TEXT` liegt in `src/lib`,** nicht in der Server-Schicht: eine Client-Komponente hätte
sonst die komplette Rechtetabelle ins Browser-Bündel gezogen.
### Probleme
- **Der Platzhalter-Hash gegen den Zeitkanal war erfunden** und hätte sofort geworfen statt Zeit
zu kosten. Die Zusage wäre damit schlicht falsch gewesen. Jetzt ein echter, lazy erzeugter
argon2-Hash; nachgemessen rund 1 ms Unterschied über je 60 Anfragen.
- **Beim Verschieben der Übersicht nach `(app)/` blieb `<Huelle>` in der Seite stehen.** Die Hülle
wurde doppelt gerendert — zwei Kopfzeilen, das innere Menü ohne Benutzer. Es war im Screenshot
zu sehen und ist mir trotzdem entgangen; gefunden hat es der Review-Agent. Ein Bild anzusehen
ersetzt nicht, gezielt nach dem zu suchen, was sich gerade geändert hat.
- **Prisma-Modelltypen kommen aus `@/generated/prisma/client`,** nicht aus `.../models` — dort
liegen nur die Hilfstypen der Abfragen. Steht jetzt in `CLAUDE.md`.
- **`networkidle` als Wartebedingung in Playwright** läuft bei einer Anwendung mit offenen
Verbindungen in den Timeout. `load` plus geladene Schriften ist deterministisch.
### Nächster Schritt
- #008 Betriebsmodus und Kunden: `betriebsmodus()`, `kundeErmitteln()`, Moduswechsel in beiden
Richtungen, Kundenverwaltung, Admin-Einstellungen.