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]>
This commit is contained in:
@@ -27,12 +27,27 @@ function forbidden(res) {
|
||||
return json(res, { ok: false, error: "Keine Berechtigung." }, 403);
|
||||
}
|
||||
|
||||
/* Erlaubte Auswahlbilder (21.08.2026). Muss mit der Liste in
|
||||
assets/js/data-stimmen-avatare.js übereinstimmen.
|
||||
|
||||
Warum überhaupt eine Liste auf dem Server? Weil /testimonials/submit
|
||||
öffentlich und ohne Login erreichbar ist. Ohne diese Prüfung könnte man
|
||||
in das Feld einen beliebigen Text schreiben (z.B. eine fremde Bild-
|
||||
Adresse oder etwas, das später als HTML/Adresse ausgewertet wird) und so
|
||||
auf einer freigegebenen Stimme fremde Inhalte einbetten. Es werden
|
||||
deshalb ausschließlich diese festen Ids angenommen. */
|
||||
const ERLAUBTE_AVATARE = [
|
||||
"husky-hoodie", "hase-hoodie", "husky-kopf", "husky-kosmos",
|
||||
"hase-kosmos", "casper", "casper-blick", "logo",
|
||||
];
|
||||
|
||||
function oeffentlichesFeld(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
tiktokUsername: row.tiktok_username,
|
||||
nachricht: row.nachricht,
|
||||
avatar: row.avatar || null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -42,6 +57,7 @@ function vollesFeld(row) {
|
||||
name: row.name,
|
||||
tiktokUsername: row.tiktok_username,
|
||||
nachricht: row.nachricht,
|
||||
avatar: row.avatar || null,
|
||||
status: row.status,
|
||||
createdAt: row.created_at,
|
||||
entschiedenAm: row.entschieden_am,
|
||||
@@ -63,15 +79,57 @@ export async function submitTestimonial(req, res) {
|
||||
if (!nachricht) return json(res, { ok: false, error: "Bitte schreib eine Nachricht." }, 400);
|
||||
if (nachricht.length < 10) return json(res, { ok: false, error: "Die Nachricht ist noch sehr kurz -- magst du noch etwas mehr schreiben?" }, 400);
|
||||
|
||||
// Selbst gewähltes Auswahlbild. Unbekannte Werte werden still verworfen
|
||||
// (statt die Einreichung abzulehnen) — die Nachricht ist das Wichtige,
|
||||
// und ein fehlendes Bild ist kein Grund, jemanden abzuweisen.
|
||||
const gewaehlt = String(body.avatar || "").trim();
|
||||
const avatar = ERLAUBTE_AVATARE.includes(gewaehlt) ? gewaehlt : null;
|
||||
|
||||
const id = generateId();
|
||||
db.prepare(
|
||||
`INSERT INTO testimonials (id, name, tiktok_username, nachricht, status, created_at) VALUES (?, ?, ?, ?, 'eingereicht', ?)`
|
||||
).run(id, name, tiktokUsername, nachricht, nowIso());
|
||||
`INSERT INTO testimonials (id, name, tiktok_username, nachricht, avatar, status, created_at) VALUES (?, ?, ?, ?, ?, 'eingereicht', ?)`
|
||||
).run(id, name, tiktokUsername, nachricht, avatar, nowIso());
|
||||
|
||||
logAction("anonym", "testimonial.submitted", id, { name });
|
||||
return json(res, { ok: true });
|
||||
}
|
||||
|
||||
/** Verwaltung: Profilbild einer Stimme setzen oder entfernen.
|
||||
Erlaubt hier ZUSÄTZLICH zu den Auswahlbildern eine hochgeladene Datei
|
||||
(siehe uploadTestimonialAvatar) — anders als beim öffentlichen Formular
|
||||
ist hier eine angemeldete, berechtigte Person am Werk. */
|
||||
export async function setTestimonialAvatar(req, res, session) {
|
||||
if (!can(session, "TESTIMONIALS_MANAGE")) return forbidden(res);
|
||||
const id = String(req.body?.id || "");
|
||||
const roh = String(req.body?.avatar || "").trim();
|
||||
|
||||
let avatar = null;
|
||||
if (roh) {
|
||||
const istEigenesFoto = roh.startsWith("https://postfach.dogfather-universe.com/uploads/testimonials/");
|
||||
if (!ERLAUBTE_AVATARE.includes(roh) && !istEigenesFoto) {
|
||||
return json(res, { ok: false, error: "Unbekanntes Profilbild." }, 400);
|
||||
}
|
||||
avatar = roh;
|
||||
}
|
||||
|
||||
const info = db.prepare(`UPDATE testimonials SET avatar = ? WHERE id = ?`).run(avatar, id);
|
||||
if (info.changes === 0) return json(res, { ok: false, error: "Nicht gefunden." }, 404);
|
||||
logAction(session.actor, "testimonial.avatar_changed", id, { avatar });
|
||||
return json(res, { ok: true });
|
||||
}
|
||||
|
||||
/** NUR Datei-Upload (multipart, siehe index.js). Für den Fall, dass jemand
|
||||
Dogi sein Foto auf anderem Weg geschickt hat (TikTok, WhatsApp …) —
|
||||
das öffentliche Formular selbst nimmt bewusst KEINE Dateien an, sonst
|
||||
könnte ohne Anmeldung beliebiges Material hochgeladen werden. */
|
||||
export async function uploadTestimonialAvatar(req, res, session) {
|
||||
if (!can(session, "TESTIMONIALS_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/testimonials/${req.file.filename}`;
|
||||
logAction(session.actor, "testimonial.avatar_uploaded", req.file.filename, null);
|
||||
return json(res, { ok: true, url });
|
||||
}
|
||||
|
||||
/** Öffentlich, keine Session: nur freigegebene Stimmen, neueste zuerst. */
|
||||
export async function getApprovedTestimonialsPublic(req, res) {
|
||||
const rows = db
|
||||
|
||||
Reference in New Issue
Block a user