Files
dogfather-universe/server/index.js
T
DogFatherGitandClaude Opus 5 7a393b9335 Workspace: naechtliche Sicherung, die auch wirklich Daten enthaelt
Bisher wurde nur EINMAL gesichert: direkt vor einer Umstellung der
Datenbank. Faellt die Platte aus, ist alles weg -- die komplette
Betreuung, jede Aufgabe, jedes Protokoll. Das war das einzige Risiko im
System, das auf einen Schlag ALLES kostet.

Beim Nachsehen auf dem Server kam ein zweiter, schwerwiegenderer Befund
dazu:

    workspace.db          4.096 B
    workspace.db-wal  2.101.232 B

Im WAL-Betrieb landen Schreibvorgaenge zuerst im -wal und wandern erst
beim Checkpoint in die Hauptdatei. Der lief seit dem ersten Tag nicht.
Wer also workspace.db kopiert haette -- der naheliegendste Sicherungsweg
ueberhaupt -- haette eine LEERE Datenbank gesichert und es nicht gemerkt.
Der Test stellt genau das nach: aus der reinen Dateikopie waren 0 von 200
Aufgaben lesbar.

Deshalb:
- Nie eine Dateikopie. Immer VACUUM INTO -- SQLite schreibt selbst, liest
  das WAL mit, Ergebnis ist in sich stimmig auch waehrend Schreibzugriffen.
- Vor jeder Sicherung ein erzwungener Checkpoint (TRUNCATE). Haelt die
  Hauptdatei aktuell und das WAL klein.
- Jede frische Sicherung wird SOFORT wieder geoeffnet und geprueft: auf
  Unversehrtheit UND darauf, dass Personen darin stehen -- gegen genau den
  Fall einer technisch einwandfreien, aber leeren Datei. Faellt sie durch,
  wird sie geloescht statt behalten.
- Erst unter Zwischennamen schreiben, dann pruefen, dann umbenennen. Sonst
  stuende nach einem Abbruch eine halbe Datei unter dem richtigen Namen.
- Aufbewahrung 7 taeglich / 4 woechentlich / 6 monatlich, je Art getrennt
  aufgeraeumt, damit taegliche nie die monatlichen verdraengen.

Laeuft IM Prozess, nicht als Systemdienst: keine Aenderung an Systemdateien
noetig, die Datenbank ist ohnehin offen, und es kommt mit jedem git pull
mit. Statt setTimeout auf 24 h wird viertelstuendlich geprueft, ob fuer
HEUTE schon eine liegt -- das ueberlebt Neustarts und Ausfaelle.

Dazu eine Zustandsansicht auf der Automationen-Seite: wann zuletzt, wie
gross, wie viele Personen darin, wie viel wartet noch im WAL, wie viel
Platz ist frei. Plus "Jetzt sichern" von Hand. Nur Leitung, 404 statt 403
fuer alle anderen.

Geprueft (server/pruef-sicherung.mjs, 27 Pruefungen) einschliesslich des
Ernstfalls: Sicherung schreiben, Datenbank zerstoeren, zurueckspielen,
Vollstaendigkeit und Schreibbarkeit nachweisen. Eine ungetestete Sicherung
ist eine Vermutung.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-31 18:20:35 +02:00

185 lines
8.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
index.js — Haupteinstiegspunkt für DogFather Universe. Liefert die statische Website aus
(../ ist der Ordner mit allen .html-Dateien) plus die Zugangsschranke (gate.js), 1:1 wie
vorher der Cloudflare Worker (gate-worker.js).
===================================================================== */
import "dotenv/config";
import express from "express";
import cookieParser from "cookie-parser";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { gateMiddleware } from "./gate.js";
import { webdesignGate } from "./webdesign-gate.js";
import { inhaltsrichtlinie } from "./inhaltsrichtlinie.js";
import { workspaceRouter } from "./workspace.js";
import { aufgabenRouter } from "./workspace-aufgaben.js";
import { personenRouter } from "./workspace-personen.js";
import { profilRouter } from "./workspace-profil.js";
import { kalenderRouter } from "./workspace-kalender.js";
import { dateienRouter } from "./workspace-dateien.js";
import { bereicheRouter } from "./workspace-bereiche.js";
import { reportRouter } from "./workspace-reports.js";
import { scoutRouter } from "./workspace-scouts.js";
import { callRouter } from "./workspace-calls.js";
import { hinweisRouter } from "./workspace-hinweise.js";
import { sucheRouter } from "./workspace-suche.js";
import { startcheckRouter } from "./workspace-startcheck.js";
import { wissenRouter } from "./workspace-wissen.js";
import { kiRouter } from "./workspace-ki.js";
import { sicherungRouter, sicherungStarten } from "./workspace-sicherung.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const SITE_DIR = join(__dirname, "..");
const PORT = Number(process.env.PORT || 4100);
const app = express();
app.set("trust proxy", 1);
app.disable("x-powered-by"); // verrät sonst unnötig den eingesetzten Technik-Stack
/* Sicherheits-Header, ergänzt 05.08.2026. Vorher standen diese in der Datei `_headers` — die
funktioniert aber NUR bei Cloudflare Pages/Netlify und wurde auf dem eigenen Server schlicht
ignoriert, d.h. die Seite lief seit dem Umzug komplett ohne diese Schutzmaßnahmen (nachgeprüft
an den Live-Antwortheadern). */
app.use((req, res, next) => {
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("X-Frame-Options", "SAMEORIGIN");
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
res.setHeader("Permissions-Policy", "geolocation=(), microphone=(), camera=(), payment=()");
res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
next();
});
/* Inhaltsrichtlinie (26.08.2026). Steht bewusst HIER -- vor jeder
Schranke und vor express.static: Auch die Zugangswand und die
404-Seite sollen sie bekommen, denn gerade dort werden Eingaben
entgegengenommen.
Warum die Pruefsummen nicht fest im Code stehen, ist im Kopf von
inhaltsrichtlinie.js begruendet -- kurz: Statische Dateien gehen hier
ohne Neustart live, feste Summen waeren nach der naechsten
Textaenderung falsch und wuerden die Seite lahmlegen. */
app.use(inhaltsrichtlinie(SITE_DIR));
app.use(cookieParser());
app.use(express.json({ limit: "1mb" }));
app.use(gateMiddleware);
/* Zugangsschutz für /webdesign (22.08.2026). Muss NACH gateMiddleware und
VOR express.static stehen. Eine eigene Schranke ist zwingend: gate.js
lässt seit dem öffentlichen Start am 21.08.2026 jeden durch
(SITE_PUBLIC_LAUNCH_AT liegt in der Vergangenheit) — der ausdrücklich
nicht-öffentliche Webdesign-Bereich wäre darüber sofort für alle
lesbar. Ausführliche Begründung im Kopf von webdesign-gate.js. */
app.use(webdesignGate);
/* Creator Workspace (27.08.2026): Anmeldung, Sitzungen und Audit-Log für
/workspace. Muss VOR express.static stehen, weil der Router auch die
angemeldeten Seiten schützt -- stünde er dahinter, würde express.static
/workspace/start.html vorher ungeschützt ausliefern.
Das Modul ist so gebaut, dass es diese Website nicht mitreißen kann:
Es öffnet beim Laden keine Datenbank, und jede Route fängt ihre Fehler
selbst ab (Begründung im Kopf von workspace.js). */
app.use(workspaceRouter);
app.use(aufgabenRouter);
app.use(personenRouter);
app.use(profilRouter);
app.use(kalenderRouter);
app.use(dateienRouter);
app.use(bereicheRouter);
app.use(reportRouter);
app.use(scoutRouter);
app.use(callRouter);
app.use(hinweisRouter);
app.use(sucheRouter);
app.use(startcheckRouter);
app.use(wissenRouter);
app.use(kiRouter);
app.use(sicherungRouter);
/* Naechtliche Sicherung. Startet einen eigenen Takt im Prozess -- siehe
workspace-sicherung.js, dort steht auch, warum sie NICHT als eigener
Systemdienst laeuft und warum eine Dateikopie hier falsch waere. */
sicherungStarten();
/* Sicherheits-Fix 20.08.2026 (Audit vor dem geplanten öffentlichen Start morgen).
SITE_DIR ist der GESAMTE Repo-Ordner (join(__dirname, "..")) — express.static liefert
ohne diese Sperre daher nicht nur die eigentliche Website aus, sondern auch den Server-
Quellcode (server/, server-internal/, cloudflare-worker/) und das komplette
.git-Verzeichnis samt Commit-Historie.
Live nachgewiesen (mit gültigem Zugangscode, der morgen für alle wegfällt):
/server/gate.js und /.git/config lieferten HTTP 200. Ursache: serve-static blockt per
Voreinstellung nur Dateien, deren EIGENER Name mit einem Punkt beginnt (server/.env ->
404 durch diese Regel) — aber NICHT rekursiv. Eine Datei wie .git/config wird trotzdem
ausgeliefert, weil "config" selbst nicht mit einem Punkt beginnt, nur der Ordnername davor.
Diese Sperre läuft daher bewusst unabhängig von gateMiddleware (bleibt auch nach dem
morgigen Entfernen der Zugangsschranke wirksam) und blockt ganze Ordner/Muster statt
einzelner Dateien, damit neue Dateien darin automatisch mitgeschützt sind. */
const GESPERRTE_ORDNER = ["/server", "/server-internal", "/cloudflare-worker"];
const GESPERRTE_DATEIEN = [
"/CLAUDE.md", "/DEPLOY.md", "/wrangler.toml", "/netlify.toml",
"/gate-worker.js", "/_headers", "/.gitignore", "/.nojekyll", "/.assetsignore",
];
app.use((req, res, next) => {
const pfad = req.path;
const hatVerstecktenOrdner = pfad
.split("/")
.some((teil) => teil.startsWith(".") && teil.length > 1 && teil !== ".well-known");
const inGesperrtemOrdner = GESPERRTE_ORDNER.some((p) => pfad === p || pfad.startsWith(p + "/"));
const istGesperrteDatei = GESPERRTE_DATEIEN.includes(pfad);
const istBackupDatei = /\.bak(-|\.|$)/i.test(pfad);
if (hatVerstecktenOrdner || inGesperrtemOrdner || istGesperrteDatei || istBackupDatei) {
return res.status(404).sendFile(join(SITE_DIR, "404.html"), (err) => {
if (err) res.status(404).send("Nicht gefunden.");
});
}
next();
});
/* Bugfix 20.08.2026 (Nutzer-Report: "ich kann sie immer noch nicht runter
laden und die leiste oben ist immer noch so groß", NACHDEM der jeweilige
Fix schon deployed war): Express selbst schickt standardmäßig GAR KEIN
Cache-Control mit (nur ETag/Last-Modified). Weil dogfather-universe.com
hinter Cloudflare liegt (orange-cloud-Proxy), springt Cloudflare bei
fehlendem Origin-Cache-Control mit seinem EIGENEN Standardwert ein
("Browser Cache TTL", per curl bestätigt: 4 Stunden = max-age=14400) —
ein frischer Deploy war dadurch für Besucher:innen bis zu 4 Std lang im
eigenen Browser-Cache unsichtbar, obwohl der Server längst die neue
Version ausliefert. Cloudflare respektiert laut Doku ein vom Origin
gesetztes Cache-Control statt seinen Standardwert zu benutzen — deshalb
hier explizit "no-cache" (erzwingt bei jedem Laden eine Revalidierung
per ETag/If-None-Match, liefert bei unverändertem Inhalt ein schnelles
304 statt die ganze Datei erneut zu senden — kein "gar nicht cachen",
nur "nie blind auf einen alten Stand vertrauen"). */
app.use((req, res, next) => {
res.setHeader("Cache-Control", "no-cache");
next();
});
app.use(express.static(SITE_DIR, { index: "index.html" }));
app.use((req, res) => {
res.status(404).sendFile(join(SITE_DIR, "404.html"), (err) => {
if (err) res.status(404).send("Nicht gefunden.");
});
});
/* Fehler-Handler (muss NACH allen Routen stehen), ergänzt 05.08.2026: antwortet mit einer
schlichten Meldung statt einer Express-Standardseite mit komplettem Stacktrace und Dateipfaden. */
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
console.error(`[fehler] ${req.method} ${req.originalUrl}:`, err?.stack || err);
if (res.headersSent) return;
res.status(500).send("Es ist ein Fehler aufgetreten. Bitte später erneut versuchen.");
});
/* Letztes Netz: Ein unbehandelter Fehler darf die Website nicht offline nehmen. */
process.on("unhandledRejection", (grund) => console.error("[unhandledRejection]", grund));
process.on("uncaughtException", (fehler) => console.error("[uncaughtException]", fehler?.stack || fehler));
app.listen(PORT, "127.0.0.1", () => {
console.log(`DogFather Universe – Server läuft auf http://127.0.0.1:${PORT}`);
});