Node.js/Express-Server als Ersatz für Cloudflare Pages Functions + D1 (läuft auf eigenem Server statt Cloudflare)
Build & Deploy / deploy (push) Waiting to run

This commit is contained in:
qcigano
2026-08-05 15:06:17 +02:00
parent 53e7bb158a
commit b8b8ec7b7a
19 changed files with 1322 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
/* =====================================================================
middleware/gate.js — Zugangs-Schranke vor der ganzen Website. 1:1 portiert aus
functions/_middleware.js (onRequest), auf Express-Middleware-Stil umgestellt (next() statt
context.next(), req/res statt Request/Response). Gleiche Regeln, gleiche Reihenfolge:
- /admin/* nie anfassen (eigener, separater Worker mit GitHub-OAuth).
- /gate-logout immer erreichbar.
- Rechtstexte (Impressum, Datenschutz, AGB, Widerruf) immer ohne Code erreichbar.
- Fail-open, falls die Zugangs-Secrets fehlen (lieber offen als sich selbst aussperren).
- /verwaltung/* + /api/verwaltung/* bleiben IMMER geschützt, auch wenn SITE_PUBLIC=true.
- SITE_PUBLIC=true schaltet den Rest der Seite öffentlich (außer /verwaltung).
- API-Routen bekommen bei fehlender Sitzung 401-JSON statt HTML-Redirect.
===================================================================== */
import { COOKIE_NAME, ROLE_COOKIE_NAME, SESSION_TAGE, signSession, verifySession } from "../lib/auth.js";
import {
VERWALTUNG_COOKIE_NAME,
VERWALTUNG_SESSION_STUNDEN,
signVerwaltungSession,
verifyVerwaltungSession,
} from "../lib/verwaltung-auth.js";
const OEFFENTLICHE_SEITEN = new Set(["/impressum", "/datenschutz", "/agb", "/widerruf", "/muster-widerrufsformular"]);
function istOeffentlichErreichbar(pfad) {
const ohneSlash = pfad.endsWith("/") && pfad.length > 1 ? pfad.slice(0, -1) : pfad;
if (OEFFENTLICHE_SEITEN.has(ohneSlash)) return true;
if (pfad.startsWith("/_astro/") && pfad.endsWith(".css")) return true;
if (pfad === "/img/gate-avatar-dogi.webp" || pfad === "/img/gate-avatar-vanvan.webp") return true;
if (/^\/(logo\.png|favicon\.ico|favicon-\d+x\d+\.png|apple-touch-icon\.png|icon-\d+(-maskable)?\.png|sw\.js|manifest\.webmanifest)$/.test(pfad)) return true;
return false;
}
export async function gateAuthHandler(req, res) {
const code = (req.body?.code || "").trim();
const next = typeof req.body?.next === "string" && req.body.next.startsWith("/") ? req.body.next : "/";
let role = null;
if (process.env.SITE_ACCESS_CODE_QCIGA && code === process.env.SITE_ACCESS_CODE_QCIGA) role = "qciga";
else if (process.env.SITE_ACCESS_CODE_VANVAN && code === process.env.SITE_ACCESS_CODE_VANVAN) role = "vanvan";
if (!role) {
return res.status(401).json({ ok: false, error: "Falscher Zugangscode." });
}
const session = await signSession(role, process.env.SITE_ACCESS_SECRET);
const maxAge = SESSION_TAGE * 24 * 60 * 60 * 1000;
res.cookie(COOKIE_NAME, session, { path: "/", maxAge, httpOnly: true, secure: true, sameSite: "lax" });
res.cookie(ROLE_COOKIE_NAME, role, { path: "/", maxAge, secure: true, sameSite: "lax" });
return res.json({ ok: true, next });
}
export async function verwaltungAuthHandler(req, res) {
const code = (req.body?.code || "").trim();
const next = typeof req.body?.next === "string" && req.body.next.startsWith("/") ? req.body.next : "/verwaltung/";
const codeGueltig =
(process.env.SITE_ACCESS_CODE_QCIGA && code === process.env.SITE_ACCESS_CODE_QCIGA) ||
(process.env.SITE_ACCESS_CODE_VANVAN && code === process.env.SITE_ACCESS_CODE_VANVAN);
if (!codeGueltig) {
return res.status(401).json({ ok: false, error: "Falscher Code." });
}
const session = await signVerwaltungSession(process.env.VERWALTUNG_SESSION_SECRET);
const maxAge = VERWALTUNG_SESSION_STUNDEN * 60 * 60 * 1000;
res.cookie(VERWALTUNG_COOKIE_NAME, session, { path: "/", maxAge, httpOnly: true, secure: true, sameSite: "lax" });
return res.json({ ok: true, next });
}
export function logoutHandler(req, res) {
res.clearCookie(COOKIE_NAME, { path: "/" });
res.clearCookie(ROLE_COOKIE_NAME, { path: "/" });
res.clearCookie(VERWALTUNG_COOKIE_NAME, { path: "/" });
return res.json({ ok: true });
}
/** Die eigentliche Middleware, vor JEDER Anfrage montiert (siehe index.js). */
export function gateMiddleware(req, res, next) {
const pfad = req.path;
if (pfad.startsWith("/admin")) return next();
if (pfad === "/gate-logout" && req.method === "POST") return logoutHandler(req, res);
if (istOeffentlichErreichbar(pfad)) return next();
const codesKonfiguriert = !!(process.env.SITE_ACCESS_SECRET && (process.env.SITE_ACCESS_CODE_QCIGA || process.env.SITE_ACCESS_CODE_VANVAN));
if (!codesKonfiguriert) return next();
const immerGeschuetzterPfad = pfad.startsWith("/verwaltung") || pfad.startsWith("/api/verwaltung");
const sitePublic = process.env.SITE_PUBLIC === "true";
if (sitePublic && !immerGeschuetzterPfad) return next();
if (pfad === "/gate-auth" && req.method === "POST") return gateAuthHandler(req, res);
const sessionToken = req.cookies?.[COOKIE_NAME];
(async () => {
const payload = sessionToken ? await verifySession(sessionToken, process.env.SITE_ACCESS_SECRET) : null;
if (payload) {
if (!immerGeschuetzterPfad) return next();
if (pfad === "/verwaltung-code.html" || pfad === "/verwaltung-code") return next();
if (pfad === "/verwaltung-auth" && req.method === "POST") return verwaltungAuthHandler(req, res);
if (!process.env.VERWALTUNG_SESSION_SECRET) return next();
const vwToken = req.cookies?.[VERWALTUNG_COOKIE_NAME];
const vwPayload = vwToken ? await verifyVerwaltungSession(vwToken, process.env.VERWALTUNG_SESSION_SECRET) : null;
if (vwPayload) return next();
if (pfad.startsWith("/api/")) {
return res.status(401).json({ ok: false, error: "Zusätzlicher Verwaltungs-Code erforderlich." });
}
const vwNext = pfad + (req.originalUrl.includes("?") ? "?" + req.originalUrl.split("?")[1] : "");
return res.redirect(302, `/verwaltung-code.html?next=${encodeURIComponent(vwNext)}`);
}
if (pfad === "/gate.html" || pfad === "/gate") return next();
if (pfad.startsWith("/api/")) {
return res.status(401).json({ ok: false, error: "Nicht angemeldet." });
}
const nextParam = pfad + (req.originalUrl.includes("?") ? "?" + req.originalUrl.split("?")[1] : "");
return res.redirect(302, `/gate?next=${encodeURIComponent(nextParam)}`);
})();
}