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
| Rolle | Person | Bezug zur Plattform |
|---|---|---|
ADMIN | Marcel | Nutzer-/Rollenverwaltung, Systemeinstellungen, Periodensperre, Audit, alles |
BUCHHALTUNG | Mariela | interne Nutzerin: Login, Self-Profil, Audit lesen, Belege/Notifications |
OPERATIONS | Markus | interner Nutzer: Login, Self-Profil, KYC-/Beleg-Upload, Audit lesen |
FAHRER | Arkys | interner Nutzer (minimal): Login, Self-Profil, Liefernachweis-Upload |
AFFILIATE | extern | Login, Self-Profil, nur Eigenes (Doku/Notifications zu eigenen Sendungen) |
READONLY | Treuhänder/Gast | Login, 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).
US-PLT-01 — Passwortlos anmelden (Magic-Link / OTP)
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 Triggerhandle_new_user()legt meinapp_users-Profil an (Defaultpreferred_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
ADMINdie 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üftis_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
signOutgelö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=falsegesetzt (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(mitgranted_by=auth.uid(),granted_at) und erzeugt eineaudit_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) mitlabel_<locale>zur Wahl — keine Freitext-Rolle. - Gegeben ich bin der letzte aktive
ADMIN, wenn ich mir dieADMIN-Rolle entziehen oder mich deaktivieren will, dann wird die Aktion abgelehnt (Guard „mindestens 1 aktiver ADMIN"). - Gegeben
user_rolesschreiben/lesen, dann ist das nurADMINerlaubt; 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 Tabelleuser_roles) — nicht über einen JWT-Claimapp_role/role. - Gegeben ein Angreifer, der
user_metadata.rolein 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_rolesbefüllt (vom Client nicht setzbar) und bleibt mithas_role()konsistent. - Gegeben ein beliebiger Rollenwert, dann sind nur die sechs UPPERCASE-Codes gültig (FK
user_roles.role_code → roles.code); Schreibweisen wieadmin/office/ops/driver/managerexistieren 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 eineaffiliate_users-Zeile (1:1) undcurrent_affiliate_id()liefert ab sofort dessenaffiliate_id. - Gegeben ein bereits verknüpftes Login oder ein bereits belegtes Affiliate-Konto, wenn ich es erneut verknüpfen will, dann wird es abgelehnt (
uniqueaufaffiliate_users.user_idund.affiliate_id). - Gegeben ein eingeloggter
AFFILIATE, wenn er Daten liest, dann sieht er ausschließlich Zeilen, deren Owner ihm übercurrent_affiliate_id()gehört (z. B. eigenestorage_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),themeoderphone_e164ändere, dann werden genau diese Felder meiner eigenenapp_users-Zeile gespeichert (RU(self)). - Gegeben ein Self-Update-Versuch auf ein nicht erlaubtes Feld (
email,is_activeoder 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 derpref_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_localepersistiert; 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 auslabel_<locale>der Lookups. - Gegeben ein fehlender Übersetzungs-Key, wenn er aufgelöst wird, dann erscheint der
keyselbst (nie leer/Crash) plus ein Eintrag im „i18n-Lücken"-Report fürADMIN. - 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_settingsaktualisiert (updated_by/updated_at) und eineaudit_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 (
ADMINwrite, 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 denalt→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_emailmitgeschrieben wurde). - Gegeben das Journal, dann gibt es keine Edit-/Lösch-Controls;
audit_logwird nur durchfn_audit()(Trigger, SECURITY DEFINER) befüllt und ist gegen UPDATE/DELETE per Rules + Grant-Entzug gesperrt — auch fürADMINundservice_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_loggefiltert 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 mitbucket,doc_class,owner_type/owner_id,uploaded_byund (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_classvor der Persistenz und lehnt sonst ab (kein verwaister Storage-Eintrag). - Gegeben ein KYC-Ausweis im
kyc-Bucket, dann dürfen ihn nurADMIN/OPERATIONS/BUCHHALTUNGlesen; einFAHRERdarf nurdelivery_proof(eigene Zustellung) ablegen/sehen, einAFFILIATEnur 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ßerpublic-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_objectsals 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 einenotifications-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 dieunique-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 Fehlerstatus='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), dannstatus='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 wirdstatus='queued'gesetzt und der Worker versendet erneut (Idempotenz überdedupe_keygewahrt). - 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 incontext). - 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
READONLYeingeloggt, 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*, keincedula/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;
READONLYhat nirgendsC/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/BUCHHALTUNGsich 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)
| Story | Größe | Priorität (MoSCoW) |
|---|---|---|
| US-PLT-01 Passwortlos anmelden | M | Must |
| US-PLT-02 Konto wartet auf Freischaltung | S | Must |
| US-PLT-03 Abmelden | S | Must |
| US-PLT-04 Nutzer einladen/deaktivieren (invite-only) | M | Must |
| US-PLT-05 Rollen zuweisen/entziehen | M | Must |
US-PLT-06 EIN AuthZ-Modell (has_role) | M | Must |
| US-PLT-07 Affiliate-Login verknüpfen | S | Must |
| US-PLT-08 Eigenes Profil & Sprache | S | Must |
| US-PLT-09 Sprache umschalten DE/ES/EN | M | Must |
| US-PLT-10 Systemeinstellungen-Screen | M | Must |
| US-PLT-11 Rollen-/Rechte-Matrix (read-only) | S | Should |
| US-PLT-12 Audit-Journal lesen | M | Must |
| US-PLT-13 Objekt-Verlauf je Entität | M | Should |
| US-PLT-14 Beleg/Dokument hochladen | M | Must |
| US-PLT-15 Signierte URL für Ansicht | S | Must |
| US-PLT-16 Notification-Outbox (idempotent) | M | Must |
| US-PLT-17 Benachrichtigungs-Protokoll & Resend | M | Should |
| US-PLT-18 Login/Export-Audit + CSRF/Origin | M | Must |
| US-PLT-19 Error-Logging (≠ Audit) | S | Should |
| US-PLT-20 READONLY lesend & maskiert | S | Must |
| US-PLT-21 2FA Finanzrollen | M | Could (Entscheid) |
| US-PLT-22 Kundenportal vorbereiten | S | Could (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 inuser_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).