diff --git a/app/lib/payload.ts b/app/lib/payload.ts index 4442ad3..52fa7a0 100644 --- a/app/lib/payload.ts +++ b/app/lib/payload.ts @@ -1251,20 +1251,27 @@ export async function getSeoSettings(): Promise { }; } -// Powers app/r/[code]/route.ts — a static short link (e.g. printed on a QR -// code) that redirects to a `targetPath` editable in Payload at any time, -// so the QR code itself never needs reprinting. `cache: "no-store"` -// (unlike this file's other public-catalog fetches) since a stale hit here -// would send a visitor to a since-changed target, and the PATCH below needs -// the just-fetched id/clickCount, not a 60s-old ISR snapshot. +// Powers app/r/[code]/route.ts and app/sticker/[code]/route.ts — a static +// short link (e.g. printed on a QR code) that redirects to a `targetPath` +// editable in Payload at any time, so the QR code itself never needs +// reprinting. `cache: "no-store"` (unlike this file's other public-catalog +// fetches) since a stale hit here would send a visitor to a since-changed +// target, and the PATCH below needs the just-fetched id/clickCount, not a +// 60s-old ISR snapshot. type PayloadRedirect = { id: number; targetPath: string; clickCount: number }; -// PATCH failure only logs — click tracking is informational, never worth -// stranding a visitor on a broken link over. -export async function resolveAndTrackRedirect(code: string): Promise { +// `urlPrefix` filters the match to whichever fixed frontend route is +// actually calling this — Redirects.urlPrefix is otherwise just an admin +// label, not something either route enforced, so a code edited to +// urlPrefix "/sticker" in Payload would silently keep resolving under +// /r/ too without this. Each call site passes its own literal +// prefix (see route.ts files), so this actually makes the field mean +// something rather than only decorate the admin list. +export async function resolveAndTrackRedirect(code: string, urlPrefix: "/r" | "/sticker"): Promise { const params = new URLSearchParams({ "where[tenant.slug][equals]": TENANT_SLUG, "where[code][equals]": code, + "where[urlPrefix][equals]": urlPrefix, "where[active][equals]": "true", limit: "1", }); diff --git a/app/r/[code]/route.ts b/app/r/[code]/route.ts index c0118d5..537181d 100644 --- a/app/r/[code]/route.ts +++ b/app/r/[code]/route.ts @@ -9,7 +9,7 @@ import { resolveAndTrackRedirect } from "../../lib/payload"; // non-cacheable client-side since the target can change at any time. export async function GET(_request: NextRequest, { params }: { params: Promise<{ code: string }> }) { const { code } = await params; - const targetPath = await resolveAndTrackRedirect(code); + const targetPath = await resolveAndTrackRedirect(code, "/r"); // Defense in depth — Redirects.targetPath is already validated in Payload // to start with "/", same open-redirect guard as api/preview/route.ts. diff --git a/app/sticker/[code]/route.ts b/app/sticker/[code]/route.ts index 661521e..233a8d5 100644 --- a/app/sticker/[code]/route.ts +++ b/app/sticker/[code]/route.ts @@ -5,11 +5,12 @@ import { resolveAndTrackRedirect } from "../../lib/payload"; // Legacy sticker QR codes already printed as https://einfach-produktiv.com/sticker/ // before the /r/[code] short-link scheme existed — can't be reprinted, so // this mirrors app/r/[code]/route.ts's exact lookup (same Redirects -// collection, matched by `code`) under the fixed /sticker prefix instead. -// Keeps a single admin-editable/toggleable source of truth for both. +// collection, matched by `code` + urlPrefix "/sticker") under the fixed +// /sticker prefix instead. Keeps a single admin-editable/toggleable source +// of truth for both. export async function GET(_request: NextRequest, { params }: { params: Promise<{ code: string }> }) { const { code } = await params; - const targetPath = await resolveAndTrackRedirect(code); + const targetPath = await resolveAndTrackRedirect(code, "/sticker"); if (!targetPath || !targetPath.startsWith("/") || targetPath.startsWith("//")) { notFound();