Phase 10: Mandantenfähigkeit (#15)
This commit was merged in pull request #15.
This commit is contained in:
+124
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user