/* ===================================================================== routes/events.js — "Event des Jahres" auf der Startseite (index.html), admin-pflegbar aus der Verwaltung heraus (Nutzer-Wunsch 20.08.2026: "will ich in der admin seite das selbst gestalten können, mit bild und text und am besten auch einen link"). Zeigt IMMER genau 2 feste Slots (1 und 2) — kein wachsender Katalog, deshalb genügt die bereits bestehende app_settings-Tabelle (key/value) statt einer eigenen Tabelle. Öffentlicher Lesezugriff (getEventsPublic) ohne Session, damit index.html die Inhalte für jede Besucherin/jeden Besucher laden kann — Schreibzugriff nur mit EVENTS_MANAGE-Berechtigung (Owner immer erlaubt, siehe can()). ===================================================================== */ import { db } from "../db.js"; import { hasPermission } from "../lib/permissions.js"; import { json } from "../lib/http.js"; import { logAction } from "../lib/audit.js"; import { uebersetzeAlleSprachen } from "../lib/translate.js"; function can(session, key) { if (!session) return false; if (session.isOwner) return true; return hasPermission(session.permissions, session.overrides, key); } function forbidden(res) { return json(res, { ok: false, error: "Keine Berechtigung." }, 403); } function settingsKey(slot) { return `event_des_jahres_${slot}`; } function ladeSlot(slot) { const row = db.prepare(`SELECT value FROM app_settings WHERE key = ?`).get(settingsKey(slot)); if (!row) return null; try { return JSON.parse(row.value); } catch { return null; } } /** Öffentlich, keine Session: liefert beide Slots (oder null, wenn noch nie gespeichert -- index.html behält dann seinen eingebauten Standardinhalt). */ export async function getEventsPublic(req, res) { return json(res, { ok: true, events: [ladeSlot(1), ladeSlot(2)] }); } /** NUR Bild-Upload, kein JSON-Body (multipart, siehe index.js multer-Middleware). */ export async function uploadEventImage(req, res, session) { if (!can(session, "EVENTS_MANAGE")) return forbidden(res); if (!req.file) return json(res, { ok: false, error: "Keine Bilddatei erhalten." }, 400); const url = `/uploads/events/${req.file.filename}`; logAction(session.actor, "events.image_uploaded", req.file.filename, null); return json(res, { ok: true, url }); } export async function saveEvent(req, res, session) { if (!can(session, "EVENTS_MANAGE")) return forbidden(res); const body = req.body || {}; const slot = Number(body.slot); if (slot !== 1 && slot !== 2) return json(res, { ok: false, error: "Ungültiger Slot (nur 1 oder 2)." }, 400); const datum = String(body.datum || "").trim().slice(0, 80); const titelDe = String(body.titel || "").trim().slice(0, 120); const textDe = String(body.text || "").trim().slice(0, 400); const link = String(body.link || "").trim().slice(0, 300) || null; const bild = String(body.bild || "").trim().slice(0, 300) || null; if (!titelDe || !textDe) { return json(res, { ok: false, error: "Titel und Text sind Pflicht." }, 400); } // Nur echte http(s)-Links zulassen (kein javascript:-Link etc.) if (link && !/^https?:\/\//i.test(link)) { return json(res, { ok: false, error: "Link muss mit http:// oder https:// beginnen." }, 400); } const [titel, text] = await Promise.all([uebersetzeAlleSprachen(titelDe), uebersetzeAlleSprachen(textDe)]); const eintrag = { datum, titel, text, link, bild, aktualisiertVon: session.actor, aktualisiertAm: new Date().toISOString(), }; db.prepare( `INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at` ).run(settingsKey(slot), JSON.stringify(eintrag), eintrag.aktualisiertAm); logAction(session.actor, "events.saved", String(slot), { titel: titelDe }); return json(res, { ok: true, event: eintrag }); }