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
| Frage | Heute (einladungsbasiert) | Mit Portal (persistent) |
|---|---|---|
| Kunde aktualisiert Infos | nur wenn der Berater einen Token-Link schickt (Einmal-Strecke) | jederzeit selbst, ohne Anstoß durch den Berater |
| Kunde sieht seinen Stand | nein (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ät | anonymer Token je Strecke | persistentes 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:
| Baustein | Vorhanden als | Portal nutzt es für |
|---|---|---|
| submitted ≠ actual | Einreichung → …/uebernahme-vorschlag (Vorschlag) ≠ Stammdaten | Kunden-Einreichung landet als Vorschlag |
| 4-Augen feldweise | domain/feld-sichtung.ts (akzeptiert/abgelehnt + Grund), …/feld-sichten, …/sichtung-abschliessen | Berater-Freigabe je Feld |
| Korrektur-Schleife | abgelehnte Felder wieder geöffnet, akzeptierte gesperrt (MED-D-89) | Kunde bessert nur Abgelehntes nach |
| Versionierte Übernahme + Herkunft | VersionedField, 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 + Ablauf | Ausfuellformular.token/ablaufAm, Onboarding-Einladung | Magic-Link = Token mit TTL (dieselbe Machart) |
| Bot-Schutz | auth/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-Kontakt | Mandant.email (R1-F12), Kontaktpersonen | Konto-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). AnalogAusfuellformular.tokenin 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 zuletzte_aktivitaet_am, z. B. 30 min) und Absolut-TTL (relativ zuerstellt_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.
4. Auth — passwortlos / Magic-Link (Empfehlung) vs. Alternativen
| Option | Portal-Erlebnis | Sicherheit / Compliance | Empfehlung |
|---|---|---|---|
| Passwortlos / Magic-Link | Konto, „jederzeit per Mail-Link rein" (+ optional Passkey) | kein Passwort-Hash at rest, kleine Angriffsfläche, DSGVO-arm, nutzt bestehende Token-Infra | ✅ Default |
| Passwort-Login | klassisch vertraut | Credential-at-rest (Hash), Reset-Flows, Breach-/Reuse-Risiko, mehr Härtung | begründungspflichtig |
| OIDC (Kunde bringt Google/MS) | kein Medidentas-Secret | externe IdP-Abhängigkeit, für KMU-Kontakte oft unpassend | Nische |
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.
Versand-Nachweis im Audit (MED-D-678): Jede Anforderung schreibt portal.anmeldung_angefordert mit zustellung (ok/fehler/kein_kanal) und kanal (Adapter-Name). Der Kanal ist nötig, weil der Log-Kanal nie wirft und damit ebenfalls ok erzeugt — erst kanal: "cloudflare-email" statt "log" belegt einen echten Versand (OP-DM-8-Gate). PII-arm: nur Status-Enum und Adapter-Name, kein Empfänger, kein Link-Inhalt. Gilt gleichermaßen für den Staff-Magic-Link (staff.anmeldung_angefordert). Ausgeweitet (MED-D-730) auf alle Mail-Sendepfade: kanal (Adapter-Name, PII-arm) steht jetzt auch in formular.erinnert, unterlagen_erinnerung.gesendet, dexman.monatsmeldung.abgeschlossen und wiedervorlage.zugewiesen (dort nur, wenn eine Mail versucht wird, zusätzlich mit zustellung); formular.gesichtet (feedbackKanal), fragebogen.entwurf_abgelaufen und wiedervorlage.erinnert hatten ihn schon; fragebogen.entwurf_abgelaufen trägt zusätzlich zustellung, ein werfender Adapter bricht den Cron-Lauf nicht mehr ab.
Erneut senden (MED-D-674): Die "Postfach prüfen"-Seite bietet "E-Mail erneut senden" — dieselbe
Anfrage an POST /oeffentlich/portal/anmelden (kein zweiter Endpunkt, G-1/G-2), damit eine verlorene oder
im Spam gelandete Mail keine Sackgasse ist. Bestätigung und Fehlertexte bleiben enumeration-frei
("falls ein Konto existiert …"); serverseitig gilt unverändert 5 Anfragen je Adresse / 10 min (plus
20 je IP), die Oberfläche legt zusätzlich 60 s Wartezeit zwischen zwei Klicks, damit niemand dieses
Kontingent versehentlich aufbraucht. Das Turnstile-Widget wird dafür explizit gerendert
(rendereTurnstile), weil Cloudflares Auto-Rendering nur beim Script-Load greift und ein nach einem
Ansichtswechsel eingefügter Container sonst leer bliebe.
Mail-Versand läuft über den Cloudflare Email Service (OP-DM-8 gebaut, MED-D-314 — plattform-natives
send_email-Binding, löst den MailChannels-Adapter ab); ohne das EMAIL-Binding fällt er deploy-sicher auf
den Log-Kanal zurück (keine Mail). Das Portal ist seit
MED-D-310 scharfgeschaltet (PORTAL_AKTIV="true"); öffentlich erreichbar wird /portal erst mit dem
Cloudflare-Access-Bypass (Deploy.md §7e).
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.tomlträgt nur die D1-ID,--location weursteht 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)
| Slice | Inhalt | Risiko / Gate |
|---|---|---|
| S0 — Design (dieser PR) | dieses Doc + Decision-Log (MED-D-279) + Risikoregister (RISK-33) + Kunden-Besprechungspunkt (MED-KB-13) + Lastenheft/Feature-Liste-Stub | keins (Doku) |
| S1 — Fundament | Datenmodell 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 — Dashboard ✅ gebaut (0.132.0, MED-D-304) | Read-only „offen/eingereicht/freigegeben" je eigenem Mandanten (rein abgeleitet G-2): PortalService.dashboard + GET /oeffentlich/portal/dashboard, flag-gated (dormant → 404, 401 ohne Session). | niedrig (nichts geschrieben) |
S4 — Proaktive Einreichung ✅ gebaut (0.132.0, MED-D-304) | Kunde schlägt Stammdaten-Updates vor (POST /oeffentlich/portal/stammdaten-vorschlag, whitelisted G-6) → kein Stammdatum geschrieben: Einreichung als self_service-FormularVorgang (status eingereicht) durch dieselbe 4-Augen-feld-sichtung/uebernahme (typ-bewusstes Regelset, Quelle self_service); Berater-Sicht GET /api/mandanten/:id/selbstauskuenfte + Sektion in mandant-formulare. | mittel (schreibt Vorschlag, nicht Stammdaten) |
S5-UI — Client-UI ✅ gebaut (dormant) (0.135.0, MED-D-308) | Die Kundenoberfläche zu S1–S4: öffentliche Route /portal (portal-public.component.ts) — passwortloses Login (Magic-Link) + read-only Dashboard (S3) + proaktive Stammdaten-Einreichung (S4). Dormant hinter PORTAL_AKTIV (alle Endpunkte → 404 → „nicht verfügbar"). Reine Client-Anbindung an die bestehenden Endpunkte. | niedrig (nichts exponiert, dormant) |
S5 — Go-Live 🟡→✅ Code scharf (0.137.0/0.138.0, MED-D-310/311) | MED-KB-13 (DSGVO/AVV) erteilt → PORTAL_AKTIV="true"; echter Mailversand gebaut (OP-DM-8, Cloudflare Email Service, MED-D-314 — dormant ohne EMAIL-Binding); Magic-Link-Bug behoben (Link → Client-Route /portal?token= statt API-JSON); Staff-„Portal-Zugang einrichten" im Kunden-Detail. Login-Härtung ✅ gebaut (MED-D-311, MED-OP-PORTAL-3): Turnstile + Rate-Limiting am anmelden. Deploy-Restschritte (Deploy.md §7e): Absender-Domain im Cloudflare Email Service onboarden · TURNSTILE_SITEKEY/TURNSTILE_SECRET · EU-/CH-Residenz-/Provider-Nachweis (AVV, §6) · Cloudflare-Access-Bypass für /portal. optional Passkey später. | Gate Kundenfreigabe ✅, Login-Härtung ✅; verbleibend: Infra (Secrets/DNS/Access-Bypass) + Residenz-/AVV-Nachweis (§6) |
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) ✅ erteilt (2026-09-23, MED-D-310): (a) Auth-Mechanismus passwortlos/ Magic-Link ✅, (b) DSGVO-Rechtsgrundlage ✅ + AVV/EU-CH-Residenz-Nachweis mit dem Mail-Provider den Cloudflare Email Service bleibt vor dem Access-Bypass zu belegen (§6), (c) mehrere Kontaktpersonen je Mandant → MED-OP-PORTAL-2 Teil 2 ✅ gebaut (MED-D-320) — mehrere Kontakt-Logins je Mandant, Staff-verwaltet, (d) proaktiv aktualisierbare Felder ✅ (S4-Whitelist).
- MED-OP-PORTAL-2 (Portal löst die Einmal-Links vollständig ab): Teil 1 (Dokument-Upload) ✅ gebaut
(MED-D-316) — der eingeloggte Kunde lädt seine angeforderten Dokumente direkt im Portal hoch
(
POST /oeffentlich/portal/dokumente/:docId/upload, Scope-Guard eigener Mandant,pruefeUpload+ bestehendeDokumentService.hochladen). Teil 2 ✅ gebaut (MED-D-320): mehrere Kontakt-Logins je Mandant — Staff legt je Mandant mehrere Portal-Kontakte an (anlegen/auflisten/sperren/reaktivieren,mandant-einladungen), jeder mit eigenem Magic-Link-Login, alle auf denselben Mandanten scoped; Sperre beendet laufende Sessions; Staff-gated (Zero-Trust), Autorisierung je Kontakt beim Staff (RISK-36). MED-OP-PORTAL-2 damit abgeschlossen. - OP-DM-8 (Mail-Versand) ✅ gebaut (MED-D-314): Cloudflare Email Service (
send_email-Binding); live braucht er nur noch das Onboarding der Absender-Domain im Email Service. - Dexman-Monatsmeldung (MED-D-700) ✅: bei laufendem Dexman-Mandat steht "Umsätze
melden" in der To-do-Liste; die Eingabe klappt in der Liste auf (Routen GET/POST /oeffentlich/portal/dexman/:praxisId/:monat, Session + Scope eigener Kunde). Übernahme erst nach Feld-Sichtung durch den Berater (MED-D-89), Quelleportal. Erweitert (Issue #790, MED-D-790): freiwillig meldet die Praxis zusätzlich die ausgezahlte Umsatzbeteiligung je Person (laufender Lohn / Einmalzahlung) und die Kassen- und Privatleistungen des Monats; jedes Feld wird einzeln gesichtet (Feldschlüsselbeteiligung.<personId>undleistung), angenommene Werte gehen mit Quelleportalin dieselben Fakten wie die Berater-Erfassung. - Eine To-do-Liste (MED-D-333, MED-OP-UX-15 Slice B) ✅: das Dashboard zeigt "Was Sie noch tun müssen" —
abgeleitet je Abruf (
PortalDashboard.aufgaben, nichts persistiert, G-2) aus offener Onboarding-Einladung, Ausfüll-Formularen im Entwurf/zur Korrektur, Fragebogen-Einladungen und angeforderten Dokumenten; je Aufgabe ein Knopf auf die bestehende Token-Strecke bzw. Upload direkt in der Liste (keine neue öffentliche Route). Interne Checklisten-Items sind für den Kunden unsichtbar. Keep-alive: Tippen/Klicken hält die Sitzung über/oeffentlich/portal/ichaktiv (höchstens alle 5 Min., Timeout unverändert). Einladungs-/Anmelde-Mail erklären Zweck, Gültigkeit (15 Min., einmalig) und den Weg zu einem neuen Link. - Passkey (WebAuthn): optionaler Zusatz-Faktor für wiederkehrende Logins — nach S3.
Kunden-Erinnerung an fehlende Unterlagen (MED-D-638, 2026-09-27): Der stündliche Cron schickt je Kunde alle 7 Tage eine Sammel-Mail mit allen offenen Basis-Unterlagen/-Angaben und angeforderten bzw. abgelaufenen Dokumenten (inkl. Mandats-Unterlagen) und dem Portal-Link — höchstens 3 Mails je Zyklus, 7 Tage nach der dritten eine Wiedervorlage an den Betreuer (einmal je Zyklus). Der Zyklus beginnt, sobald der Cron offene Unterlagen sieht (erste Mail nach 7 Tagen), und endet, wenn nichts mehr fehlt (Zähler zurück). Standardmäßig für alle Kunden an; seit MED-D-654 kein Schalter mehr im Kunden-Detail (zu viel UI), abschaltbar nur über PUT /api/mandanten/:id/unterlagen-erinnerung; der Stand erscheint leise in der Kopfzeile der Basis-Dokumente ("2 von 3 Erinnerungen gesendet" bzw. "ausgeschaltet"). Nur für Kunden mit Kontakt-E-Mail, nicht für lead/archiviert; dormant, solange PORTAL_AKTIV nicht true ist oder kein Mail-Kanal besteht. Zustand in unterlagen_erinnerung (Migration v67, nur Zähler/Zeitpunkte); Mail enthält nur Unterlagen-Bezeichnungen und den Link, das Audit nur Zahlen (G-6).
Stand (MED-D-310, 2026-09-23): S1–S5 sind gebaut und der Code ist scharfgeschaltet — MED-KB-13 (DSGVO/AVV) vom Kunden erteilt → PORTAL_AKTIV="true", echter Mailversand gebaut (OP-DM-8, Cloudflare Email Service, MED-D-314 — dormant ohne EMAIL-Binding), Magic-Link zeigt auf die Client-Route. Der Portal-Zugang entsteht automatisch beim Kunden-Anlegen bei bekannter Kontakt-E-Mail (MED-D-315: Konto + Einladungs-Magic-Link automatisch, idempotent/nicht-fatal; der manuelle Staff-Weg im Kunden-Detail bleibt Fallback für abweichende E-Mails). Das Portal löst die Einmal-„Aktualisierung anfordern"-Links als Stammdaten-Self-Service ab; der Dokument-Upload läuft ab MED-D-316 ebenfalls im Portal (angemeldeter Kunde lädt angeforderte Dokumente selbst hoch, MED-OP-PORTAL-2 Teil 1 ✅). Ab MED-D-637 gibt es keinen manuell erzeugten Einmal-Link mehr: die Erstonboarding-Einladung (Einwilligung + Start-Angaben) entsteht automatisch, sobald der Kunde im Portal ist und das Onboarding läuft (idempotent, solange keine Einladung abgeschlossen ist); der Abschnitt "Dokumente & Onboarding anfordern" im Kunden-Detail entfällt, die Übernahme eingereichter Angaben bleibt. MED-OP-PORTAL-2 Teil 2 ✅ gebaut (MED-D-320): mehrere Kontakt-Logins je Mandant (Staff-verwaltet) → MED-OP-PORTAL-2 abgeschlossen. Login-Härtung ✅ gebaut (MED-D-311, MED-OP-PORTAL-3): Turnstile + fensterbasiertes Rate-Limiting am anmelden. Verbleibend (Infra/Nachweis, kein Code, Deploy.md §7e): Absender-Domain im Cloudflare Email Service onboarden · TURNSTILE_SITEKEY/TURNSTILE_SECRET setzen · EU-/CH-Residenz-/Provider-Nachweis (AVV) gemäß §6 (die MED-KB-13-Kundenfreigabe belegt nicht die Residenz) · dann Cloudflare-Access-Bypass für /portal. Bis der Access-Bypass gesetzt ist, liegt das Portal weiter hinter Access.