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
Build & Deploy / deploy (push) Waiting to run
This commit is contained in:
@@ -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)}`);
|
||||
})();
|
||||
}
|
||||
Reference in New Issue
Block a user