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"; import { findCodeBySlug } from "@/lib/slug-resolve"; export const dynamic = "force-dynamic"; // Ein Kurz-Link darf „/" enthalten (z. B. kunde-a/sommer), deshalb eine // Sammelroute: Next liefert die Abschnitte dann als Liste. 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 abschnitte = (await context.params).slug; const slug = (abschnitte ?? []).join("/"); 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 gefunden = await findCodeBySlug(domain.id, slug); const code = gefunden?.code; 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/${code.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", }, }); }