# Consent-Gap-Plan — Art. 9 DSGVO Mail-Auto-Delete Stand: 2026-05-13 Autor: rebreak-backend-agent Status: Implementiert (Backend), TODOs für mo + rebreak-native-ui --- ## Was wurde implementiert ### Schema (Migration 20260513_art9_consent_log) Neue Spalten in `mail_connections`: - `consent_at TIMESTAMPTZ NULL` — wann eingewilligt, NULL = "Re-Consent pending" - `consent_version TEXT NULL` — z.B. "art9-mail-v1-2026-05-13" - `consent_ip_address TEXT NULL` — IP zum Zeitpunkt der Einwilligung Neue Tabelle `consent_logs`: - Append-only Audit-Trail für alle Einwilligungen und Widerrufe - Wird NIEMALS gelöscht (Beweispflicht Art. 7 Abs. 1 DSGVO) ### Backend-Dateien | Datei | Zweck | |---|---| | `server/utils/consent-texts.ts` | Versionierte Consent-Texte (DE + EN) | | `server/db/consent.ts` | DB-Layer: writeConsentGrant, writeConsentRevoke, getConsentLogsByUser, setMailConnectionConsent | | `server/api/mail-connections/consent.post.ts` | POST /api/mail-connections/consent | | `server/api/mail-connections/[id].post.ts` | POST /api/mail-connections/:id (mit Consent-Gate 412) | | `server/api/mail-connections/[id].delete.ts` | DELETE /api/mail-connections/:id (mit Widerruf-Log) | | `server/api/user/delete.delete.ts` | Erweitert: schreibt Widerruf für alle Connections bei Account-Löschung | ### Aktuelle Consent-Version `"art9-mail-v1-2026-05-13"` — definiert in `server/utils/consent-texts.ts`. --- ## TODO #1 — mo (Mail-Stack / Daemon) **Daemon pausiert Verarbeitung wenn consent_at = NULL** Kontext: Alle Bestandsrows nach Migration haben `consent_at = NULL`. Das bedeutet "Re-Consent pending". Der Daemon darf für diese Connections KEIN Auto-Delete ausführen bis der User explizit eingewilligt hat. Implementierung in `imap-idle/index.mjs` (oder wherever der Scan-Loop läuft): ```js // Beim Laden einer Connection für den Scan-Loop: if (!connection.consent_at) { log(`[consent] Skipping ${connection.email}: Re-Consent pending (consent_at = NULL)`); // Verbindung aus dem aktiven Scan-Pool auslassen — KEIN Fehler, kein Error-State. // isActive bleibt true. Wenn User Re-Consent gibt, wird consent_at gesetzt // und die Connection beim nächsten Loop-Cycle wieder aufgenommen. continue; } ``` Checklist: - [ ] mo — `imap-idle/index.mjs`: consent_at-Check im Connection-Load - [ ] mo — `scan.post.ts` / `scan-internal.post.ts`: gleiches Check (On-Demand-Scans) --- ## TODO #2 — mo (OAuth Token-Revoke bei Disconnect) **Wenn OAuth-Connections getrennt werden: Token bei Microsoft revoken** Kontext: Wenn `authMethod === 'oauth2_microsoft'` (Outlook-OAuth, noch nicht live), muss beim Disconnect der Refresh-Token bei Microsoft widerrufen werden. Placeholder-Comments existieren bereits in: - `server/api/mail-connections/[id].delete.ts` (User-Disconnect) - `server/api/user/delete.delete.ts` (Account-Löschung) Implementierung wenn OAuth-Phase startet: ```ts if (conn.authMethod === 'oauth2_microsoft') { const refreshToken = decrypt(conn.oauthRefreshToken); let revoked = false; for (let attempt = 0; attempt < 3; attempt++) { try { await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'revoke', token: refreshToken, client_id: process.env.MS_OAUTH_CLIENT_ID, client_secret: process.env.MS_OAUTH_CLIENT_SECRET, }), }); revoked = true; break; } catch (e) { // Retry } } // Audit-Log: Token-Revoke success/failure // Dann trotzdem DB-Row löschen (DSB-Memo Abschnitt 5.1) } ``` --- ## TODO #3 — Datenexport (Art. 15 DSGVO) **consent_logs für den User in den Datenexport aufnehmen** Es gibt aktuell keinen `/api/data-export`-Endpoint im Backend. Wenn er gebaut wird (separater Sprint), muss er enthalten: - `consent_logs` für den User (aus `getConsentLogsByUser(userId)` in `server/db/consent.ts`) - Für jede MailConnection: Provider, verbundene E-Mail-Adresse, Verbindungs-Zeitpunkt, erteilte OAuth-Scopes (lesbar, kein Token-Inhalt), Token-Ablaufdatum DSB-Memo Abschnitt 5.2 + Hans-Müller-To-Do-Liste Item #6. --- ## Frontend-Spec für rebreak-native-ui (UI-Agent) ### Re-Consent-Modal (für Bestandsuser) Trigger: App-Open + Auth-User eingeloggt + mindestens eine MailConnection mit `consent_at = NULL` (Backend: GET /api/mail/status liefert connections mit `consentAt`-Feld — Null-Check im Frontend). Alternativ: neuer Endpoint `GET /api/mail-connections/pending-consent` der nur die IDs der Connections ohne Consent zurückgibt. Modal-Inhalt: - Titel: aus `getConsentText("art9-mail-v1-2026-05-13").de` (oder .en je nach Locale) - Zwei Buttons: "Einwilligen" → POST /api/mail-connections/consent, "Verbindung trennen" → DELETE /api/mail-connections/:id Nach "Einwilligen": - Pro Connection ein POST /api/mail-connections/consent senden - Body: `{ mailConnectionId: "", consentVersion: "art9-mail-v1-2026-05-13" }` - Bei 200: Modal schließen, Toast "Einwilligung erteilt" - Bei 409 (version_mismatch): sollte nicht passieren wenn Frontend aktuelle Version nutzt ### ConnectMailSheet — Consent-Gate Neuer Flow: 1. User gibt Email + Passwort ein 2. Vor dem Abschicken: Consent-Text anzeigen (Checkbox oder expliziter Button) 3. POST /api/mail-connections/:id mit `consentVersion` im Body 4. Bei 412 (`consent_required`): Consent-Modal anzeigen (sollte nicht vorkommen wenn Schritt 2 korrekt implementiert ist) 5. Bei 200: Connection verbunden Der `consentVersion`-Wert muss das Frontend kennen. Empfehlung: Backend liefert ihn via `GET /api/mail/status` oder einem neuen `GET /api/mail-connections/consent-version`- Endpoint. Alternativ: hardcoded im Frontend, aber dann muss er bei jedem Text-Bump synchronisiert werden. ### Empfehlung: `GET /api/mail-connections/consent-version` Einfacher Endpoint ohne Auth: ``` GET /api/mail-connections/consent-version → { version: "art9-mail-v1-2026-05-13", texts: { de: "...", en: "..." } } ``` Damit muss das Frontend die Version nie hardcoden. Noch nicht implementiert — als TODO für rebreak-backend falls UI-Agent es braucht. --- ## Consent-Text-Bump-Workflow (für künftige DSB-Updates) 1. Hans-Müller gibt neuen Text frei 2. Neuen Eintrag in `server/utils/consent-texts.ts` hinzufügen 3. `CURRENT_ART9_MAIL_VERSION` auf neue Version setzen 4. Alle bestehenden Connections mit alter Version bekommen automatisch Re-Consent-Modal (Daemon-Check: `connection.consent_version !== CURRENT_ART9_MAIL_VERSION` → pausieren) 5. Deploy via GitHub Actions Pipeline --- ## Nicht gemacht (explizit aus Scope ausgeschlossen) - Migration lokal ausgeführt — nein, Pipeline deployt - Frontend-Änderungen — UI-Agent-Task - Daemon-Logik angefasst — mo's Domain - git push — kein User-GO erteilt