/* ===================================================================== lib/auth.js — gemeinsame Session-Logik für die Zugangscode-Schranke (qciga/VanVan). 1:1 portiert aus functions/_shared/auth.js — Node 22 hat crypto.subtle, btoa/atob global verfügbar (Web-Crypto-Standard), deshalb konnte die Logik praktisch unverändert übernommen werden. Einziger Unterschied: getCookie() liest jetzt aus Express' geparsten req.cookies statt selbst den Cookie-Header zu zerlegen (cookie-parser übernimmt das). ===================================================================== */ export const COOKIE_NAME = "vandiy_gate_session"; export const ROLE_COOKIE_NAME = "vandiy_gate_role"; /* Zugang gilt nur für die laufende Browser-Sitzung (Wunsch 05.08.2026: "jedes Mal wenn wir den Browser zu machen, soll der Code wieder gefragt werden"). Umgesetzt über ein reines Sitzungs-Cookie OHNE maxAge/expires in middleware/gate.js — das löscht der Browser beim Schließen selbst. SESSION_STUNDEN ist nur die zusätzliche Notbremse im signierten Token für den Fall, dass ein Browser tage-/wochenlang offen bleibt (dann läuft der Zugang trotzdem ab). Betrifft NUR die Zugangsschranke — Kundenkonten (customer-auth.js, 30 Tage) bleiben unberührt, echte Kund:innen sollen sich nicht bei jedem Besuch neu anmelden müssen. */ export const SESSION_STUNDEN = 12; 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_STUNDEN * 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; } } /** Liefert die Rolle ("qciga"/"vanvan") bei gültiger Sitzung, sonst null. */ export async function getGateRole(req) { if (!process.env.SITE_ACCESS_SECRET) return null; const token = req.cookies?.[COOKIE_NAME]; const payload = token ? await verifySession(token, process.env.SITE_ACCESS_SECRET) : null; return payload?.role ?? null; } /** Standard-401-Antwort für API-Routen ohne gültige Sitzung. */ export function unauthorizedJson(res) { return res.status(401).json({ ok: false, error: "Nicht angemeldet." }); }