Phase 10: Mandantenfähigkeit (#15)

This commit was merged in pull request #15.
This commit is contained in:
2026-09-05 13:47:46 +00:00
parent 2b53556175
commit 91742a0dd7
98 changed files with 3810 additions and 201 deletions
+124
View File
@@ -6,6 +6,130 @@ Alle nennenswerten Änderungen an FLORIAN. Format nach
Die Regeln dazu stehen in [docs/VERSIONIERUNG.md](docs/VERSIONIERUNG.md).
## [0.11.0] — 2026-09-05
Phase 10: Mandantenfähigkeit. Ein Konto, mehrere Wehren, eine Kreisebene, die
zusieht — und eine Verwaltung für den, der die Installation betreibt.
**Das Anmeldekonto ist von der Personenakte gelöst.** Bis hierher war die Akte
zugleich das Konto: Passwort-Hash und OIDC-Subject hingen an `person`, und
`person` gehört genau einer Wehr. Ein Mensch konnte damit nicht zu zwei Wehren
gehören, ohne zwei Passwörter zu haben — und die Anmeldung per E-Mail war
mehrdeutig, sobald dieselbe Adresse in zwei Wehren stand, weil die
Eindeutigkeit nur je Organisation galt.
**Das Konto trägt keine Mandantenkennung — und kann keine tragen.** Es ist der
Angelpunkt, an dem ein Mensch zu mehreren Wehren gehört. Geschützt wird es
deshalb wie `Berechtigung` aus Phase 0: über Rechte statt über Row Level
Security. Was ein Konto in einer Wehr darf, entscheidet nicht es selbst,
sondern seine `Mitgliedschaft` — und die trägt `organisationId` wie jede
andere Tabelle.
**Die Kreisebene sieht und ändert nichts.** Ihre Rechte werden nicht
aufgezählt, sondern gefiltert: alles, was `.lesen` heißt, und sonst nichts.
Eine Liste von Hand wäre bei der nächsten Phase veraltet, und zwar unbemerkt.
### Hinzugefügt
**Datenmodell**
- `Benutzer` — das Anmeldekonto. Global eindeutige E-Mail, ein Passwort, ein
OIDC-Subject. Ohne `organisationId`, mit Begründung im Schema.
- `Mitgliedschaft` — die Zugehörigkeit eines Kontos zu einer Wehr, als
`MITGLIED` (mit Personenakte) oder `AUFSICHT` (ohne).
- `Person` verliert die Anmeldefelder. Der Passwort-Hash kann damit aus einer
Personenabfrage gar nicht mehr herausfallen — die drei Stellen, die ihn
bisher vorsichtshalber abgeschnitten haben, sind entfallen.
**Was die Datenbank durchsetzt** (Migrationen `20260905090000` und
`20260905091500`)
- RLS für `mitgliedschaft` wie für jede andere Tabelle. Eine Wehr sieht damit
auch nicht, in welchen anderen ein Konto noch geführt wird.
- Trigger `mitgliedschaft_akte_gleiche_wehr` — eine Zuordnung, deren
Personenakte in einer anderen Wehr liegt, wird abgelehnt. Ein Fremdschlüssel
prüft nur, *dass* es die Akte gibt, nicht *wo* sie liegt; ohne den Trigger
ließe sich ein Konto über eine fremde Akte in eine Wehr hängen.
- Trigger `benutzer_kennung_unveraenderlich` — E-Mail, OIDC-Subject und das
Verwaltermerkmal lassen sich nicht aus einer Wehr heraus ändern.
- `REVOKE INSERT, DELETE ON benutzer` für die Anwendungsrolle.
- CHECKs: `MITGLIED` braucht eine Akte, `AUFSICHT` darf keine haben; ein Konto
braucht einen Anmeldeweg; eine E-Mail ohne @ ist keine.
**Die Umstellung ist datenerhaltend.** Jede Person mit Anmeldedaten bekommt
ein Konto und eine Mitgliedschaft, bevor die alten Spalten fallen. Wer schon
mit derselben Adresse in zwei Wehren stand, bekommt **ein** Konto mit **zwei**
Mitgliedschaften — genau das, was vorher nicht möglich war. Eine Prüfung am
Ende bricht die Migration ab, statt einen halben Zustand zu hinterlassen.
**Oberfläche**
- **Wehrwähler** in der Seitenleiste. Er erscheint nur bei mehr als einer Wehr
— ein Auswahlfeld mit genau einem Eintrag ist eine Frage ohne
Antwortmöglichkeit.
- Nach der Anmeldung öffnet die **eigene** Wehr, nicht die betreute.
- Ein dauerhafter Hinweis, wenn man eine Wehr als Kreisaufsicht sieht — nicht
als kurz aufblitzende Meldung.
- **`/wehren`** für den Verwalter: Wehren anlegen (samt Systemrollen), Konten
führen, Zuordnungen vergeben und beenden. Jede Zuordnung wird in der
betroffenen Wehr protokolliert, nicht beim Verwalter — sonst hätte der
Betreiber einen Weg, sich still in fremde Daten zu setzen.
**Rechte**
- Neue Rolle `AUFSICHT` — bewusst **nicht** im Prisma-Enum: sie wird nicht
vergeben, sondern ergibt sich aus der Mitgliedschaftsart.
- Ausdrücklich ausgenommen, obwohl sie auf `.lesen` enden: `kasse.lesen` (die
Kasse gehört dem Verein oder der Kommune, nicht der Kreisaufsicht),
`person.export`, `system.auditlog`, `ki.protokoll`.
- `istVerwalter` als Merkmal des Kontos, nicht als Recht.
**Tests**
- 19 neue Unit-Tests, davon 11 gegen eine echte Datenbank: Doppelmitgliedschaft,
der Trigger gegen wehrfremde Akten, die CHECKs und die Trennung der
Zuordnungen über RLS.
- 13 E2E-Tests: kein Wähler bei einer Wehr, Wechsel in die betreute und
zurück, die Mitglieder der anderen Wehr, die verschlossene Kasse, und dass
der Administrator die Umstellung überlebt hat.
**Seed**
- Eine zweite, kleine Wehr je Mandant und ein Konto, das in der einen Mitglied
und in der anderen Kreisaufsicht ist. Ohne sie ließe sich die Phase nicht
ausprobieren — und eine Mandantentrennung, die nie zwei Mandanten hatte, ist
keine geprüfte.
### Geändert
- Eine Sitzung ohne Personenakte ist gültig. Bis hierher galt genau das als
„nicht angemeldet"; für die Kreisaufsicht, die in der betreuten Wehr keine
Akte hat, hätte jeder Aufruf auf der Anmeldeseite geendet.
- `Sitzung.personId` ist nullbar. Der Typechecker hat damit die fünf Stellen
gezeigt, an denen eine Aufsichtssitzung nichts verloren hat — Anmeldemarke,
Dienstausweis, Prüfungswizard, Dateiupload, Atemschutz.
- Die Mitgliederansicht zeigt statt eines ungenutzten Zeitstempels, ob jemand
überhaupt ein Konto hat. Das ist die nützlichere Auskunft: ohne Konto kommt
er nicht in FLORIAN hinein.
### Behoben
- **Ein Testaufbau war nicht wiederholbar.** `ki-datenbank.test.ts` legte Wehr
und Person idempotent an, die Wiki-Seite aber nicht — blieb aus einem
abgebrochenen Lauf etwas stehen, scheiterte der ganze Datei-Lauf an einer
Eindeutigkeit, die mit dem Geprüften nichts zu tun hat.
- **`db:test-reset` ließ die Nachbarwehr stehen** und der Seed hielt daraufhin
alles für erledigt — die Mitgliedschaft in der neu angelegten Testwehr
entstand nie wieder. Der Reset nimmt sie jetzt mit, und der Seed prüft
Nachbarwehr und Kreiskonto getrennt statt mit einem gemeinsamen Ausstieg.
- **Der Fehlversuch-Test sperrte das gemeinsame Testkonto.** Fünf Fehlversuche
sperren ein Konto für fünf Minuten; bei zwei Projekten und Wiederholungen
kam das im vollen Lauf zusammen, und fremde Testdateien fielen mit
„Anmeldung fehlgeschlagen“ aus. Er nimmt jetzt eine Adresse, die es nicht
gibt — die Meldung muss ohnehin dieselbe sein.
---
## [0.10.0] — 2026-09-04
Phase 9: Kassenverwaltung. Kassenbuch, Mitgliedsbeiträge, Rechnungen an