User Stories — Epic 8: Plattform

Format-Muster: Pilot Logistik (50-us-logistik.md). Methodik: Connextra (Als/möchte/damit) + Gherkin-Akzeptanzkriterien (Gegeben/Wenn/Dann) + INVEST-Qualitätsgate. Sprache der Stories DE; Fachbegriffe/Status/Rollen wie im Schema. Bezug: Modul 8 (17-plattform.md), Schema Dok 30 §6.1 (app_users/roles/user_roles/audit_log/app_settings), §6.2 (locale_strings), §6.3 (storage_objects/notifications), §13 (RLS-Matrix), Compliance Dok 45. Rollen-Codes kanonisch (UPPERCASE): ADMIN, BUCHHALTUNG, OPERATIONS, FAHRER, AFFILIATE, READONLY. IDs: US-PLT-NN (stabil). Status: Entwurf (zur Freigabe).


Personas in diesem Epic

RollePersonBezug zur Plattform
ADMINMarcelNutzer-/Rollenverwaltung, Systemeinstellungen, Periodensperre, Audit, alles
BUCHHALTUNGMarielainterne Nutzerin: Login, Self-Profil, Audit lesen, Belege/Notifications
OPERATIONSMarkusinterner Nutzer: Login, Self-Profil, KYC-/Beleg-Upload, Audit lesen
FAHRERArkysinterner Nutzer (minimal): Login, Self-Profil, Liefernachweis-Upload
AFFILIATEexternLogin, Self-Profil, nur Eigenes (Doku/Notifications zu eigenen Sendungen)
READONLYTreuhänder/GastLogin, Self-Profil, lesend (Audit, Rechte-Matrix, Dashboards)
System— (kein Login)Audit-Trigger, Notification-Outbox-Worker, Storage-Lifecycle

INVEST-Gate (für alle Stories dieses Epics geprüft): jede Story ist unabhängig schneidbar, verhandelbar, liefert Geschäftswert, schätzbar, klein genug für einen Schritt und über die Gherkin-Szenarien testbar (inkl. Negativfälle/RLS/Security-Härtung).


Als interner Nutzer oder AFFILIATE möchte ich mich allein über meine E-Mail (Magic-Link oder 6-stelliger OTP) anmelden, damit kein Passwort existiert, das geleakt werden kann.

Akzeptanzkriterien

  • Gegeben der Login-Screen, wenn ich meine E-Mail eingebe und „Link senden" auslöse, dann versendet Supabase Auth einen Magic-Link und einen 6-stelligen OTP (E-Mail in meiner Sprache, Template AUTH_MAGIC_LINK) — ohne Passwortfeld.
  • Gegeben ein gültiger Klick/Code, wenn ich ihn einlöse, dann entsteht/aktualisiert sich auth.users, der Trigger handle_new_user() legt mein app_users-Profil an (Default preferred_locale='es') und ich bin angemeldet.
  • Gegeben ein abgelaufener Link (Default 1 h), wenn ich ihn öffne, dann erscheint eine klare Meldung plus „neuen Link senden"; die OTP-Eingabe (inputmode=numeric, Auto-Advance) bleibt als Fallback auf demselben Screen verfügbar.

Traceability: §4.1, app_users (§6.1), Template AUTH_MAGIC_LINK (§6.3). (Auth-Härtung Compliance §8.1.)


US-PLT-02 — Konto wartet auf Freischaltung (keine Rolle = kein Zugriff)

Als Plattform möchte ich, dass ein eingeloggter Nutzer ohne zugewiesene Rolle keinerlei Daten sieht, damit ein bloßer Login nie ein Sicherheitsleck ist.

Akzeptanzkriterien

  • Gegeben ein Profil mit leerem user_roles, wenn der Nutzer sich anmeldet, dann zeigt die App „Konto wartet auf Freischaltung" und kein Datenzugriff ist möglich (RLS deny-by-default greift ohnehin).
  • Gegeben dasselbe Profil, wenn ein ADMIN die Nutzerliste öffnet, dann erscheint der Nutzer als „Pending" und ist zur Rollen-Zuweisung wählbar.
  • Gegeben ein Profil mit is_active=false, wenn der Nutzer sich anmeldet, dann wirft ein globaler Guard (Layout-Loader prüft is_active) ihn sofort raus — unabhängig von etwaigen Rollen.

Traceability: §4.1 (Edge-Cases 3–5), RLS deny-by-default (§13). (Negativseite zu sec-04: Login ≠ Zugriff.)


US-PLT-03 — Abmelden

Als angemeldeter Nutzer möchte ich mich aktiv abmelden, damit an einem geteilten/öffentlichen Gerät keine Sitzung offen bleibt.

Akzeptanzkriterien

  • Gegeben eine aktive Sitzung, wenn ich im Profil-Menü (Sidebar-Footer / Bottom-Tab „Konto") „Abmelden" wähle, dann wird die Supabase-Session via signOut gelöscht und ich werde auf die Login-Karte umgeleitet.
  • Gegeben ich bin abgemeldet, wenn ich eine geschützte Route direkt aufrufe, dann lande ich auf dem Login (kein Zugriff aus Rest-Cookies).

Traceability: §11.4 (Logout).


US-PLT-04 — Nutzer einladen (invite-only) & deaktivieren (Negativfall: keine offene Self-Registrierung)

Als ADMIN möchte ich neue Nutzer ausschließlich per Einladung anlegen und ausgeschiedene deaktivieren (nicht löschen), damit der Zugang kontrolliert bleibt und die Audit-Historie erhalten bleibt.

Akzeptanzkriterien

  • Gegeben „Team & Zugriff", wenn ich eine Person über ihre E-Mail einlade, dann wird ein Konto provisioniert und sie erhält einen Magic-Link — eine offene Selbst-Registrierung über den Login-Screen gibt es nicht (eingeladen oder kein nutzbarer Account).
  • Gegeben eine unbekannte E-Mail, die sich selbst einen Login-Link anfordert, wenn sie ihn einlöst, dann existiert zwar ein Profil, aber ohne Rollen-Zuweisung kein Datenzugriff (US-PLT-02) — die Freischaltung bleibt allein beim ADMIN.
  • Gegeben ein ausgeschiedener Mitarbeiter, wenn ich ihn deaktiviere, dann wird app_users.is_active=false gesetzt (kein Hard-Delete); seine Bewegungen/Audit-Einträge bleiben ihm zugeordnet.

Traceability: §4.2, §7 (Deaktivieren statt Löschen), app_users (§6.1). (Deckt Audit-Befund sec-04 „offene Self-Registrierung → invite-only" direkt ab.)


US-PLT-05 — Rollen zuweisen / entziehen

Als ADMIN möchte ich einem Nutzer Rollen zuweisen oder entziehen, damit Berechtigungen zentral und nachvollziehbar gesteuert sind.

Akzeptanzkriterien

  • Gegeben ein Nutzer, wenn ich eine Rolle hinzufüge/entferne, dann schreibt das System user_roles (mit granted_by=auth.uid(), granted_at) und erzeugt eine audit_log-Zeile (action='update', table_name='user_roles').
  • Gegeben die Rollen-Auswahl, wenn ich sie öffne, dann stehen genau die sechs kanonischen Codes (ADMIN/BUCHHALTUNG/OPERATIONS/FAHRER/AFFILIATE/READONLY) mit label_<locale> zur Wahl — keine Freitext-Rolle.
  • Gegeben ich bin der letzte aktive ADMIN, wenn ich mir die ADMIN-Rolle entziehen oder mich deaktivieren will, dann wird die Aktion abgelehnt (Guard „mindestens 1 aktiver ADMIN").
  • Gegeben user_roles schreiben/lesen, dann ist das nur ADMIN erlaubt; alle internen Rollen lesen die Zuordnung, AFFILIATE/extern nicht.

Traceability: §4.2, user_roles/roles (§6.1), RLS §13 (user_roles: ADM CRUD, intern R). Entscheid-abhängig (E1 Mehrfachrollen): Schema erlaubt mehrere Rollen je Nutzer (z. B. Markus OPERATIONS+BUCHHALTUNG); ob real genutzt, ist Geschäftsentscheid — die Zuweisungs-UI unterstützt beides.


US-PLT-06 — EIN Berechtigungs-Modell: has_role(), Rolle nie im Token-Metadata (Security-Invariante)

Als Plattform möchte ich, dass jede Berechtigungsprüfung über genau ein Modell läuft (user_roles + has_role()/is_internal()/current_affiliate_id()) und die Rolle niemals aus user_metadata/app_metadata des JWT stammt, damit Privilege Escalation strukturell unmöglich ist.

Akzeptanzkriterien

  • Gegeben irgendeine RLS-Policy in irgendeinem Modul, wenn sie eine Rolle prüft, dann tut sie das über has_role('CODE') / is_internal() / current_affiliate_id() (Quelle stets Tabelle user_roles) — nicht über einen JWT-Claim app_role/role.
  • Gegeben ein Angreifer, der user_metadata.role in seinem Profil manipuliert, wenn er eine geschützte Aktion versucht, dann ändert das nichts an seiner Autorisierung (der Wert wird nirgends ausgewertet).
  • Gegeben ein optionaler JWT-Rollen-Claim für Performance, dann wird er ausschließlich serverseitig über einen Auth-Hook aus user_roles befüllt (vom Client nicht setzbar) und bleibt mit has_role() konsistent.
  • Gegeben ein beliebiger Rollenwert, dann sind nur die sechs UPPERCASE-Codes gültig (FK user_roles.role_code → roles.code); Schreibweisen wie admin/office/ops/driver/manager existieren nicht.

Traceability: §3.5/§6.1 (RLS-Helfer), §13. (Deckt Audit-Headline X-1 / sec-01 + content-01 als verbindliches Szenario ab: EIN AuthZ-Modell, Rolle nie in user_metadata.)


US-PLT-07 — Affiliate-Login mit eigenem Affiliate-Konto verknüpfen (RLS-Anker)

Als ADMIN möchte ich einen AFFILIATE-Login mit genau einem Affiliate-Konto verbinden, damit die „nur Eigenes"-Isolation greift.

Akzeptanzkriterien

  • Gegeben ein Nutzer mit Rolle AFFILIATE, wenn ich ihn mit einem Affiliate-Konto (Modul 7) verknüpfe, dann entsteht eine affiliate_users-Zeile (1:1) und current_affiliate_id() liefert ab sofort dessen affiliate_id.
  • Gegeben ein bereits verknüpftes Login oder ein bereits belegtes Affiliate-Konto, wenn ich es erneut verknüpfen will, dann wird es abgelehnt (unique auf affiliate_users.user_id und .affiliate_id).
  • Gegeben ein eingeloggter AFFILIATE, wenn er Daten liest, dann sieht er ausschließlich Zeilen, deren Owner ihm über current_affiliate_id() gehört (z. B. eigene storage_objects/notifications), nie fremde Kunden-/Finanzdaten.

Traceability: affiliate_users (§6.1), current_affiliate_id() (§3.5), Affiliate-Isolation (§3.12), RLS §13.


US-PLT-08 — Eigenes Profil & Sprache pflegen (sec-03: nur erlaubte Self-Felder)

Als angemeldeter Nutzer möchte ich meinen Anzeigenamen, meine bevorzugte Sprache und meine Telefonnummer selbst pflegen, damit UI und Notifications zu mir passen — ohne sicherheitsrelevante Felder ändern zu können.

Akzeptanzkriterien

  • Gegeben meine Profil-Einstellungen, wenn ich display_name, preferred_locale (de/es/en), theme oder phone_e164 ändere, dann werden genau diese Felder meiner eigenen app_users-Zeile gespeichert (RU(self)).
  • Gegeben ein Self-Update-Versuch auf ein nicht erlaubtes Feld (email, is_active oder eine fremde Zeile), wenn er erfolgt, dann wird er von der RLS/Spalten-Policy abgewiesen (Self-Update ist auf die Whitelist beschränkt, nicht „ganze Zeile").
  • Gegeben ich ändere preferred_locale, dann wird der pref_lang-Cookie gespiegelt und die UI re-rendert sofort in der Zielsprache.

Traceability: §11.4 (eigene E-Mail nicht selbst änderbar), app_users (§6.1), RLS §13 (app_users: RU(self)). (Deckt Audit-Befund sec-03 ab: jede U(self)-Zelle = konkrete Spalten-Whitelist, kein Voll-Row-Update.)


US-PLT-09 — Sprache umschalten (DE/ES/EN)

Als beliebiger Nutzer (auch anonym auf dem Login) möchte ich die Anzeigesprache zwischen Deutsch, Spanisch und Englisch umschalten, damit das dreisprachige Team und externe Empfänger jeweils ihre Sprache sehen.

Akzeptanzkriterien

  • Gegeben ein Sprachwähler (Segmented-Control ES/DE/EN, auch in der ⌘K-Palette), wenn ich die Sprache wechsle, dann wird der pref_lang-Cookie gesetzt und — falls angemeldet — app_users.preferred_locale persistiert; die UI re-rendert.
  • Gegeben eine Route, wenn sie serverseitig rendert, dann lädt sie die benötigten locale_strings-Namespaces in der Zielsprache (kein Sprach-Flash); Fach-Labels kommen aus label_<locale> der Lookups.
  • Gegeben ein fehlender Übersetzungs-Key, wenn er aufgelöst wird, dann erscheint der key selbst (nie leer/Crash) plus ein Eintrag im „i18n-Lücken"-Report für ADMIN.
  • Gegeben kein Cookie und kein Profil, dann greift die Fallback-Kette Cookie → app_users.preferred_locale → Browser (ES-priorisiert) → 'es'.

Traceability: §4.3, locale_strings (§6.2), RLS §13 (locale_strings: alle R inkl. anon).


US-PLT-10 — Systemeinstellungen-Screen (ADMIN)

Als ADMIN möchte ich die globalen Konfig-Flags an einem Ort pflegen, damit verstreute Magic-Numbers durch eine wartbare Heimat ersetzt sind (Caja-Pendant zum IST-Settings.html).

Akzeptanzkriterien

  • Gegeben der Einstellungs-Screen, wenn ich ihn öffne, dann sehe ich gruppierte Karten: Finanzen (vat_enabled, vat_method, default_currency), Tarif (volume_tariff_unit), Anfangssalden (opening_balances_done), GS1/SSCC (gs1_sscc_enabled, gs1_company_prefix, gs1_extension_digit).
  • Gegeben ich ändere einen Wert, wenn ich speichere, dann wird app_settings aktualisiert (updated_by/updated_at) und eine audit_log-Zeile geschrieben; kritische Schalter (vat_enabled) verlangen eine Bestätigung (Bottom-Sheet-Confirm am Handy).
  • Gegeben eine interne Nicht-ADMIN-Rolle, wenn sie den Screen aufruft, dann sieht sie die Werte lesend, kann aber nichts speichern (ADMIN write, intern read).

Traceability: §10.2/§11.1, app_settings (§6.1), RLS §13 (app_settings: ADM write, intern read).


US-PLT-11 — Rollen- & Rechte-Matrix ansehen (read-only)

Als ADMIN/READONLY (Treuhänder) möchte ich die Rolle×Tabelle×CRUD-Matrix einsehen, damit „wer darf was" für Audit/Onboarding transparent ist — ohne sie hier editieren zu können.

Akzeptanzkriterien

  • Gegeben der Matrix-Screen, wenn ich ihn öffne, dann rendert er die in Dok 30 §13 normierte Matrix (Zeilen = Entitäten, Spalten = 6 Rollen, Zellen = C/R/U/D/R*-Badges), durchsuchbar/sticky.
  • Gegeben die Matrix, dann zeigt ein Hinweis-Banner „Rollenrechte sind als Policy/Code definiert; Änderungen via Migration (E2), nicht hier" — die Ansicht ist bewusst nicht editierend.
  • Gegeben ein Bedarf, ein Recht zu ändern, dann ist der einzige editierbare Pfad die Rollen-Zuweisung (US-PLT-05); die Rechte selbst erfordern eine Migration.

Traceability: §11.2, RLS-Matrix §13. (Architektur-Entscheid E2: keine pflegbare actions/permissions-Tabelle — Feingranularität lebt in RLS.)


US-PLT-12 — Globales Audit-Journal lesen (intern, read-only)

Als interne Rolle möchte ich das append-only Änderungs-Journal durchsuchen, damit jede schreibende Operation revisionssicher nachvollziehbar ist (OR 957/957a, GeBüV).

Akzeptanzkriterien

  • Gegeben das Audit-Journal, wenn ich es öffne, dann sehe ich je Eintrag Zeit, Akteur (actor_email, denormalisiert), Tabelle, Aktion und eine Diff-Vorschau; ein Detail-Drawer/Bottom-Sheet zeigt den alt→neu-JSON-Diff farbcodiert. Filter: Tabelle, Akteur, Zeitraum, Periode.
  • Gegeben ein deaktivierter/umbenannter Nutzer, wenn ich seine alten Einträge ansehe, dann bleibt der Akteur lesbar (weil actor_email mitgeschrieben wurde).
  • Gegeben das Journal, dann gibt es keine Edit-/Lösch-Controls; audit_log wird nur durch fn_audit() (Trigger, SECURITY DEFINER) befüllt und ist gegen UPDATE/DELETE per Rules + Grant-Entzug gesperrt — auch für ADMIN und service_role.
  • Gegeben ein AFFILIATE, wenn er das Journal aufruft, dann hat er keinen Zugriff; interne Rollen lesen.

Traceability: §3.8/§4.4, audit_log (§6.1), RLS §13 (audit_log: intern R, nie U/D). (Unveränderbarkeit als testbare Invariante.)


US-PLT-13 — Objekt-Verlauf je Entität („Historial de cambios")

Als interne Rolle möchte ich direkt am Kontakt/Auftrag/Rechnung/Box einen „Verlauf"-Tab sehen, damit ich die Änderungshistorie eines Objekts ohne Umweg über das globale Journal nachvollziehe.

Akzeptanzkriterien

  • Gegeben ein Detail-Screen mit „Verlauf"-Tab, wenn ich ihn öffne, dann liest die Komponente audit_log gefiltert auf (table_name, row_pk) der Entität und rendert eine chronologische Timeline (Akteur · Aktion · Zeit · Feld-Diff).
  • Gegeben dieselbe Komponente, dann ist sie als wiederverwendbares Pattern in Kontaktdetail (Modul 1), Auftrag (Modul 5), Rechnung (Modul 6) und Sendungs-/Box-Detail (Modul 5, neben der Tracking-Timeline) eingebunden.
  • Gegeben der Verlauf, dann ist er read-only (append-only Audit); ein Detail-Diff öffnet im Bottom-Sheet (Handy) / Drawer (Desktop).

Traceability: §11.3, audit_log (table_name, row_pk) (§6.1, Index idx_audit_table).


US-PLT-14 — Beleg/Dokument hochladen (signierte URL, KYC zugriffsbeschränkt)

Als OPERATIONS/BUCHHALTUNG möchte ich Belege und Ausweis-Scans sicher hochladen, damit sie der Quell-Entität zugeordnet, bucket-getrennt und revisionssicher abgelegt sind — ohne dass der Client je den Service-Key sieht.

Akzeptanzkriterien

  • Gegeben ein Upload (Kamera/Datei), wenn ich ihn auslöse, dann liefert eine Edge-Function eine signierte Upload-URL; nach dem Upload entsteht eine storage_objects-Zeile mit bucket, doc_class, owner_type/owner_id, uploaded_by und (bei KYC) is_sensitive=true.
  • Gegeben eine zu große oder falsch typisierte Datei, wenn ich sie hochlade, dann validiert die Edge-Function mime_type/byte_size/bucket/doc_class vor der Persistenz und lehnt sonst ab (kein verwaister Storage-Eintrag).
  • Gegeben ein KYC-Ausweis im kyc-Bucket, dann dürfen ihn nur ADMIN/OPERATIONS/BUCHHALTUNG lesen; ein FAHRER darf nur delivery_proof (eigene Zustellung) ablegen/sehen, ein AFFILIATE nur Objekte, deren Owner ihm gehört.

Traceability: §3.10/§4.5, storage_objects (§6.3, doc_class inkl. signature/customs_report/shipment_list), RLS §13 (storage_objects bucket-/zeilenweise).


US-PLT-15 — Signierte URL für Beleg-Ansicht (kein Public-Read)

Als berechtigte Rolle möchte ich einen abgelegten Beleg ansehen/herunterladen, damit ich PDF/Scan prüfen kann — ohne dass Dateien dauerhaft öffentlich erreichbar sind.

Akzeptanzkriterien

  • Gegeben ein storage_objects-Eintrag in einem privaten Bucket (kyc/documents/receipts), wenn ich ihn ansehe, dann erzeugt der Server eine kurzlebige signierte Lese-URL; ein direkter Bucket-Pfad ohne Signatur liefert nichts (kein Public-Read außer public-assets).
  • Gegeben ein KYC-Objekt, dann trägt es in der UI ein sichtbares „Sensibel"-Schild und die signierte URL wird nur für die berechtigten Rollen ausgestellt.
  • Gegeben Ablauf der KYC-Notwendigkeit (DSG), wenn der Storage-Lifecycle-Job läuft, dann entfernt er die Datei und markiert storage_objects als gelöscht — der Audit-Eintrag der Löschung bleibt.

Traceability: §3.10 (Buckets private), §4.5 (Löschpflicht), Compliance §8 (revDSG/DSG, KYC-Zweckbindung).


US-PLT-16 — Notification-Outbox: zuverlässig & idempotent versenden

Als System möchte ich jede ausgehende Nachricht (E-Mail/WhatsApp) als Outbox-Zeile anlegen und asynchron versenden, damit Versand idempotent, retry-fähig und protokolliert ist.

Akzeptanzkriterien

  • Gegeben ein operatives Event (z. B. Offerte versendet, Zahlung verbucht, Sendungs-Status), wenn enqueue_notification(...) aufgerufen wird, dann entsteht eine notifications-Zeile (status='queued', Empfänger-locale, template_code, dedupe_key, Owner-Bezug).
  • Gegeben ein Retry/Race mit demselben dedupe_key (z. B. payment_received:<movement_id>), wenn er erfolgt, dann verhindert die unique-Constraint einen Doppelversand (eine Zahlung = eine „Zahlung erhalten"-Mail).
  • Gegeben der Worker zieht eine Zeile, wenn der Versand klappt, dann status='sent' + provider_message_id; bei Fehler status='failed', error=…, attempts++ — der echte Fehlertext bleibt im Server-Log/error-Feld, nie roh in der Client-UI.
  • Gegeben fehlende Empfängeradresse (to_email/to_phone_e164), dann status='skipped' mit Begründung (kein Hard-Fail des auslösenden Workflows).

Traceability: §4.6, notifications (§6.3, dedupe_key unique), Templates §6.3.


US-PLT-17 — Benachrichtigungs-Protokoll & erneut senden (Mail/WhatsApp-Matrix)

Als interne Rolle möchte ich sehen, „was wurde wann an wen über welchen Kanal geschickt", und Fehlversände erneut anstoßen, damit Kommunikation nachvollziehbar und reparierbar ist.

Akzeptanzkriterien

  • Gegeben das Protokoll, wenn ich es öffne, dann sehe ich je Eintrag Zeit, Kanal (E-Mail/WhatsApp), Template, Empfänger, Status-Ampel und Provider-ID; Fehlgeschlagene stehen oben (am Handy status-gruppierte Karten).
  • Gegeben ein Eintrag status='failed', wenn ich „erneut senden" auslöse (Swipe am Handy), dann wird status='queued' gesetzt und der Worker versendet erneut (Idempotenz über dedupe_key gewahrt).
  • Gegeben die Template-Matrix, dann spiegelt das Protokoll die definierten Anlässe/Kanäle wider: QUOTE_SENT (E-Mail+WhatsApp, Kunde), PAYMENT_RECEIVED (E-Mail, Kunde), SHIPMENT_STATUS (E-Mail+WhatsApp, Kunde + Empfänger-DR), AFFILIATE_STATEMENT (E-Mail, Affiliate), AUTH_MAGIC_LINK (E-Mail).
  • Gegeben ein roher Provider-Fehlertext, dann erscheint er nur im Detail-Drawer für interne Rollen, nie im allgemeinen UI.

Traceability: §5.6/§6.3 (Template-Matrix), notifications (§6.3), RLS §13 (notifications: intern R, AFFILIATE nur an ihn adressierte). WhatsApp: heute Deep-Link an +41 79 199 93 93; Cloud-API später (dann füllt der Worker provider_message_id) — Entscheid offen (§9).


US-PLT-18 — Login & Export ins Audit schreiben (Origin-/CSRF-Härtung)

Als Plattform möchte ich sicherheitsrelevante Aktionen (Login, Datenexport) im Audit festhalten und mutierende Calls gegen CSRF/Fremd-Origin absichern, damit Zugriffe nachvollziehbar und Cookie-CSRF strukturell entschärft sind.

Akzeptanzkriterien

  • Gegeben ein erfolgreicher Login bzw. ein Datenexport, wenn er erfolgt, dann schreibt das System eine audit_log-Zeile (action='login' bzw. action='export', Akteur + Zeit, ggf. IP/User-Agent in context).
  • Gegeben ein mutierender Call, wenn er ausgeführt wird, dann läuft er über Next.js Server Actions / Route Handlers mit Supabase Bearer-Token (kein Ambient-Cookie-Auth); Session-Cookies sind Secure/HttpOnly/SameSite=lax.
  • Gegeben eine Server Action oder ein Webhook von fremdem Origin/Referer (bzw. ohne gültige HMAC-Signatur), wenn sie eintrifft, dann wird sie abgewiesen (Origin-/Referer-Validierung).

Traceability: §10.1 (CSRF/Origin-Schutz), audit_log.action in ('login','export') (§6.1). (Deckt Audit-Befund G8 / BR-S12 ab.)


US-PLT-19 — Technische Fehler zentral erfassen (Error-Logging ≠ Audit) (Betriebsanforderung)

Als ADMIN/Betrieb möchte ich technische Exceptions zentral erfasst haben — getrennt vom Änderungs-Journal — damit Störungen auffindbar sind, ohne die Revisionssicherheit des audit_log zu verwässern.

Akzeptanzkriterien

  • Gegeben eine unbehandelte Exception (Server/Edge/Client), wenn sie auftritt, dann wird sie über Vercel-Observability bzw. ein Error-Tracking (Sentry-Äquivalent) erfasst — nicht als audit_log-Zeile (das bleibt insert/update/delete/login/export/close/reopen vorbehalten).
  • Gegeben ein erfasster Fehler, dann enthält er die Diagnose-Felder (Subject/Message/URL/User/Kontext) als Ersatz für das IST-AdminErrors-Journal.
  • Gegeben PII-Risiko, dann werden personenbezogene Daten in Fehler-/Log-Payloads minimiert (kein Klartext-Ausweis/Cédula in Logs).

Traceability: §10.3 (Application-Error-Logging). (Deckt Audit-Befund G9 / BR-S07 ab; PII-Minimierung vgl. comp-11.)


US-PLT-20 — READONLY/Treuhänder: lesend & maskiert (RLS-Negativfall)

Als READONLY (Treuhänder/Gast) möchte ich über die operativen und finanziellen Dashboards lesen können, damit ich prüfen kann — ohne je etwas zu schreiben oder sensible Felder zu sehen.

Akzeptanzkriterien

  • Gegeben ich bin als READONLY eingeloggt, wenn ich Daten öffne, dann sehe ich Dashboards/Listen lesend (inkl. Audit-Journal und Rechte-Matrix), aber keine Schreib-/Edit-Controls.
  • Gegeben sensible Felder (Cédula/Ausweis-Scan), wenn ich Kontakte ansehe, dann sind sie maskiert/ausgeblendet (R*, kein cedula/scan).
  • Gegeben ein Schreibversuch über die API (Insert/Update/Delete auf einer Geschäftstabelle), wenn er erfolgt, dann wird er von der RLS abgewiesen (deny-by-default; READONLY hat nirgends C/U/D).

Traceability: RLS §13 (READONLY = R/R*), §3.12 (deny-by-default), Compliance §8.


US-PLT-21 — 2FA für Finanzrollen (entscheid-abhängig)

Als ADMIN/BUCHHALTUNG möchte ich optional eine zweite Authentifizierungs-Stufe (TOTP), damit Finanzdaten zusätzlich gegen Konto-Übernahme geschützt sind.

Akzeptanzkriterien

  • Gegeben der Entscheid „2FA für Finanzrollen aktiv" ist getroffen, wenn ein ADMIN/BUCHHALTUNG sich anmeldet, dann verlangt der Login nach Magic-Link/OTP zusätzlich einen TOTP-Faktor.
  • Gegeben der Entscheid ist nicht getroffen, dann bleibt es beim passwortlosen Magic-Link/OTP ohne zweite Stufe (kein Bruch der übrigen Stories).
  • Gegeben ein 2FA-Enrollment, dann ist es self-service im Profil mit Recovery-Pfad (kein dauerhafter Lockout).

Traceability: §9 (offener Punkt „Auth-Provider-Umfang"), Compliance §8.1. Entscheid-abhängig: 2FA (TOTP) für ADMIN/BUCHHALTUNG ist 🔲 zu bestätigen — Story nur bei „Ja" einzuplanen.


US-PLT-22 — Kundenportal vorbereiten (current_contact_id()) (Phase 2+, entscheid-abhängig)

Als Architekt möchte ich den RLS-Helfer current_contact_id() analog current_affiliate_id() von Anfang an mitdenken, damit ein späteres Kundenportal ohne RLS-Umbau aktivierbar ist.

Akzeptanzkriterien

  • Gegeben der Start-Scope, dann haben Endkunden kein Login (nur Team + Affiliates); die IST-Isolation „Kunde sieht nur Eigenes" ist vorerst gegenstandslos.
  • Gegeben Phase 2+ wird beschlossen, wenn ein Kundenportal aktiviert wird, dann existiert das Muster current_contact_id() bereits konzeptionell (analog Affiliate-Anker), sodass Kunden-Policies ohne Schema-Umbau ergänzbar sind.
  • Gegeben der heutige Stand, dann wird kein Kunden-Login ausgeliefert (bewusster Verzicht), die Vorbereitung bleibt dokumentiert.

Traceability: §10.5 (E8 Kundenportal). Entscheid-abhängig: Kundenportal ist Phase 2+ (🔲); diese Story ist vorbereitend, nicht im MVP-Liefergegenstand.


Schätzung & Priorität (Vorschlag — vom Team final zu bestätigen)

StoryGrößePriorität (MoSCoW)
US-PLT-01 Passwortlos anmeldenMMust
US-PLT-02 Konto wartet auf FreischaltungSMust
US-PLT-03 AbmeldenSMust
US-PLT-04 Nutzer einladen/deaktivieren (invite-only)MMust
US-PLT-05 Rollen zuweisen/entziehenMMust
US-PLT-06 EIN AuthZ-Modell (has_role)MMust
US-PLT-07 Affiliate-Login verknüpfenSMust
US-PLT-08 Eigenes Profil & SpracheSMust
US-PLT-09 Sprache umschalten DE/ES/ENMMust
US-PLT-10 Systemeinstellungen-ScreenMMust
US-PLT-11 Rollen-/Rechte-Matrix (read-only)SShould
US-PLT-12 Audit-Journal lesenMMust
US-PLT-13 Objekt-Verlauf je EntitätMShould
US-PLT-14 Beleg/Dokument hochladenMMust
US-PLT-15 Signierte URL für AnsichtSMust
US-PLT-16 Notification-Outbox (idempotent)MMust
US-PLT-17 Benachrichtigungs-Protokoll & ResendMShould
US-PLT-18 Login/Export-Audit + CSRF/OriginMMust
US-PLT-19 Error-Logging (≠ Audit)SShould
US-PLT-20 READONLY lesend & maskiertSMust
US-PLT-21 2FA FinanzrollenMCould (Entscheid)
US-PLT-22 Kundenportal vorbereitenSCould (Phase 2+)

Größe = grobe Aufwands-Indikation (S/M/L), nicht Story Points. Priorität nach MoSCoW (Must/Should/Could). Beides ist ein Vorschlag zur Release-Planung — die verbindliche Schätzung/Priorisierung macht das Team im Backlog.


Abdeckung & offene Punkte (dieses Epic)

  • 22 Stories decken das Fundament-Modul end-to-end ab: Auth (passwortlos Magic-Link/OTP, Login/Logout, Pending-/Deaktiviert-Guards), Nutzer-/Rollenverwaltung (invite-only anlegen, deaktivieren, Rollen zuweisen, Affiliate-Verknüpfung), Rechte-Transparenz (read-only Matrix), Systemeinstellungen (app_settings), Audit (globales Journal + Objekt-Verlauf, Login/Export), Storage/Belege (signierte Up-/Download-URLs, KYC-Schutz, Lifecycle-Löschung), Notifications (Outbox + Protokoll + Mail/WhatsApp-Matrix), i18n (DE/ES/EN Umschaltung + Fallback) sowie der READONLY-Negativfall.
  • Audit-Verzahnung (zentral): US-PLT-06 (EIN AuthZ-Modell has_role(), Rolle nie in user_metadata — X-1/sec-01), US-PLT-04 (invite-only, keine offene Self-Registrierung — sec-04), US-PLT-08 (Self-/Spalten-Update nur erlaubte Felder — sec-03), US-PLT-18 (CSRF/Origin + Login/Export-Audit — G8), US-PLT-19 (App-Error-Logging getrennt vom Audit — G9), US-PLT-12 (Audit-Unveränderbarkeit — K-15). Die Akzeptanzkriterien encodieren diese Härtungen als testbare Szenarien.
  • Entscheid-abhängig: US-PLT-05 (E1 Mehrfachrollen), US-PLT-21 (2FA TOTP für Finanzrollen), US-PLT-22 (E8 Kundenportal Phase 2+, current_contact_id()); zusätzlich offen: WhatsApp Deep-Link vs. Cloud-API (US-PLT-17), Label-Strategie/Reopen-Kompetenz/KYC-Frist (§9).
  • Nächster Schritt: Format-/Inhalts-Freigabe → INVEST-Red-Team-Pass; danach ist die Story-Reihe über alle acht Epics komplett (Logistik-Pilot + CRM, WhatsApp/OCR, Produkte/Preise, Offerten, Finanzen, Affiliate, Plattform).