Schreibt jemand "Welche Daten haben Sie ueber mich?" oder "Bitte loeschen Sie meine Daten", laeuft eine Frist von einem Monat (Art. 12 Abs. 3 DSGVO). Bisher haette man dafuer von Hand durch ein Dutzend Tabellen suchen muessen -- und uebersieht man eine, ist die Auskunft unvollstaendig, ohne dass man es ihr ansieht. ⚠️ LOESCHEN IST NICHT EINFACH LOESCHEN Der naheliegende Weg waere ein "Alles loeschen". Das waere bequem und doppelt falsch: Rechnungen und Buchungsbelege unterliegen einer gesetzlichen Aufbewahrungspflicht -- § 147 Abs. 3 AO nennt acht Jahre fuer Buchungsbelege, zehn fuer Handelsbuecher, gerechnet ab dem Ende des Kalenderjahres (Abs. 4). Wer sie auf Zuruf loescht, verstoesst gegen Steuerrecht, um Datenschutzrecht zu erfuellen. Die DSGVO nimmt diesen Fall selbst aus (Art. 17 Abs. 3 lit. b). Fuer das, was bleiben muss, sieht sie die Einschraenkung der Verarbeitung vor (Art. 18) -- genau so wird es ausgewiesen, samt Datum, ab dem geloescht werden darf. Dasselbe bei Widerrufserklaerungen: Sie sind der Nachweis, DASS und WANN widerrufen wurde. Wer sie loescht, vernichtet seinen eigenen Beleg in genau der Sache, in der es spaeter Streit geben koennte. EIN FEHLER, DEN DER TEST VOR DEM ERSTEN LAUF GEFUNDEN HAT Die Tabelle wd_widerrufe fuehrt die Adresse nicht als "email", sondern als "kontakt". Die erste Fassung suchte nach "email" -- sie haette dort NIE einen Treffer geliefert, und die Auskunft haette trotzdem sauber ausgesehen. Aufgefallen nur, weil die Testdaten gegen das echte Schema angelegt wurden statt gegen die Annahme. Deshalb steht in der Quellenliste jetzt die Spalte, nicht eine Vermutung. WEITERE ENTSCHEIDUNGEN - Die Suche geht ueber die Adresse UND ueber die Kundenkennung. Vieles haengt nicht an der Adresse; ohne diesen Schritt bliebe die halbe Auskunft leer. - Gross- und Kleinschreibung spielt keine Rolle -- sonst bekaeme jemand eine leere Auskunft, weil er seine Adresse anders schreibt. - Die Loeschung verlangt die Adresse ein zweites Mal. Der Unterschied zwischen .org und .com ist ein Buchstabe, die Folge unwiederbringlich. - Sie laeuft in einer Transaktion: Bricht sie ab, bliebe sonst ein halbgeloeschter Bestand, und niemand wuesste, welcher Teil weg ist. - Der Vorgang wird protokolliert, aber OHNE die geloeschten Inhalte -- sie dabei erneut zu speichern waere das Gegenteil des Zwecks. 15 Pruefungen gruen, mit Gegenprobe (fremde Adresse liefert nichts). Die Oberflaeche in der Verwaltung folgt. Deploy braucht Filipe: server-internal liegt unter /home/dogiintern mit Rechten 700. Co-Authored-By: Claude Opus 5 <[email protected]>
145 lines
5.5 KiB
JavaScript
145 lines
5.5 KiB
JavaScript
/* =====================================================================
|
|
webdesign-datenschutz.js — Auskunft und Löschung (DSGVO Art. 15/17)
|
|
|
|
Drei Endpunkte, und die Reihenfolge ist Absicht:
|
|
|
|
auskunft — was ist gespeichert (unschädlich, jederzeit)
|
|
loeschvorschau — was geschähe bei einer Löschung
|
|
loeschen — führt aus, und nur mit ausdrücklicher Bestätigung
|
|
|
|
Eine Löschung ist endgültig. Sie gehört nicht hinter einen Knopf, der
|
|
sofort zuschlägt — deshalb ist die Vorschau ein eigener Schritt und
|
|
die Ausführung verlangt zusätzlich die Adresse als Bestätigung, so
|
|
wie man es beim Löschen eines Kontos kennt.
|
|
===================================================================== */
|
|
|
|
import { db } from "../db.js";
|
|
import { json } from "../lib/http.js";
|
|
import { requireSession } from "../lib/auth.js";
|
|
import { hasPermission } from "../lib/permissions.js";
|
|
import { ausweisPruefen } from "../lib/webdesign-ausweis.js";
|
|
import { text, verlauf } from "../lib/webdesign-helfer.js";
|
|
import { datenZuPerson, loeschvorschau } from "../lib/webdesign-personendaten.js";
|
|
|
|
async function darf(req, recht) {
|
|
const ausweis = ausweisPruefen(req);
|
|
if (ausweis) return { erlaubt: true, sitzung: { actor: ausweis.rolle, isOwner: true } };
|
|
const sitzung = await requireSession(req);
|
|
if (!sitzung) return { erlaubt: false, grund: 401, sitzung: null };
|
|
if (sitzung.isOwner) return { erlaubt: true, sitzung };
|
|
const ok = hasPermission(sitzung.permissions, sitzung.overrides, recht);
|
|
return { erlaubt: ok, grund: ok ? null : 403, sitzung };
|
|
}
|
|
|
|
/* Auskunft und Löschung berühren ALLE Kundendaten. Das ist bewusst an
|
|
das Recht zum Verwalten von Kunden geknüpft, nicht an das blosse
|
|
Ansehen von Anfragen. */
|
|
const RECHT = "WD_KUNDEN_MANAGE";
|
|
|
|
export async function dsAuskunft(req, res) {
|
|
const p = await darf(req, RECHT);
|
|
if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund);
|
|
|
|
try {
|
|
const daten = datenZuPerson(text(req.body?.email, 200));
|
|
|
|
/* Der Zugriff wird protokolliert. Eine Auskunft ist ein
|
|
Verwaltungsvorgang mit Frist -- man will später belegen können,
|
|
wann sie erstellt wurde. */
|
|
verlauf("datenschutz", daten.email, "auskunft erstellt",
|
|
p.sitzung?.actor || "unbekannt", { datensaetze: daten.datensaetze });
|
|
|
|
return json(res, { ok: true, ...daten });
|
|
} catch (e) {
|
|
return json(res, { ok: false, error: e.message || "Fehlgeschlagen." }, 400);
|
|
}
|
|
}
|
|
|
|
export async function dsLoeschvorschau(req, res) {
|
|
const p = await darf(req, RECHT);
|
|
if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund);
|
|
|
|
try {
|
|
return json(res, { ok: true, ...loeschvorschau(text(req.body?.email, 200)) });
|
|
} catch (e) {
|
|
return json(res, { ok: false, error: e.message || "Fehlgeschlagen." }, 400);
|
|
}
|
|
}
|
|
|
|
export async function dsLoeschen(req, res) {
|
|
const p = await darf(req, RECHT);
|
|
if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund);
|
|
|
|
const email = text(req.body?.email, 200);
|
|
const bestaetigung = text(req.body?.bestaetigung, 200);
|
|
|
|
/* Die Adresse muss ein zweites Mal eingegeben werden. Das ist keine
|
|
Schikane: Der Unterschied zwischen "[email protected]" und
|
|
"[email protected]" ist ein Buchstabe, und die Folge wäre, dass die
|
|
Daten der falschen Person gelöscht werden -- unwiederbringlich. */
|
|
if (!email || email.toLowerCase() !== bestaetigung.toLowerCase()) {
|
|
return json(res, {
|
|
ok: false,
|
|
error: "Zur Bestätigung bitte dieselbe E-Mail-Adresse ein zweites Mal eingeben.",
|
|
}, 400);
|
|
}
|
|
|
|
let vorschau;
|
|
try {
|
|
vorschau = loeschvorschau(email);
|
|
} catch (e) {
|
|
return json(res, { ok: false, error: e.message }, 400);
|
|
}
|
|
|
|
if (!vorschau.loeschbar.length) {
|
|
return json(res, {
|
|
ok: true, geloescht: 0,
|
|
hinweis: vorschau.bleibt.length
|
|
? "Es gibt nichts zu löschen — alles Gefundene unterliegt einer Aufbewahrungspflicht."
|
|
: "Zu dieser Adresse ist nichts gespeichert.",
|
|
bleibt: vorschau.bleibt,
|
|
});
|
|
}
|
|
|
|
/* Alles oder nichts. Bricht es mittendrin ab, bliebe ein
|
|
halbgelöschter Bestand zurück -- und niemand wüsste, welcher Teil
|
|
schon weg ist. */
|
|
const daten = datenZuPerson(email);
|
|
let geloescht = 0;
|
|
|
|
const durchfuehren = db.transaction(() => {
|
|
for (const bereich of vorschau.loeschbar) {
|
|
const b = daten.bereiche.find((x) => x.tabelle === bereich.tabelle);
|
|
if (!b) continue;
|
|
for (const zeile of b.zeilen) {
|
|
if (!zeile.id) continue;
|
|
const r = db.prepare(`DELETE FROM ${bereich.tabelle} WHERE id = ?`).run(zeile.id);
|
|
geloescht += r.changes;
|
|
}
|
|
}
|
|
});
|
|
|
|
try {
|
|
durchfuehren();
|
|
} catch (e) {
|
|
return json(res, { ok: false, error: "Löschung abgebrochen: " + e.message }, 500);
|
|
}
|
|
|
|
/* Der Vorgang selbst wird festgehalten -- OHNE die gelöschten Inhalte.
|
|
Man muss belegen können, DASS und WANN gelöscht wurde; die Daten
|
|
dabei erneut zu speichern wäre das Gegenteil des Zwecks. */
|
|
verlauf("datenschutz", email, "daten geloescht",
|
|
p.sitzung?.actor || "unbekannt",
|
|
{ geloescht, bereiche: vorschau.loeschbar.map((x) => x.was), aufbewahrt: vorschau.bleibt.length });
|
|
|
|
return json(res, {
|
|
ok: true,
|
|
geloescht,
|
|
bleibt: vorschau.bleibt,
|
|
hinweis: vorschau.bleibt.length
|
|
? "Gelöscht, soweit zulässig. Der Rest unterliegt einer Aufbewahrungspflicht "
|
|
+ "und wird nur noch aufbewahrt, nicht mehr verwendet (Art. 18 DSGVO)."
|
|
: "Alle Daten zu dieser Adresse wurden gelöscht.",
|
|
});
|
|
}
|