fix(gdpr): explicit radering av ai_corrections/scan_jobs + S3-bilder vid DELETE /v1/me

- DELETE /v1/me soft-deletar users, så users-cascade fyrar aldrig.
- Samlar distinkta bildnycklar från ai_corrections, ai_training_bank och
  scan_jobs och raderar dem via storage.deleteObject innan DB-radering.
- S3-fel loggas och avbryter inte raderingen.
- Raderar explicit ai_corrections (ai_training_bank cascadar) och scan_jobs.
- Lägger till deleteObject i StorageService (mock + AWS/DeleteObjectCommand).
- Uppdaterar docs/28-lärande-loop.md med faktisk mekanism och retention.
- Tester: verifierar noll rader kvar och storage.deleteObject-anrop.

Relaterat: skiva-1-fixrunda, blockerande GDPR-hål.
This commit is contained in:
Sven (AAMOS AI)
2026-08-08 04:20:03 +07:00
parent c2cc3878dd
commit 28908d5257
4 changed files with 327 additions and 15 deletions
+62 -3
View File
@@ -1,5 +1,5 @@
import type { FastifyInstance } from "fastify";
import { eq } from "drizzle-orm";
import { eq, inArray } from "drizzle-orm";
import { deleteUserAnalyticsEvents, schema } from "@app/database";
import {
consentInputSchema,
@@ -307,8 +307,67 @@ export async function meRoutes(app: FastifyInstance) {
// --- GDPR: radera konto (spec §56, §32) ---
app.delete("/v1/me", auth, async (req) => {
const userId = req.userId;
// Hård radering av persondata via FK-cascade; users-raden anonymiseras
// och soft-deletas för att bevara referensintegritet i aggregat.
// Eftersom users-raden soft-deletas (anonymiseras) fyras INTE users.onDelete
// cascade på ai_corrections / scan_jobs. Vi måste explicit radera träningsdata
// och skanningar, plus tillhörande bilder i objektlagring.
const correctionRows = await app.db
.select({ id: schema.aiCorrections.id, imageS3Key: schema.aiCorrections.imageS3Key })
.from(schema.aiCorrections)
.where(eq(schema.aiCorrections.userId, userId));
const bankRows = await app.db
.select({ imageS3Key: schema.aiTrainingBank.imageS3Key })
.from(schema.aiTrainingBank)
.innerJoin(
schema.aiCorrections,
eq(schema.aiTrainingBank.correctionId, schema.aiCorrections.id),
)
.where(eq(schema.aiCorrections.userId, userId));
const scanRows = await app.db
.select({ s3Keys: schema.scanJobs.s3Keys })
.from(schema.scanJobs)
.where(eq(schema.scanJobs.userId, userId));
const imageKeys = new Set<string>();
for (const row of correctionRows) {
if (row.imageS3Key) imageKeys.add(row.imageS3Key);
}
for (const row of bankRows) {
if (row.imageS3Key) imageKeys.add(row.imageS3Key);
}
for (const row of scanRows) {
for (const key of row.s3Keys ?? []) {
if (key) imageKeys.add(key);
}
}
for (const key of imageKeys) {
try {
await app.storage.deleteObject(key);
} catch (err) {
app.log.warn({ err, key, userId }, "Kunde inte radera bild vid GDPR-radering");
}
}
// ai_training_bank försvinner via correction_id ON DELETE CASCADE.
if (correctionRows.length > 0) {
await app.db
.delete(schema.aiCorrections)
.where(
inArray(
schema.aiCorrections.id,
correctionRows.map((r) => r.id),
),
);
}
// scan_jobs har också ON DELETE CASCADE på users.id, men eftersom vi
// soft-deletar användaren måste vi radera explicit.
await app.db.delete(schema.scanJobs).where(eq(schema.scanJobs.userId, userId));
// Hård radering av övrig persondata.
await app.db.delete(schema.memoryItems).where(eq(schema.memoryItems.userId, userId));
await app.db.delete(schema.tasteSignals).where(eq(schema.tasteSignals.userId, userId));
await app.db.delete(schema.meals).where(eq(schema.meals.userId, userId));