fix(auth): use public URL for magic-link redirects
This commit is contained in:
@@ -3,6 +3,7 @@ import { createAuthRepository, ipObservations, recordEvent } from "@minecraft-ac
|
|||||||
import { getClientIp } from "@minecraft-account-manager/network";
|
import { getClientIp } from "@minecraft-account-manager/network";
|
||||||
import type { NextRequest } from "next/server";
|
import type { NextRequest } from "next/server";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
import { applicationUrl } from "@/lib/application-url";
|
||||||
import { db } from "@/lib/database";
|
import { db } from "@/lib/database";
|
||||||
import { getIpIntelligence, toAuditIpData } from "@/lib/ip-intelligence";
|
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 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, {
|
response.cookies.set(SESSION_COOKIE_NAME, result.sessionToken, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === "production",
|
secure: process.env.NODE_ENV === "production",
|
||||||
@@ -49,7 +50,7 @@ export async function GET(request: NextRequest) {
|
|||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof InvalidLoginCodeError) {
|
if (error instanceof InvalidLoginCodeError) {
|
||||||
return NextResponse.redirect(new URL("/auth/error", request.url));
|
return NextResponse.redirect(applicationUrl("/auth/error"));
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## 2026-08-01
|
## 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.
|
* **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.
|
* **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.
|
* **Verify**: Confirmed the `v1.1.0` Discord bot image and matching web, migration, and Velocity artifacts.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ type: User Story
|
|||||||
title: Authenticate with a Discord magic link
|
title: Authenticate with a Discord magic link
|
||||||
description: Discord users receive private single-use links that establish secure portal sessions.
|
description: Discord users receive private single-use links that establish secure portal sessions.
|
||||||
tags: [player, discord, authentication, security]
|
tags: [player, discord, authentication, security]
|
||||||
timestamp: 2026-08-01T18:43:58Z
|
timestamp: 2026-08-01T20:43:46Z
|
||||||
story_id: US-002
|
story_id: US-002
|
||||||
status: verified
|
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 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 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 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.
|
- [x] Given an invalid, expired, or consumed link, then the user sees a safe recovery page instructing them to request another link.
|
||||||
|
|
||||||
# Implementation
|
# 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/auth/src/index.ts`](../packages/auth/src/index.ts)
|
||||||
- [`packages/database/src/auth-repository.ts`](../packages/database/src/auth-repository.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/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
|
# Validation
|
||||||
|
|
||||||
- [`packages/auth/test/magic-link.test.ts`](../packages/auth/test/magic-link.test.ts)
|
- [`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.
|
- Discord command and authentication workspaces pass TypeScript validation.
|
||||||
|
|
||||||
# Related Stories
|
# Related Stories
|
||||||
|
|||||||
Reference in New Issue
Block a user