Neue Seite Verwaltung -> QR-Typen mit einem Schalter je Typ. Abgeschaltete Typen verschwinden vollstaendig von der Startseite und lassen sich nicht mehr neu anlegen. - Neue Tabelle QrTypeSetting; fehlender Eintrag heisst freigegeben, damit eine frische Installation ohne Vorbelegung laeuft und spaetere Typen automatisch erscheinen. - Registry-Feld 'active' heisst jetzt 'implemented' und beschreibt nur noch, ob der Typ ausprogrammiert ist. Ob er erscheint, entscheidet zusaetzlich die Freigabe aus der Datenbank (src/lib/qr/availability.ts). - Bestehende dynamische Codes bleiben von einer Abschaltung unberuehrt: die Weiterleitung funktioniert weiter und Bearbeiten bleibt moeglich. Ein gedruckter Code darf durch einen Schalter nicht ungueltig werden. - resolveQrRequest ist dadurch asynchron geworden; QrRequestError liegt jetzt in type-module.ts, sonst gaebe es einen Ringschluss mit availability.ts. - Startseite meldet jetzt auch den Fall, dass nur noch Platzhalter freigegeben sind - vorher standen dort 14 tote Kacheln ohne Erklaerung. - Kopfzeile: Schriftzug auf schmalen Geraeten ausgeblendet, sonst passte die dritte Navigationsschaltflaeche nicht mehr in die Zeile (7px Ueberlauf).
91 lines
2.7 KiB
TypeScript
91 lines
2.7 KiB
TypeScript
import type { NextRequest } from "next/server";
|
|
import { z } from "zod";
|
|
|
|
import { toJsonInput, toQrCodeDto } from "@/lib/codes";
|
|
import { prisma } from "@/lib/db";
|
|
import { errorResponse, firstZodMessage, jsonError } from "@/lib/http";
|
|
import { requireAvailableQrType } from "@/lib/qr/availability";
|
|
import { designSchema } from "@/lib/qr/design";
|
|
import { QR_TYPE_IDS, QrRequestError } from "@/lib/qr/type-module";
|
|
import { generateSlug } from "@/lib/slug";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
const createSchema = z.object({
|
|
type: z.enum(QR_TYPE_IDS, { error: "Unbekannter QR-Typ." }),
|
|
data: z.record(z.string(), z.unknown()),
|
|
design: designSchema,
|
|
label: z
|
|
.string()
|
|
.trim()
|
|
.max(120, { error: "Der Name darf höchstens 120 Zeichen lang sein." })
|
|
.optional(),
|
|
});
|
|
|
|
function isUniqueViolation(error: unknown): boolean {
|
|
return (
|
|
typeof error === "object" &&
|
|
error !== null &&
|
|
"code" in error &&
|
|
(error as { code?: unknown }).code === "P2002"
|
|
);
|
|
}
|
|
|
|
/** Alle dynamischen Codes, neueste zuerst. */
|
|
export async function GET(): Promise<Response> {
|
|
try {
|
|
const codes = await prisma.qrCode.findMany({ orderBy: { createdAt: "desc" } });
|
|
return Response.json({ codes: codes.map(toQrCodeDto) });
|
|
} catch (error) {
|
|
return errorResponse(error);
|
|
}
|
|
}
|
|
|
|
/** Legt einen dynamischen Code an und vergibt dafür eine freie Kurz-URL. */
|
|
export async function POST(request: NextRequest): Promise<Response> {
|
|
try {
|
|
const parsed = createSchema.safeParse(await request.json());
|
|
if (!parsed.success) {
|
|
return jsonError(firstZodMessage(parsed.error));
|
|
}
|
|
|
|
const { type, design, label } = parsed.data;
|
|
const qrType = await requireAvailableQrType(type);
|
|
if (!qrType.supportsDynamic) {
|
|
throw new QrRequestError(
|
|
`Der QR-Typ „${qrType.title}“ unterstützt keine dynamischen Codes.`,
|
|
);
|
|
}
|
|
|
|
const validated = qrType.validate(parsed.data.data);
|
|
if (!validated.ok) {
|
|
return jsonError(validated.message);
|
|
}
|
|
|
|
// Slug-Kollisionen sind extrem unwahrscheinlich, aber nicht unmöglich.
|
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
try {
|
|
const created = await prisma.qrCode.create({
|
|
data: {
|
|
slug: generateSlug(),
|
|
type: qrType.id,
|
|
label: label && label.length > 0 ? label : null,
|
|
data: toJsonInput(validated.data),
|
|
...design,
|
|
},
|
|
});
|
|
return Response.json({ code: toQrCodeDto(created) }, { status: 201 });
|
|
} catch (error) {
|
|
if (!isUniqueViolation(error)) throw error;
|
|
}
|
|
}
|
|
|
|
throw new QrRequestError(
|
|
"Es konnte keine freie Kurz-URL erzeugt werden. Bitte versuche es erneut.",
|
|
500,
|
|
);
|
|
} catch (error) {
|
|
return errorResponse(error);
|
|
}
|
|
}
|