Zum Hauptinhalt springen

Kunden-Self-Service-Portal (persistentes Kunden-Login) — Design (MED-D-279, MED-OP-PORTAL-1)

Kurz (Kernaussage zuerst): Ein persistentes, authentifiziertes Kundenportal, in dem die Kontaktperson eines Mandanten sich jederzeit einloggt, ihren eigenen Stand sieht („was ist offen / eingereicht / freigegeben?") und proaktiv Info-Aktualisierungen einreicht. Die Einreichung ist „submitted", nicht der tatsächliche Datenstand — sie wird zum Vorschlag und erst nach feldweiser Freigabe durch einen Medidentas-Berater (4-Augen-Prinzip) in die Stammdaten übernommen (versioniert, Quelle self_service). Das ist kein neuer Datenpfad: die komplette submitted-vs-actual-/4-Augen-/Übernahme- Maschinerie existiert bereits (domain/feld-sichtung.ts, …/uebernahme-vorschlag…/uebernehmen, VersionedField). Neu ist allein eine persistente Identitäts- und Übersichts-Schicht darüber. Auth-Default: passwortlos / Magic-Link (+ optional Passkey) — kein Passwort-Hash at rest (G-5/G-6). Golden Rule & DSGVO: eine neue extern-authentifizierte Fläche → dokumentierte, begründete Zero-Trust-Ausnahme + DSGVO/AVV-Freigabe vor Go-Live (MED-KB-13, RISK-33).

True North: beantwortet #1 „Wo steht der Kunde?" (Kunde sieht/pflegt seinen Stand selbst, entlastet das Backoffice) und #3 „vollständig & rechtssicher dokumentiert?" (jede Einreichung/Freigabe append-only auditiert, 4-Augen erzwungen). Zielgruppen dieses Docs: IT-Dev/Architektur (Reuse-Map, Datenmodell, Slices) · CISO/Security (Auth, Zero-Trust, Angriffsfläche) · Business/Management (Nutzen, Compliance, Fahrplan).


1. Warum (Nutzen) & Abgrenzung

FrageHeute (einladungsbasiert)Mit Portal (persistent)
Kunde aktualisiert Infosnur wenn der Berater einen Token-Link schickt (Einmal-Strecke)jederzeit selbst, ohne Anstoß durch den Berater
Kunde sieht seinen Standnein (nur das eine Formular hinter dem Link)Dashboard: offen / eingereicht / freigegeben
submitted ≠ tatsächlich✅ bereits so (Vorschlag → Übernahme)unverändert (gleiche Maschine)
4-Augen-Freigabe✅ bereits so (feld-sichtung)unverändert (gleiche Queue)
Identitätanonymer Token je Streckepersistentes Konto (E-Mail, an Mandant gebunden)

Abgrenzung: Das Portal ersetzt nicht die vier öffentlichen Kundenstrecken (Onboarding/Formular/ Fragebogen/Lead) — diese bleiben für den Erstkontakt/nicht-eingeloggten Fall. Das Portal ist die wiederkehrende, eingeloggte Sicht für bestehende Mandanten.


2. Reuse-Map (G-1 integrate-before-build · G-2 eine Wahrheit)

Der Kern ist schon gebaut — das Portal ist eine dünne Schicht darüber. Nichts davon wird nachgebaut:

BausteinVorhanden alsPortal nutzt es für
submitted ≠ actualEinreichung → …/uebernahme-vorschlag (Vorschlag) ≠ StammdatenKunden-Einreichung landet als Vorschlag
4-Augen feldweisedomain/feld-sichtung.ts (akzeptiert/abgelehnt + Grund), …/feld-sichten, …/sichtung-abschliessenBerater-Freigabe je Feld
Korrektur-Schleifeabgelehnte Felder wieder geöffnet, akzeptierte gesperrt (MED-D-89)Kunde bessert nur Abgelehntes nach
Versionierte Übernahme + HerkunftVersionedField, StammdatenHerkunft.quelle (StammdatenQuelle-Enum), append-only Audit (G-4)Übernahme mit bestehender Quelle self_service (kein neuer Enum-Wert; das Portal ist derselbe „Kunde über den Aktualisierungs-Link"-Pfad)
Unguessbare Tokens + AblaufAusfuellformular.token/ablaufAm, Onboarding-EinladungMagic-Link = Token mit TTL (dieselbe Machart)
Bot-Schutzauth/turnstile.ts (Cloudflare Turnstile)Login-/Anforderungs-Formular gegen Bots
Scope-Guard „nur eigenes"ladeDokScope (fremder Mandant → 404, kein Existenz-Leak)Kunde sieht/ändert nur den eigenen Mandanten
Mandant-KontaktMandant.email (R1-F12), KontaktpersonenKonto-Bindung (E-Mail = Identität)

Bewusst NEU (die eigentliche Arbeit): persistente Kunden-Identität + Session + Dashboard + proaktiver Einreichungs-Einstieg. Alles andere = Verdrahtung an Bestehendes.


3. Datenmodell (neu, minimal — G-6 Datensparsamkeit)

  • portal_konto — persistente Kunden-Identität. Felder: id · mandant_id (FK, Scope) · email (Identität; kein Passwort-Hash) · status (eingeladen/aktiv/gesperrt) · erstellt_am · letzter_login_am. Kein zusätzliches PII über die ohnehin vorhandene Kontakt-E-Mail hinaus (G-6).
  • portal_anmelde_token — Magic-Link: token_hash (Digest/HMAC des Tokens, nie der Rohwert — der Klartext-Token steht nur im Link, at rest liegt nur der Hash; ein DB-Leak ist damit nicht replaybar) · konto_id · ablauf_am (kurze TTL, z. B. 15 min) · verbraucht_am (Einmalgebrauch). Analog Ausfuellformular.token in der Ausgabe, aber gehasht gespeichert (Bearer-Credential).
  • portal_session — nach erfolgreichem Magic-Link: session_id_hash (Digest des Session-Cookies, nicht der Rohwert) · konto_id · erstellt_am · letzte_aktivitaet_am · zwei getrennte Ablauf- Grenzen: Idle-TTL (relativ zu letzte_aktivitaet_am, z. B. 30 min) und Absolut-TTL (relativ zu erstellt_am, z. B. 12 h) — beide werden bei jeder Anfrage geprüft · widerrufen_am (Logout/Sperre). Server-seitig, kein JWT-at-rest nötig.
  • Optional Passkey (WebAuthn) — späterer Slice: portal_passkey (credential_id, public_key) für wiederkehrende Logins ohne Mail-Runde. Kein Secret at rest (nur Public Key).

Derived, nie gespeichert (G-2): „was ist offen/eingereicht/freigegeben" wird abgeleitet aus den bestehenden Dokument-/Einreichungs-/Sichtungs-Zuständen — kein neuer Statusspeicher.


OptionPortal-ErlebnisSicherheit / ComplianceEmpfehlung
Passwortlos / Magic-LinkKonto, „jederzeit per Mail-Link rein" (+ optional Passkey)kein Passwort-Hash at rest, kleine Angriffsfläche, DSGVO-arm, nutzt bestehende Token-InfraDefault
Passwort-Loginklassisch vertrautCredential-at-rest (Hash), Reset-Flows, Breach-/Reuse-Risiko, mehr Härtungbegründungspflichtig
OIDC (Kunde bringt Google/MS)kein Medidentas-Secretexterne IdP-Abhängigkeit, für KMU-Kontakte oft unpassendNische

Härtung (verbindlich, alle Optionen): Rate-Limiting je IP+Konto, keine Konto-Enumeration (immer „falls ein Konto existiert, wurde eine Mail gesendet"), Turnstile am Login, Magic-Link Einmalgebrauch + kurze TTL, Session-Cookie HttpOnly/Secure/SameSite, absolute + Idle-Expiry, Logout=Session-Widerruf. Mail-Versand hängt am noch offenen realen Mail-Seam OP-DM-8 (heute Fake) — bis dahin ist das Portal dormant/flag-gated.


5. RBAC & Isolation (strikt getrennt von Staff)

  • Zwei getrennte Prinzipale: Staff = Akteur über Cloudflare Access (unverändert, Staff-only) · Kunde = portal_konto über eigene Session-Cookies (getrennter Realm, andere Middleware).
  • Kunde sieht ausschließlich den eigenen Mandanten — jeder Portal-Endpunkt trägt den ladeDokScope- artigen Guard (fremder Mandant → 404, kein Existenz-Leak). Ein Portal-Konto kann nie eine Staff-Route erreichen und umgekehrt.
  • Vorschlagend, nicht ausführend: der Kunde kann nur einreichen (Vorschlag). Die Übernahme in die Stammdaten bleibt ausschließlich beim Berater (4-Augen) — der Kunde hat keinen Schreibzugriff auf den tatsächlichen Datenstand.

6. Compliance — proaktiv geflaggt (verbindlich, DE/AT/CH)

  • Golden Rule „Zero Trust vor Public" (RISK-33): Das Portal ist eine neue öffentlich-authentifizierte Fläche. Es wird als bewusste, begründete Ausnahme dokumentiert (wie die vier Kundenstrecken) — mit Rate-Limiting, No-Enumeration, Turnstile, Token-TTL, Session-Härtung. Nicht offen als Default; Go-Live erst nach Sign-off.
  • DSGVO/revDSG (MED-KB-13): persistente Kundenkonten = neue Verarbeitung → Rechtsgrundlage (Art. 6(1)(b) Vertragserfüllung), Betroffenenrechte (Auskunft/Berichtigung/Löschung/Übertragbarkeit), AVV mit Mail-/Auth-Provider. EU-/CH-Residenz ist vor Go-Live nachzuweisen (noch keine belegte Kontrolle): server/wrangler.toml trägt nur die D1-ID, --location weur steht bisher nur in Kommentar/ Runbook — Worker-Placement, D1-Region, Cloudflare-Account-Einstellungen und die realen Mail-/Auth-Provider- Bedingungen sind als Deployment-Evidenz zu belegen (S5-Gate). Medidentas = Verantwortlicher.
  • G-5/G-6 Datensparsamkeit: Konto = E-Mail + Mandant-Link, kein zusätzliches PII; Magic-Link vermeidet Secret-at-rest; Login-/Session-Events PII-arm auditiert (nur IDs/Enums).
  • G-4 Audit: Portal-Login, Einreichung, Berater-Freigabe/Ablehnung append-only auditiert (Actor = Portal-Konto bzw. Berater).
  • Aufbewahrung: Konto-/Session-Daten mit definierter Retention; Session-Widerruf bei Mandant-Archivierung.

7. Slice-Plan (MED-OP-PORTAL-1)

SliceInhaltRisiko / Gate
S0 — Design (dieser PR)dieses Doc + Decision-Log (MED-D-279) + Risikoregister (RISK-33) + Kunden-Besprechungspunkt (MED-KB-13) + Lastenheft/Feature-Liste-Stubkeins (Doku)
S1 — FundamentDatenmodell portal_konto/portal_anmelde_token/portal_session (Token/Session-ID nur als Hash at rest; Session mit Idle- + Absolut-TTL) + Migration + Repo + Domänen-Kern (Token-Ausgabe/-Prüfung + atomarer Claim wie MED-OP-TIME-5, Session — reine Funktionen) + Vitest. Dormant, keine public Route.niedrig (nichts exponiert)
S2 — Auth ✅ gebaut (0.125.0, MED-D-283)Passwortlose Magic-Link-Routen (No-Enumeration, atomarer Einmal-Claim, HttpOnly/Secure/SameSite-Cookie mit Idle+Absolut-TTL) + Staff-Route „Portal-Login anlegen". Endpunkte: POST /oeffentlich/portal/anmelden · GET /oeffentlich/portal/login?token= · GET /oeffentlich/portal/ich · POST /oeffentlich/portal/abmelden · POST /api/mandanten/:id/portal-konto (Staff). Flag-gated: dormant, bis PORTAL_AKTIV=true; PORTAL_TEST_MODE=true gibt den Magic-Link in der Antwort zurück (Test/Demo, solange kein echter Mailversand OP-DM-8). Auth-Mechanismus per Nutzer bestätigt (MED-KB-13 (a): passwortlos, zum Testen). +9 Vitest (inkl. CodeRabbit-#320-Härtung: Login-Atomarität, Sperr-Check, istEmail).Gate erst bei S5-Aktivierung: DSGVO/AVV + Rate-Limiting/Turnstile (RISK-33) + realer Mailversand (OP-DM-8)
S3 — DashboardRead-only „offen/eingereicht/freigegeben" je eigenem Mandanten (reuse bestehender abgeleiteter Endpunkte)niedrig
S4 — Proaktive EinreichungKunde öffnet „Stammdaten aktualisieren" → Vorschlag über bestehende feld-sichtung/uebernahme (Quelle self_service, bestehender Enum-Wert)mittel (schreibt Vorschlag, nicht Stammdaten)
S5 — Go-LiveDSGVO/AVV-Freigabe, Zero-Trust-Ausnahme dokumentiert, Rate-Limits verifiziert, Mail live → aktivieren; optional PasskeyGate: Kundenfreigabe

Reihenfolge-Logik: S1 ist komplett ungefährlich (nur Datenmodell + pure Funktionen, dormant) → sofort baubar. Die öffentlich exponierten Slices (S2/S5) hängen an MED-KB-13 (Auth-Mechanismus + DSGVO/AVV- Freigabe durch den Kunden) und am realen Mail-Versand (OP-DM-8).


8. Offene Entscheidungen

  • MED-KB-13 (Kundenfreigabe, vor S2/S5): (a) Auth-Mechanismus final (Default passwortlos/Magic-Link; Passwort nur auf ausdrücklichen Wunsch), (b) DSGVO-Rechtsgrundlage + AVV mit Mail-/Auth-Provider, (c) mehrere Kontaktpersonen je Mandant mit eigenem Login? (d) welche Stammdatenfelder darf der Kunde proaktiv aktualisieren?
  • OP-DM-8 (Mail-Versand): realer E-Mail-Seam — Voraussetzung für Magic-Link live.
  • Passkey (WebAuthn): optionaler Zusatz-Faktor für wiederkehrende Logins — nach S3.

Nächster Schritt: nach S0-Merge S1 (Fundament, dormant) bauen — risikofrei, ohne öffentliche Fläche.