feat(gdpr): fullständig raderingsflöde med pseudonym-retention, residual-test och hushållshantering

- Implementerar eraseUser() i packages/database/src/gdpr-erasure.ts som
  raderar/anonymiserar/pseudonymiserar alla personliga tabeller.
- DELETE /v1/me orkestrerar nu plan + transaktion + lagringsrensning.
- Enpersonshushåll raderas automatiskt; publika recept anonymiseras;
  draft/private-recept raderas.
- Finansiella poster (subscriptions/store_transactions/subscription_events)
  behålls under retention med slumpmässig pseudonym från
  gdpr_retention_pseudonyms.
- audit_logs/domain_events anonymiseras; kvitton i kvarvarande hushåll
  strippas på bild och OCR-text.
- Lägger till permanent residual-test (me.residual.test.ts) med
  schema-diff-assertion mot information_schema.
- Uppdaterar docs/23 och docs/29 med implementerad mekanism.
- Migration 0021 för gdpr_retention_pseudonyms och nullable
  cooking_sessions.started_by_user_id.
This commit is contained in:
Sven (AAMOS AI)
2026-08-08 05:45:49 +07:00
parent 1d78dbcad0
commit ddd6b736ba
14 changed files with 11183 additions and 79 deletions
+33
View File
@@ -43,6 +43,12 @@ export interface StorageService {
getObject(key: string): Promise<Buffer | null>;
/** GDPR / rättning: radera ett objekt. Får inte kasta om nyckeln saknas. */
deleteObject(key: string): Promise<void>;
/**
* Försök extrahera lagringsnyckeln ur en läs-URL. Returnerar null om URL:en
* inte känns igen (t.ex. extern bild). Används vid GDPR-radering så att vi
* kan radera objekt även när endast URL:en är sparad.
*/
extractKeyFromUrl(url: string): string | null;
}
declare module "fastify" {
@@ -99,6 +105,17 @@ class MockStorage implements StorageService {
await rm(path.join(MOCK_ROOT, key), { force: true }).catch(() => {});
}
extractKeyFromUrl(url: string): string | null {
try {
const parsed = new URL(url);
const prefix = "/v1/mock-s3/";
if (!parsed.pathname.startsWith(prefix)) return null;
return decodeURIComponent(parsed.pathname.slice(prefix.length));
} catch {
return null;
}
}
verifySignature(key: string, sig: string): boolean {
return this.sign(key) === sig;
}
@@ -177,6 +194,22 @@ export class AwsStorage implements StorageService {
}
}
}
extractKeyFromUrl(url: string): string | null {
try {
const parsed = new URL(url);
if (parsed.hostname.startsWith(`${this.bucket}.`)) {
return decodeURIComponent(parsed.pathname.replace(/^\//, ""));
}
const pathPrefix = `/${this.bucket}/`;
if (parsed.pathname.startsWith(pathPrefix)) {
return decodeURIComponent(parsed.pathname.slice(pathPrefix.length));
}
return null;
} catch {
return null;
}
}
}
export const storagePlugin = fp(async (app: FastifyInstance) => {