Statische Codes benennen und auf Wunsch ablegen
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
This commit is contained in:
@@ -4,6 +4,7 @@ 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";
|
||||
@@ -49,7 +50,12 @@ export async function GET(
|
||||
margin: code.margin,
|
||||
});
|
||||
|
||||
const content = shortUrl(code.slug, code.domain.hostname);
|
||||
// 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))
|
||||
|
||||
@@ -96,6 +96,15 @@ export async function PATCH(
|
||||
/** Dateien, die nach der Änderung zu diesem Code gehören. */
|
||||
let neueDateien: string[] | null = null;
|
||||
|
||||
// Der Inhalt eines statischen Codes steckt im gedruckten Bild. Ihn hier zu
|
||||
// ändern würde die Ablage von dem entfernen, was tatsächlich im Umlauf
|
||||
// ist — der Eintrag ist ein Schnappschuss, kein steuerbares Ziel.
|
||||
if (parsed.data.data !== undefined && existing.mode === "static") {
|
||||
return jsonError(
|
||||
"Der Inhalt eines statischen Codes lässt sich nicht ändern — er steckt im gedruckten Code selbst. Änderbar ist nur der Name.",
|
||||
);
|
||||
}
|
||||
|
||||
if (parsed.data.data !== undefined) {
|
||||
// Bewusst nur „ausprogrammiert“, nicht „freigegeben“: ein in der
|
||||
// Verwaltung abgeschalteter Typ soll bestehende Codes nicht
|
||||
@@ -127,8 +136,16 @@ export async function PATCH(
|
||||
/* ------------------------------------------------------- Kurz-Link */
|
||||
|
||||
const neuerSlug = parsed.data.slug?.trim();
|
||||
// Ein statischer Code hat keinen Kurz-Link — sein Inhalt steckt in ihm
|
||||
// selbst. Ein Umbenennen waere gegenstandslos.
|
||||
if (existing.mode === "static" && neuerSlug) {
|
||||
return jsonError(
|
||||
"Ein statischer Code hat keinen Kurz-Link. Sein Inhalt steckt im Code selbst.",
|
||||
);
|
||||
}
|
||||
const umbenannt =
|
||||
neuerSlug !== undefined &&
|
||||
existing.slug !== null &&
|
||||
normalizeSlug(neuerSlug) !== normalizeSlug(existing.slug);
|
||||
|
||||
/**
|
||||
@@ -191,7 +208,8 @@ export async function PATCH(
|
||||
await tx.qrCodeAlias.create({
|
||||
data: {
|
||||
domainId: existing.domainId,
|
||||
slug: existing.slug,
|
||||
// `umbenannt` gilt nur bei vorhandenem Kurz-Link.
|
||||
slug: existing.slug as string,
|
||||
qrCodeId: existing.id,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -28,6 +28,12 @@ const createSchema = z.object({
|
||||
.trim()
|
||||
.max(120, { error: "Der Name darf höchstens 120 Zeichen lang sein." })
|
||||
.optional(),
|
||||
/**
|
||||
* Statisch oder dynamisch. Ein statischer Code wird nur abgelegt, damit man
|
||||
* ihn wiederfindet — er bekommt keinen Kurz-Link und zählt keine Scans.
|
||||
*/
|
||||
mode: z.enum(["static", "dynamic"], { error: "Ungültiger Modus." })
|
||||
.default("dynamic"),
|
||||
/**
|
||||
* Selbst gewählter Kurz-Link. Ohne Angabe wird einer erzeugt.
|
||||
* Geprüft wird er in `describeSlugProblem` — hier nur die grobe Länge.
|
||||
@@ -101,8 +107,25 @@ export async function POST(request: NextRequest): Promise<Response> {
|
||||
return jsonError(validated.message);
|
||||
}
|
||||
|
||||
// Erst hier, nach der Validierung: die Probe braucht die geprüften Daten.
|
||||
assertDynamicPossible(qrType, validated.data);
|
||||
const istStatisch = parsed.data.mode === "static";
|
||||
|
||||
if (istStatisch) {
|
||||
// Ein statischer Code trägt seinen Inhalt selbst — der Typ muss das
|
||||
// können, sonst gäbe es nichts zu kodieren.
|
||||
if (!qrType.supportsStatic) {
|
||||
return jsonError(
|
||||
`Der QR-Typ „${qrType.title}“ funktioniert nur als dynamischer Code.`,
|
||||
);
|
||||
}
|
||||
if (parsed.data.slug) {
|
||||
return jsonError(
|
||||
"Ein statischer Code hat keinen Kurz-Link. Sein Inhalt steckt im Code selbst.",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Erst hier, nach der Validierung: die Probe braucht die geprüften Daten.
|
||||
assertDynamicPossible(qrType, validated.data);
|
||||
}
|
||||
|
||||
// Ein selbst gewählter Kurz-Link wird geprüft, bevor irgendetwas entsteht —
|
||||
// sonst stünde am Ende ein Code mit einer Adresse, die niemand wollte.
|
||||
@@ -117,6 +140,30 @@ export async function POST(request: NextRequest): Promise<Response> {
|
||||
await assertSlugAvailable(domainId, gewuenscht);
|
||||
}
|
||||
|
||||
if (istStatisch) {
|
||||
const created = await prisma.qrCode.create({
|
||||
data: {
|
||||
mode: "static",
|
||||
slug: null,
|
||||
type: qrType.id,
|
||||
label: label && label.length > 0 ? label : null,
|
||||
data: toJsonInput(validated.data),
|
||||
groupId,
|
||||
domainId,
|
||||
createdById: actor.kind === "user" ? actor.user.id : null,
|
||||
...design,
|
||||
},
|
||||
include: {
|
||||
group: { select: { name: true } },
|
||||
domain: { select: { hostname: true } },
|
||||
createdBy: { select: { name: true, email: true } },
|
||||
aliases: { select: { slug: true }, orderBy: { createdAt: "desc" } },
|
||||
},
|
||||
});
|
||||
await attachUploads(created.id, qrType.uploadIds(validated.data));
|
||||
return Response.json({ code: toQrCodeDto(created) }, { status: 201 });
|
||||
}
|
||||
|
||||
// Slug-Kollisionen sind extrem unwahrscheinlich, aber nicht unmöglich.
|
||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||
try {
|
||||
|
||||
@@ -46,7 +46,7 @@ export async function POST(request: NextRequest): Promise<Response> {
|
||||
return new Response(body, {
|
||||
headers: {
|
||||
"Content-Type": CONTENT_TYPES[format],
|
||||
"Content-Disposition": `attachment; filename="${downloadFilename(qrType, data, format)}"`,
|
||||
"Content-Disposition": `attachment; filename="${downloadFilename(qrType, data, format, parsed.data.label)}"`,
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user