Das Feld „Name (optional)" stand nur im Dynamisch-Block. Bei einem statischen Code gab es damit keine Moeglichkeit, ihm einen Namen zum Wiederfinden zu geben. Jetzt gilt es in beiden Betriebsarten und tut zweierlei: Es bestimmt den Dateinamen beim Herunterladen — aus „Plakat Eingang" wird qr-plakat-eingang.png statt qr-example-com-plakat.png — und es laesst sich damit ein statischer Code in „Meine Codes" ablegen. Ein abgelegter statischer Code ist ein Schnappschuss, kein steuerbares Ziel. Aenderbar ist nur der Name; Inhalt und Gestaltung bleiben, wie sie beim Ablegen waren, denn genau das steckt im gedruckten Bild. Er bekommt keinen Kurz-Link, ist unter /r nicht erreichbar und zaehlt keine Scans — in der Liste steht dort „statisch" und ein Strich. Wer nach dem Ablegen den Inhalt aendert, bekommt das Ablegen erneut angeboten; der vorherige Eintrag bleibt als eigener Schnappschuss. Dafuer traegt QrCode jetzt `mode`, und `slug` darf leer sein. In PostgreSQL schliessen sich mehrere NULL in einem eindeutigen Index nicht aus, die Bedingung „je Domain eindeutig" gilt fuer dynamische Codes also unveraendert. Refs #2
57 lines
1.8 KiB
TypeScript
57 lines
1.8 KiB
TypeScript
import type { NextRequest } from "next/server";
|
|
import { z } from "zod";
|
|
|
|
import { getActor, requireSignedIn } from "@/lib/auth/actor";
|
|
import { errorResponse, firstZodMessage, jsonError } from "@/lib/http";
|
|
import { CONTENT_TYPES, renderPng, renderSvg } from "@/lib/qr/render";
|
|
import {
|
|
downloadFilename,
|
|
qrRequestSchema,
|
|
QrRequestError,
|
|
resolveQrRequest,
|
|
} from "@/lib/qr/request";
|
|
|
|
const downloadSchema = qrRequestSchema.extend({
|
|
format: z.enum(["png", "svg"], { error: "Ungültiges Dateiformat." }),
|
|
});
|
|
|
|
/** Liefert den fertigen QR-Code als PNG- oder SVG-Datei zum Herunterladen. */
|
|
export async function POST(request: NextRequest): Promise<Response> {
|
|
try {
|
|
// Der Generator steht nur Angemeldeten offen; die Weiterleitung
|
|
// unter /r/{slug} bleibt davon unberührt und öffentlich.
|
|
requireSignedIn(await getActor());
|
|
|
|
const parsed = downloadSchema.safeParse(await request.json());
|
|
if (!parsed.success) {
|
|
return jsonError(firstZodMessage(parsed.error));
|
|
}
|
|
|
|
const { format, design } = parsed.data;
|
|
|
|
// Ein dynamischer Code ohne Slug wäre eine Weiterleitung ins Leere.
|
|
if (parsed.data.mode === "dynamic" && !parsed.data.slug) {
|
|
throw new QrRequestError(
|
|
"Bitte speichere den dynamischen Code zuerst — erst dann existiert die Kurz-URL.",
|
|
);
|
|
}
|
|
|
|
const { qrType, data, content } = await resolveQrRequest(parsed.data);
|
|
|
|
const body =
|
|
format === "png"
|
|
? new Uint8Array(await renderPng(content, design))
|
|
: await renderSvg(content, design);
|
|
|
|
return new Response(body, {
|
|
headers: {
|
|
"Content-Type": CONTENT_TYPES[format],
|
|
"Content-Disposition": `attachment; filename="${downloadFilename(qrType, data, format, parsed.data.label)}"`,
|
|
"Cache-Control": "no-store",
|
|
},
|
|
});
|
|
} catch (error) {
|
|
return errorResponse(error);
|
|
}
|
|
}
|