From e5c321b2c09d4b12fa60ae2b4e6027d90b664d35 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Mon, 26 Oct 2026 16:59:37 +0000 Subject: [PATCH] fix(invites): /i/ redirects to the public host, not localhost:3000 (0.6.1) Behind nginx, request.url is the container address, so the short invite link sent people to https://localhost:3000/signup. Build the Location from the Host header, as /api/invites already does. Co-Authored-By: Claude Opus 5.5 --- package.json | 2 +- src/app/i/[code]/route.test.ts | 8 ++++++++ src/app/i/[code]/route.ts | 9 ++++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2ff4bf9e..9d5f6881 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "media-streamer", - "version": "0.6.0", + "version": "0.6.1", "private": true, "type": "module", "scripts": { diff --git a/src/app/i/[code]/route.test.ts b/src/app/i/[code]/route.test.ts index 7bd20803..683b3b62 100644 --- a/src/app/i/[code]/route.test.ts +++ b/src/app/i/[code]/route.test.ts @@ -22,6 +22,14 @@ describe('/i/', () => { ); }); + it('answers with the public host, not the container address behind nginx', async () => { + const res = await GET( + new NextRequest('http://localhost:3000/i/ABCDEFGHJK', { headers: { host: 'bittorrented.com' } }), + { params: Promise.resolve({ code: 'ABCDEFGHJK' }) } + ); + expect(res.headers.get('location')).toBe('https://bittorrented.com/signup?invite=ABCDE-FGHJK'); + }); + it('sends a malformed code to the invite-only screen', async () => { expect((await go('/i/nope')).location).toBe('https://bittorrented.com/invite-only'); }); diff --git a/src/app/i/[code]/route.ts b/src/app/i/[code]/route.ts index d42ec385..6494c80a 100644 --- a/src/app/i/[code]/route.ts +++ b/src/app/i/[code]/route.ts @@ -12,10 +12,17 @@ import { formatInviteCode, isInviteCodeShape, normalizeInviteCode } from '@/lib/ export const dynamic = 'force-dynamic'; +/** The public origin: behind nginx, request.url is the container's localhost:3000. */ +function origin(request: NextRequest): string { + const host = request.headers.get('x-forwarded-host') ?? request.headers.get('host') ?? request.nextUrl.host; + const proto = host.startsWith('localhost') || host.startsWith('127.') ? 'http' : 'https'; + return `${proto}://${host}`; +} + export async function GET(request: NextRequest, { params }: { params: Promise<{ code: string }> }): Promise { const { code: raw } = await params; const code = normalizeInviteCode(decodeURIComponent(raw ?? '')); - const target = new URL(isInviteCodeShape(code) ? '/signup' : '/invite-only', request.url); + const target = new URL(isInviteCodeShape(code) ? '/signup' : '/invite-only', origin(request)); if (isInviteCodeShape(code)) target.searchParams.set('invite', formatInviteCode(code)); const ref = request.nextUrl.searchParams.get('ref'); if (ref && /^[a-zA-Z0-9_-]{1,64}$/.test(ref)) target.searchParams.set('ref', ref);