/* ===================================================================== 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, signSession, verifySession } from "../lib/auth.js"; import { VERWALTUNG_COOKIE_NAME, 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); // Bewusst OHNE maxAge/expires → reines Sitzungs-Cookie, verfällt beim Schließen des Browsers. res.cookie(COOKIE_NAME, session, { path: "/", httpOnly: true, secure: true, sameSite: "lax" }); res.cookie(ROLE_COOKIE_NAME, role, { path: "/", 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); // Ebenfalls reines Sitzungs-Cookie (siehe gateAuthHandler oben). res.cookie(VERWALTUNG_COOKIE_NAME, session, { path: "/", 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)}`); })(); }