import type { NextRequest } from "next/server";
import { prisma } from "@/lib/db";
import { ensureDefaultDomain, resolveRequestDomain } from "@/lib/domains";
import { getQrType } from "@/lib/qr/registry";
import { isValidSlug } from "@/lib/slug";
export const dynamic = "force-dynamic";
type Context = { params: Promise<{ slug: string }> };
function notFound(message: string): Response {
return new Response(
`
QR-Code nicht gefunden
🔍
QR-Code nicht gefunden
${message}
`,
{ status: 404, headers: { "Content-Type": "text/html; charset=utf-8" } },
);
}
/**
* Weiterleitung eines dynamischen QR-Codes.
*
* Bewusst 302 (temporär): Das Ziel ist jederzeit änderbar, ein permanenter
* Redirect würde in Browsern und Zwischenspeichern hängen bleiben.
*
* Der Slug ist nur je Domain eindeutig, deshalb wird zuerst die Domain aus
* dem `Host`-Header bestimmt. Ein unbekannter Hostname endet hier — sonst
* ließe sich über einen gefälschten Header ein fremder Code ansprechen.
*/
export async function GET(
request: NextRequest,
context: Context,
): Promise {
const { slug } = await context.params;
if (!isValidSlug(slug)) {
return notFound("Diese Kurz-Adresse ist ungĂĽltig.");
}
const matched = await resolveRequestDomain(request.headers);
// Aufruf über eine nicht registrierte Adresse — etwa direkt über die IP im
// LAN. Dann gilt die Standard-Domain aus BASE_URL.
const domain = matched ?? {
...(await ensureDefaultDomain()),
isActive: true,
};
if (!domain.isActive) {
return notFound(
"Diese Adresse ist zurzeit abgeschaltet. Bitte wende dich an denjenigen, der den Code erstellt hat.",
);
}
const code = await prisma.qrCode.findUnique({
where: { domainId_slug: { domainId: domain.id, slug } },
});
if (!code) {
return notFound(
"Der Code wurde gelöscht oder hat nie existiert. Bitte wende dich an denjenigen, der ihn erstellt hat.",
);
}
const qrType = getQrType(code.type);
if (!qrType) {
return notFound("Der Typ dieses Codes wird nicht mehr unterstĂĽtzt.");
}
let target: string;
if (qrType.hasLandingPage) {
// Kein fremdes Ziel — die Seite liefern wir selbst aus. Bewusst relativ,
// damit der Aufruf auf der Adresse bleibt, ĂĽber die er hereinkam (auch
// wenn das eine nicht registrierte ist, etwa eine IP im LAN).
target = `/s/${slug}`;
} else {
try {
// Der User-Agent geht mit: Typen wie der App-Link entscheiden daran,
// wohin die Weiterleitung fĂĽhrt.
target = qrType.redirectTarget(code.data, {
userAgent: request.headers.get("user-agent") ?? undefined,
});
} catch {
return notFound("Das Ziel dieses Codes ist nicht mehr gĂĽltig.");
}
}
// Der Zähler darf die Weiterleitung nicht ausbremsen oder blockieren.
try {
await prisma.qrCode.update({
where: { id: code.id },
data: { scans: { increment: 1 }, lastScanAt: new Date() },
});
} catch (error) {
console.error("Scan-Zähler konnte nicht erhöht werden:", error);
}
return new Response(null, {
status: 302,
headers: {
Location: target,
// Ohne dies wĂĽrden Browser die Weiterleitung zwischenspeichern und
// spätere Zieländerungen sowie der Scan-Zähler blieben wirkungslos.
"Cache-Control": "no-store, no-cache, must-revalidate",
},
});
}