Nutzer-Wunsch 20.08.2026: "diese zwei 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 ... wen ich drauf druecke dass dann ein kleines fenster aufgeht wo das bild bissl groesser ist mit einem groesseren text und beschreibung, und mit einem link ... die kacheln sollen auch bissl spezieller sein." - Kein hartkodierter Standardinhalt mehr auf der Startseite -- die Kachel UND der ganze Abschnitt bleiben komplett unsichtbar, bis mindestens ein Event in der Verwaltung ausgefuellt UND ueber einen neuen "Aktiv"-Schalter freigeschaltet ist. Genau 1 aktives Event -> zentrierte Einzelkachel statt halbleerem Zwei-Spalten-Raster. - Klick auf eine Kachel oeffnet jetzt ein Detail-Fenster (groesseres Bild, groesserer Titel/Text, optionaler direkter Link) statt sofort wegzunavigieren. - Kacheln bekommen einen goldenen Trophaeen-Akzent + dezenten wandernden Lichtschimmer statt der neutralen Standardkarten-Optik. - Bugfix unterwegs gefunden (Playwright-Screenshot): .jahres-event-card blieb trotz [hidden]-Attribut sichtbar (dieselbe Ursache wie der frühere .jahres-event-bild-Bug: display:block ueberschreibt die eingebaute [hidden]-Regel bei gleicher Spezifitaet) -- explizite [hidden]-Regel ergaenzt. - Backend: server-internal/routes/events.js liefert oeffentlich NUR noch aktivierte Slots aus (getEventsPublic), neuer authentifizierter Endpunkt getEventsAdmin liefert der Verwaltung auch Entwuerfe zum Vorausfuellen. 10 automatisierte Tests gegen eine Fake-DB bestanden. Backend-Teil (server-internal/) noch ohne Deploy-Zugriff -- Dogi muss ihn manuell auf dogiintern ausrollen, sonst bleibt die Startseite beim alten Verhalten (immer beide Slots zeigen, kein Aktiv-Schalter in der Verwaltung). Co-Authored-By: Claude Opus 5 <[email protected]>
115 lines
4.9 KiB
JavaScript
115 lines
4.9 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). */
|
|
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;
|
|
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 });
|
|
}
|