From 5e693e2cddf5909bea568687bc375e360a0da354 Mon Sep 17 00:00:00 2001 From: Dylan Garvis Date: Sat, 1 Aug 2026 16:44:01 -0400 Subject: [PATCH] fix(auth): use public URL for magic-link redirects --- apps/web/src/app/auth/discord/route.ts | 5 +++-- apps/web/src/lib/application-url.test.ts | 9 +++++++++ apps/web/src/lib/application-url.ts | 4 ++++ design/log.md | 1 + design/us-002-discord-magic-link.md | 5 ++++- 5 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/lib/application-url.test.ts create mode 100644 apps/web/src/lib/application-url.ts diff --git a/apps/web/src/app/auth/discord/route.ts b/apps/web/src/app/auth/discord/route.ts index f84204e..674185a 100644 --- a/apps/web/src/app/auth/discord/route.ts +++ b/apps/web/src/app/auth/discord/route.ts @@ -3,6 +3,7 @@ import { createAuthRepository, ipObservations, recordEvent } from "@minecraft-ac import { getClientIp } from "@minecraft-account-manager/network"; import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; +import { applicationUrl } from "@/lib/application-url"; import { db } from "@/lib/database"; import { getIpIntelligence, toAuditIpData } from "@/lib/ip-intelligence"; @@ -38,7 +39,7 @@ export async function GET(request: NextRequest) { ]); const destination = result.user.firstName ? "/account" : "/welcome"; - const response = NextResponse.redirect(new URL(destination, request.url)); + const response = NextResponse.redirect(applicationUrl(destination)); response.cookies.set(SESSION_COOKIE_NAME, result.sessionToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", @@ -49,7 +50,7 @@ export async function GET(request: NextRequest) { return response; } catch (error) { if (error instanceof InvalidLoginCodeError) { - return NextResponse.redirect(new URL("/auth/error", request.url)); + return NextResponse.redirect(applicationUrl("/auth/error")); } throw error; } diff --git a/apps/web/src/lib/application-url.test.ts b/apps/web/src/lib/application-url.test.ts new file mode 100644 index 0000000..9e4bfee --- /dev/null +++ b/apps/web/src/lib/application-url.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from "vitest"; +import { applicationUrl } from "./application-url"; + +describe("applicationUrl", () => { + it("builds browser redirects from the configured public application URL", () => { + expect(applicationUrl("/welcome", "https://portal.somc.club")) + .toEqual(new URL("https://portal.somc.club/welcome")); + }); +}); diff --git a/apps/web/src/lib/application-url.ts b/apps/web/src/lib/application-url.ts new file mode 100644 index 0000000..b2d3d57 --- /dev/null +++ b/apps/web/src/lib/application-url.ts @@ -0,0 +1,4 @@ +export function applicationUrl(path: string, baseUrl = process.env.APP_URL) { + if (!baseUrl) throw new Error("APP_URL is required to build public application URLs"); + return new URL(path, baseUrl); +} diff --git a/design/log.md b/design/log.md index a20b567..b0d23f3 100644 --- a/design/log.md +++ b/design/log.md @@ -2,6 +2,7 @@ ## 2026-08-01 +* **Fix**: Build magic-link redirects from the configured public portal URL instead of the reverse proxy's internal request origin. * **Verify**: Confirmed `v1.1.1` left all pre-existing `latest` digests unchanged while publishing versioned artifacts. * **Refine**: Removed mutable `latest` publication so all deployable artifacts use explicit semantic versions. * **Verify**: Confirmed the `v1.1.0` Discord bot image and matching web, migration, and Velocity artifacts. diff --git a/design/us-002-discord-magic-link.md b/design/us-002-discord-magic-link.md index 716d23e..67058b8 100644 --- a/design/us-002-discord-magic-link.md +++ b/design/us-002-discord-magic-link.md @@ -3,7 +3,7 @@ type: User Story title: Authenticate with a Discord magic link description: Discord users receive private single-use links that establish secure portal sessions. tags: [player, discord, authentication, security] -timestamp: 2026-08-01T18:43:58Z +timestamp: 2026-08-01T20:43:46Z story_id: US-002 status: verified --- @@ -19,6 +19,7 @@ As a Discord community member, I want `/register` and `/account` to issue a priv - [x] Given a login token, then it expires after ten minutes and can be consumed only once. - [x] Given repeated link requests, then requests are rate limited per Discord user and older active links are invalidated. - [x] Given a valid link, when it is consumed, then the Discord user is created or refreshed and a secure seven-day session is established. +- [x] Given a magic-link result behind a reverse proxy, then the browser is redirected through the configured public application URL rather than an internal container address. - [x] Given an invalid, expired, or consumed link, then the user sees a safe recovery page instructing them to request another link. # Implementation @@ -27,10 +28,12 @@ As a Discord community member, I want `/register` and `/account` to issue a priv - [`packages/auth/src/index.ts`](../packages/auth/src/index.ts) - [`packages/database/src/auth-repository.ts`](../packages/database/src/auth-repository.ts) - [`apps/web/src/app/auth/discord/route.ts`](../apps/web/src/app/auth/discord/route.ts) +- [`apps/web/src/lib/application-url.ts`](../apps/web/src/lib/application-url.ts) # Validation - [`packages/auth/test/magic-link.test.ts`](../packages/auth/test/magic-link.test.ts) +- [`apps/web/src/lib/application-url.test.ts`](../apps/web/src/lib/application-url.test.ts) - Discord command and authentication workspaces pass TypeScript validation. # Related Stories