import type { NextRequest } from "next/server"; import { z } from "zod"; import { prisma } from "@/lib/db"; import { errorResponse, firstZodMessage, jsonError } from "@/lib/http"; export const dynamic = "force-dynamic"; const feedbackSchema = z.object({ codeId: z.string().trim().min(1), rating: z .number() .int() .min(1, { error: "Bitte wähle eine Bewertung." }) .max(5, { error: "Bitte wähle eine Bewertung." }), comment: z .string() .trim() .max(1000, { error: "Der Text ist zu lang (höchstens 1000 Zeichen)." }) .default(""), }); /** * Grobe Bremse gegen Fluten. * * Im Arbeitsspeicher und damit pro Container — bei einem Dienst dieser Größe * genügt das. Die Adresse wird nur als Schlüssel benutzt und nirgends * gespeichert; nach dem Zeitfenster ist sie wieder weg. */ const FENSTER_MS = 60_000; const MAX_PRO_FENSTER = 10; const zaehler = new Map(); function zuSchnell(schluessel: string): boolean { const jetzt = Date.now(); const eintrag = zaehler.get(schluessel); if (!eintrag || eintrag.bis < jetzt) { zaehler.set(schluessel, { anzahl: 1, bis: jetzt + FENSTER_MS }); // Gelegentlich ausmisten, damit die Tabelle nicht wächst. if (zaehler.size > 5000) { for (const [key, wert] of zaehler) { if (wert.bis < jetzt) zaehler.delete(key); } } return false; } eintrag.anzahl += 1; return eintrag.anzahl > MAX_PRO_FENSTER; } /** Nimmt eine Rückmeldung von einer öffentlichen Landingpage entgegen. */ export async function POST(request: NextRequest): Promise { try { const parsed = feedbackSchema.safeParse(await request.json()); if (!parsed.success) { return jsonError(firstZodMessage(parsed.error)); } const herkunft = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || request.headers.get("x-real-ip") || "unbekannt"; if (zuSchnell(herkunft)) { return jsonError( "Zu viele Rückmeldungen in kurzer Zeit. Bitte versuche es gleich noch einmal.", 429, ); } // Nur Codes, die tatsächlich Feedback einsammeln — sonst ließen sich // beliebige Codes mit Einträgen zumüllen. const code = await prisma.qrCode.findUnique({ where: { id: parsed.data.codeId }, select: { id: true, type: true }, }); if (!code || code.type !== "feedback") { return jsonError("Zu diesem Code lässt sich keine Rückmeldung abgeben.", 404); } await prisma.feedback.create({ data: { qrCodeId: code.id, rating: parsed.data.rating, comment: parsed.data.comment.length > 0 ? parsed.data.comment : null, }, }); return Response.json({ ok: true }, { status: 201 }); } catch (error) { return errorResponse(error); } }