Filipe, 22.09.2026: "ersetze die zentrale durch, Die IrrenAnstalt.
genau so geschrieben bitte. und alles was treff heißt oder wo treff
steht soll durch Rudel ersetzt werden bitte."
Die Schreibweise "IrrenAnstalt" mit grossem A in der Mitte ist so
gewollt. Das steht als Hinweis daneben, damit sie beim naechsten Mal
niemand "korrigiert".
WAS UMBENANNT WURDE: alles, was jemand LIEST -- Kachelnamen,
Ueberschriften, Markenzeilen, Saetze, Meldungen, Aufgabenvorlagen.
116 Zeilen in 42 Dateien.
WAS BLEIBT: Adressen (treff-regeln.html), Bezeichner (TREFF_ROLLEN),
Datenbankwerte (bereich = 'treff'), Abfrageteile (b=treff). Eine
Adresse umzubenennen bricht jedes Lesezeichen, und ein Datenbankwert
umzuschreiben waere eine Umstellung ohne Gegenwert. Dieselbe
Entscheidung wie heute frueh bei Dogi-Media, wo material.html auch
material.html geblieben ist.
DREI GRAMMATIKFEHLER IM EIGENEN ENTWURF, alle vor dem Ausliefern
gefunden -- ein blindes Ersetzen reicht hier nicht:
1. "der Treff" ist maennlich, "das Rudel" saechlich. Ohne Tabelle
waere ueberall "Der Rudel" gestanden. (Und "Treffer" waere zu
"Rudeler" geworden, "Treffen" zu "Rudelen" -- deshalb greift die
Regel nur bei Wortgrenze und nie vor einem Kleinbuchstaben.)
2. BINDESTRICH-ZUSAMMENSETZUNGEN. In "der Treff-Chat" gehoert der
Artikel zu "Chat", nicht zu "Treff". Der erste Durchlauf machte
daraus "das Rudel-Chat", "das Rudel-Kacheln" und "ein Rudel-Raum".
Jetzt greift die Artikelregel nur, wenn das Wort allein steht.
3. GESCHUETZTE LEERZEICHEN. In den Markenzeilen steht
`Der Treff`, damit die zwei Woerter nicht umbrechen. Das
Muster hat daran vorbeigegriffen: "Der Rudel", auf fuenf
Seiten. Jetzt wird das Trennzeichen mitgefasst und unveraendert
wieder eingesetzt.
Gefunden wurden alle drei, weil jede einzelne der 116 Zeilen vor dem
Schreiben als ALT/NEU ausgegeben und gelesen wurde -- und danach
gezielt nach falschen Artikeln gesucht ("den Rudel", "der Rudel",
"einen Rudel"). Uebrig blieben sechs Treffer, und die sind alle
richtig: "einen Rudel-Raum", "der Rudel-Chat", "den Rudel-Regeln" --
Zusammensetzungen, bei denen der Artikel zum letzten Wort gehoert.
UND ZWEI PRUEFUNGEN, die die Umbenennung nicht mitbekommen haetten:
`/Treff/.test(...)` und `/Treff/i.test(...)`. Ein regulaerer Ausdruck
ist keine Zeichenkette -- sie haetten ab sofort nach einem Wort
gesucht, das die Seite nicht mehr sagt, und waeren still rot geworden,
ohne dass etwas kaputt ist.
Geprueft, alle gruen: pruef-treff, pruef-treffchat,
pruef-deutsche-texte, pruef-uebernahme, pruef-crew-adresse,
pruef-willkommen, pruef-wege-nach-draussen, pruef-vorlagen,
pruef-start-ansicht, pruef-struktur.
Co-Authored-By: Claude Opus 5 <[email protected]>
422 lines
18 KiB
JavaScript
422 lines
18 KiB
JavaScript
/* =====================================================================
|
||
Das Löschkonzept — als Tabelle, die sich selbst durchsetzt (15.09.2026)
|
||
|
||
Stufe 8 aus [[Creator Workspace – Plan zur Perfektion Stufe 2]]:
|
||
„Löschkonzept je Datenart · IP-Adressen im Protokoll nach 30 Tagen
|
||
weg. Je mehr Daten, desto teurer das Nachholen."
|
||
|
||
---------------------------------------------------------------------
|
||
WARUM DAS KEIN DOKUMENT IST
|
||
|
||
Ein Löschkonzept als Notiz ist nach dem ersten neuen Feld falsch, und
|
||
niemand merkt es. Diese Datei ist deshalb beides: die Übersicht, was
|
||
wie lange aufgehoben wird — UND das Programm, das es tut. Was hier
|
||
nicht steht, wird nicht geräumt; was hier steht, wird geräumt.
|
||
|
||
---------------------------------------------------------------------
|
||
WAS GEMESSEN WURDE, BEVOR HIER ETWAS STAND
|
||
|
||
42 Tabellen durchgesehen, drei halten IP-Adressen:
|
||
|
||
versuche.ip wird beim Anmelden selbst im Fenster geräumt (ok)
|
||
sitzungen.ip wird beim Abmelden geloescht -- ABGELAUFENE
|
||
Sitzungen aber nie (Luecke)
|
||
protokoll.ip wird nirgends geraeumt, waechst ewig (Luecke)
|
||
|
||
Zwei echte Luecken also, nicht fuenf vermutete.
|
||
|
||
---------------------------------------------------------------------
|
||
DREI SORTEN EINTRAG, UND DIE DRITTE IST DIE WICHTIGSTE
|
||
|
||
raeumen Diese Datei loescht selbst.
|
||
fremd Eine andere Stelle loescht -- hier wird nur GEZAEHLT.
|
||
Bricht die andere Stelle, steht die Zahl trotzdem da.
|
||
bleibt Wird absichtlich aufgehoben, mit Begruendung.
|
||
|
||
Ein Loeschkonzept, das nur Loeschungen auffuehrt, ist ein halbes:
|
||
Die begruendete Aufbewahrung ist der Teil, nach dem gefragt wird.
|
||
===================================================================== */
|
||
|
||
import express from "express";
|
||
/* Die Frist steht dort, wo auch die Begruendung steht -- in
|
||
`hilfe-tabellen.js`. Sie hier ein zweites Mal hinzuschreiben hiesse
|
||
zwei Wahrheiten, von denen eine veraltet. `hilfe-tabellen.js`
|
||
importiert absichtlich nichts, es kann also kein Kreis entstehen. */
|
||
import { AUFBEWAHRUNG_TAGE as HILFE_TAGE, FALL_STAND } from "./hilfe-tabellen.js";
|
||
import {
|
||
db, sitzungLesen, istDogFather, protokolliere, echteIp,
|
||
einstellung, einstellungSetzen,
|
||
} from "./workspace.js";
|
||
|
||
export const aufbewahrungRouter = express.Router();
|
||
|
||
/** Auf welcher Grundlage die Daten eines Menschen verarbeitet werden.
|
||
*
|
||
* ABGELEITET, NICHT GESPEICHERT. Ein Modi arbeitet hier — das ist ein
|
||
* Vertragsverhaeltnis. Ein Mitglied der Community ist freiwillig da —
|
||
* das ist eine Einwilligung. Die Grundlage folgt damit aus der Rolle,
|
||
* und was sich ableiten laesst, bekommt keine eigene Spalte: Sonst
|
||
* gaebe es zwei Wahrheiten, von denen eine veraltet, sobald jemand
|
||
* die Rolle wechselt.
|
||
*
|
||
* Gebraucht wird sie an zwei Stellen: in der Auskunft (Art. 15 Abs. 1
|
||
* lit. a — der Mensch darf den Zweck erfahren) und im Loeschkonzept. */
|
||
export function grundlageVon(rolle) {
|
||
if (rolle === "gast") {
|
||
return {
|
||
grundlage: "Art. 6 Abs. 1 lit. a DSGVO – Einwilligung",
|
||
warum: "Du bist freiwillig im Rudel. Du kannst jederzeit gehen, und deine "
|
||
+ "Daten gehen mit.",
|
||
};
|
||
}
|
||
return {
|
||
grundlage: "Art. 6 Abs. 1 lit. b DSGVO – zur Durchführung des Vertrags",
|
||
warum: "Du arbeitest hier. Ohne diese Daten lässt sich die Zusammenarbeit "
|
||
+ "nicht durchführen.",
|
||
};
|
||
}
|
||
|
||
/** Wie lange IP-Adressen im Protokoll stehen bleiben.
|
||
*
|
||
* 30 Tage: lang genug, um einen Missbrauch nachzuvollziehen, den man
|
||
* erst spaeter bemerkt -- kurz genug, dass daraus kein Bewegungsbild
|
||
* wird. Dieselbe Frist, die Filipe im Plan genannt hat. */
|
||
export const IP_TAGE = 30;
|
||
|
||
const vorTagen = (n) => new Date(Date.now() - n * 86400000).toISOString();
|
||
const jetzt = () => new Date().toISOString();
|
||
|
||
/** Zaehlt, ohne zu aendern. Ein Fehler hier ist kein Befund, sondern
|
||
* eine Zahl, die fehlt -- deshalb null statt 0, damit „konnte nicht
|
||
* nachsehen" nicht wie „nichts zu tun" aussieht. */
|
||
function zaehle(d, sql, ...werte) {
|
||
try { return d.prepare(sql).get(...werte)?.n ?? null; } catch { return null; }
|
||
}
|
||
|
||
/* =====================================================================
|
||
DIE TAFEL
|
||
===================================================================== */
|
||
export const FRISTEN = [
|
||
{
|
||
/* =================================================================
|
||
DER VERTRAULICHE MELDEWEG (nachgetragen am 20.09.2026)
|
||
|
||
DIESE TABELLE FEHLTE HIER -- und das ist der teuerste Eintrag,
|
||
den man vergessen kann: In `hilfe_nachrichten` stehen die
|
||
empfindlichsten Texte des ganzen Hauses. Jemand beschreibt ein
|
||
Problem, oft mit anderen Menschen darin.
|
||
|
||
Die Frist selbst war nie strittig: `AUFBEWAHRUNG_TAGE = 90`
|
||
steht seit dem 19.09. in `hilfe-tabellen.js`, mit Begruendung.
|
||
Und `hilfeAufraeumen()` gab es auch. Nur hat sie NIEMAND
|
||
aufgerufen: Gesucht im ganzen Verzeichnis war der einzige
|
||
Aufrufer ihre eigene Pruefung. Der Kommentar darueber behauptete
|
||
„Wird beim Start aufgerufen (siehe index.js)" -- das war nie
|
||
wahr. Die Pruefung war gruen und bewies nichts ueber den
|
||
Betrieb.
|
||
|
||
Jetzt steht es hier, wo es sich selbst durchsetzt: Diese Datei
|
||
ist beides -- die Uebersicht, was wie lange aufgehoben wird, UND
|
||
das Programm, das es tut. Nebenbei taucht der Meldeweg damit in
|
||
der Auskunft auf (Art. 15 DSGVO), und das gehoert sich: Wer
|
||
gemeldet hat, darf erfahren, wie lange das noch gespeichert ist.
|
||
|
||
GEMESSEN VOR DEM EINSCHALTEN: In der echten Datenbank steht
|
||
heute KEIN einziger Fall. Der erste Lauf loescht also nichts,
|
||
und der erste Fall kann fruehestens in drei Monaten 90 Tage alt
|
||
werden. Einschalten ist jetzt frei -- spaeter waere es der
|
||
riskante Moment gewesen.
|
||
|
||
DIE NACHRICHTEN GEHEN MIT: `hilfe_nachrichten.fall_id` traegt
|
||
`ON DELETE CASCADE`, und `PRAGMA foreign_keys` steht im Dienst
|
||
auf 1. Beides nachgemessen an einem echten Fall mit Nachricht:
|
||
vorher 1/1, nachher 0/0. Ohne diese Messung waere der Fall
|
||
verschwunden und der eigentliche Text liegengeblieben. */
|
||
schluessel: "hilfe_geschlossen",
|
||
was: "Abgeschlossene vertrauliche Meldungen samt Verlauf",
|
||
art: "raeumen",
|
||
frist: `${HILFE_TAGE} Tage nach dem Abschluss`,
|
||
zweck: "Ein abgeschlossener Fall kommt manchmal wieder auf "
|
||
+ "(oft Monate spaeter) – dann muss man nachlesen "
|
||
+ "können, was damals vereinbart wurde.",
|
||
grundlage: "Art. 6 Abs. 1 lit. f DSGVO – berechtigtes Interesse "
|
||
+ "(Schutz der Mitglieder), Art. 5 Abs. 1 lit. e – Speicherbegrenzung",
|
||
wirkung: "Der Fall und ALLE Nachrichten darin verschwinden ganz. "
|
||
+ "Offene Fälle werden nie geräumt – nur abgeschlossene, und erst "
|
||
+ `${HILFE_TAGE} Tage danach.`,
|
||
offen: (d) => zaehle(d,
|
||
"SELECT COUNT(*) AS n FROM hilfe_faelle WHERE stand = ?"
|
||
+ " AND geschlossen_am IS NOT NULL AND geschlossen_am < ?",
|
||
FALL_STAND.zu, vorTagen(HILFE_TAGE)),
|
||
raeumen: (d) => d.prepare(
|
||
"DELETE FROM hilfe_faelle WHERE stand = ?"
|
||
+ " AND geschlossen_am IS NOT NULL AND geschlossen_am < ?")
|
||
.run(FALL_STAND.zu, vorTagen(HILFE_TAGE)).changes,
|
||
},
|
||
{
|
||
schluessel: "protokoll_ip",
|
||
was: "IP-Adressen im Protokoll",
|
||
art: "raeumen",
|
||
frist: `${IP_TAGE} Tage`,
|
||
zweck: "Einen Missbrauch nachvollziehen können, der erst später auffällt.",
|
||
grundlage: "Art. 6 Abs. 1 lit. f DSGVO – berechtigtes Interesse",
|
||
wirkung: "Nur die IP wird geleert. Wer wann was getan hat, bleibt stehen – "
|
||
+ "das Protokoll ist die Sicherung gegen stille Änderungen.",
|
||
offen: (d) => zaehle(d,
|
||
"SELECT COUNT(*) AS n FROM protokoll WHERE ip IS NOT NULL AND zeitpunkt < ?", vorTagen(IP_TAGE)),
|
||
raeumen: (d) => d.prepare(
|
||
"UPDATE protokoll SET ip = NULL WHERE ip IS NOT NULL AND zeitpunkt < ?")
|
||
.run(vorTagen(IP_TAGE)).changes,
|
||
},
|
||
{
|
||
schluessel: "sitzungen_abgelaufen",
|
||
was: "Abgelaufene Anmeldungen (mit IP und Browser)",
|
||
art: "raeumen",
|
||
frist: "sofort nach Ablauf",
|
||
zweck: "Angemeldet bleiben, solange die Sitzung gilt.",
|
||
grundlage: "Art. 6 Abs. 1 lit. b DSGVO – zur Durchführung erforderlich",
|
||
wirkung: "Die Zeile verschwindet ganz. Nach Ablauf ist sie zu nichts mehr nütze – "
|
||
+ "sie stand bisher nur da und trug eine IP mit sich.",
|
||
offen: (d) => zaehle(d,
|
||
"SELECT COUNT(*) AS n FROM sitzungen WHERE gueltig_bis < ?", jetzt()),
|
||
raeumen: (d) => d.prepare("DELETE FROM sitzungen WHERE gueltig_bis < ?")
|
||
.run(jetzt()).changes,
|
||
},
|
||
{
|
||
schluessel: "versuche",
|
||
was: "Fehlversuche beim Anmelden (IP)",
|
||
art: "fremd",
|
||
frist: "10 Minuten",
|
||
zweck: "Ausprobieren von Codes bremsen.",
|
||
grundlage: "Art. 6 Abs. 1 lit. f DSGVO – berechtigtes Interesse",
|
||
wirkung: "Wird beim Anmelden selbst geräumt (workspace.js). Hier steht die Zahl "
|
||
+ "trotzdem – bricht die andere Stelle, sieht man es hier.",
|
||
offen: (d) => zaehle(d,
|
||
"SELECT COUNT(*) AS n FROM versuche WHERE zeitpunkt < ?",
|
||
new Date(Date.now() - 60 * 60000).toISOString()),
|
||
},
|
||
{
|
||
schluessel: "protokoll_eintraege",
|
||
was: "Die Protokolleinträge selbst (ohne IP)",
|
||
art: "bleibt",
|
||
frist: "unbefristet",
|
||
zweck: "Nachvollziehen, wer wann einen Zugang angelegt, gesperrt oder gelöscht hat.",
|
||
grundlage: "Art. 6 Abs. 1 lit. f DSGVO – berechtigtes Interesse an Nachvollziehbarkeit",
|
||
wirkung: "Bleibt. Ein Protokoll, das sich selbst löscht, sichert nichts.",
|
||
offen: (d) => zaehle(d, "SELECT COUNT(*) AS n FROM protokoll"),
|
||
},
|
||
{
|
||
schluessel: "treff_massnahmen",
|
||
was: "Maßnahmen im Rudel (Hinweis, Pause, Ausschluss) mit Begründung",
|
||
art: "bleibt",
|
||
frist: "solange die Person ein Konto hat",
|
||
zweck: "Eine Entscheidung über einen Menschen muss begründet nachlesbar sein – "
|
||
+ "das schützt beide Seiten.",
|
||
grundlage: "Art. 6 Abs. 1 lit. f DSGVO – berechtigtes Interesse",
|
||
wirkung: "Bleibt, solange es den Zugang gibt. Wird der Zugang gelöscht, geht die "
|
||
+ "Maßnahme mit (ON DELETE CASCADE).",
|
||
offen: (d) => zaehle(d, "SELECT COUNT(*) AS n FROM treff_massnahmen"),
|
||
},
|
||
{
|
||
schluessel: "alter_bestaetigt",
|
||
was: "Der Zeitpunkt der Altersbestätigung (Community)",
|
||
art: "bleibt",
|
||
frist: "solange der Zugang besteht",
|
||
zweck: "Nachweisen können, dass das Mindestalter bestätigt wurde – "
|
||
+ "eine Regel, die man nicht belegen kann, hat man nicht.",
|
||
grundlage: "Art. 6 Abs. 1 lit. f DSGVO – berechtigtes Interesse an Nachweisbarkeit",
|
||
wirkung: "Gespeichert wird NUR der Zeitpunkt, kein Geburtsdatum. Für die Frage "
|
||
+ "„hat bestätigt, und wann?“ reicht das – ein Geburtstag wären mehr Daten "
|
||
+ "für dieselbe Auskunft.",
|
||
offen: (d) => zaehle(d,
|
||
"SELECT COUNT(*) AS n FROM personen WHERE alter_bestaetigt_am IS NOT NULL"),
|
||
},
|
||
{
|
||
/* ERLEDIGTE AUFGABEN (nachgetragen am 20.09.2026)
|
||
|
||
Aufgefallen beim Suchen nach dem naechsten `hilfe_faelle`:
|
||
Genau ZWEI Tabellen im ganzen Haus tragen eine Spalte, die
|
||
sagt „hier ist etwas zu Ende" -- `hilfe_faelle.geschlossen_am`
|
||
und `aufgaben.erledigt_am`. Die erste war gar nicht im Konzept,
|
||
die zweite ebenso wenig.
|
||
|
||
Hier ist die Antwort aber eine andere: Sie BLEIBEN. Eine
|
||
erledigte Aufgabe ist Arbeitsdokumentation, keine Meldung ueber
|
||
einen Menschen -- und wer im Dezember wissen will, was im
|
||
September verabredet war, findet es sonst nicht mehr.
|
||
|
||
`aufgaben.verantwortlich_id` traegt ON DELETE SET NULL, nicht
|
||
CASCADE: Wird eine Person geloescht, bleibt die Aufgabe stehen
|
||
und verliert nur den Namen. Auch das ist Absicht -- eine
|
||
Aufgabe, die verschwindet, weil jemand das Team verlaesst,
|
||
reisst die Geschichte des Projekts mit. */
|
||
schluessel: "aufgaben_erledigt",
|
||
was: "Erledigte und abgebrochene Aufgaben",
|
||
art: "bleibt",
|
||
frist: "unbefristet",
|
||
zweck: "Nachlesen können, was wann verabredet und getan wurde.",
|
||
grundlage: "Art. 6 Abs. 1 lit. f DSGVO – berechtigtes Interesse an "
|
||
+ "nachvollziehbarer Zusammenarbeit",
|
||
wirkung: "Bleiben. Beim Löschen einer Person verlieren sie nur den Namen "
|
||
+ "(ON DELETE SET NULL) – die Aufgabe selbst bleibt lesbar.",
|
||
offen: (d) => zaehle(d,
|
||
"SELECT COUNT(*) AS n FROM aufgaben WHERE erledigt_am IS NOT NULL"),
|
||
},
|
||
{
|
||
schluessel: "personen_geloescht",
|
||
was: "Daten einer gelöschten Person",
|
||
art: "bleibt",
|
||
frist: "gehen mit dem Zugang",
|
||
zweck: "—",
|
||
grundlage: "Art. 17 DSGVO – Recht auf Löschung",
|
||
wirkung: "Wer in „Personen & Zugänge“ gelöscht wird, nimmt Sitzungen, Maßnahmen, "
|
||
+ "Entwicklungsstände und Chat-Zugehörigkeit mit – die Datenbank erzwingt das "
|
||
+ "über ON DELETE CASCADE, nicht eine Liste, die jemand pflegt.",
|
||
offen: (d) => zaehle(d, "SELECT COUNT(*) AS n FROM personen WHERE aktiv = 0"),
|
||
},
|
||
];
|
||
|
||
/* =====================================================================
|
||
DER LAUF
|
||
===================================================================== */
|
||
|
||
/** Räumt alles, was zu räumen ist.
|
||
*
|
||
* @returns {{getan: Array, fehler: Array}} was passiert ist
|
||
*/
|
||
export function aufbewahrungLauf() {
|
||
const d = db();
|
||
const getan = [];
|
||
const fehler = [];
|
||
for (const f of FRISTEN) {
|
||
if (typeof f.raeumen !== "function") continue;
|
||
try {
|
||
const anzahl = f.raeumen(d);
|
||
if (anzahl) getan.push({ schluessel: f.schluessel, anzahl });
|
||
} catch (fe) {
|
||
/* EIN FEHLSCHLAG DARF DEN REST NICHT AUFHALTEN -- und er darf
|
||
nicht still bleiben. Waere er still, hiesse „aufgeraeumt"
|
||
irgendwann „einmal probiert". */
|
||
fehler.push({ schluessel: f.schluessel, grund: String(fe?.message || fe).slice(0, 200) });
|
||
console.error("[aufbewahrung]", f.schluessel, fe?.message);
|
||
}
|
||
}
|
||
try {
|
||
einstellungSetzen("aufbewahrung_letzte",
|
||
JSON.stringify({ zeitpunkt: jetzt(), getan, fehler }));
|
||
} catch { /* der Lauf zaehlt, nicht sein Vermerk */ }
|
||
|
||
/* Nur protokollieren, wenn wirklich etwas passiert ist. Ein taeglicher
|
||
Eintrag „nichts zu tun" waere genau die Art Rauschen, die das
|
||
Protokoll unlesbar macht -- und ausgerechnet dieses Protokoll ist
|
||
die Sicherung gegen stille Aenderungen. */
|
||
if (getan.length || fehler.length) {
|
||
try {
|
||
protokolliere("aufbewahrung", {
|
||
detail: getan.map((g) => `${g.schluessel}: ${g.anzahl}`).join(", ")
|
||
+ (fehler.length ? ` | FEHLER: ${fehler.map((f) => f.schluessel).join(", ")}` : ""),
|
||
});
|
||
} catch { /* egal */ }
|
||
}
|
||
return { getan, fehler };
|
||
}
|
||
|
||
/** Einmal am Tag, und einmal kurz nach dem Start.
|
||
*
|
||
* Nicht im Startmoment: Erst soll die Website antworten. Dieselbe
|
||
* Ueberlegung wie bei der Sicherung -- ein Aufraeumlauf im
|
||
* Startmoment wuerde die Datenbank nur unnoetig frueh oeffnen und
|
||
* einen Fehler dort zum Startfehler machen. */
|
||
export function aufbewahrungStarten() {
|
||
const ersterLauf = setTimeout(() => { try { aufbewahrungLauf(); } catch { /* egal */ } }, 90_000);
|
||
const takt = setInterval(() => { try { aufbewahrungLauf(); } catch { /* egal */ } }, 24 * 3600_000);
|
||
ersterLauf.unref?.();
|
||
takt.unref?.();
|
||
}
|
||
|
||
/* =====================================================================
|
||
DIE TAFEL ZUM ANSEHEN
|
||
===================================================================== */
|
||
|
||
/** Kommt die Anfrage von unserer eigenen Seite?
|
||
*
|
||
* DERSELBE WORTLAUT WIE IN DEN ANDEREN MODULEN und bewusst keine
|
||
* eigene Erfindung: Eine zweite, leicht andere Fassung waere eine
|
||
* zweite Wahrheit, und beim naechsten Nachschaerfen wuerde genau die
|
||
* eine vergessen.
|
||
*
|
||
* KEIN `origin` HEISST DURCHLASSEN, und das ist Absicht: Browser
|
||
* schicken das Feld bei schreibenden Anfragen immer mit. Fehlt es,
|
||
* kommt die Anfrage aus einem Werkzeug (curl, eine Pruefung) -- und
|
||
* dort ist die Sitzung ohnehin nur da, wenn jemand sie absichtlich
|
||
* mitgibt. Der Angriff, um den es geht, braucht einen Browser. */
|
||
function gleicheHerkunft(req, res, next) {
|
||
const herkunft = req.get("origin");
|
||
if (!herkunft) return next();
|
||
let erlaubt;
|
||
try { erlaubt = new URL(herkunft).host === req.get("host"); } catch { erlaubt = false; }
|
||
if (!erlaubt) return res.status(403).json({ fehler: "fremde_herkunft" });
|
||
next();
|
||
}
|
||
|
||
function nurDogFather(req, res, next) {
|
||
const person = sitzungLesen(req);
|
||
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
||
/* 404 und nicht 403: Wer „kein Zugriff" liest, weiss, dass es die
|
||
Seite gibt. Dieselbe Antwort wie ueberall im Haus. */
|
||
if (!istDogFather(person)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
req.person = person;
|
||
next();
|
||
}
|
||
|
||
aufbewahrungRouter.get("/workspace/api/aufbewahrung", nurDogFather, (req, res) => {
|
||
try {
|
||
const d = db();
|
||
let letzte = null;
|
||
try { letzte = JSON.parse(einstellung("aufbewahrung_letzte") || "null"); } catch { letzte = null; }
|
||
|
||
const zeilen = FRISTEN.map((f) => ({
|
||
schluessel: f.schluessel,
|
||
was: f.was,
|
||
art: f.art,
|
||
frist: f.frist,
|
||
zweck: f.zweck,
|
||
grundlage: f.grundlage,
|
||
wirkung: f.wirkung,
|
||
/* `offen` heisst bei „raeumen": so viele warten. Bei „bleibt":
|
||
so viele liegen da. Zwei verschiedene Bedeutungen unter einem
|
||
Namen waeren eine Falle -- deshalb steht `art` daneben und die
|
||
Oberflaeche beschriftet die Zahl danach. */
|
||
anzahl: f.offen ? f.offen(d) : null,
|
||
}));
|
||
|
||
res.json({ fristen: zeilen, letzte, ip_tage: IP_TAGE });
|
||
} catch (fehler) {
|
||
console.error("[aufbewahrung] Tafel:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/** Von Hand anstossen.
|
||
*
|
||
* Damit es kein schwarzer Kasten ist: Wer sehen will, ob es wirkt,
|
||
* drueckt drauf und sieht die Zahlen fallen. */
|
||
/* DER RIEGEL STEHT VOR DER RECHTEPRUEFUNG.
|
||
Reihenfolge wie im ganzen Haus: erst "kommt das von uns?", dann
|
||
"darf der das?". Andersherum waere es nicht falsch, aber die
|
||
Abweichung faellt beim Lesen auf und kostet jedes Mal einen
|
||
Moment. */
|
||
aufbewahrungRouter.post("/workspace/api/aufbewahrung/jetzt", gleicheHerkunft, nurDogFather, (req, res) => {
|
||
try {
|
||
const ergebnis = aufbewahrungLauf();
|
||
protokolliere("aufbewahrung_von_hand", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
detail: ergebnis.getan.map((g) => `${g.schluessel}: ${g.anzahl}`).join(", ") || "nichts zu tun",
|
||
});
|
||
res.json(ergebnis);
|
||
} catch (fehler) {
|
||
console.error("[aufbewahrung] Lauf:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|