ddd6b736ba
- 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.
395 lines
24 KiB
Markdown
395 lines
24 KiB
Markdown
# GDPR-radering — kompletthetsaudit (cibello, HEAD `c2cc387` → `28908d5`)
|
|
|
|
> **Syfte:** identifiera varje tabell som bär `user_id` eller annan persondata, klassa den enligt GDPR, hitta luckor mot nuvarande `DELETE /v1/me`, och designa ett permanent residual-test.
|
|
>
|
|
> **Status:** ✅ Implementerad i efterföljande commits. Residual-testet `apps/api/test/me.residual.test.ts` ingår permanent i `pnpm test` och failar automatiskt om nya `user_id`-liknande kolumner läggs till utan klassning.
|
|
>
|
|
> **Audit gjord mot:**
|
|
> - `packages/database/src/schema/*.ts`
|
|
> - `infrastructure/migrations/*.sql`
|
|
> - `apps/api/src/routes/me.ts` (nuvarande `DELETE /v1/me`)
|
|
> - `packages/database/src/analytics-gdpr.ts`
|
|
|
|
---
|
|
|
|
## 1. Metod
|
|
|
|
1. Enumrerade alla tabeller exporterade från `packages/database/src/schema/index.ts`.
|
|
2. Grep:ade `user_id`, `references(() => users.id)` och fria text/PGD-fält som kan bära PII (foton, hälsa, kvitton, betyg, kommentarer).
|
|
3. Klassade varje tabell i tre kategorier:
|
|
- **RADERA** — direkt personlig, ingen laglig grund att behålla.
|
|
- **ANONYMISERA** — behövs för referensintegritet/aggregat, men `user_id` och PII-fält ska skrubbas eller sättas `NULL`.
|
|
- **BEHÅLL MED LAGLIG GRUND** — t.ex. bokföring, säkerhet, rättslig förpliktelse. Får inte raderas, men PII ska minimeras.
|
|
4. Jämförde klassningen mot vad `DELETE /v1/me` faktiskt gör idag.
|
|
|
|
---
|
|
|
|
## 2. Tabell-för-tabell-klassning
|
|
|
|
### 2.1 Användarkonto & autentisering — RADERA
|
|
|
|
| Tabell | Motivering | Nuvarande `DELETE /v1/me` |
|
|
|---|---|---|
|
|
| `users` | Personliga grunduppgifter (e-post, namn). Soft-delete/anonymisering accepteras för att bevara referensintegritet, men övriga personfält måste skrubbas. | ✅ anonymiserar `email`, `displayName`, sätter `deletedAt`. |
|
|
| `user_credentials` | Lösenordshash. Ingen laglig grund efter radering. | ❌ **saknas** — cascade på `users.id` fyrar inte vid soft-delete. |
|
|
| `admin_totp` | TOTP-secret. Säkerhetsuppgift. | ❌ **saknas** — cascade fyrar inte. |
|
|
| `email_verification_tokens` | Engångstokens kopplade till användaren. | ❌ **saknas** — cascade fyrar inte. |
|
|
| `password_reset_tokens` | Engångstokens kopplade till användaren. | ❌ **saknas** — cascade fyrar inte. |
|
|
| `refresh_tokens` | Sessions-token-hash, IP, user-agent. | ✅ raderas explicit. |
|
|
|
|
**Bedömning:** Fyra auth-tabeller är ohanterade eftersom `users` soft-deletas. Måste explicit raderas.
|
|
|
|
---
|
|
|
|
### 2.2 Hälsa, preferenser, samtycken & locale — RADERA
|
|
|
|
| Tabell | Motivering | Nuvarande `DELETE /v1/me` |
|
|
|---|---|---|
|
|
| `user_health_profiles` | Hälsodata (längd, vikt, födelseår, kön, aktivitet). Känslig persondata. | ✅ raderas explicit. |
|
|
| `user_preferences` | Kost/mål/allergier/intoleranser. Personlig profilering. | ✅ raderas explicit. |
|
|
| `user_consents` | Samtyckeshistorik kopplad till person. | ❌ **saknas** — cascade på `users.id` fyrar inte vid soft-delete. |
|
|
| `user_locale_preferences` | Språk, region, tidszon, valuta. Personlig preferens. | ❌ **saknas** — cascade fyrar inte. |
|
|
|
|
**Bedömning:** `user_consents` och `user_locale_preferences` är nya luckor.
|
|
|
|
---
|
|
|
|
### 2.3 Hushåll & medlemskap — BESLUT KRÄVS
|
|
|
|
| Tabell | Motivering | Nuvarande `DELETE /v1/me` |
|
|
|---|---|---|
|
|
| `household_members` | Länkar användare till hushåll. Ingen laglig grund att behålla kopplingen efter radering. | ❌ **saknas helt**. |
|
|
| `households` | Om användaren är **enda medlemmen/ägaren** innehåller hushållet personliga spår (namn, budget, aktiveringsdatum, storlek). Om flera medlemmar finns är hushållet en delad resurs. | ❌ **saknas helt**. |
|
|
| `storage_locations` | Hushålls-specifika förvaringsplatser. Delad resurs om hushållet har flera medlemmar. | ❌ **saknas helt**. |
|
|
|
|
**Bedömning:** Detta är den största strukturella luckan. Förslag (se öppna frågor nedan):
|
|
- Ta bort användarens `household_members`-rad(er).
|
|
- Om användaren är ensam ägare/medlem i ett hushåll: **radera hela hushållet och dess cascading data** (`storage_locations`, `inventory_items`, `inventory_transactions`, `shopping_lists`, `week_plans`, `meal_boxes`, `receipts`, `cooking_sessions`, etc.).
|
|
- Om andra medlemmar finns: låt hushållet leva, anonymisera användarreferenser i delade data.
|
|
|
|
**Öppen fråga till Johan:** Vill vi automatiskt radera ett enpersonshushåll vid kontoradering, eller ska användaren först tvingas överföra ägarskap? (GDPR-mässigt är automatisk radering av ett enpersonshushåll det säkraste.)
|
|
|
|
---
|
|
|
|
### 2.4 Lager, inköp, matplanering — hushållsdata (ANONYMISERA eller RADERA beroende på hushåll)
|
|
|
|
| Tabell | Klassning | Motivering |
|
|
|---|---|---|
|
|
| `inventory_items` | ANONYMISERA / RADERA | Hushållsdata. Ingen personlig info i sig, men `source`, `verifiedByUser` etc. kan kopplas till användaren. Om hushållet överlever: sätt `verifiedByUser = false` där aktuell användare verifierat. Om hushållet raderas: cascade bort. |
|
|
| `inventory_transactions` | ANONYMISERA | `actor_user_id` är en personlig referens. Sätt `NULL`. Data behövs för hushållets saldo/historik. |
|
|
| `inventory_item_decay_profile` | BEHÅLL / RADERA | Teknisk koppling till `inventory_items`. Följer hushållets öde. |
|
|
| `inventory_conflicts` | ANONYMISERA | `resolved_by_user_id` ska vara `NULL`. Övriga fält är hushållsdata. |
|
|
| `shopping_lists` | BEHÅLL | Hushållsdata, ingen PII. |
|
|
| `shopping_list_items` | ANONYMISERA | `added_by_user_id` ska vara `NULL`. |
|
|
| `receipts` | RADERA / ANONYMISERA | Kvittobild (`image_url`) kan innehålla personuppgifter (butik, kortnummer, artiklar). Om hushållet överlever och kvittot är delat: radera bild-URL:en i objektlagring och anonymisera. Om hushållet raderas: cascade bort. |
|
|
| `receipt_lines` | RADERA / ANONYMISERA | `raw_text` är OCR-text från kvitto (potentiellt personligt). Samma logik som `receipts`. |
|
|
| `price_observations` | BEHÅLL | Aggregatdata, anonymiserad prisinformation. `store_name` är inte personligt i GDPR-mening. |
|
|
| `meal_boxes` | ANONYMISERA | `reserved_for_user_id` ska vara `NULL`. |
|
|
| `meals` | RADERA | Direkt personlig måltidslogg (`user_id`, `photo_url`). Raderas explicit idag. |
|
|
| `week_plans` | BEHÅLL | Hushållsdata, ingen PII. |
|
|
| `week_plan_entries` | BEHÅLL | Hushållsdata. |
|
|
| `cooking_sessions` | ANONYMISERA | `started_by_user_id` ska vara `NULL`. Sessionen är hushållshistorik. |
|
|
| `cooking_assumption_profiles` | BEHÅLL / RADERA | Hushållsaggregat. Följer hushållets öde. |
|
|
| `waste_summaries` | BEHÅLL | Hushållsaggregat, ingen PII. |
|
|
|
|
**Bedömning:** Dessa tabeller är i princip ohanterade idag. De flesta är hushållsdata och ska anonymiseras snarare än raderas, förutsatt att hushållet överlever.
|
|
|
|
---
|
|
|
|
### 2.5 Skanningar, AI-korrigeringar & träningsbank — RADERA
|
|
|
|
| Tabell | Motivering | Nuvarande `DELETE /v1/me` |
|
|
|---|---|---|
|
|
| `scan_jobs` | Innehåller uppladdade bilder (`s3_keys`) och användarspecifika resultat. | ✅ raderas explicit. |
|
|
| `ai_corrections` | Innehåller personliga korrigeringar och bildnyckel. | ✅ raderas explicit. |
|
|
| `ai_training_bank` | Innehåller träningsbild och korrigeringsdata. Cascadar via `ai_corrections`. | ✅ raderas via `ai_corrections` cascade. |
|
|
|
|
**Bedömning:** Godkänd i föregående fix. Ingen ytterligare åtgärd.
|
|
|
|
---
|
|
|
|
### 2.6 Recept & matminnen — RADERA eller ANONYMISERA
|
|
|
|
| Tabell | Klassning | Motivering |
|
|
|---|---|---|
|
|
| `recipes` | ANONYMISERA | Publik/verifierad receptdata är inte personlig, men `creator_user_id` och `creator_display_name` är personliga. **Draft/private**-recept däremot är personliga och bör raderas. |
|
|
| `recipe_ratings` | RADERA | Användarbetyg och kommentar är personligt. | ✅ raderas ej idag (lucka). |
|
|
| `recipe_favorites` | RADERA | Användarens favoriter är personligt. | ❌ **saknas** (lucka). |
|
|
| `recipe_cooks` | RADERA / ANONYMISERA | Logg över vilka recept användaren lagat. Personligt. | ❌ **saknas** (lucka). |
|
|
| `food_memories` | RADERA | Matminne med eventuell bild (`photo_url`). Personligt. | ❌ **saknas** (lucka). |
|
|
| `memory_items` | RADERA | AAMOS-minne — personligt. | ✅ raderas explicit. |
|
|
| `taste_signals` | RADERA | Smakpreferenser kopplade till användaren. | ✅ raderas explicit. |
|
|
| `creator_stats` | RADERA | Creator-profil, följare, statistik. Personligt. | ❌ **saknas** (lucka). |
|
|
| `creator_follows` | RADERA | Följer/följare-relationer. Personliga. | ❌ **saknas** (lucka). |
|
|
|
|
**Bedömning:** Receptinteraktioner är i stort sett ohanterade.
|
|
|
|
---
|
|
|
|
### 2.7 Notiser & push — RADERA
|
|
|
|
| Tabell | Motivering | Nuvarande `DELETE /v1/me` |
|
|
|---|---|---|
|
|
| `notifications` | Push-/inapp-notiser till användaren. | ❌ **saknas** (cascade fyrar inte). |
|
|
| `push_tokens` | Push-token för Expo. Teknisk identifierare kopplad till användaren. | ❌ **saknas** (cascade fyrar inte). |
|
|
|
|
**Bedömning:** Båda är luckor.
|
|
|
|
---
|
|
|
|
### 2.8 Betalningar & prenumerationer — BEHÅLL MED LAGLIG GRUND, MEN ANONYMISERA
|
|
|
|
| Tabell | Klassning | Motivering |
|
|
|---|---|---|
|
|
| `subscriptions` | BEHÅLL | Bokförings- och rättslig grund för att bevisa köp/åtkomst. `user_id` bör pseudonymiseras eller sättas `NULL` efter en viss period, men transaktionens existens måste kunna verifieras. Retention: minst 7 år (Bokföringslagen) eller tills rättslig tvist är avslutad. |
|
|
| `subscription_events` | BEHÅLL | Händelselogg för prenumeration. Samma grund. `user_id` kan anonymiseras. |
|
|
| `store_transactions` | BEHÅLL | Råa store-transaktioner för revision. `raw_payload` kan innehålla PII (store-kvitto) — **ska skrubbas** så att endast transaktions-ID, produkt-ID och belopp återstår. `user_id` anonymiseras. Retention: 7 år. |
|
|
| `store_notifications` | BEHÅLL | App/Play-servernotiser. Ingen direkt PII i sig, men kan innehålla transaktions-ID. Behålls för spårbarhet. |
|
|
| `trials` | RADERA / BEHÅLL | Trial-spårning. För rättvis kvot/fair-use kan vi behöva spara att denna person redan provat. Men efter kontoradering bör raden raderas eller pseudonymiseras (t.ex. hash av ursprungligt user_id för anti-fraud). **Förslag:** radera vid GDPR-radering; anti-fraud hanteras via store-transaktioner istället. | ❌ **saknas** idag. |
|
|
| `ai_usage_counters` | RADERA | Månatlig AI-användning. Ingen laglig grund efter kontoradering. | ❌ **saknas** idag. |
|
|
|
|
**Bedömning:** Finansiella poster får inte raderas, men PII måste skalas bort. `subscriptions`, `subscription_events`, `store_transactions` behöver en anonymiseringsfunktion, inte deletion.
|
|
|
|
---
|
|
|
|
### 2.9 Analytics & audit — BEHÅLL MED LAGLIG GRUND, MEN ANONYMISERA
|
|
|
|
| Tabell | Klassning | Motivering |
|
|
|---|---|---|
|
|
| `product_analytics_events` | RADERA | Pseudonyma events, men `anonymous_id` kan länka till användaren. Hard-delete görs redan via `deleteUserAnalyticsEvents`. | ✅ raderas explicit. |
|
|
| `analytics_daily_snapshots` | BEHÅLL | Hushållsaggregat, ingen PII. |
|
|
| `audit_logs` | BEHÅLL | Säkerhets-/rättslig grund. `actor_user_id`, `ip` och ev. PII i `metadata` ska anonymiseras (`actor_user_id` → `NULL`, `ip` → `NULL`, metadata skrubbas). Retention: minst 2 år, alternativt tills rättslig tvist är avslutad. | ❌ **saknas** idag. |
|
|
| `domain_events` | BEHÅLL / ANONYMISERA | Event-sourcing outbox. `user_id` och PII i `payload` ska anonymiseras för att inte bryta downstream-konsumenter. | ❌ **saknas** idag. |
|
|
| `idempotency_keys` | RADERA | Caching av API-svar kopplade till användaren. Ingen laglig grund. | ❌ **saknas** idag. |
|
|
|
|
---
|
|
|
|
### 2.10 Globala system- och översättningstabeller — BEHÅLL (ingen PII)
|
|
|
|
| Tabell | Klassning | Motivering |
|
|
|---|---|---|
|
|
| `canonical_ingredients` | BEHÅLL | Global produktdata. |
|
|
| `ingredient_translations` | BEHÅLL | Översättningar av globala ingredienser. |
|
|
| `unit_translations` | BEHÅLL | Översättningar av enheter. |
|
|
| `recipe_translations` | BEHÅLL | Översättningar av recept. |
|
|
| `recipe_step_translations` | BEHÅLL | Översättningar av receptsteg. |
|
|
| `recipe_source_registry` | BEHÅLL | Juridisk källregister. |
|
|
| `recipe_ingredients` | BEHÅLL | Receptdata. |
|
|
| `recipe_steps` | BEHÅLL | Receptdata. |
|
|
| `recipe_similarities` | BEHÅLL | Systemdata. |
|
|
| `substitutions` | BEHÅLL | Systemdata. |
|
|
| `nutrition_display_profiles` | BEHÅLL | Systemdata. |
|
|
| `allergen_market_rules` | BEHÅLL | Systemdata. |
|
|
| `season_events` | BEHÅLL | Systemdata. |
|
|
| `feature_flags` | BEHÅLL | Systemdata. |
|
|
| `release_gates` | BEHÅLL | Systemdata. |
|
|
| `inventory_decay_profiles` | BEHÄLL | Systemdata. |
|
|
| `ai_eval_cases` | BEHÅLL | Testdata. |
|
|
| `ai_eval_runs` | BEHÅLL | Testkörningsresultat. |
|
|
|
|
---
|
|
|
|
## 3. Gap-tabell mot nuvarande `DELETE /v1/me`
|
|
|
|
Nedan är tabeller som **idag varken raderas, anonymiseras eller motiveras att behållas** vid kontoradering.
|
|
|
|
| # | Tabell | Kategori | Risk | Föreslagen åtgärd |
|
|
|---|---|---|---|---|
|
|
| 1 | `user_credentials` | Auth | Lösenordshash överlever radering. | `DELETE FROM user_credentials WHERE user_id = $1` |
|
|
| 2 | `admin_totp` | Auth | TOTP-secret överlever. | `DELETE` |
|
|
| 3 | `email_verification_tokens` | Auth | Tokens överlever. | `DELETE` |
|
|
| 4 | `password_reset_tokens` | Auth | Tokens överlever. | `DELETE` |
|
|
| 5 | `user_consents` | Samtycken | Samtyckeshistorik överlever. | `DELETE` |
|
|
| 6 | `user_locale_preferences` | Preferenser | Locale-preferenser överlever. | `DELETE` |
|
|
| 7 | `household_members` | Hushåll | Användaren kvarstår som medlem. | `DELETE` |
|
|
| 8 | `households` (+ cascading data) | Hushåll | Enpersonshushåll läcker persondata. | **Beslut krävs** (se §2.3) |
|
|
| 9 | `inventory_transactions` | Hushåll | `actor_user_id` kvarstår. | `SET actor_user_id = NULL` |
|
|
| 10 | `inventory_conflicts` | Hushåll | `resolved_by_user_id` kvarstår. | `SET resolved_by_user_id = NULL` |
|
|
| 11 | `shopping_list_items` | Hushåll | `added_by_user_id` kvarstår. | `SET added_by_user_id = NULL` |
|
|
| 12 | `meal_boxes` | Hushåll | `reserved_for_user_id` kvarstår. | `SET reserved_for_user_id = NULL` |
|
|
| 13 | `cooking_sessions` | Hushåll | `started_by_user_id` kvarstår. | `SET started_by_user_id = NULL` |
|
|
| 14 | `receipts` | Hushåll | Kvittobild (`image_url`) kan innehålla PII. | Radera bild i lagring, sätt `image_url = NULL` |
|
|
| 15 | `receipt_lines` | Hushåll | OCR-text kan innehålla PII. | Skrubba `raw_text` eller radera rader om hushåll raderas. |
|
|
| 16 | `recipe_ratings` | Recept | Betyg/kommentar överlever. | `DELETE` |
|
|
| 17 | `recipe_favorites` | Recept | Favoriter överlever. | `DELETE` |
|
|
| 18 | `recipe_cooks` | Recept | Lagningslogg överlever. | `DELETE` |
|
|
| 19 | `food_memories` | Matminnen | Matminnen + bild överlever. | `DELETE` (+ bild i lagring) |
|
|
| 20 | `creator_stats` | Creator | Creator-profil överlever. | `DELETE` |
|
|
| 21 | `creator_follows` | Creator | Följer/följare överlever. | `DELETE` |
|
|
| 22 | `notifications` | Notiser | Notiser överlever. | `DELETE` |
|
|
| 23 | `push_tokens` | Push | Push-token överlever. | `DELETE` |
|
|
| 24 | `trials` | Trial | Trial-info överlever. | `DELETE` |
|
|
| 25 | `ai_usage_counters` | Usage | AI-användning överlever. | `DELETE` |
|
|
| 26 | `audit_logs` | Audit | `actor_user_id`, `ip`, metadata överlever. | Anonymisera (`NULL` + skrubba metadata) |
|
|
| 27 | `domain_events` | Events | `user_id` + PII i payload överlever. | Anonymisera (`NULL` + skrubba payload) |
|
|
| 28 | `idempotency_keys` | API | Cachade svar kopplade till användaren överlever. | `DELETE` |
|
|
| 29 | `subscriptions` | Betalning | `user_id` kvarstår för evigt. | Pseudonymisera efter retention (eller direkt vid radering om ekonomiskt spår finns kvar i `store_transactions`) |
|
|
| 30 | `subscription_events` | Betalning | `user_id` + payload kvarstår. | Anonymisera (`user_id = NULL`, skrubba payload) |
|
|
| 31 | `store_transactions` | Betalning | `raw_payload` kan innehålla PII. | Skrubba `raw_payload`, anonymisera `user_id` |
|
|
|
|
**Totalt:** 31 tabell-förekomster som behöver åtgärdas (många är `SET NULL`, inte radering).
|
|
|
|
---
|
|
|
|
## 4. Bilder / objektlagring
|
|
|
|
Förutom redan hanterade `scan_jobs.s3_keys`, `ai_corrections.image_s3_key` och `ai_training_bank.image_s3_key` finns följande användarbilder:
|
|
|
|
| Källa | Kolumn | Risk | Åtgärd vid GDPR-radering |
|
|
|---|---|---|---|
|
|
| `meals.photo_url` | Foto på måltid | Personlig bild. | Radera objektet i lagring; sätt `photo_url = NULL`. |
|
|
| `food_memories.photo_url` | Foto från matminne | Personlig bild. | Radera objektet i lagring; sätt `photo_url = NULL`. |
|
|
| `receipts.image_url` | Kvittobild | Potentiellt personlig (kortnummer, butik, köp). | Radera objektet i lagring; sätt `image_url = NULL`. |
|
|
| `recipes.image_urls` | Receptbild | Om receptet är **draft/private** och skapats av användaren: personlig. Om publikt/verifierat: delad resurs. | För draft/private: radera bilderna. För publika: behåll (om användaren samtyckt) eller anonymisera skaparen. |
|
|
|
|
**Notering:** Det finns ingen dedikerad profilbildskolumn i `users`. Om sådan läggs till i framtiden måste den inkluderas i residual-testet.
|
|
|
|
---
|
|
|
|
## 5. Design — RESIDUAL-TEST
|
|
|
|
Mål: en permanent test som skapar en användare med **en rad i varje personlig tabell**, kör `DELETE /v1/me`, och verifierar att inga personspår kvarstår (utom motiverat-behåll som måste vara PII-fritt/anonymiserat).
|
|
|
|
### 5.1 Teststruktur (förslag)
|
|
|
|
```ts
|
|
describe("DELETE /v1/me — GDPR residual test", () => {
|
|
it("lämnar inga personspår efter kontoradering", async () => {
|
|
const { token, userId } = await registerUser("residual@example.invalid");
|
|
|
|
// 1. Skapa en rad i varje personlig tabell
|
|
await seedAllPersonalTables(userId, token);
|
|
|
|
// 2. Kör DELETE /v1/me
|
|
const res = await app.inject({
|
|
method: "DELETE",
|
|
url: "/v1/me",
|
|
headers: { authorization: `Bearer ${token}` },
|
|
});
|
|
expect(res.statusCode).toBe(200);
|
|
|
|
// 3. Verifiera: varje tabell är antingen tom eller anonym/PII-fri
|
|
await assertNoPersonalTraces(app.db, userId);
|
|
});
|
|
});
|
|
```
|
|
|
|
### 5.2 Hjälpfunktioner
|
|
|
|
`seedAllPersonalTables(userId, token)` ska skapa exempeldata i:
|
|
- `user_credentials` (skapas vid registrering)
|
|
- `user_health_profiles`, `user_preferences`, `user_locale_preferences`, `user_consents`
|
|
- `households` + `household_members` (+ `storage_locations`)
|
|
- `scan_jobs` + `ai_corrections` + `ai_training_bank`
|
|
- `meals`, `memory_items`, `taste_signals`, `food_memories`
|
|
- `recipe_ratings`, `recipe_favorites`, `recipe_cooks`, `creator_stats`, `creator_follows`
|
|
- `notifications`, `push_tokens`
|
|
- `trials`, `ai_usage_counters`
|
|
- `audit_logs`, `domain_events`, `idempotency_keys`
|
|
- `subscriptions`, `subscription_events`, `store_transactions`
|
|
- `inventory_transactions`, `inventory_conflicts`, `shopping_list_items`, `meal_boxes`, `cooking_sessions`
|
|
- `receipts` + `receipt_lines`
|
|
|
|
`assertNoPersonalTraces(db, userId)` ska:
|
|
1. För **RADERA**-tabeller: `SELECT count(*) = 0 WHERE user_id = $userId`.
|
|
2. För **ANONYMISERA**-tabeller: `SELECT count(*) = 0 WHERE user_id = $userId` (eftersom `user_id` ska vara `NULL`).
|
|
3. För **BEHÅLL**-tabeller: tillåta rader med `user_id = $userId` **men** kontrollera att känsliga fält (`raw_payload`, `metadata`, `ip`) är skrubbade.
|
|
4. För bilder: mocka `storage.deleteObject` och verifiera att alla personliga bildnycklar raderats.
|
|
5. För `users`: kontrollera att `email`, `displayName` är anonymiserade och `deletedAt` är satt.
|
|
|
|
### 5.3 Mekanism för att upptäcka nya tabeller
|
|
|
|
För att residual-testet automatiskt ska upptäcka framtida tabeller som lägger till `user_id`:
|
|
|
|
```ts
|
|
// I testet: reflektera schemat och jämför med en allow-list
|
|
const userIdColumns = await db.execute(sql`
|
|
SELECT table_name, column_name
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'public'
|
|
AND column_name IN ('user_id', 'actor_user_id', 'creator_user_id',
|
|
'started_by_user_id', 'resolved_by_user_id',
|
|
'added_by_user_id', 'reserved_for_user_id')
|
|
`);
|
|
// Jämför med allow-list över tabeller som testet täcker.
|
|
// Om en ny tabell dyker upp: testet failar tills den klassas.
|
|
```
|
|
|
|
Denna "schema-diff"-assertion gör testet till en permanent vakt.
|
|
|
|
### 5.4 Förväntat testresultat idag (före implementation)
|
|
|
|
Testet kommer att **faila** på de ~31 luckor listade i §3. Det är avsiktligt — det blir den röda tråden som implementationen ska göra grön.
|
|
|
|
---
|
|
|
|
## 6. Öppna beslut innan implementation
|
|
|
|
Följande punkter behöver ditt (Johans) godkännande eller vägledning:
|
|
|
|
1. **Enpersonshushåll vid radering:** Ska vi automatiskt radera hela hushållet (inkl. lager, listor, kvitton, matplaner) när enda medlemmen begär GDPR-radering? Alternativet är att tvinga användaren överföra ägarskap först, vilket är mer komplext.
|
|
- **Rekommendation:** Automatisk radering av enpersonshushåll.
|
|
|
|
2. **Publika recept:** Ska ett publikt/verifierat recept som användaren skapat anonymiseras (`creator_user_id = NULL`, `creator_display_name = NULL`) eller raderas helt?
|
|
- **Rekommendation:** Anonymisera publika recept, radera draft/private.
|
|
|
|
3. **Finansiella poster:** Ska `subscriptions.user_id` pseudonymiseras direkt vid GDPR-radering, eller först efter 7 år? Bokföringslagen kräver att vi kan bevisa köpet, men inte nödvändigtvis vem som gjorde det.
|
|
- **Rekommendation:** Sätt `subscriptions.user_id = NULL` direkt vid radering; ekonomisk spårbarhet finns kvar i `store_transactions` (som skrubbas men behåller transaktions-ID).
|
|
|
|
4. **Audit logs:** Ska `audit_logs` behållas med `actor_user_id = NULL` i 2+ år, eller raderas efter kortare tid?
|
|
- **Rekommendation:** Behåll 2 år med anonymiserad aktör, för säkerhetsincident-utredning.
|
|
|
|
5. **Domain events:** Ska `domain_events` behållas för event-sourcing, men med `user_id = NULL` och skrubbad payload?
|
|
- **Rekommendation:** Ja, behåll för downstream replay/konsumenter.
|
|
|
|
---
|
|
|
|
## 7. Sammanfattning & rekommenderad ordningsföljd
|
|
|
|
### 7.1 Snabbfix (högsta riskerna)
|
|
1. Radera auth-tabeller: `user_credentials`, `admin_totp`, `email_verification_tokens`, `password_reset_tokens`.
|
|
2. Radera preferenser/samtycken: `user_consents`, `user_locale_preferences`.
|
|
3. Radera notiser/push: `notifications`, `push_tokens`.
|
|
4. Radera trials/usage: `trials`, `ai_usage_counters`.
|
|
5. Radera idempotency: `idempotency_keys`.
|
|
6. Radera receptinteraktioner: `recipe_ratings`, `recipe_favorites`, `recipe_cooks`, `creator_stats`, `creator_follows`, `food_memories`.
|
|
7. Anonymisera hushållsreferenser: `inventory_transactions.actor_user_id`, `inventory_conflicts.resolved_by_user_id`, `shopping_list_items.added_by_user_id`, `meal_boxes.reserved_for_user_id`, `cooking_sessions.started_by_user_id`.
|
|
8. Hantera kvitton: radera `receipts.image_url` i lagring, skrubba `receipt_lines.raw_text`.
|
|
|
|
### 7.2 Större beslut
|
|
9. Hushållsstrategi: radera enpersonshushåll, anonymisera delade hushåll.
|
|
10. Finansiella poster: anonymisera `subscriptions`/`subscription_events`/`store_transactions` enligt retention.
|
|
11. Audit/domain events: anonymisera men behåll enligt retention.
|
|
|
|
### 7.3 Permanent vakt
|
|
12. Implementera residual-testet i `apps/api/test/me.residual.test.ts` med schema-diff-assertion.
|
|
|
|
---
|
|
|
|
## 8. Bilaga — fullständig tabellista från schema
|
|
|
|
(Används för att säkerställa att inga tabeller missats.)
|
|
|
|
```
|
|
adminTotp, aiCorrections, aiEvalCases, aiEvalRuns, aiTrainingBank,
|
|
aiUsageCounters, allergenMarketRules, analyticsDailySnapshots, auditLogs,
|
|
canonicalIngredients, cookingAssumptionProfiles, cookingSessions,
|
|
creatorFollows, creatorStats, domainEvents, emailVerificationTokens,
|
|
featureFlags, foodMemories, householdMembers, households, idempotencyKeys,
|
|
ingredientTranslations, inventoryConflicts, inventoryDecayProfiles,
|
|
inventoryItemDecayProfile, inventoryItems, inventoryTransactions,
|
|
mealBoxes, meals, memoryItems, notifications, nutritionDisplayProfiles,
|
|
passwordResetTokens, priceObservations, productAnalyticsEvents, products,
|
|
pushTokens, receiptLines, receipts, recipeCooks, recipeFavorites,
|
|
recipeIngredients, recipeRatings, recipeSimilarities, recipeSourceRegistry,
|
|
recipeStepTranslations, recipeSteps, recipeTranslations, recipes,
|
|
refreshTokens, releaseGates, scanJobs, seasonEvents, shoppingListItems,
|
|
shoppingLists, storageLocations, storeNotifications, storeTransactions,
|
|
subscriptionEvents, subscriptions, substitutions, tasteSignals, trials,
|
|
unitTranslations, userConsents, userCredentials, userHealthProfiles,
|
|
userLocalePreferences, userPreferences, users, wasteSummaries,
|
|
weekPlanEntries, weekPlans
|
|
```
|
|
|
|
Alla tabeller är granskade och klassade ovan.
|