Wird PayPal in den Mandant-Einstellungen abgeschaltet, waehrend noch Zahlungen unzugeordnet in der Warteschlange liegen, kaeme ohne diese Ausnahme niemand mehr an sie heran - weder ueber das Menue noch ueber die URL. paypal_inbox_accessible() haelt Menuepunkt und Seite deshalb offen, solange paypal_count_unmatched() etwas findet; die Seite weist per Hinweis darauf hin, dass sie nach dem Abarbeiten verschwindet. Eine Sperre durch den Betreiber sticht die Ausnahme. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9.2 KiB
Back-Office (Platform-Admin)
Stand: 2026-07-16
Ergänzung außerhalb der ursprünglichen M0–M9-Meilensteine: ein Betreiber-Zugang, der mandantenübergreifend alle Kunden einsehen kann – getrennt von der normalen, streng mandantengebundenen Rollenlogik.
Design-Entscheidung: getrennt von tenant_memberships
Platform-Admin-Rechte sind bewusst kein Teil von
saas_user_has_role()/tenant_memberships, sondern eine eigene, komplett
separate Tabelle platform_admins (user_id → users.id). Das ist
kein Zufall: Alle bestehenden Mandanten-Seiten und die automatisierten
Isolationstests (scripts/check-m8-tenant-isolation.php,
scripts/check-m8-role-matrix.php) verlassen sich darauf, dass ein
Login niemals automatisch mandantenübergreifenden Zugriff bekommt. Ein
Platform-Admin-Flag direkt in tenant_memberships oder users einzubauen
hätte dieses Fundament unterlaufen können. Stattdessen prüft
ausschließlich der neue Code-Pfad in app/platform-admin.php
(app_require_platform_admin()) auf Back-Office-Seiten die
Platform-Admin-Eigenschaft; alle bestehenden Seiten sind unverändert und
bleiben strikt mandantengebunden.
Umgesetzte Dateien
database/migrations/0013_saas_platform_admins.sql
app/platform-admin.php
scripts/grant-platform-admin.php
backoffice.php
backoffice-mandant.php
backoffice-export.php
Erster Platform-Admin
Es gibt bewusst keine Weboberfläche, um den ersten Platform-Admin zu setzen – das wäre ein öffentlich erreichbarer "werde Admin"-Endpunkt und ein erhebliches Risiko. Stattdessen: Person registriert sich normal als Mandant (oder nutzt einen bestehenden Login), danach per Shell-Zugriff auf dem Server:
php scripts/grant-platform-admin.php person@example.com
Das Skript prüft, dass der Account existiert, und legt nur dann den
platform_admins-Eintrag an. Zugang entziehen aktuell nur per direktem
DELETE FROM platform_admins WHERE user_id = ? (keine UI dafür – aus
demselben Grund wie beim Setzen).
Funktionsumfang
backoffice.php: Übersicht aller Mandanten mit Kürzel, Status, Erstelldatum, Teilnehmerzahl (aktiv/gesamt) und Saldensumme.backoffice-mandant.php?tenant_id=X: Ansicht eines einzelnen Mandanten – Einstellungen, Mitglieder mit Rolle, letzte 20 Buchungen, letzte 20 Admin-Aktionen, dazu die Funktions-Schalter (siehe unten). Absichtlich kein Schreibzugriff auf die eigentlichen Mandantendaten (Buchungen, Mitglieder, Einstellungen des Kunden) von hier aus, um das Risiko einer versehentlichen Fremdänderung auszuschließen.backoffice-export.php: nutzt dieselbeapp_export_tenant_data()- Funktion wie der Selbstbedienungs-Export, aber ausgelöst durch den Platform-Admin für einen beliebigen Mandanten.
Funktions-Schalter je Mandant
Stand: 2026-08-17. Zentralstelle des Betreibers, um einzelne Funktionen
pro Mandant freizuschalten oder zu sperren – gepflegt in
backoffice-mandant.php, umgesetzt in app/features.php und der Tabelle
tenant_features (Migration 0025_tenant_features.sql).
Schaltbar sind: FAQ-Seite, Striche selbst eintragen, Kaffeeliste als PDF, PayPal-Zahlungseingang, CSV-Import, Info-Mails und Erinnerungen, Jahresabschluss, Datenexport, eigenes Design.
Design-Entscheidungen:
- Getrennt von
tenant_settings. Dort stellt der Kunde ein, wie eine Funktion arbeitet; hier entscheidet der Betreiber, ob sie ihm überhaupt zur Verfügung steht. Beide Prüfungen stehen nebeneinander, z. B. beim Selbsteintrag:self_entry_enabled(Kunde) und Featureself_entry(Betreiber). - Ohne Zeile = freigeschaltet.
tenant_featureshält nur die Abweichungen vom Standard; ein neuer Mandant bekommt automatisch den vollen Funktionsumfang, ohne dass der Betreiber etwas einschalten muss. Fehlt die Tabelle (Migration noch nicht ausgerollt), bleibt ebenfalls alles freigeschaltet – eine ausstehende Migration darf keine Funktion sperren. - Gesperrt heißt unsichtbar. Die betroffenen Menüpunkte und
Schaltflächen verschwinden (
footer.php,kaffeeliste.php,einzahlung.php,konto.php); ein direkter Aufruf der Seite endet mit einem Hinweis statt mit einem Fehler. Die POST-Verarbeitung der betroffenen Seiten hängt an derselben Prüfung, eine gesperrte Funktion lässt sich also auch nicht per Formular-POST auslösen. - Abgeschaltet vom Kunden heißt ebenfalls unsichtbar. Für Funktionen
mit eigenem Schalter in den Mandant-Einstellungen zieht
footer.php(und die Anleitung)app_feature_available()heran: erst die Freischaltung durch den Betreiber, dann der Schalter des Kunden. Die Zuordnung steht inapp_feature_tenant_switch()– aktuellpaypal_inbox→paypal_enabledundself_entry→self_entry_enabled. Wer PayPal nicht als Zahlungsweg anbietet, sieht den Menüpunkt „PayPal-Zahlungen" also gar nicht erst; beim direkten Aufruf erscheint statt des Betreiber-Hinweisesapp_feature_tenant_notice_html()mit dem Weg zurück in die Mandant-Einstellungen. - Ausnahme: offene Warteschlange. Schaltet ein Mandant PayPal ab,
während noch nicht zugeordnete Zahlungen liegen, wären diese weder über
Menü noch über URL erreichbar.
paypal_inbox_accessible()(app/paypal-inbox.php) hält Menüpunkt und Seite deshalb offen, solangepaypal_count_unmatched()etwas findet – mit einem Hinweis auf der Seite. Ist die Warteschlange abgearbeitet, schließt sie sich von selbst. Eine Sperre durch den Betreiber sticht auch diese Ausnahme. - Jede Änderung landet als
backoffice.features_updatedim Audit-Log des betroffenen Mandanten – wie jeder andere Back-Office-Zugriff auch.
Das Back-Office ist außerdem jetzt für Platform-Admins im Menü verlinkt
(Gruppe „Betreiber" in footer.php); vorher war es nur über die direkte
URL erreichbar.
Eigenes Design je Mandant
Stand: 2026-08-17, hängt am Feature branding. Der Kunde hinterlegt in
mandant-einstellungen.php eine Akzentfarbe und ein Logo:
- Die Farbe (
tenant_settings.brand_color, validiert als#rrggbb) wird inheader.phpals kleines Stylesheet eingebettet (app/branding.php), das die Akzentfarbe des Templates überschreibt. Eine eigene CSS-Datei je Mandant wäre ein zusätzlicher Request für wenige Zeilen. - Das Logo (
tenant_settings.brand_logo) liegt wie das PDF-Wasserzeichen invar/tenant_logosund ist damit nicht direkt per URL abrufbar – sonst wäre jedes Kundenlogo unter einer ratbaren Adresse öffentlich. Ausgeliefert wird es übertenant-logo-anzeigen.php, das ausschließlich das Logo des Mandanten des angemeldeten Nutzers ausgibt. - Wasserzeichen (Ausdruck) und Marken-Logo (Oberfläche) sind getrennte
Dateien mit eigenem Namenspräfix (
logo_bzw.brand_). - Sperrt der Betreiber
branding, verschwindet der Abschnitt aus den Einstellungen und die Oberfläche fällt aufs Standarddesign zurück. Die gespeicherte Farbe bleibt erhalten und gilt nach einer erneuten Freischaltung wieder.
Verhältnis zum Selbstbedienungs-Export (datenexport.php)
Bewusste Entscheidung: Der bestehende Selbstbedienungs-Export für
Mandanten-Owner/Admin (datenexport.php, aus M8) bleibt zusätzlich
bestehen, statt ihn durch das Back-Office zu ersetzen. Begründung: Der
Mandant ist im Auftragsverarbeitungs-Verhältnis der Verantwortliche
(Controller), der Betreiber dieser App der Auftragsverarbeiter
(Processor). Art. 15/20 DSGVO geben den betroffenen Personen ein
Auskunfts-/Portabilitätsrecht, und Art. 28 DSGVO verpflichtet den
Auftragsverarbeiter, den Verantwortlichen bei der Erfüllung dieser
Rechte zu unterstützen sowie Daten am Vertragsende zurückzugeben – ein
jederzeit verfügbarer Selbstbedienungs-Export erfüllt genau das. Das
Back-Office ergänzt das um einen Betreiber-seitigen Zugriff für Support,
Migrationen oder eigene Nachweispflichten, ersetzt den
Mandanten-Selbstexport aber nicht.
Audit-Trail
Jede Back-Office-Ansicht und jeder Back-Office-Export wird im Audit-Log
des betroffenen Mandanten protokolliert
(backoffice.tenant_viewed, backoffice.tenant_exported, mit dem
Platform-Admin als actor_user_id). Ein Mandant sieht damit über sein
eigenes Protokoll (mandant-einstellungen.php), wann der Betreiber auf
seine Daten zugegriffen hat – Transparenzpflicht statt stiller
Einsicht.
Prüfstatus
scripts/check-m8-tenant-isolation.php: weiterhin grün mit 10 Assertions – das Back-Office berührt die geprüften Pfade nicht.scripts/check-m8-role-matrix.php: weiterhin grün mit 55 Assertions.- HTTP-Smoke: grün mit 30 geprüften Seiten (zwei neue Login-Schutz-Checks
für
backoffice.php/backoffice-mandant.php). - Golden Master: grün mit 104 Assertions.
Live getestet: eigens angelegter Test-Mandant, per CLI-Skript zum
Platform-Admin gemacht, Back-Office-Übersicht zeigt korrekt alle
Mandanten (inklusive der echten Bestandsmandanten), Detailansicht und
Export für den eigenen Test-Mandanten funktionieren, Audit-Log-Einträge
korrekt geschrieben. Kritischer Isolationstest bestätigt: derselbe
Platform-Admin sieht auf regulären Mandanten-Seiten (kaffeeliste.php)
weiterhin ausschließlich seinen eigenen Mandanten. Ein zweiter,
eingeloggter, aber nicht privilegierter Testnutzer bekommt auf
backoffice.php korrekt 403 Forbidden. Alle Testdaten anschließend
vollständig entfernt.