Files
kaffeekasse-saas/docs/m5-app-kern.md
T
clemensandClaude Sonnet 5 aeb687f42e M5: Hinweise auf tenant-scoped Notices umziehen, Mitgliederzugang auf Rollen umstellen
Hinweise:
- Neue Tabelle notices (tenant-scoped, Soft-Delete via deleted_at) loest
  die global unscoped kl_hinweise als aktive Datenquelle ab; Migration
  uebernimmt einmalig aktuell gueltige kl_hinweise-Eintraege fuer den
  Default-Mandanten. kl_hinweise bleibt als Golden-Master-Referenz stehen.
- hinweise.php und der Banner in header.php sind tenant-scoped umgestellt.

Mitgliederverwaltung:
- mitarbeiterverwalten.php verwaltet jetzt participants (tenant-scoped)
  statt der global unscoped kl_Mitarbeiter-Tabelle als primaere Quelle.
  Das behebt nebenbei ein Mandanten-Datenleck: jeder SaaS-Mandant mit
  Owner/Admin-Rolle haette zuvor die komplette Default-Mandanten-
  Mitgliederliste sehen und bearbeiten koennen.
- Fuer den Default-Mandanten bleibt Dual-Write nach kl_Mitarbeiter
  bestehen, damit stricheintragen.php/einzahlung.php weiter funktionieren;
  andere Mandanten werden rein participant-nativ verwaltet.
- Die Legacy-Administrator-Checkbox ist raus. Stattdessen kann ein Admin
  je Mitglied unabhaengig von Name/E-Mail einen Login-Zugang mit Rolle
  (member/treasurer/admin) gewaehren oder entziehen
  (saas_grant_participant_access / saas_revoke_participant_access).
  Einladung laeuft ueber den bestehenden Passwort-Reset-Mechanismus,
  Entzug setzt die Mitgliedschaft auf revoked statt sie zu loeschen.
- Kompletter Flow live getestet: anlegen, Zugang gewaehren, Einladungsmail,
  Passwort setzen, Login, Rollenschutz, Zugang entziehen, Login-Sperre.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 00:18:55 +02:00

279 lines
13 KiB
Markdown

# M5 App-Kern
Stand: 2026-07-14 (fortgeführt)
M5 stellt die operativen App-Seiten schrittweise auf das neue tenant-sichere
Modell um. Der Start erfolgt bewusst read-only, damit Summen, Links und
Darstellung gegen die bestehende Legacy-Oberfläche vergleichbar bleiben.
## Ziel
- Kernseiten aus `app/ledger.php` lesen lassen.
- Bestehendes Tabellenlayout und Bediengefühl erhalten.
- Schreibende Legacy-Flows erst nach stabiler Leseparität umstellen.
- Tenant-Kontext serverseitig bestimmen, nicht aus Formularfeldern.
## Umgesetzte Schritte
Umgesetzte Dateien:
```text
kaffeeliste.php
teilnehmerauswertung.php
index.php
```
Umgesetzter Umfang `kaffeeliste.php`:
- Die Gesamtübersicht liest aktive Teilnehmer aus
`ledger_fetch_participant_summaries()`.
- Angezeigte Werte bleiben fachlich gleich: aktueller Stand, Gesamtausgabe,
Gesamtstriche und Gesamteinzahlungen.
- Links zur bestehenden `teilnehmerauswertung.php` bleiben über
`participants.legacy_mitarbeiter_id` erhalten.
- Owner, Admin und Treasurer dürfen die SaaS-Ansicht lesen.
- Der lokale Legacy-/Dev-Admin-Fallback nutzt nur ohne SaaS-Session den
Default-Tenant.
- Die Sidebar zeigt Legacy-Schreibseiten weiterhin nur für Legacy-Admins;
Ledger-Leseansichten sind für passende SaaS-Rollen sichtbar.
- Export, letzte Einträge, CSV-Upload und Info-Mail bleiben noch Legacy-Flows.
Umgesetzter Umfang `teilnehmerauswertung.php`:
- Die Detailauswertung liest Teilnehmerdaten über
`ledger_fetch_participant_summary_by_legacy_id()`.
- Die Route bleibt kompatibel zu bestehenden Links:
`teilnehmerauswertung.php?user_id=<legacy_mitarbeiter_id>`.
- Gesamtwerte, Jahresübersicht und letzte Buchungen werden aus
`ledger_entries` geladen.
- PayPal-Anzeige nutzt die neuen Tenant-Settings statt `kl_config`.
- Owner, Admin und Treasurer dürfen die SaaS-Ansicht lesen; der lokale
Legacy-/Dev-Admin-Fallback nutzt weiterhin den Default-Tenant.
Ergänzte Ledger-Helfer:
- `ledger_fetch_default_tenant()`
- `ledger_fetch_participant_summary_by_legacy_id()`
- `ledger_mirror_legacy_consumption()`
- Filter `legacy_mitarbeiter_ids` in `ledger_fetch_participant_summaries()`
Umgesetzter Umfang `index.php`:
- Das persönliche Dashboard liest den aktuellen Stand, Jahreswerte und letzte
Buchungen aus `ledger_entries`.
- Die Teilnehmerzuordnung erfolgt über den aktuellen SaaS-User oder im lokalen
Legacy-/Dev-Fallback über die E-Mail-Adresse des Legacy-Mitarbeiters.
- PayPal-Anzeige und Preis pro Strich nutzen die Tenant-Settings.
- Eigene Web-Striche bleiben mit dem bestehenden Legacy-Datensatz kompatibel
und werden direkt ins Ledger gespiegelt.
## Fortsetzung: Schreibseiten und Storno
Umgesetzte Dateien:
```text
stricheintragen.php
einzahlung.php
letzteneintraege.php
app/ledger.php
scripts/check-m4-ledger-migration.php
```
Umgesetzter Umfang `stricheintragen.php` und `einzahlung.php`:
- Beide Sammelerfassungsseiten waren zuvor ohne jede Zugriffsprüfung
erreichbar (nur CSRF-Schutz, kein `checkKaffeelisteAdmin`- oder Rollen-Check).
Das ist jetzt behoben: Zugriff erfordert SaaS-Rolle `owner`, `admin` oder
`treasurer`, oder im Legacy-/Dev-Fallback `checkKaffeelisteAdmin` mit dem
Default-Tenant.
- Jede eingetragene Legacy-Zeile (`kl_Kaffeeverbrauch` beziehungsweise
`kl_Einzahlungen`) wird innerhalb derselben Transaktion sofort über
`ledger_mirror_legacy_consumption()` beziehungsweise die neue
`ledger_mirror_legacy_payment()` ins Ledger gespiegelt. Schlägt die
Spiegelung fehl, wird die gesamte Sammelerfassung zurückgerollt statt
teilweise gespeichert zu werden.
- Ein vorbestehender Bug wurde nebenbei behoben: Bei einer POST-Anfrage blieb
`$sqlMitarbeiter` unbelegt, was einen PHP-Warning erzeugte und die
Mitarbeiterliste nach dem Speichern leer ließ.
Umgesetzter Umfang `letzteneintraege.php`:
- Der Seitenzugriff nutzte bisher ausschließlich den Legacy-Check
`checkKaffeelisteAdmin`, wodurch neu registrierte SaaS-Mandanten (ohne
Legacy-`kl_Mitarbeiter`-Zeile) die Seite nie hätten nutzen können. Jetzt gilt
dieselbe Rollen-/Fallback-Logik wie in `kaffeeliste.php`.
- Löschen erzeugt keinen harten Delete im Ledger mehr. Neue Funktion
`ledger_void_entry_by_legacy_id()` setzt `voided_at` auf dem gespiegelten
Ledger-Eintrag, bevor die Legacy-Zeile aus `kl_Einzahlungen` oder
`kl_Kaffeeverbrauch` entfernt wird (in einer Transaktion). Der Ledger-Eintrag
bleibt damit für die Revision erhalten; alle Lesepfade filtern bereits
konsistent auf `voided_at IS NULL`.
- Zwei nie aufgerufene Funktionen (`berechneGesamtausgabe`,
`berechneGesamtstriche`, `berechneGesamteinzahlungen`) wurden als toten Code
entfernt.
Angepasstes Check-Skript:
- `scripts/check-m4-ledger-migration.php` ging bisher davon aus, dass jeder
gespiegelte Ledger-Eintrag eine noch existierende Legacy-Zeile hat. Das ist
durch das Storno-Modell nicht mehr korrekt: Ein stornierter Eintrag hat
absichtlich keine Legacy-Zeile mehr, bleibt aber im Ledger stehen. Die
Prüfungen filtern jetzt zusätzlich auf `voided_at IS NULL`, sodass echte
Dateninkonsistenzen weiterhin erkannt werden, stornierte Einträge aber nicht
mehr als Fehler zählen.
Alle drei Flows wurden gegen die Remote-Dev-Datenbank live per HTTP getestet
(Sammeleintrag Striche, Sammeleinzahlung, Storno beider Buchungsarten über
`letzteneintraege.php`) und anschließend wieder auf den Ausgangsstand
zurückgesetzt.
## Fortsetzung: Mitgliederverwaltung
Umgesetzte Dateien:
```text
mitarbeiterverwalten.php
app/ledger.php
```
Umfang:
- Zugriff nutzte bisher ausschließlich `checkKaffeelisteAdmin` und hätte neue
SaaS-Mandanten ohne Legacy-Mitarbeiterzeile ausgeschlossen. Jetzt gilt die
gleiche Rollen-/Fallback-Logik wie in `kaffeeliste.php`, mit den Rollen
`owner` und `admin` (nicht `treasurer`, da Mitgliederpflege sensibler ist
als reine Zahlungsvorgänge).
- Anlegen, Bearbeiten, Aktivieren und Deaktivieren schreiben weiterhin zuerst
in `kl_Mitarbeiter` und spiegeln danach in derselben Transaktion über die
neue Funktion `ledger_mirror_legacy_participant()` nach `participants`.
Ledger-Ansichten (Dashboard, Kaffeeliste, Teilnehmerauswertung) sehen neue
oder geänderte Mitglieder damit sofort, ohne auf einen manuellen Lauf von
`scripts/backfill-default-tenant.php` zu warten.
- Eine gespeicherte-XSS-Lücke wurde behoben: Name und E-Mail wurden beim
Bearbeiten-Formular und in der Mitgliederliste bisher ungeschützt
ausgegeben (nur `paypalname` war escaped). Beide Stellen nutzen jetzt
`saas_html()`.
- Live gegen die Dev-Datenbank getestet: Anlegen mit einem
HTML/Skript-Payload im Namen (korrekt escaped in der Ausgabe, korrekt
gespiegelt in `participants`), Deaktivieren (spiegelt `active = 0`).
Bewusst nicht umgesetzt:
- Der Haken "Administrator" bleibt ein reines Legacy-Feld auf
`kl_Mitarbeiter.admin` und wird nicht automatisch in eine
`tenant_memberships`-Rolle übersetzt. Das würde einen Login-Account ohne
Einladung/Passwort-Setzung anlegen, was ein eigenes, sauber zu
bauendes Einladungs-Flow braucht (E-Mail-Versand, Token, Passwortsetzung).
Admin-Rollen für neue SaaS-Nutzer laufen bis dahin weiter über
`scripts/backfill-default-tenant.php` oder die Registrierung.
## Fortsetzung: Hinweise und rollenbasierter Zugang
Umgesetzte Dateien:
```text
database/migrations/0007_saas_notices.sql
app/notices.php
hinweise.php
header.php
mitarbeiterverwalten.php (grundlegend neu)
app/ledger.php (Participant-CRUD)
app/saas-auth.php (Zugangsvergabe/-entzug)
app/saas-mail.php (Einladungsmail)
```
### Hinweise auf Notices umgezogen
- Neue Tabelle `notices` (tenant-scoped, mit `deleted_at` für Soft-Delete)
ersetzt `kl_hinweise` als aktive Datenquelle. `kl_hinweise` bleibt
unangetastet als Golden-Master-Referenz.
- Die Migration übernimmt einmalig alle zum Migrationszeitpunkt noch
gültigen `kl_hinweise`-Einträge in `notices` des Default-Mandanten, damit
keine sichtbaren Banner verloren gehen.
- `hinweise.php` liest/schreibt jetzt ausschließlich `notices`, tenant-scoped
mit dem gleichen Rollen-/Legacy-Fallback-Muster wie andere Admin-Seiten.
Löschen ist ein Soft-Delete (`deleted_at`), kein Hard-Delete.
- `header.php` (Banner-Anzeige auf allen App-Seiten) löst den Mandanten
jetzt selbst auf (SaaS-Session oder Default-Tenant-Fallback) und zeigt den
aktuell gültigen Hinweis dieses Mandanten statt eines global-legacy
Hinweises.
### Mitgliederverwaltung: participant-nativ mit Rollen-Zugang
`mitarbeiterverwalten.php` wurde grundlegend umgebaut, nicht mehr additiv
gepatcht:
- Datenquelle ist jetzt `participants` (tenant-scoped), nicht mehr die
global unscoped `kl_Mitarbeiter`-Tabelle. Das behebt nebenbei ein
Datenleck: Da `kl_Mitarbeiter` keine `tenant_id` hat, hätte jeder
SaaS-Mandant mit Owner/Admin-Rolle über die vorherige Version dieser
Seite die komplette Mitgliederliste des Default-Mandanten sehen und
bearbeiten können.
- Für den Default-Mandanten wird weiterhin dual-write nach
`kl_Mitarbeiter` betrieben (`ledger_create_participant()`,
`ledger_update_participant()`, `ledger_set_participant_active()`), damit
`stricheintragen.php` und `einzahlung.php` (deren Mitarbeiter-Picker
weiterhin direkt aus `kl_Mitarbeiter` liest) neue Mitglieder sofort
anzeigen. Andere Mandanten haben keine Legacy-Schattentabelle und werden
rein participant-nativ verwaltet.
- Die Legacy-„Administrator"-Checkbox ist aus dem Anlegen-/Bearbeiten-
Formular entfernt. Stattdessen gibt es je Mitglied eine eigene
„Zugang"-Spalte: Rolle wählen (`member`, `treasurer`, `admin`) und
„Zugang gewähren", oder bei bestehendem Zugang Rolle ändern beziehungsweise
„Zugang entziehen". `owner` ist über diese UI nicht vergebbar oder
entziehbar (nur bei Registrierung gesetzt).
- Name und E-Mail eines Mitglieds (`participants.display_name`/`email`)
bleiben unabhängig vom Login: Ein Mitglied kann ohne jeden Zugang
existieren (nur für Kaffeeliste/Benachrichtigung), und ein Zugang kann
jederzeit gewährt oder entzogen werden, ohne den Mitgliedsdatensatz zu
berühren.
- Zugangsvergabe (`saas_grant_participant_access()`) legt bei Bedarf einen
`users`-Datensatz ohne Passwort an, setzt/aktualisiert die
`tenant_memberships`-Rolle und verknüpft `participants.user_id`.
Anschließend wird ein Einladungslink über den bestehenden
Passwort-Reset-Mechanismus verschickt (`saas_send_invite_mail()`, gleicher
Token-Typ `password_reset`, gleiche Zielseite
`passwort-zuruecksetzen.php` wie beim regulären Passwort-Reset).
- Zugangsentzug (`saas_revoke_participant_access()`) setzt die
`tenant_memberships`-Zeile auf `status = 'revoked'`, statt sie zu löschen
oder den `user_id`-Verweis zu entfernen. Der Zugang kann später erneut
gewährt werden, ohne den Account neu anzulegen. Der Login prüft bereits
überall auf `tm.status = 'active'`, wodurch ein entzogener Zugang sofort
wirkt.
- Live gegen die Dev-Datenbank getestet: Mitglied anlegen (inklusive
Dual-Write-Check), Zugang mit Rolle `treasurer` gewähren, Einladungsmail
geprüft, Passwort über den Einladungslink gesetzt, erfolgreicher Login,
Rollenschutz geprüft (kein Zugriff auf `mitarbeiterverwalten.php`, Zugriff
auf `kaffeeliste.php`), Zugang entzogen, anschließender Login-Versuch
korrekt mit „kein aktiver Mandant" abgelehnt.
## Noch offen
- Eigene PayPal-/Zahlungsbereich als eigenständiger App-Screen (aktuell nur im
Dashboard integriert).
- `stricheintragen.php` und `einzahlung.php` lesen ihre Mitarbeiter-Picker
weiterhin aus der global unscoped `kl_Mitarbeiter`-Tabelle. Für den
Default-Mandanten funktioniert das unverändert; für jeden anderen Mandanten
ist die Liste faktisch leer beziehungsweise zeigt (nur lesend, Schreiben
schlägt dank Tenant-Scope in `ledger_mirror_legacy_*` sicher fehl) die
Namen der Default-Mandanten-Mitglieder an. Das ist ein bestehendes,
eigenständiges Scope-Thema für eine spätere Iteration, keine Regression
dieser Session.
- Export, Mail und Jahresprozesse bleiben M6-Themen.
## Aktueller Prüfstatus
- M4 Ledger-Migration: grün mit 73 Assertions (Storno-fähig).
- M4 Ledger-Service: grün mit 115 Assertions.
- Golden Master: grün mit 104 Assertions.
- HTTP-Smoke: grün mit 23 geprüften Seiten.
- Live-Test der Schreibflows gegen die Dev-DB: Sammelstriche, Sammeleinzahlung
und Storno beider Buchungsarten erfolgreich geprüft.
- Live-Test der Mitgliederverwaltung gegen die Dev-DB: Anlegen (inklusive
XSS-Payload-Check), Deaktivieren, participants-Spiegelung erfolgreich
geprüft.
- Live-Test Hinweise/Notices: Carry-over-Migration, Anlegen mit HTML-Payload
(korrekt escaped), Soft-Delete, Banner-Anzeige auf Mandant geprüft.
- Live-Test Zugangsvergabe/-entzug: kompletter Flow von Einladung bis
Login-Sperre nach Entzug erfolgreich geprüft (siehe oben).