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) <noreply@anthropic.com>
This commit is contained in:
JP
2026-08-08 21:03:06 +00:00
parent a6cabc5178
commit c81d1eefb3

View File

@@ -13,12 +13,26 @@ import { INVITE_COOKIE, INVITE_COOKIE_MAX_AGE, inspectInvite } from "@/lib/invit
*/ */
export const dynamic = "force-dynamic" 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( export async function GET(
request: Request, request: Request,
{ params }: { params: { token: string } } { params }: { params: { token: string } }
) { ) {
const { state } = await inspectInvite(params.token) const { state } = await inspectInvite(params.token)
const origin = new URL(request.url).origin const origin = publicOrigin(request)
if (state !== "valid") { if (state !== "valid") {
return NextResponse.redirect(new URL(`/join?error=${state}`, origin)) return NextResponse.redirect(new URL(`/join?error=${state}`, origin))