Files
dogfather-universe/server-internal/routes/testimonials.js
T
DogFatherGitandClaude Opus 5 399260b805 "Stimmen" verwandelt: Fans reichen selbst Testimonials ein
Nutzer-Wunsch 20.08.2026: "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 ... das wird mega persoenlich
zu den fans, bitte wirklich krass geil speziell."

- Neue, augenschonend gestaltete Einreich-Kachel auf stimmen.html: Name,
  TikTok-Name (optional), Nachricht -- landet NIE automatisch oeffentlich,
  sondern immer erst als Entwurf in der Verwaltung.
  Warmer Babyblau/Rosé-Farbverlauf, wandernder Lichtschein, pulsierendes
  Herz -- ersetzt die 3 ewigen "Hier steht bald..."-Platzhalterkarten.
- Neue Verwaltungs-Sektion "💬 Stimmen verwalten": Warteschlange (offene
  zuerst), Freigeben/Ablehnen/Löschen pro Eintrag.
- Freigegebene Stimmen erscheinen automatisch in einer neuen, spezielleren
  Zitat-Kartenoptik (großes Anführungszeichen, Name + TikTok-Chip) --
  Abschnitt bleibt komplett unsichtbar, solange keine einzige freigegeben
  wurde.
- Backend: neue Tabelle `testimonials` (Migration 0006), routes/
  testimonials.js (oeffentliches Einreichen + Lesen freigegebener,
  admin-Warteschlange + Status/Loeschen mit neuer TESTIMONIALS_MANAGE-
  Berechtigung). 18 automatisierte Tests gegen eine Fake-DB bestanden.
- Alles per Playwright visuell durchgespielt: leerer Zustand, Einreichen
  -> Erfolgsmeldung, freigegebene Stimmen-Anzeige, Verwaltungs-Warteschlange
  mit allen Aktionen.

Backend-Teil (server-internal/, cloudflare-worker/migrations/) noch ohne
Deploy-Zugriff -- Dogi muss ihn manuell auf dogiintern ausrollen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 23:37:31 +02:00

119 lines
5.0 KiB
JavaScript

/* =====================================================================
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 });
}