diff --git a/src/__tests__/proxy-manager-mode.test.ts b/src/__tests__/proxy-manager-mode.test.ts deleted file mode 100644 index dbe8e5e..0000000 --- a/src/__tests__/proxy-manager-mode.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { NextRequest } from "next/server"; - -const isInstalledForRequest = vi.fn(); - -// 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. -vi.mock("@/lib/db/prisma", () => { - throw new Error("Prisma must not be imported by the Manager proxy path."); -}); - -vi.mock("@/lib/installation/installation-state", () => ({ isInstalledForRequest })); - -import { handleProxyRequest } from "@/proxy"; - -function request(pathname: string) { - return new NextRequest(`https://manager.example.com${pathname}`); -} - -describe("proxy deployment separation", () => { - const originalManagerMode = process.env.MANAGER_MODE; - - afterEach(() => { - if (originalManagerMode === undefined) delete process.env.MANAGER_MODE; - else process.env.MANAGER_MODE = originalManagerMode; - isInstalledForRequest.mockReset(); - }); - - it("serves the Manager root without loading or checking business installation state", async () => { - process.env.MANAGER_MODE = "true"; - - const response = await handleProxyRequest(request("/")); - - expect(response.status).toBe(200); - expect(response.headers.get("content-security-policy")).toContain("default-src 'self'"); - expect(isInstalledForRequest).not.toHaveBeenCalled(); - }); - - it("checks installation state for a normal business request", async () => { - process.env.MANAGER_MODE = "false"; - isInstalledForRequest.mockResolvedValue(false); - - const response = await handleProxyRequest(request("/")); - - expect(response.status).toBe(307); - expect(response.headers.get("location")).toBe("https://manager.example.com/install"); - expect(isInstalledForRequest).toHaveBeenCalledOnce(); - }); - - it("preserves protected business route redirects after installation", async () => { - process.env.MANAGER_MODE = "false"; - isInstalledForRequest.mockResolvedValue(true); - - const response = await handleProxyRequest(request("/owner")); - - expect(response.status).toBe(307); - expect(response.headers.get("location")).toContain("/sign-in?callbackUrl=%2Fowner"); - }); -}); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c3a3219..54b2541 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata, Viewport } from "next"; import { Inter } from "next/font/google"; import { Providers } from "@/components/providers"; +import { getBranding } from "@/lib/business/branding"; import "./globals.css"; const inter = Inter({ @@ -17,13 +18,6 @@ const inter = Inter({ * than "NextChair" (see that file's own fallback-order comment). */ export async function generateMetadata(): Promise { - if (process.env.MANAGER_MODE === "true") { - return { - title: { default: "NextChair Manager", template: "%s | NextChair Manager" }, - description: "Manage NextChair business deployments.", - }; - } - const { getBranding } = await import("@/lib/business/branding"); const branding = await getBranding(); return { @@ -47,17 +41,6 @@ export async function generateMetadata(): Promise { } export async function generateViewport(): Promise { - if (process.env.MANAGER_MODE === "true") { - return { - width: "device-width", - initialScale: 1, - maximumScale: 1, - userScalable: false, - viewportFit: "cover", - themeColor: "#111b15", - }; - } - const { getBranding } = await import("@/lib/business/branding"); const branding = await getBranding(); return { width: "device-width", diff --git a/src/proxy.ts b/src/proxy.ts index 3e2f0d8..b47e45b 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -1,6 +1,7 @@ import NextAuth from "next-auth"; -import { NextResponse, type NextRequest } from "next/server"; +import { NextResponse } from "next/server"; import { authConfig } from "@/auth.config"; +import { isInstalledForRequest } from "@/lib/installation/installation-state"; // /home, /appointments and /settings are intentionally NOT hard-gated here // as of Phase 4 — Home must work for guests (public business browsing + @@ -20,6 +21,7 @@ const PROTECTED_PREFIXES = ["/staff", "/owner"]; // business data guard themselves (see /api/health and // /api/cron/process-notifications). const INSTALL_EXEMPT_PREFIXES = [ + "/manager", "/install", "/offline", "/manifest.webmanifest", @@ -82,23 +84,17 @@ function buildCsp(): string { ].join("; "); } -export async function handleProxyRequest(req: NextRequest) { +export default auth(async (req) => { const { pathname } = req.nextUrl; const csp = buildCsp(); - // Manager is a separate deployment and hostname, so its root pathname is - // normally `/`, not `/manager`. Branch before loading any business module. - if (process.env.MANAGER_MODE === "true") { - const response = NextResponse.next(); - response.headers.set("Content-Security-Policy", csp); - return response; - } - const isInstallExempt = INSTALL_EXEMPT_PREFIXES.some( (prefix) => pathname === prefix || pathname.startsWith(prefix.endsWith("/") ? prefix : `${prefix}/`), ); - const { isInstalledForRequest } = await import("@/lib/installation/installation-state"); - const installed = await isInstalledForRequest(); + // The platform Manager is a separate deployment and deliberately has no + // business database. Its own session guard runs inside /manager pages/API. + const isManagerPath = pathname === "/manager" || pathname.startsWith("/manager/"); + const installed = isManagerPath ? true : await isInstalledForRequest(); if (!installed && !isInstallExempt) { const response = NextResponse.redirect(new URL("/install", req.nextUrl.origin)); @@ -130,9 +126,7 @@ export async function handleProxyRequest(req: NextRequest) { const response = NextResponse.next(); response.headers.set("Content-Security-Policy", csp); return response; -} - -export default auth(handleProxyRequest); +}); export const config = { matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],