From c81d1eefb3bc38dcf995d99d1cd3be61bafca791 Mon Sep 17 00:00:00 2001 From: JP Date: Sat, 8 Aug 2026 21:03:06 +0000 Subject: [PATCH] Build invite redirects from the public origin request.url carries the app's internal bind address (0.0.0.0:3000) because it runs behind a reverse proxy, so every invite link - valid or not - redirected to an unreachable https://0.0.0.0:3000/join. Uses NEXTAUTH_URL, which is the configured public origin and cannot be influenced by a request header, falling back to forwarded headers. Co-Authored-By: Claude Opus 5 (1M context) --- src/app/invite/[token]/route.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/app/invite/[token]/route.ts b/src/app/invite/[token]/route.ts index f731a00..9400162 100644 --- a/src/app/invite/[token]/route.ts +++ b/src/app/invite/[token]/route.ts @@ -13,12 +13,26 @@ import { INVITE_COOKIE, INVITE_COOKIE_MAX_AGE, inspectInvite } from "@/lib/invit */ export const dynamic = "force-dynamic" +/** + * The app binds 0.0.0.0:3000 behind a reverse proxy, so `request.url` carries the + * internal address and redirects built from it are unreachable. NEXTAUTH_URL is the + * configured public origin and cannot be influenced by a request header. + */ +function publicOrigin(request: Request): string { + const configured = process.env.NEXTAUTH_URL + if (configured) return configured.replace(/\/$/, "") + + const host = request.headers.get("x-forwarded-host") ?? request.headers.get("host") + const proto = request.headers.get("x-forwarded-proto") ?? "https" + return host ? `${proto}://${host}` : new URL(request.url).origin +} + export async function GET( request: Request, { params }: { params: { token: string } } ) { const { state } = await inspectInvite(params.token) - const origin = new URL(request.url).origin + const origin = publicOrigin(request) if (state !== "valid") { return NextResponse.redirect(new URL(`/join?error=${state}`, origin))