Zur Wahl stehen zwei Quellen: das Logo der Installation aus dem Erscheinungsbild oder ein eigenes je Code. Die Groesse ist zwischen 10 und 25 Prozent der Kantenlaenge einstellbar, hinter dem Bild liegt eine Flaeche in der Hintergrundfarbe. Mit Logo gilt die Fehlerkorrektur H, unabhaengig von der Auswahl — was das Bild verdeckt, muss sie ausgleichen. Die Auswahl steht dann fest auf H und sagt auch warum; heimlich geschieht das nicht. Ab 22 Prozent kommt eine Warnung dazu. Gerendert wird weiterhin serverseitig, damit Vorschau und Download uebereinstimmen: das SVG bekommt ein eingebettetes Bild, das PNG wird mit sharp zusammengesetzt. sharp lag ohnehin als Abhaengigkeit von Next im Baum und steht jetzt ausdruecklich in der package.json. Ein eigenes Logo gehoert zur Gestaltung, nicht zum Inhalt — es muss trotzdem an den Code gehaengt werden. Sonst raeumt die Aufraeumroutine es nach einem Tag weg und der Code verliert sein Bild. Refs #8
90 lines
3.1 KiB
TypeScript
90 lines
3.1 KiB
TypeScript
import type { NextRequest } from "next/server";
|
|
|
|
import { getActor } from "@/lib/auth/actor";
|
|
import { requirePermission } from "@/lib/auth/policy";
|
|
import { prisma } from "@/lib/db";
|
|
import { shortUrl } from "@/lib/env";
|
|
import { requireImplementedQrType } from "@/lib/qr/availability";
|
|
import { errorResponse } from "@/lib/http";
|
|
import { designSchema } from "@/lib/qr/design";
|
|
import { getQrType } from "@/lib/qr/registry";
|
|
import { ladeLogo } from "@/lib/qr/logo";
|
|
import { CONTENT_TYPES, renderPng, renderSvg } from "@/lib/qr/render";
|
|
import { downloadFilename, QrRequestError } from "@/lib/qr/request";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
type Context = { params: Promise<{ id: string }> };
|
|
|
|
/**
|
|
* Rendert einen gespeicherten dynamischen Code mit seiner hinterlegten
|
|
* Gestaltung. Wird für die Vorschaubilder in „Meine Codes“ und für die
|
|
* Download-Links dort verwendet.
|
|
*/
|
|
export async function GET(
|
|
request: NextRequest,
|
|
context: Context,
|
|
): Promise<Response> {
|
|
try {
|
|
const { id } = await context.params;
|
|
const format =
|
|
request.nextUrl.searchParams.get("format") === "png" ? "png" : "svg";
|
|
const isDownload = request.nextUrl.searchParams.get("download") === "1";
|
|
|
|
const code = await prisma.qrCode.findUnique({
|
|
where: { id },
|
|
include: { domain: { select: { hostname: true } } },
|
|
});
|
|
if (!code) {
|
|
throw new QrRequestError("Dieser QR-Code existiert nicht (mehr).", 404);
|
|
}
|
|
|
|
// Das Bild verrät die Kurz-URL und damit das Ziel — es gehört hinter
|
|
// dieselbe Prüfung wie die Liste.
|
|
requirePermission(await getActor(), "codes:read", { groupId: code.groupId });
|
|
|
|
const design = designSchema.parse({
|
|
fgColor: code.fgColor,
|
|
bgColor: code.bgColor,
|
|
size: code.size,
|
|
ecLevel: code.ecLevel,
|
|
margin: code.margin,
|
|
logoSource: code.logoSource,
|
|
logoUploadId: code.logoUploadId,
|
|
logoScale: code.logoScale,
|
|
});
|
|
const logo = await ladeLogo(design);
|
|
|
|
// Was im Bild steckt, haengt an der Betriebsart: Ein dynamischer Code
|
|
// traegt die Kurz-URL, ein statischer seinen Inhalt selbst.
|
|
const content =
|
|
code.mode === "static" || !code.slug
|
|
? requireImplementedQrType(code.type).encode(code.data)
|
|
: shortUrl(code.slug, code.domain.hostname);
|
|
const body =
|
|
format === "png"
|
|
? new Uint8Array(await renderPng(content, design, logo))
|
|
: await renderSvg(content, design, logo);
|
|
|
|
const headers: Record<string, string> = {
|
|
"Content-Type": CONTENT_TYPES[format],
|
|
// Das Ziel eines dynamischen Codes ist änderbar; die Kurz-URL im Bild
|
|
// nicht. Trotzdem kein Caching, damit Gestaltungsänderungen sofort
|
|
// sichtbar werden.
|
|
"Cache-Control": "no-store",
|
|
};
|
|
|
|
if (isDownload) {
|
|
const qrType = getQrType(code.type);
|
|
const name = qrType
|
|
? downloadFilename(qrType, code.data, format)
|
|
: `qr-${code.slug}.${format}`;
|
|
headers["Content-Disposition"] = `attachment; filename="${name}"`;
|
|
}
|
|
|
|
return new Response(body, { headers });
|
|
} catch (error) {
|
|
return errorResponse(error);
|
|
}
|
|
}
|