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]>
126 lines
5.8 KiB
JavaScript
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 });
|
|
}
|