From 1c281c9f5d774bc89c65cc93415fe9f44d874512 Mon Sep 17 00:00:00 2001 From: Stefan Lazic Date: Fri, 4 Sep 2026 22:28:49 +0100 Subject: [PATCH] Auth fix Implemented - Root cause fixed in [src/proxy.ts]: Auth.js is now initialized lazily and never invoked when MANAGER_MODE=true. - Manager requests call handleProxyRequest directly, preserving redirects and CSP. - Business requests continue through Auth.js with existing protected-route behavior. - Added coverage in [proxy-manager-mode.test.ts] --- src/__tests__/proxy-manager-mode.test.ts | 30 +++++++++++++++++++++--- src/proxy.ts | 21 ++++++++++------- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/__tests__/proxy-manager-mode.test.ts b/src/__tests__/proxy-manager-mode.test.ts index 8e4f4fe..ee284a7 100644 --- a/src/__tests__/proxy-manager-mode.test.ts +++ b/src/__tests__/proxy-manager-mode.test.ts @@ -2,6 +2,16 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { NextRequest } from "next/server"; const isInstalledForRequest = vi.fn(); +// Keep the Auth.js wrapper observable. Manager requests must not construct or +// invoke it; business requests still must use it for req.auth handling. +const { nextAuth, handleProxyRequestForTest } = vi.hoisted(() => { + const handleProxyRequestForTest = vi.fn(async (_request: NextRequest) => new Response(null, { status: 200 })); + const nextAuth = vi.fn(() => ({ + auth: (request: NextRequest) => handleProxyRequestForTest(request), + })); + + return { nextAuth, handleProxyRequestForTest }; +}); // If proxy.ts or its Manager request path statically imports Prisma, this // mock throws while the test module is loading. Manager mode must not reach it. @@ -11,7 +21,9 @@ vi.mock("@/lib/db/prisma", () => { vi.mock("@/lib/installation/installation-state", () => ({ isInstalledForRequest })); -import { handleProxyRequest } from "@/proxy"; +vi.mock("next-auth", () => ({ default: nextAuth })); + +import proxy, { handleProxyRequest } from "@/proxy"; function request(pathname: string) { return new NextRequest(`https://manager.example.com${pathname}`); @@ -24,23 +36,26 @@ describe("proxy deployment separation", () => { if (originalManagerMode === undefined) delete process.env.MANAGER_MODE; else process.env.MANAGER_MODE = originalManagerMode; isInstalledForRequest.mockReset(); + nextAuth.mockClear(); + handleProxyRequestForTest.mockClear(); }); it("routes the Manager root before loading or checking business installation state", async () => { process.env.MANAGER_MODE = "true"; - const response = await handleProxyRequest(request("/")); + const response = await proxy(request("/")); expect(response.status).toBe(307); expect(response.headers.get("location")).toBe("https://manager.example.com/manager"); expect(response.headers.get("content-security-policy")).toContain("default-src 'self'"); expect(isInstalledForRequest).not.toHaveBeenCalled(); + expect(nextAuth).not.toHaveBeenCalled(); }); it("serves Manager routes without business installation state", async () => { process.env.MANAGER_MODE = "true"; - const response = await handleProxyRequest(request("/manager")); + const response = await proxy(request("/manager")); expect(response.status).toBe(200); expect(isInstalledForRequest).not.toHaveBeenCalled(); @@ -57,6 +72,15 @@ describe("proxy deployment separation", () => { expect(isInstalledForRequest).toHaveBeenCalledOnce(); }); + it("uses Auth.js for normal business requests", async () => { + process.env.MANAGER_MODE = "false"; + + await proxy(request("/")); + + expect(nextAuth).toHaveBeenCalledOnce(); + expect(handleProxyRequestForTest).toHaveBeenCalledOnce(); + }); + it("preserves protected business route redirects after installation", async () => { process.env.MANAGER_MODE = "false"; isInstalledForRequest.mockResolvedValue(true); diff --git a/src/proxy.ts b/src/proxy.ts index ba7288d..94784c2 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -29,13 +29,15 @@ const INSTALL_EXEMPT_PREFIXES = [ "/sw.js", ]; -// Lightweight instance built from the DB-free config — see auth.config.ts. -// Next.js 16 renamed this file convention from `middleware` to `proxy` -// (https://nextjs.org/docs/messages/middleware-to-proxy); it now always -// runs on the Node.js runtime rather than Edge, but this stays split from -// the full `@/auth` regardless, so checking "is there a session" doesn't -// spin up a Postgres connection pool on every request. -const { auth } = NextAuth(authConfig); +// Build the business Auth.js middleware only when a business request needs +// it. Manager requests must not construct or invoke this wrapper at all. +type BusinessProxy = (req: NextRequest) => Response | Promise; +let authenticatedProxy: BusinessProxy | undefined; + +function getAuthenticatedProxy() { + authenticatedProxy ??= NextAuth(authConfig).auth as unknown as BusinessProxy; + return authenticatedProxy; +} /** * Nonce-gated 'strict-dynamic' script-src (Next's own documented pattern) @@ -139,7 +141,10 @@ export async function handleProxyRequest(req: AuthenticatedNextRequest) { return response; } -export default auth(handleProxyRequest); +export default function proxy(req: NextRequest) { + if (process.env.MANAGER_MODE === "true") return handleProxyRequest(req); + return getAuthenticatedProxy()(req); +} export const config = { matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],