"Event des Jahres" jetzt in der Verwaltung pflegbar (Bild, Text, Link)
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." Auf Rueckfrage entschieden: Titel/Text nur auf Deutsch eingeben, die anderen 4 Sprachen werden beim Speichern automatisch uebersetzt (MyMemory, 0€, kein API-Key) -- bewusste Ausnahme von der sonst geltenden "immer echte Uebersetzung"-Regel, klar dokumentiert und im Admin-UI selbst als Hinweis sichtbar. de-CH bekommt denselben deutschen Text (Dialekt ist keine von Uebersetzungs-APIs unterstuetzte Zielsprache). Backend (server-internal): - routes/events.js: getEventsPublic (oeffentlich, keine Session), saveEvent + uploadEventImage (beide hinter neuer EVENTS_MANAGE- Berechtigung, Owner immer erlaubt). Speichert in der bereits bestehenden app_settings-Tabelle (2 feste Slots) statt einer neuen Tabelle -- es gibt nie mehr als genau 2 Events. - lib/translate.js: MyMemory-Anbindung mit "fail closed auf Deutsch" pro Sprache, falls der Dienst mal nicht antwortet. - Bild-Upload per multer, zufaelliger Dateiname (crypto.randomUUID, verhindert Path-Traversal ueber den Originalnamen komplett), 5 MB Limit, nur jpeg/png/webp, Ablage unter /var/lib/dogfather-internal/ uploads/events (NICHT im Git-Ordner -- uebersteht Deploys), oeffentlich ausgeliefert unter /uploads. - Neue Berechtigung EVENTS_MANAGE im Katalog (Gruppe "Startseite"). Frontend: - index.html: laedt /events-of-year beim Aufruf, ueberschreibt pro Slot Datum/Titel/Text/Bild/Link NUR wenn dort tatsaechlich etwas gespeichert ist -- bleibt der Abruf aus oder ist ein Slot leer, bleibt der bisherige fest eingebaute Standardinhalt (dieselben zwei echten Events) stehen. Kein Blocker, kein sichtbarer Fehler bei Ausfall. - verwaltung.html: neue Sektion "🏆 Event des Jahres" (nur mit EVENTS_MANAGE sichtbar), 2 Karten mit Bild-Upload+Vorschau, Datum, Titel, Text, Link, eigenem Speichern-Knopf pro Event. Ausfuehrlich getestet, weil server-internal wegen fehlender Visual- Studio-Build-Tools auf dieser Windows-Maschine nicht lokal mit echtem better-sqlite3 laufen kann: routes/events.js komplett isoliert gegen eine Fake-DB getestet (11 Szenarien: oeffentlicher Abruf, fehlende Session, Session ohne Recht, Owner, Rolle MIT EVENTS_MANAGE, alle Validierungen, Bild-Upload inkl. falscher Dateityp, Abruf des hochgeladenen Bilds). index.html per Playwright mit echtem Netzwerk-Mocking gegen zwei Szenarien getestet (API nicht erreichbar -> Standardinhalt bleibt; API liefert echte Daten -> nur der befuellte Slot wird ueberschrieben, der leere bleibt Standard). Dabei einen echten CSS-Bug gefunden und behoben (display:block auf .jahres-event-bild überschrieb die [hidden]-Regel des Browsers, leeres Bild waere immer sichtbar gewesen). verwaltung.html per Playwright mit gemocktem Login+API end-to-end getestet: Formular wird korrekt vorbefuellt, Bild-Upload + Speichern senden die richtigen Daten. WICHTIG: server-internal laeuft unter einem eigenen Systembenutzer (dogiintern), auf den ich (claudian) bewusst KEINEN Zugriff habe -- diese Aenderung kann ich anders als sonst nicht selbst bis auf den Server bringen. Dogi muss den Deploy-Schritt fuer server-internal selbst ausfuehren (git pull + npm install + Neustart des dogiintern-Dienstes). Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -27,6 +27,9 @@ import "dotenv/config";
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import cron from "node-cron";
|
||||
import multer from "multer";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { extname } from "node:path";
|
||||
|
||||
import { initDb } from "./db.js";
|
||||
import { json } from "./lib/http.js";
|
||||
@@ -60,6 +63,7 @@ import {
|
||||
import {
|
||||
listSupporters, getSupporterDetail, updatePremium, listCollections, upsertCollection, getAdminOverview,
|
||||
} from "./routes/supporter-admin.js";
|
||||
import { getEventsPublic, saveEvent, uploadEventImage } from "./routes/events.js";
|
||||
|
||||
initDb();
|
||||
|
||||
@@ -68,6 +72,27 @@ const app = express();
|
||||
app.set("trust proxy", 1);
|
||||
app.disable("x-powered-by"); // verrät sonst unnötig den eingesetzten Technik-Stack
|
||||
|
||||
/* Bild-Uploads für admin-gepflegte Inhalte (aktuell: "Event des Jahres",
|
||||
20.08.2026). Liegt bewusst NEBEN der Datenbank unter /var/lib/... statt im
|
||||
Git-Repo-Ordner — Uploads sollen einen `git pull`/Deploy unbeschadet
|
||||
überstehen und nicht versehentlich mit eingecheckt werden. */
|
||||
const UPLOADS_BASIS_DIR = process.env.UPLOADS_DIR || "/var/lib/dogfather-internal/uploads";
|
||||
const EVENTS_UPLOADS_DIR = `${UPLOADS_BASIS_DIR}/events`;
|
||||
mkdirSync(EVENTS_UPLOADS_DIR, { recursive: true });
|
||||
|
||||
const ERLAUBTE_BILD_TYPEN = new Set(["image/jpeg", "image/png", "image/webp"]);
|
||||
const bildUpload = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, EVENTS_UPLOADS_DIR),
|
||||
// Zufälliger Dateiname (crypto.randomUUID) statt des Originalnamens —
|
||||
// verhindert Pfad-/Namenskollisionen und Path-Traversal-Versuche über
|
||||
// einen manipulierten Dateinamen komplett, ganz ohne Validierung nötig.
|
||||
filename: (req, file, cb) => cb(null, `${crypto.randomUUID()}${extname(file.originalname).toLowerCase()}`),
|
||||
}),
|
||||
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB
|
||||
fileFilter: (req, file, cb) => cb(null, ERLAUBTE_BILD_TYPEN.has(file.mimetype)),
|
||||
});
|
||||
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader("X-Content-Type-Options", "nosniff");
|
||||
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
||||
@@ -76,6 +101,10 @@ app.use((req, res, next) => {
|
||||
});
|
||||
|
||||
app.use(cors());
|
||||
// Bild-Uploads oeffentlich ausliefern (index.html zeigt "Event des Jahres"-Bilder
|
||||
// JEDER Besucherin/JEDEM Besucher, ohne Login) — nur lesend, Schreiben bleibt
|
||||
// ausschliesslich ueber /admin/events/upload-image mit EVENTS_MANAGE-Berechtigung.
|
||||
app.use("/uploads", express.static(UPLOADS_BASIS_DIR));
|
||||
app.use(
|
||||
express.json({
|
||||
limit: "2mb",
|
||||
@@ -124,6 +153,7 @@ app.get("/health", (req, res) => json(res, { ok: true, service: "dogfather-unive
|
||||
app.post("/submit", wrap(submitApplication));
|
||||
app.get("/live-status", wrap(handleLiveStatusGet));
|
||||
app.post("/auth/login", wrap(login));
|
||||
app.get("/events-of-year", wrap(getEventsPublic));
|
||||
|
||||
app.post("/supporter/register", wrap(registerSupporter));
|
||||
app.post("/supporter/verify-email", wrap(verifySupporterEmail));
|
||||
@@ -156,6 +186,10 @@ app.post("/admin/notes/delete", requireTeamSession, withSession(deleteNote));
|
||||
app.post("/admin/drafts/list", requireTeamSession, withSession(listDrafts));
|
||||
app.post("/admin/drafts/save", requireTeamSession, withSession(saveDraft));
|
||||
|
||||
// Startseite: "Event des Jahres" (20.08.2026)
|
||||
app.post("/admin/events/save", requireTeamSession, withSession(saveEvent));
|
||||
app.post("/admin/events/upload-image", requireTeamSession, bildUpload.single("image"), withSession(uploadEventImage));
|
||||
|
||||
app.post("/admin/roles/list", requireTeamSession, withSession(listRoles));
|
||||
app.post("/admin/roles/create", requireTeamSession, withSession(createRole));
|
||||
app.post("/admin/roles/update", requireTeamSession, withSession(updateRole));
|
||||
@@ -212,6 +246,10 @@ app.use((err, req, res, next) => {
|
||||
if (err?.type === "entity.too.large") {
|
||||
return json(res, { ok: false, error: "Die Anfrage ist zu groß." }, 413);
|
||||
}
|
||||
if (err instanceof multer.MulterError) {
|
||||
const nachricht = err.code === "LIMIT_FILE_SIZE" ? "Das Bild ist zu groß (maximal 5 MB)." : "Bild-Upload fehlgeschlagen.";
|
||||
return json(res, { ok: false, error: nachricht }, 400);
|
||||
}
|
||||
console.error(`[fehler] ${req.method} ${req.originalUrl}:`, err?.stack || err);
|
||||
if (res.headersSent) return;
|
||||
return json(res, { ok: false, error: "Interner Fehler." }, 500);
|
||||
|
||||
Reference in New Issue
Block a user