Phase 10: Mandantenfaehigkeit
Ein Server, mehrere Wehren. Ein Konto, mehrere Zugehoerigkeiten. Eine Kreisebene, die zusieht. Datenmodell und RLS trugen seit Phase 0 mehrere Wehren. Was fehlte, war alles darueber: bis hierher war die Personenakte zugleich das Anmeldekonto, und die gehoert genau einer Wehr. Wer in zweien gefuehrt wurde, brauchte zwei Passwoerter, und die Anmeldung per E-Mail war mehrdeutig. Neu sind `Benutzer` (das Konto) und `Mitgliedschaft` (die Zugehoerigkeit). Das Konto traegt bewusst keine Mandantenkennung und kann keine tragen - es ist der Angelpunkt, an dem ein Mensch zu mehreren Wehren gehoert. Geschuetzt wird es deshalb ueber Rechte statt ueber RLS, wie `Berechtigung` seit Phase 0. Was es in einer Wehr darf, entscheidet seine Mitgliedschaft. Die Sitzung behaelt ihre Form und entsteht nur anders - aus der aktiven Mitgliedschaft statt aus der Person. Von 414 Fundstellen in 61 Dateien musste dadurch keine angefasst werden. Die eine Ausnahme ist Absicht: `personId` ist nullbar geworden, weil eine Aufsichtssitzung keine Akte hat, und der Typechecker hat daraus genau die fuenf Stellen gemacht, an denen die Kreisaufsicht nichts verloren hat. Ihre Rechte werden gefiltert statt aufgezaehlt: alles, was `.lesen` heisst, minus Kasse, Audit-Log, KI-Protokoll und Personenexport. So bekommt sie neue Leserechte automatisch und neue Schreibrechte nie. Die Migration ist datenerhaltend und bricht ab, statt einen halben Zustand zu hinterlassen. Nebenbei behoben: `db:test-reset` liess die Nachbarwehr stehen, der Fehlversuch-Test sperrte das gemeinsame Testkonto, ein Testaufbau war nicht wiederholbar, und die Wartelogik der Bildschirmfotos konnte ausloesen, bevor eine Seite ueberhaupt geladen war. 581 Unit-Tests und 266 E2E-Tests gruen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2b53556175
commit
ff40437a1a
+103
-1
@@ -22,7 +22,109 @@ Die aufgewendete Zeit wandert nach jedem Abschnitt in die Spalte
|
||||
| Phase 8 — Lokale KI | 3:40 h | 1 | 04.09.2026 |
|
||||
| Aufräumarbeiten (Issues #10–#12) | 1:20 h | 1 | 04.09.2026 |
|
||||
| Phase 9 — Kassenverwaltung | 4:10 h | 1 | 04.09.2026 |
|
||||
| **Summe** | **28:30 h** | **12** | |
|
||||
| Phase 10 — Mandantenfähigkeit | 4:20 h | 1 | 05.09.2026 |
|
||||
| **Summe** | **32:50 h** | **13** | |
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-05 · Phase 10 — Mandantenfähigkeit
|
||||
|
||||
**08:10 – 12:30 · 4:20 h · Version 0.11.0**
|
||||
|
||||
### Was entstanden ist
|
||||
|
||||
Die einzige Phase bisher, die die Anmeldung selbst anfasst.
|
||||
|
||||
| Bereich | Umfang |
|
||||
|---|---|
|
||||
| Datenmodell | `Benutzer`, `Mitgliedschaft`, `MitgliedschaftArt`; `Person` verliert die Anmeldefelder |
|
||||
| Absicherung | RLS für `mitgliedschaft`, zwei Trigger, vier CHECKs, ein REVOKE |
|
||||
| Anmeldung | `lib/auth.ts` neu: Konto statt Akte, Mitgliedschaftswahl, Wehrwechsel im Token |
|
||||
| Rechte | Rolle `AUFSICHT`, gefiltert statt aufgezählt; `istVerwalter` als Kontomerkmal |
|
||||
| Oberfläche | Wehrwähler, Aufsichtshinweis, `/wehren` zur Verwaltung |
|
||||
| Tests | 19 neue Unit-Tests (11 gegen die Datenbank), 13 E2E-Tests |
|
||||
|
||||
### Entscheidungen
|
||||
|
||||
**Die Sitzung behält ihre Form.** `sitzung.organisationId` und
|
||||
`sitzung.personId` werden an 329 bzw. 85 Stellen in 61 Dateien gelesen. Hätte
|
||||
sich ihre Bedeutung geändert, wäre das ein Umbau durch das halbe Projekt
|
||||
geworden. Stattdessen entsteht die Sitzung nur *anders* — aus der aktiven
|
||||
Mitgliedschaft statt aus der Person. Keine dieser 61 Dateien musste angefasst
|
||||
werden.
|
||||
|
||||
**`personId` ist nullbar geworden.** Das war die eine Ausnahme davon, und zwar
|
||||
absichtlich: eine Aufsichtssitzung hat keine Akte. Der Typechecker hat daraus
|
||||
eine Liste gemacht — genau fünf Stellen verlangen eine Person, und alle fünf
|
||||
sind Dinge, an denen die Kreisaufsicht nichts verloren hat (Anmeldemarke,
|
||||
Dienstausweis, Prüfungswizard, Dateiupload, Atemschutz). Ein `?? ""` hätte
|
||||
dieselben fünf Stellen still passieren lassen.
|
||||
|
||||
**`Benutzer` ohne `organisationId`.** Die zweite Ausnahme von der
|
||||
Architekturregel nach `Berechtigung` — und dieselbe Begründung: die Tabelle
|
||||
steht über allen Mandanten, weil sie der Angelpunkt ist, an dem ein Mensch zu
|
||||
mehreren Wehren gehört. Geschützt über Rechte statt RLS.
|
||||
|
||||
**`AUFSICHT` steht nicht im Prisma-Enum.** Alle anderen Rollen werden einem
|
||||
Menschen in einer Wehr zugewiesen. Diese ergibt sich aus der Mitgliedschaftsart.
|
||||
Sie vergebbar zu machen hiesse, dass jemand innerhalb einer Wehr Rechte an
|
||||
einer anderen verteilen könnte.
|
||||
|
||||
**Ihre Rechte werden gefiltert, nicht aufgezählt.** Alles, was `.lesen` heißt,
|
||||
minus vier ausdrückliche Ausnahmen. Eine Liste von Hand wäre bei Phase 11
|
||||
veraltet, und zwar unbemerkt — ein neues Modul brächte Rechte mit, die niemand
|
||||
nachträgt. So bekommt die Aufsicht neue Leserechte automatisch und neue
|
||||
Schreibrechte nie. Der Test dazu ist entsprechend formuliert: nicht „diese
|
||||
dreizehn Rechte", sondern „kein einziges, das nicht lesen heißt".
|
||||
|
||||
**Die Kasse ist ausgenommen**, obwohl `kasse.lesen` auf `.lesen` endet. Sie
|
||||
gehört dem Verein oder der Kommune, nicht der Kreisaufsicht. Sie wäre nur
|
||||
deshalb dabei gewesen, weil das Recht zufällig richtig heißt — und genau
|
||||
dagegen ist die Ausnahmeliste da.
|
||||
|
||||
**Die Migration ist datenerhaltend, mit Abbruchprüfung.** Konten und
|
||||
Zuordnungen entstehen, *bevor* die alten Spalten fallen; am Ende prüft ein
|
||||
`DO`-Block, dass keine Person mit Anmeldedaten ohne Konto dasteht, und bricht
|
||||
sonst ab. Eine Migration, die die Anmeldung anfasst und auf halbem Weg
|
||||
liegenbleibt, sperrt alle aus.
|
||||
|
||||
### Wo es gehakt hat
|
||||
|
||||
| Problem | Ursache | Lösung |
|
||||
|---|---|---|
|
||||
| Nach dem Wehrwechsel landete alles auf der Anmeldeseite | `aktuelleSitzung()` verlangte eine Personenkennung — die Aufsicht hat keine und galt damit als nicht angemeldet | Eine Sitzung ist gültig, wenn Wehr und Konto feststehen; die Akte darf fehlen |
|
||||
| Der Wechsel schien nicht zu greifen | Er griff: das Token trug die neue Wehr. Es war der dritte hängengebliebene Entwicklungsserver dieser Woche — `npm run build` neben laufendem `next dev` macht `.next` unbrauchbar, und der Server antwortet danach nur noch mit 500 | Server beenden, `.next` löschen. Diesmal notiert, damit es nicht ein viertes Mal passiert |
|
||||
| Der Seed brach ab | Er legt die Rollen aus `ROLLEN` an, und dort steht jetzt `AUFSICHT` — die es im Prisma-Enum bewusst nicht gibt | Im Seed gefiltert, mit Begründung |
|
||||
| `unstable_update` nahm den Wechselwunsch nicht an | Es akzeptiert nur `Partial<Session>` | Der Wunsch reist als `user.mitgliedschaftId` und wird serverseitig gegen die Mitgliedschaften geprüft |
|
||||
| Die Nachbarwehr war fest verdrahtet | `ff-nachbarshausen` für jeden Mandanten — genau die Vermischung von Test- und Beispieldaten, die Phase 8 beseitigt hat | Aus dem Mandanten abgeleitet: `<slug>-nachbar` |
|
||||
| Ein alter Datenbanktest wurde unzuverlässig | `ki-datenbank.test.ts` legte Wehr und Person idempotent an, die Wiki-Seite aber nicht. Die neue Testdatei verschob die Reihenfolge, und ein Rest aus einem abgebrochenen Lauf ließ den ganzen Datei-Lauf scheitern | Auch dort ein `upsert` |
|
||||
| Nach `db:test-reset` zeigte der Wehrwähler nur noch die betreute Wehr | Der Reset löschte die Testwehr, aber nicht ihre Nachbarwehr. Der Seed sah die Nachbarwehr, hielt alles für erledigt und legte die Mitgliedschaft in der neuen Testwehr nie wieder an | Der Reset nimmt Nachbarwehr und Kreiskonto mit; der Seed prüft beide Teile getrennt statt mit einem gemeinsamen Ausstieg. Gefunden hat es ein E2E-Test |
|
||||
| Im vollen Lauf fielen fremde Testdateien mit „Anmeldung fehlgeschlagen“ aus | Der Fehlversuch-Test meldete sich mit der Adresse des Administrators an. Fünf Fehlversuche sperren das Konto fünf Minuten — bei zwei Projekten und Wiederholungen kam das zusammen, und alle anderen Dateien brauchen dasselbe Konto | Der Test nimmt jetzt eine Adresse, die es nicht gibt. Die Meldung muss ohnehin dieselbe sein, damit sie nicht verrät, ob es die Adresse gibt |
|
||||
|
||||
### Abweichung von der Vorgabe
|
||||
|
||||
**Zwei Architekturregeln bekommen eine dokumentierte Ausnahme.**
|
||||
|
||||
„Jede Tabelle hat `organisationId`" gilt für `Benutzer` nicht — sie kann es
|
||||
nicht, ohne den Zweck der Tabelle aufzuheben. Die Ausnahme steht im Schema, in
|
||||
der Migration und in der Roadmap, und die Tabelle ist dafür über Rechte
|
||||
geschützt statt über RLS.
|
||||
|
||||
„Berechtigungen zentral in `lib/authz.ts`" gilt für den Verwalterzugang nicht:
|
||||
er hängt an `Benutzer.istVerwalter` und nicht an einem Recht. Rechte gelten je
|
||||
Wehr — ein Recht, das in Wehr A vergeben wird und in Wehr B wirkt, wäre die
|
||||
Hintertür, die diese Phase gerade schließen soll.
|
||||
|
||||
### Nachweise
|
||||
|
||||
- `npm run typecheck` — ohne Ausgabe
|
||||
- `npm run lint` — ohne Befund
|
||||
- `npm run test` — 581 Tests grün, davon 11 neu gegen eine echte Datenbank
|
||||
- `npm run test:e2e` — grün über Desktop und Tablet, davon 26 neu
|
||||
- `npm run db:seed` und `npm run db:test-reset` — beide Mandanten samt
|
||||
Nachbarwehr und Kreisebene
|
||||
- Anmeldung nach der Umstellung von Hand geprüft: Konto gefunden, Passwort
|
||||
richtig, Sitzung trägt Konto, Mitgliedschaft und Verwaltermerkmal
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user