/* ============================================================ GET /api/content -> öffentlich, liefert aktuelle Website-Inhalte PUT /api/content -> nur mit gültiger Session (Admin/Kunde eingeloggt) Speicherort: Cloudflare KV Namespace "CONTENT_KV", Key "site-content" ============================================================ */ const CONTENT_KEY = "site-content"; const MAX_BODY_BYTES = 200 * 1024; // 200 KB Schutz gegen überdimensionierte Requests function json(obj, status = 200) { return new Response(JSON.stringify(obj), { status, headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }, }); } function base64UrlDecode(str) { str = str.replace(/-/g, "+").replace(/_/g, "/"); while (str.length % 4) str += "="; const bin = atob(str); const bytes = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); return bytes.buffer; } async function base64UrlEncode(buffer) { const bytes = new Uint8Array(buffer); let str = ""; for (const b of bytes) str += String.fromCharCode(b); return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } async function hmacSign(secret, data) { const key = await crypto.subtle.importKey( "raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"] ); const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(data)); return base64UrlEncode(sig); } function getCookie(request, name) { const cookie = request.headers.get("Cookie") || ""; const match = cookie.match(new RegExp("(?:^|; )" + name + "=([^;]*)")); return match ? decodeURIComponent(match[1]) : null; } async function isAuthenticated(request, secret) { const token = getCookie(request, "acml_session"); if (!token || !secret) return false; const [payloadB64, sig] = token.split("."); if (!payloadB64 || !sig) return false; const expectedSig = await hmacSign(secret, payloadB64); if (expectedSig !== sig) return false; try { const payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(payloadB64))); return !!(payload.exp && payload.exp > Date.now()); } catch (e) { return false; } } // Fallback-Inhalte, falls KV noch leer ist (erster Aufruf nach Deployment) const DEFAULT_CONTENT = { phone: "+4917672530819", phoneDisplay: "0176 72530819", address: "Mühlackerstraße 77, 75417 Mühlacker", addressStreet: "Mühlackerstraße 77", addressCity: "75417 Mühlacker", ratingScore: "5.0", ratingCount: "19 Google-Rezensionen", ratingCountNumber: "19", openingHours: [ { day: "Montag", time: "16:00 – 22:00" }, { day: "Dienstag", time: "16:00 – 22:00" }, { day: "Mittwoch", time: "16:00 – 22:00" }, { day: "Donnerstag", time: "16:00 – 22:00" }, { day: "Freitag", time: "14:00 – 22:00" }, { day: "Samstag", time: "08:00 – 18:00" }, { day: "Sonntag", time: "geschlossen" }, ], prices: [ { label: "Bis 16 Zoll", amount: "50", unit: "pro Reifen" }, { label: "17 – 18 Zoll", amount: "60", unit: "pro Reifen", highlight: true }, { label: "19 – 20 Zoll", amount: "80", unit: "pro Reifen" }, { label: "Ab 21 Zoll", amount: "100", unit: "pro Reifen" }, { label: "Reifen umstecken", amount: "10", unit: "pro Reifen" }, ], reviews: [ { name: "EK40", stars: 5, time: "vor 5 Monaten", text: "Top Werkstatt. Hatte viele Probleme am Auto und keiner hatte die Fehler gefunden. Die Jungs jedoch wurden nach kurzer Zeit schon mit dem Fehler fündig und reparierten diese. Schnell, unkompliziert und vor allem ein sehr gutes Preis-Verhältnis." }, ], gallery: [], }; // Sehr einfache Validierung/Bereinigung der eingehenden Inhalte, // damit der Kunde nur Daten, keine Skripte/HTML einschleusen kann. function sanitizeString(val, maxLen) { if (typeof val !== "string") return ""; return val.replace(/<[^>]*>/g, "").slice(0, maxLen || 500); } function sanitizeContent(input) { const out = {}; out.phone = sanitizeString(input.phone, 30); out.phoneDisplay = sanitizeString(input.phoneDisplay, 30); out.address = sanitizeString(input.address, 200); out.addressStreet = sanitizeString(input.addressStreet, 100); out.addressCity = sanitizeString(input.addressCity, 100); out.ratingScore = sanitizeString(input.ratingScore, 10); out.ratingCount = sanitizeString(input.ratingCount, 60); out.ratingCountNumber = sanitizeString(input.ratingCountNumber, 10); out.openingHours = Array.isArray(input.openingHours) ? input.openingHours.slice(0, 10).map((r) => ({ day: sanitizeString(r.day, 20), time: sanitizeString(r.time, 40), })) : []; out.prices = Array.isArray(input.prices) ? input.prices.slice(0, 20).map((p) => ({ label: sanitizeString(p.label, 60), amount: sanitizeString(String(p.amount), 10), unit: sanitizeString(p.unit, 30), highlight: !!p.highlight, })) : []; out.reviews = Array.isArray(input.reviews) ? input.reviews.slice(0, 50).map((r) => ({ name: sanitizeString(r.name, 60), stars: Math.max(1, Math.min(5, parseInt(r.stars, 10) || 5)), time: sanitizeString(r.time, 40), text: sanitizeString(r.text, 1000), })) : []; out.gallery = Array.isArray(input.gallery) ? input.gallery.slice(0, 30).map((g) => ({ src: sanitizeString(g.src, 300), label: sanitizeString(g.label, 80), })) : []; return out; } export async function onRequestGet(context) { const { env } = context; if (!env.CONTENT_KV) return json(DEFAULT_CONTENT); const stored = await env.CONTENT_KV.get(CONTENT_KEY, { type: "json" }); return json(stored || DEFAULT_CONTENT); } export async function onRequestPut(context) { const { request, env } = context; const authed = await isAuthenticated(request, env.SESSION_SECRET); if (!authed) return json({ error: "Nicht angemeldet." }, 401); const contentLength = parseInt(request.headers.get("Content-Length") || "0", 10); if (contentLength > MAX_BODY_BYTES) { return json({ error: "Anfrage zu groß." }, 413); } let body; try { body = await request.json(); } catch (e) { return json({ error: "Ungültiges JSON." }, 400); } const clean = sanitizeContent(body || {}); if (!env.CONTENT_KV) return json({ error: "KV nicht konfiguriert." }, 500); await env.CONTENT_KV.put(CONTENT_KEY, JSON.stringify(clean)); return json({ ok: true, content: clean }); } // Manche Clients senden POST statt PUT - wir behandeln es identisch. export const onRequestPost = onRequestPut;