/* ===================================================================== routes/testimonials.js — "Stimmen" auf stimmen.html (20.08.2026, Nutzer-Wunsch: "eine richtig geile kachel ... wo die leute mit namen und tiktok namen mir eine nachricht schreiben können die ich in die verwaltung kriege, und dann kann ich die ausgewählten über die verwaltungsseite auf die website hinzufügen"). Öffentlich: jede/jeder darf eine Nachricht einreichen (kein Login nötig, absichtlich niedrigschwellig -- "mega persönlich zu den Fans"). Landet als Entwurf ("eingereicht"), wird NIE automatisch veröffentlicht. Nur Dogi/VanVan (TESTIMONIALS_MANAGE) sehen die Warteschlange und entscheiden pro Eintrag: freigeben (-> erscheint auf stimmen.html), ablehnen oder löschen. ===================================================================== */ import { db } from "../db.js"; import { generateId, nowIso } from "../lib/crypto.js"; import { hasPermission } from "../lib/permissions.js"; import { json } from "../lib/http.js"; import { logAction } from "../lib/audit.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 oeffentlichesFeld(row) { return { id: row.id, name: row.name, tiktokUsername: row.tiktok_username, nachricht: row.nachricht, }; } function vollesFeld(row) { return { id: row.id, name: row.name, tiktokUsername: row.tiktok_username, nachricht: row.nachricht, status: row.status, createdAt: row.created_at, entschiedenAm: row.entschieden_am, entschiedenVon: row.entschieden_von, }; } /** Öffentlich, keine Session: eine Nachricht einreichen. Bewusst simpel gehalten (kein E-Mail-Nachweis, keine Registrierung) -- die einzige Schutzschicht gegen Missbrauch ist die manuelle Freigabe durch Dogi, kein Eintrag wird je ungesehen veröffentlicht. */ export async function submitTestimonial(req, res) { const body = req.body || {}; const name = String(body.name || "").trim().slice(0, 60); const tiktokUsername = String(body.tiktokUsername || "").trim().replace(/^@/, "").slice(0, 40) || null; const nachricht = String(body.nachricht || "").trim().slice(0, 500); if (!name) return json(res, { ok: false, error: "Bitte gib deinen Namen an." }, 400); 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); const id = generateId(); db.prepare( `INSERT INTO testimonials (id, name, tiktok_username, nachricht, status, created_at) VALUES (?, ?, ?, ?, 'eingereicht', ?)` ).run(id, name, tiktokUsername, nachricht, nowIso()); logAction("anonym", "testimonial.submitted", id, { name }); return json(res, { ok: true }); } /** Öffentlich, keine Session: nur freigegebene Stimmen, neueste zuerst. */ export async function getApprovedTestimonialsPublic(req, res) { const rows = db .prepare(`SELECT * FROM testimonials WHERE status = 'freigegeben' ORDER BY entschieden_am DESC LIMIT 60`) .all(); return json(res, { ok: true, testimonials: rows.map(oeffentlichesFeld) }); } /** Für die Verwaltung: ALLE Einreichungen, neueste zuerst -- offene zuerst einsortiert, damit die Warteschlange sofort ins Auge fällt. */ export async function getTestimonialsAdmin(req, res, session) { if (!can(session, "TESTIMONIALS_MANAGE")) return forbidden(res); const rows = db .prepare( `SELECT * FROM testimonials ORDER BY CASE status WHEN 'eingereicht' THEN 0 ELSE 1 END, created_at DESC` ) .all(); return json(res, { ok: true, testimonials: rows.map(vollesFeld) }); } export async function setTestimonialStatus(req, res, session) { if (!can(session, "TESTIMONIALS_MANAGE")) return forbidden(res); const id = String(req.body?.id || ""); const status = String(req.body?.status || ""); if (!["freigegeben", "abgelehnt", "eingereicht"].includes(status)) { return json(res, { ok: false, error: "Ungültiger Status." }, 400); } const info = db .prepare(`UPDATE testimonials SET status = ?, entschieden_am = ?, entschieden_von = ? WHERE id = ?`) .run(status, nowIso(), session.actor, id); if (info.changes === 0) return json(res, { ok: false, error: "Nicht gefunden." }, 404); logAction(session.actor, "testimonial.status_changed", id, { status }); return json(res, { ok: true }); } export async function deleteTestimonial(req, res, session) { if (!can(session, "TESTIMONIALS_MANAGE")) return forbidden(res); const id = String(req.body?.id || ""); const info = db.prepare(`DELETE FROM testimonials WHERE id = ?`).run(id); if (info.changes === 0) return json(res, { ok: false, error: "Nicht gefunden." }, 404); logAction(session.actor, "testimonial.deleted", id, null); return json(res, { ok: true }); }