feat: centralize app base URL — no localhost fallback in production

Introduce getAppBaseUrl() in packages/api/src/lib/app-base-url.ts:
- Reads NEXTAUTH_URL (trimmed, trailing slash stripped)
- production: throws if NEXTAUTH_URL is missing/empty so broken
  localhost links in emails are caught at runtime, not silently sent
- development/test: falls back to http://localhost:3100 with a
  one-time console.warn

Replace the duplicated inline fallback in:
- packages/api/src/router/invite.ts (invite email link)
- packages/api/src/router/auth.ts (password reset email link)

Extend GET /api/health to report:
  "baseUrl": { "configured": bool, "isLocalhost": bool }
so deployment checks can detect a misconfigured NEXTAUTH_URL.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-04-02 14:19:19 +02:00
parent 7c0110df91
commit dc5bbdc47d
4 changed files with 227 additions and 6 deletions
+2 -2
View File
@@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "../trpc.js";
import { getAppBaseUrl } from "../lib/app-base-url.js";
import { sendEmail } from "../lib/email.js";
const RESET_TTL_MS = 60 * 60 * 1000; // 1 hour
@@ -47,8 +48,7 @@ export const authRouter = createTRPCRouter({
data: { email: input.email, token, expiresAt },
});
const baseUrl = process.env["NEXTAUTH_URL"] ?? "http://localhost:3100";
const resetUrl = `${baseUrl}/auth/reset-password/${token}`;
const resetUrl = `${getAppBaseUrl()}/auth/reset-password/${token}`;
void sendEmail({
to: input.email,