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]>
150 lines
6.3 KiB
JavaScript
150 lines
6.3 KiB
JavaScript
/* =====================================================================
|
|
Prüft Auskunft und Löschvorschau (DSGVO Art. 15/17).
|
|
|
|
Der gefährlichste Fehler hier ist eine UNVOLLSTÄNDIGE Auskunft: Sie
|
|
sieht genauso aus wie eine vollständige. Wer eine Tabelle vergisst,
|
|
merkt es nicht — die Antwort wirkt sauber, ist aber falsch, und im
|
|
Streitfall steht man mit einer nachweislich lückenhaften Auskunft da.
|
|
|
|
Deshalb wird hier eine Person angelegt, deren Daten sich über
|
|
mehrere Tabellen verteilen — auch über solche, die nicht die
|
|
E-Mail-Adresse führen, sondern nur die Kundenkennung. Genau die
|
|
werden beim Suchen von Hand übersehen.
|
|
|
|
Der zweite Test ist die Trennung beim Löschen: Rechnungen und
|
|
Widerrufe dürfen NICHT im Löschbaren landen.
|
|
===================================================================== */
|
|
import crypto from "crypto";
|
|
import fs from "fs";
|
|
import { fileURLToPath } from "url";
|
|
import { dirname, join } from "path";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const TEST_DB = join(__dirname, "test-personen.db");
|
|
|
|
for (const e of ["", "-wal", "-shm"]) fs.rmSync(TEST_DB + e, { force: true });
|
|
process.env.DB_PATH = TEST_DB;
|
|
process.env.ENCRYPTION_KEY = Buffer.from(crypto.randomBytes(32)).toString("base64");
|
|
|
|
const { db, initDb } = await import("./db.js");
|
|
initDb();
|
|
|
|
const P = await import("./lib/webdesign-personendaten.js");
|
|
|
|
let ok = 0, fehl = 0;
|
|
const pruefe = (name, gut, info) => {
|
|
if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + info : "")); }
|
|
else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); }
|
|
};
|
|
|
|
/* --- Eine Person mit verteilten Spuren ---------------------------- */
|
|
const MAIL = "[email protected]";
|
|
const jetzt = new Date().toISOString();
|
|
const kundeId = "k-test-1";
|
|
const anfrageId = "a-test-1";
|
|
|
|
/* Genau die Pflichtfelder des echten Schemas — aus PRAGMA table_info
|
|
ausgelesen, nicht geraten. Beim ersten Versuch stand hier ein Feld
|
|
"nummer", das es in wd_kunden gar nicht gibt. */
|
|
db.prepare(`INSERT INTO wd_kunden (id, email, name, firma, telefon, erstellt_am)
|
|
VALUES (?,?,?,?,?,?)`)
|
|
.run(kundeId, MAIL, "Anna Beispiel", "Beispiel GmbH", "0170 000", jetzt);
|
|
|
|
db.prepare(`INSERT INTO wd_anfragen (id, nummer, name, email, paket, ziel, erstellt_am)
|
|
VALUES (?,?,?,?,?,?,?)`)
|
|
.run(anfrageId, "A-9001", "Anna Beispiel", MAIL, "shop", "Ein Shop.", jetzt);
|
|
|
|
/* Diese hängen NUR über die Kundenkennung — der Fall, der beim Suchen
|
|
von Hand durchrutscht. */
|
|
let mitZahlung = false;
|
|
try {
|
|
db.prepare(`INSERT INTO wd_zahlungen (id, nummer, kunde_id, art, zweck_text, betrag_cent, erstellt_am)
|
|
VALUES (?,?,?,?,?,?,?)`)
|
|
.run("z1", "R-9001", kundeId, "anzahlung", "Anzahlung Shop", 120000, jetzt);
|
|
mitZahlung = true;
|
|
} catch (e) { console.log(" (Zahlungstabelle abweichend: " + String(e.message).slice(0, 60) + ")"); }
|
|
|
|
let mitWiderruf = false;
|
|
try {
|
|
/* ⚠️ Die Spalte heisst hier "kontakt", nicht "email". Genau daran
|
|
ist die erste Fassung des Moduls gescheitert -- sie haette
|
|
Widerrufe nie gefunden, und die Auskunft haette trotzdem sauber
|
|
ausgesehen. */
|
|
db.prepare(`INSERT INTO wd_widerrufe (id, nummer, name, kontakt, vertrag, eingegangen_am)
|
|
VALUES (?,?,?,?,?,?)`)
|
|
.run("w1", "W-9001", "Anna Beispiel", MAIL, "P-9001", jetzt);
|
|
mitWiderruf = true;
|
|
} catch (e) { console.log(" (Widerrufstabelle abweichend: " + String(e.message).slice(0, 60) + ")"); }
|
|
|
|
console.log("1. AUSKUNFT (Art. 15) — WIRD ALLES GEFUNDEN?");
|
|
|
|
const a = P.datenZuPerson(MAIL);
|
|
pruefe("Kundenkonto gefunden", a.bereiche.some((b) => b.tabelle === "wd_kunden"));
|
|
pruefe("Anfrage gefunden", a.bereiche.some((b) => b.tabelle === "wd_anfragen"));
|
|
pruefe("Kundenkennung ermittelt", a.kundenIds.includes(kundeId), a.kundenIds.join(","));
|
|
|
|
if (mitZahlung) {
|
|
pruefe("Zahlung ueber die Kundenkennung gefunden",
|
|
a.bereiche.some((b) => b.tabelle === "wd_zahlungen"),
|
|
"haengt NICHT an der E-Mail — der Fall, der von Hand durchrutscht");
|
|
}
|
|
if (mitWiderruf) {
|
|
pruefe("Widerruf gefunden", a.bereiche.some((b) => b.tabelle === "wd_widerrufe"));
|
|
}
|
|
|
|
pruefe("Anzahl wird ausgewiesen", a.datensaetze >= 2, a.datensaetze + " Datensaetze");
|
|
|
|
/* Gegenprobe: Eine fremde Adresse darf NICHTS liefern. Ohne sie könnte
|
|
die Abfrage alles zurückgeben und der Test wäre trotzdem grün. */
|
|
const fremd = P.datenZuPerson("[email protected]");
|
|
pruefe("fremde Adresse liefert nichts", fremd.datensaetze === 0, fremd.datensaetze + " Datensaetze");
|
|
|
|
/* Und Groß-/Kleinschreibung darf keine Rolle spielen — sonst bekommt
|
|
jemand eine leere Auskunft, weil er seine Adresse anders schreibt. */
|
|
const gross = P.datenZuPerson(MAIL.toUpperCase());
|
|
pruefe("Gross- und Kleinschreibung egal", gross.datensaetze === a.datensaetze,
|
|
gross.datensaetze + " gegen " + a.datensaetze);
|
|
|
|
console.log("\n2. LOESCHVORSCHAU (Art. 17) — WIRD RICHTIG GETRENNT?");
|
|
|
|
const v = P.loeschvorschau(MAIL);
|
|
pruefe("Anfrage steht im Loeschbaren",
|
|
v.loeschbar.some((x) => x.tabelle === "wd_anfragen"));
|
|
|
|
if (mitZahlung) {
|
|
/* Der wichtigste Test dieser Datei: Rechnungen dürfen NICHT
|
|
gelöscht werden. § 147 Abs. 3 AO. */
|
|
pruefe("Zahlungen stehen NICHT im Loeschbaren",
|
|
!v.loeschbar.some((x) => x.tabelle === "wd_zahlungen"));
|
|
const z = v.bleibt.find((x) => /Zahlungen/.test(x.was));
|
|
pruefe("Zahlungen sind als aufbewahrungspflichtig ausgewiesen", !!z,
|
|
z ? z.grund : "fehlt");
|
|
pruefe("mit einem Datum, ab dem geloescht werden darf",
|
|
!!(z && /^\d{4}-12-31$/.test(z.freiAb)), z ? z.freiAb : "—");
|
|
}
|
|
|
|
if (mitWiderruf) {
|
|
pruefe("Widerrufe stehen NICHT im Loeschbaren",
|
|
!v.loeschbar.some((x) => x.tabelle === "wd_widerrufe"));
|
|
}
|
|
|
|
pruefe("es gibt einen Hinweistext fuer die Antwort an die Person",
|
|
v.hinweis && v.hinweis.length > 40);
|
|
|
|
if (mitZahlung || mitWiderruf) {
|
|
pruefe("der Hinweis nennt die Einschraenkung nach Art. 18",
|
|
/Art\. 18/.test(v.hinweis));
|
|
}
|
|
|
|
console.log("\n3. WAS DIE VORSCHAU AUSGIBT");
|
|
console.log(" loeschbar:");
|
|
for (const l of v.loeschbar) console.log(` ${l.anzahl}x ${l.was}`);
|
|
console.log(" bleibt:");
|
|
for (const b of v.bleibt) console.log(` ${b.anzahl}x ${b.was} — ${b.grund}, frei ab ${b.freiAb}`);
|
|
|
|
db.close();
|
|
for (const e of ["", "-wal", "-shm"]) fs.rmSync(TEST_DB + e, { force: true });
|
|
|
|
console.log(`\n===== ${ok} bestanden, ${fehl} fehlgeschlagen =====`);
|
|
process.exit(fehl ? 1 : 0);
|