ZKRINET Docs
Byggblock

GDPR — export, radering och samtycken

På Zkrinet: lägg till blocket under Byggblock i panelen — ett klick, din egen kod rörs inte och en ögonblicksbild tas först. Installationsstegen nedan gäller om du kör PocketBase själv, till exempel efter en export.

För appar med användare i EU. Den inloggade användaren kan själv:

Användarna och inloggningen är PocketBases egna (samlingen users). Pluginet lägger till rutterna och loggen ovanpå.

Testat mot PocketBase 0.40.4.

Installation

Kopiera pb_hooks/ och pb_migrations/ till din PocketBase och starta om. Tre samlingar dyker upp: zk_gdpr_installningar, zk_gdpr_samtycken och zk_gdpr_raderingar.

Obs, en ändring av standardbeteendet: PocketBase låter som standard en användare radera sitt eget konto direkt (DELETE /api/collections/users/records/{id}, regeln id = @request.auth.id) — utan att bekräfta något. Med pluginet stängs den dörren, så att radering alltid går via lösenordsbekräftelsen nedan. Superusers kan alltid radera. Vill du ha kvar den gamla dörren: slå på tillat_direktradering i inställningarna.

Användning från appen

import PocketBase from "pocketbase"
const pb = new PocketBase("https://din-app.se")

// 1. Exportera min data (spara som fil)
const data = await pb.send("/api/zk/gdpr/export", { method: "GET" })
const fil = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" })

// 2. Radera mitt konto — kräver lösenordet
try {
  const svar = await pb.send("/api/zk/gdpr/radera", { method: "POST", body: { losenord } })
  if (svar.raderad) pb.authStore.clear()          // borta direkt
  else visa(`Kontot raderas ${svar.verkstalls}`)  // karenstid: kan ångras
} catch (err) {
  // 400 = lösenord saknas · 403 = fel lösenord · 429 = för många försök · 409 = se err.data.hinder
}

// 2b. Ångra under karenstiden
await pb.send("/api/zk/gdpr/angra", { method: "POST" })

// 3. Samtycken
await pb.send("/api/zk/gdpr/samtycke", { method: "POST", body: { typ: "nyhetsbrev", version: "2026-11", givet: true } })
await pb.send("/api/zk/gdpr/samtycke", { method: "POST", body: { typ: "nyhetsbrev", givet: false } }) // återkalla
const lage = await pb.send("/api/zk/gdpr/samtycke", { method: "GET" })
// → { nyhetsbrev: { givet: false, version: "", tid: "..." } }

// Hela historiken (bara mina egna rader syns)
await pb.collection("zk_gdpr_samtycken").getFullList({ sort: "tid" })

Alla rutter kräver en inloggad användare i users. Gäster får 401; en superuser-token räknas inte som användare.

Exporten

{
  "exporterad": "2026-11-07T19:00:00.000Z",
  "anvandare": { "id": "...", "email": "...", "name": "...", ... },
  "samlingar": {
    "anteckningar": { "falt": ["agare"], "poster": [ ... ], "avkortad": false },
    "zk_gdpr_samtycken": { ... }
  },
  "inloggning": { "_externalAuths": [ ... ], "_authOrigins": [ ... ] }
}

Vad som INTE är med, och varför:

zk_-samlingar från andra plugins tas med när de pekar på användaren — t.ex. zk_ai_logg (frågor till AI:n är användarens data), zk_stripe_betalningar och zk_gdpr_samtycken. Regeln är alltså: har posten ett relationsfält till användaren är den användardata, oavsett vem som skapat samlingen.

Inställningar

En rad i zk_gdpr_installningar (bara superusers ser och ändrar den; det kan bara finnas en). Den skapas vid installationen med 7 dagars ångerfrist:

FältStandard
endast[] = allaLista med samlingsnamn: bara dessa tas med i exporten
undanta[]Lista med samlingsnamn som aldrig tas med
karens_dagar7 (standardraden)Ångerfrist i dagar (max 365). 0 = radera direkt. Finns ingen rad alls raderas det direkt.
tillat_direktraderingavPå = PocketBase vanliga självradering (utan lösenord) är öppen igen

Raderingen

Bekräftelse: lösenordet skickas med ({ "losenord": "..." }) och kontrolleras mot PocketBases lösenordshash. Valet lösenord (inte "nyss inloggad") är medvetet: en stulen eller kvarglömd token räcker inte för att radera kontot. Efter 5 fel inom 15 minuter spärras kontots raderingsförsök i 15 minuter (429), även med rätt lösenord.

Vad som händer med andra poster — PocketBases egna regler för relationsfältet, testat:

RelationsfältetHänder
cascadeDelete påPosten raderas med kontot (t.ex. samtycken, AI-loggen).
Utan cascade, ej obligatorisktPosten blir kvar, kopplingen tas bort (fältet blir tomt; i fleravalsfält försvinner bara id:t). Valt för att t.ex. ordrar och betalningar ofta måste sparas enligt bokföringslagen.
Utan cascade, obligatorisktRaderingen stoppas (409) med en lista: hinder: [{ samling, falt, poster }]. Pluginet gissar inte — appens ägare får välja cascade, valfritt fält eller att ta bort posterna.

Vi anonymiserar inte innehållet i poster som blir kvar. Står användarens namn eller e-post i ett vanligt textfält i en sådan post så står det kvar — det är appens sak att ta hand om (t.ex. med en onRecordDelete-hook för users).

Karenstid: med karens_dagar > 0 blir raderingen en begäran (202, verkstalls = datum). Användaren kan logga in och ångra (/api/zk/gdpr/angra) fram till dess. Raderingarna körs varje timme (minut 7), eller direkt med POST /api/zk/gdpr/verkstall (superuser). Har ett hinder uppstått under tiden står det i svaret och i serverloggen, och begäran ligger kvar. Användaren ser sin egen begäran i zk_gdpr_raderingar.

Samtyckesloggen

zk_gdpr_samtycken: anvandare, typ (t.ex. nyhetsbrev, villkor; små bokstäver, siffror, _ . -), version (t.ex. villkorens version), givet (true/false), tid.

Begränsningar — läs innan du lovar en kund något

Testa

./bank.py gdpr

14 tester: exporten innehåller rätt och inget annat (andra användares poster, konton och e-post aldrig; dolda fält, lösenordshash och tokenKey aldrig; fleravalsrelationer), inställningarna (endast/undanta, en rad, bara superuser), gäster och superusers nekas, radering kräver lösenord, gissningsspärr, PocketBase egen självradering stängd, obligatorisk relation stoppar (och fleraval med fler kvar gör det inte), cascade/kvarlämnade poster, karenstid med ångra och verkställ (och hinder som uppstår under karenstiden), samtycken append-only (även för superuser), användaren ser bara sina egna, och validering. Säkerhetsskydden är mutantverifierade (18 mutanter, alla fångade).