Wichtigster Befund vorab: Es gab noch KEINEN PDF-Bereich. Weder auf der Website (28 Seiten, keine davon Anleitungen) noch auf dem Server (null PDFs). Es wurde also nichts umstrukturiert, sondern neu gebaut -- und damit war die erste Frage nicht "wie sieht es aus", sondern "wie kommen die PDFs spaeter rein". Eine feste Liste im Code waere nach drei Monaten unbrauchbar gewesen. Filipe hat entschieden: eigener Pflegebereich, und alles nur fuer das Team. Die Bibliothek liegt deshalb komplett im Workspace hinter dem Login, nicht auf der oeffentlichen Seite. GLIEDERUNG Zwanzig Kategorien woertlich nach Vorgabe, gebuendelt in sechs Welten: TikTok, Streaming einrichten, Bild & Ton, Uebertragung & Aussehen, Inhalt & Menschen, Hilfe & Schnellstart. Zwanzig gleich aussehende Kacheln untereinander findet niemand -- gebuendelt sucht man erst die Welt, und darin stehen nur noch drei bis vier. Jede Welt hat ihre eigene Farbe, jede Kategorie ihr eigenes Symbol (20 verschiedene, keins doppelt). Die Kategorietexte sind Filipes Beschreibungen -- sie beantworten beim Einsortieren die einzige Frage, die man wirklich hat. Die Gliederung steht im Code, nicht in der Datenbank: Sie ist eine bewusste Ordnung, keine Nutzdaten. JEDE PDF HAT GENAU EINE HAUPTKATEGORIE, alles Weitere laeuft ueber Tags -- so gibt es jede Anleitung nur einmal, sie ist aber ueber mehrere Begriffe auffindbar. Genau wie gefordert. SUCHE UND FILTER Suche ueber Titel, Beschreibung, Tags und Dateiname. Filter nach Stufe (Einsteiger / Fortgeschritten / Profi), Geraet und Tag. Zwei Details: - "PC" zeigt auch die Anleitungen, die fuer BEIDE Geraete gelten -- sonst filtert man sich versehentlich die Haelfte weg. - Der Tag-Filter sucht mit Kommas drumherum, sonst wuerde "pc" auch bei "pc-spiele" anschlagen. Sobald gesucht oder gefiltert wird, verschwinden die Kacheln und es erscheinen Treffer. Wer sucht, will nicht erst noch klicken. Der Zustand steht in der Adresse (?k=obs&tag=...). Damit funktionieren Zurueck-Taste, Neuladen und Weiterschicken. Eine Kategorie, die man niemandem verlinken kann, ist keine Seite. Geprueft. SICHERHEIT Hochgeladen wird nur, was WIRKLICH eine PDF ist -- geprueft an den ersten Bytes (%PDF-), nicht an der Dateiendung. Die kann jeder umbenennen. Geprueft mit einer als .pdf getarnten HTML-Datei mit Skript: abgewiesen. Nur weil diese Pruefung existiert, darf die Datei ueberhaupt im Browser angezeigt werden statt bloss heruntergeladen -- und selbst dann mit strenger CSP und nosniff. Lesen darf jeder Angemeldete, pflegen nur DogFather. Geprueft: Creator und Scout bekommen 403 beim Hochladen und sehen keine Bearbeiten-Knoepfe. Ohne Anmeldung ist auch die Datei selbst nicht erreichbar (401). Bricht das Anlegen nach dem Schreiben ab, wird die Datei wieder geloescht -- sonst laege sie fuer immer verwaist auf der Platte. Beim Bearbeiten wird die Datei bewusst NICHT ersetzt. Wer eine neue Fassung hat, stellt sie neu ein. So bleibt nachvollziehbar, was wann galt. Geprueft: 20 Kacheln, 20 verschiedene Symbole, Kategorie oeffnen, Suche, Tag-Klick, Browser-Zurueck, Handy ohne Ueberlauf, 0 Konsolenfehler.
176 lines
8.5 KiB
JavaScript
176 lines
8.5 KiB
JavaScript
/* =====================================================================
|
||
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";
|
||
|
||
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);
|
||
|
||
/* 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}`);
|
||
});
|