Files
dogfather-universe/server-internal/routes/events.js
T
DogFatherGitandClaude Opus 5 5d0ea11570 Arbeit der parallelen Session in git gesichert (war nur auf dem Server)
Diese Änderungen liefen bereits live auf dem Server, lagen dort aber
ausschließlich als nicht eingecheckte Arbeitskopie — bei jedem Deploy
(stash/pull/pop) und bei jedem Serverproblem wären sie verloren gewesen.
Deshalb hier unverändert in git übernommen, bevor darauf aufgebaut wird.

Enthalten (nicht von mir gebaut, nur gesichert):
- Supporter: eigener fester Zugangscode statt Einmalcode-Login
  (Migration 0009, lib/crypto.js scrypt-Hash, routes/supporter.js,
  abonnieren.html, supporter.html, i18n-abonnieren/-supporter)
- Stimmen: Profilbilder (Migration 0010, routes/testimonials.js)
- Event-Bild-Upload: voller Pfad statt relativem (routes/events.js)
- Neue/überarbeitete Hintergrundbilder für viele Seiten
- Teilen-Funktion (streamplan.js, i18n-index/-streamplan, main.css)

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-21 17:35:14 +02:00

126 lines
5.8 KiB
JavaScript

/* =====================================================================
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 nur AKTIVIERTE Slots (Nutzer-Wunsch
20.08.2026: "die kisten sollen leer sein solang wie ich nichts rein
setze, am besten sollen die sogar immer nur erscheinen wenn ich was
rein setze und sie aktiviere") -- ein Slot mit gespeichertem Inhalt,
aber ohne gesetztes "aktiv", bleibt für Besucher:innen unsichtbar
(Entwurfsmodus), obwohl er in der Verwaltung längst vorausgefüllt ist. */
export async function getEventsPublic(req, res) {
const gibFreiWennAktiv = (slot) => {
const eintrag = ladeSlot(slot);
return eintrag && eintrag.aktiv ? eintrag : null;
};
return json(res, { ok: true, events: [gibFreiWennAktiv(1), gibFreiWennAktiv(2)] });
}
/** Für die Verwaltung: liefert BEIDE Slots roh (auch inaktive/Entwürfe),
damit das Formular beim Öffnen alles vorausfüllen kann. */
export async function getEventsAdmin(req, res, session) {
if (!can(session, "EVENTS_MANAGE")) return forbidden(res);
return json(res, { ok: true, events: [ladeSlot(1), ladeSlot(2)] });
}
/** NUR Bild-Upload, kein JSON-Body (multipart, siehe index.js multer-Middleware).
Bugfix 21.08.2026 (Nutzer-Report: "wieso seh ich das bild nicht" bei
"Event 1" in der Verwaltung): vorher wurde hier nur der RELATIVE Pfad
("/uploads/events/...") zurückgegeben. verwaltung.html UND index.html
laufen aber auf dogfather-universe.com (dogiweb-Dienst), während die
hochgeladene Datei tatsächlich unter postfach.dogfather-universe.com
(dieser Dienst, dogiintern, siehe /uploads-Static-Route in index.js)
liegt -- der Browser hat den relativen Pfad also fälschlich gegen die
falsche Domain aufgelöst und ein kaputtes Bild-Symbol angezeigt, sowohl
in der Verwaltungs-Vorschau als auch (sobald ein Slot aktiviert wird)
auf der echten Startseite. Jetzt vollständige Adresse, damit das Bild
unabhängig davon lädt, von welcher Domain aus die Seite gerade läuft. */
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 = `https://postfach.dogfather-universe.com/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;
const aktiv = !!body.aktiv;
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);
}
// Aktivieren nur mit vollständigem Pflichtinhalt möglich -- verhindert ein
// leeres/kaputtes, aber "aktives" Feld auf der Startseite.
if (aktiv && (!titelDe || !textDe)) {
return json(res, { ok: false, error: "Zum Aktivieren müssen Titel und Text ausgefüllt sein." }, 400);
}
const [titel, text] = await Promise.all([uebersetzeAlleSprachen(titelDe), uebersetzeAlleSprachen(textDe)]);
const eintrag = {
datum, titel, text, link, bild, aktiv,
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 });
}