/* ===================================================================== functions/_shared/auth.js — gemeinsame Session-Logik für die Zugangscode-Schranke. Ursprünglich komplett in functions/_middleware.js, jetzt hierher ausgelagert, damit auch die neuen API-Routen (functions/api/verwaltung/*) unabhängig vom Seiten-Routing prüfen können, ob eine gültige qciga-/VanVan-Sitzung vorliegt — Verteidigung in der Tiefe: selbst wenn jemand /api/verwaltung/orders direkt anspringt (ohne vorher /verwaltung/ zu laden), greift hier dieselbe Prüfung wie bei jeder normalen Seite. ===================================================================== */ export const COOKIE_NAME = "vandiy_gate_session"; export const ROLE_COOKIE_NAME = "vandiy_gate_role"; export const SESSION_TAGE = 90; function b64urlEncode(bytes) { let bin = ""; bytes.forEach((b) => (bin += String.fromCharCode(b))); return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } function b64urlDecodeToBytes(str) { str = str.replace(/-/g, "+").replace(/_/g, "/"); while (str.length % 4) str += "="; const bin = atob(str); return Uint8Array.from(bin, (c) => c.charCodeAt(0)); } async function hmacKey(secret) { return crypto.subtle.importKey( "raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"] ); } export async function signSession(role, secret) { const payload = JSON.stringify({ role, exp: Date.now() + SESSION_TAGE * 24 * 60 * 60 * 1000 }); const payloadB64 = b64urlEncode(new TextEncoder().encode(payload)); const key = await hmacKey(secret); const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payloadB64)); const sigB64 = b64urlEncode(new Uint8Array(sig)); return `${payloadB64}.${sigB64}`; } export async function verifySession(token, secret) { if (!token || !token.includes(".")) return null; const [payloadB64, sigB64] = token.split("."); try { const key = await hmacKey(secret); const valid = await crypto.subtle.verify( "HMAC", key, b64urlDecodeToBytes(sigB64), new TextEncoder().encode(payloadB64) ); if (!valid) return null; const payload = JSON.parse(new TextDecoder().decode(b64urlDecodeToBytes(payloadB64))); if (!payload.exp || payload.exp < Date.now()) return null; return payload; // { role, exp } } catch { return null; } } export function getCookie(request, name) { const header = request.headers.get("Cookie") || ""; const match = header.match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`)); return match ? decodeURIComponent(match[1]) : null; } /** Liefert die Rolle ("qciga"/"vanvan") bei gültiger Sitzung, sonst null. Für API-Routen * gedacht, die UNABHÄNGIG vom Seiten-Routing (Middleware) noch einmal selbst prüfen wollen, * ob wirklich nur qciga/VanVan zugreifen — siehe functions/api/verwaltung/*. */ export async function getGateRole(request, env) { if (!env.SITE_ACCESS_SECRET) return null; const token = getCookie(request, COOKIE_NAME); const payload = token ? await verifySession(token, env.SITE_ACCESS_SECRET) : null; return payload?.role ?? null; } /** Standard-401-Antwort für API-Routen ohne gültige Sitzung. */ export function unauthorizedJson() { return new Response(JSON.stringify({ ok: false, error: "Nicht angemeldet." }), { status: 401, headers: { "Content-Type": "application/json" }, }); }