import { prisma } from "@/lib/prisma"; import { defaultLocale, isLocale } from "@/i18n/config"; import { personalizationSchema } from "@/lib/certificate/schema"; import { checkCertificateFits } from "@/lib/certificate/layout"; import { listTemplateVersions } from "@/lib/certificate/templates"; import { renderCertificateDocumentImage } from "@/lib/sponsorship/certificate-document"; import { REVISABLE_FIELDS, revisionTerm } from "@/lib/sponsorship/revise"; import { getAuthorizedAdmin, notFoundResponse } from "../../_lib/admin-guard"; const PREVIEW_WIDTH_PX = 900; const WATERMARK = "Korrekturvorschau"; /** Ein Korrekturformular ist klein; alles darueber ist kein legitimer Aufruf. */ const MAX_BODY_BYTES = 8 * 1024; function invalid(body: Record): Response { return Response.json(body, { status: 422, headers: { "Cache-Control": "no-store" } }); } /** * Live-Vorschau der Korrektur: dieselbe Pruefung (Schema, Glyphen, Platz) * und dieselbe Laufzeitregel wie reviseSponsorship, gerendert mit dem * Wasserzeichen "Korrekturvorschau". Ungueltige Eingaben ergeben 422 mit * Feld und Fehlercode, damit das Formular die Stelle markieren kann. * Nur fuer Admins mit 2FA, sonst 404. */ export async function POST(request: Request, ctx: RouteContext<"/admin/patenschaften/[id]/korrekturvorschau">): Promise { if (!(await getAuthorizedAdmin())) return notFoundResponse(); const { id } = await ctx.params; const sponsorship = await prisma.sponsorship.findUnique({ where: { id }, select: { number: true, locale: true, startDate: true, endDate: true, subscriptionId: true, currentRevision: true }, }); if (!sponsorship || sponsorship.currentRevision === 0) return notFoundResponse(); const raw = await request.text(); if (raw.length > MAX_BODY_BYTES) return invalid({ code: "too_large" }); let body: Record; try { const parsedBody: unknown = JSON.parse(raw); body = parsedBody && typeof parsedBody === "object" ? (parsedBody as Record) : {}; } catch { return invalid({ code: "bad_json" }); } const isSubscription = sponsorship.subscriptionId !== null; const startDate = sponsorship.startDate.toISOString().slice(0, 10); const parsed = personalizationSchema.safeParse(isSubscription ? { ...body, startDate } : body); if (!parsed.success) { const issue = parsed.error.issues[0]; const field = REVISABLE_FIELDS.find((name) => name === issue?.path[0]) ?? null; return invalid({ field, code: issue?.message ?? "invalid" }); } const templateVersion = typeof body.templateVersion === "string" ? body.templateVersion : ""; if (!listTemplateVersions().includes(templateVersion)) return invalid({ field: null, code: "unknown_template" }); const locale = isLocale(sponsorship.locale) ? sponsorship.locale : defaultLocale; const term = revisionTerm({ isSubscription, personalization: parsed.data, startDate: sponsorship.startDate, endDate: sponsorship.endDate, }); const fit = checkCertificateFits(parsed.data, locale, { term, templateVersion, number: sponsorship.number }); if (!fit.ok) { return invalid({ field: fit.field, code: fit.reason, characters: fit.reason === "glyphs" ? fit.characters : [] }); } const image = await renderCertificateDocumentImage( { personalization: parsed.data, locale, term, number: sponsorship.number, templateVersion }, { widthPx: PREVIEW_WIDTH_PX, format: "webp", watermark: WATERMARK } ); return new Response(new Uint8Array(image), { headers: { "Content-Type": "image/webp", "Cache-Control": "private, no-store" }, }); }