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

13 KiB

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:

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:

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:

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:

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).