From 343f9ab567ba2c4251fd62076d2399a58366d772 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Thu, 14 May 2026 08:29:32 +0200 Subject: [PATCH] =?UTF-8?q?fix(mail):=20DSGVO=20Art.=2017=20=E2=80=94=20ma?= =?UTF-8?q?nuelles=20Sample-Cleanup=20bei=20Account-Delete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MailClassificationSample hat keine userId-FK-Cascade im Schema (connectionId ist nullable). Samples ohne connectionId blieben nach deleteAllMailConnections() als Orphans stehen. Neuer Helper deleteUserMailClassificationSamples() löscht explizit nach userId — wird in delete.delete.ts parallel zu anderen Lösch-Ops ausgeführt. Co-Authored-By: Claude Sonnet 4.6 --- backend/server/api/user/delete.delete.ts | 9 ++++++++- backend/server/db/mail.ts | 24 ++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/backend/server/api/user/delete.delete.ts b/backend/server/api/user/delete.delete.ts index d6ee5f4..72c86de 100644 --- a/backend/server/api/user/delete.delete.ts +++ b/backend/server/api/user/delete.delete.ts @@ -9,7 +9,10 @@ import { deleteUserCoachSessions, } from "../../db/user"; import { deleteProfile } from "../../db/profile"; -import { deleteAllMailConnections } from "../../db/mail"; +import { + deleteAllMailConnections, + deleteUserMailClassificationSamples, +} from "../../db/mail"; import { writeConsentRevoke } from "../../db/consent"; import { usePrisma } from "../../utils/prisma"; @@ -44,6 +47,9 @@ export default defineEventHandler(async (event) => { } // Delete all user data (DSGVO Art. 17) + // Reihenfolge: Samples VOR Connections löschen (oder parallel — FK-Reihenfolge + // egal weil wir nach userId filtern). Samples haben keine userId-FK-Cascade + // im Schema (connectionId ist nullable), daher manuelles Cleanup zwingend. await Promise.all([ deleteUserUrgeLogs(userId), deleteUserSosSessions(userId), @@ -52,6 +58,7 @@ export default defineEventHandler(async (event) => { deleteAllUserCustomDomains(userId), deleteUserTrustedContacts(userId), deleteUserCoachSessions(userId), + deleteUserMailClassificationSamples(userId), deleteAllMailConnections(userId), ]); diff --git a/backend/server/db/mail.ts b/backend/server/db/mail.ts index c2d9004..1933a07 100644 --- a/backend/server/db/mail.ts +++ b/backend/server/db/mail.ts @@ -193,13 +193,33 @@ export async function insertMailBlocked( // ─── MailClassificationSample ───────────────────────────────────────────────── +/** + * Löscht alle MailClassificationSamples eines Users. + * + * Warum manuell und nicht via Prisma-Cascade: + * MailClassificationSample hat KEINE userId-Relation mit onDelete: Cascade im Schema + * (connectionId hat Cascade, aber connectionId ist nullable). Samples mit + * connectionId=null wären nach deleteAllMailConnections() Orphans. + * + * Muss in delete.delete.ts VOR deleteAllMailConnections() aufgerufen werden + * (oder in Promise.all parallel) — FK-Reihenfolge spielt keine Rolle weil wir + * nach userId filtern, nicht nach connectionId. + * + * DSGVO Art. 17: User-Daten müssen vollständig gelöscht werden. + */ +export async function deleteUserMailClassificationSamples(userId: string) { + const db = usePrisma(); + return db.mailClassificationSample.deleteMany({ where: { userId } }); +} + /** * Schreibt einen Klassifikations-Sample-Eintrag für ML-Phase 3. * Wird nach JEDER Klassifikation aufgerufen (außer Layer 0 / Already-blocked Skips). * * DSGVO: Nur Features, keine Mail-Inhalte (kein Body). Subject + Sender sind - * kurzlebige Detection-Signale, kein narrativer Inhalt. Cascade-Delete bei - * User-Löschung (Art. 17). + * kurzlebige Detection-Signale, kein narrativer Inhalt. + * Vollständige Löschung bei Account-Delete via deleteUserMailClassificationSamples() + * (Art. 17) — NICHT via Prisma-Cascade, da userId keine FK-Relation hat. */ export async function insertMailClassificationSample(entry: { userId: string;