From 3e9fe60faff15b32104b94165a46010d20ac5549 Mon Sep 17 00:00:00 2001 From: Stefan Lazic Date: Sat, 5 Sep 2026 09:32:23 +0100 Subject: [PATCH] Revert "Fixed the remaining Manager runtime coupling." This reverts commit bc45608023e2e45dc2a5b37b8cfd85b05f6bb4e4. --- src/__tests__/manager-auth-route.test.ts | 26 ------------- .../manager-framework-routes.test.ts | 39 ------------------- src/__tests__/proxy-manager-mode.test.ts | 14 +------ src/app/api/auth/[...nextauth]/route.ts | 21 +--------- .../api/cron/process-notifications/route.ts | 15 +++---- src/app/api/cron/sync-google-reviews/route.ts | 10 ++--- src/app/api/health/route.ts | 10 +---- src/app/layout.tsx | 13 +------ src/app/manifest.ts | 16 +------- src/components/manager-providers.tsx | 9 ----- src/proxy.ts | 5 --- 11 files changed, 17 insertions(+), 161 deletions(-) delete mode 100644 src/__tests__/manager-auth-route.test.ts delete mode 100644 src/__tests__/manager-framework-routes.test.ts delete mode 100644 src/components/manager-providers.tsx diff --git a/src/__tests__/manager-auth-route.test.ts b/src/__tests__/manager-auth-route.test.ts deleted file mode 100644 index 90ffa7f..0000000 --- a/src/__tests__/manager-auth-route.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; - -// The Manager must not load the business Auth.js module even if a stale -// client requests `/api/auth/session`. -vi.mock("@/auth", () => { - throw new Error("Business Auth.js must not load in Manager mode."); -}); - -import { GET } from "@/app/api/auth/[...nextauth]/route"; - -describe("Manager business-auth boundary", () => { - const originalManagerMode = process.env.MANAGER_MODE; - - afterEach(() => { - if (originalManagerMode === undefined) delete process.env.MANAGER_MODE; - else process.env.MANAGER_MODE = originalManagerMode; - }); - - it("returns 404 without importing business Auth.js", async () => { - process.env.MANAGER_MODE = "true"; - - const response = await GET(new Request("https://manager.example.com/api/auth/session")); - - expect(response.status).toBe(404); - }); -}); diff --git a/src/__tests__/manager-framework-routes.test.ts b/src/__tests__/manager-framework-routes.test.ts deleted file mode 100644 index 941019b..0000000 --- a/src/__tests__/manager-framework-routes.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; - -vi.mock("@/lib/db/prisma", () => { - throw new Error("Business Prisma must not load in Manager framework routes."); -}); - -vi.mock("@/lib/business/branding", () => { - throw new Error("Business branding must not load in Manager framework routes."); -}); - -import { GET as health } from "@/app/api/health/route"; -import manifest from "@/app/manifest"; - -describe("Manager framework routes", () => { - const originalManagerMode = process.env.MANAGER_MODE; - - afterEach(() => { - if (originalManagerMode === undefined) delete process.env.MANAGER_MODE; - else process.env.MANAGER_MODE = originalManagerMode; - }); - - it("serves health without Prisma", async () => { - process.env.MANAGER_MODE = "true"; - - const response = await health(); - - expect(response.status).toBe(200); - expect(await response.json()).toMatchObject({ status: "healthy", deployment: "manager" }); - }); - - it("serves Manager manifest without business branding", async () => { - process.env.MANAGER_MODE = "true"; - - const result = await manifest(); - - expect(result.name).toBe("NextChair Manager"); - expect(result.start_url).toBe("/manager"); - }); -}); diff --git a/src/__tests__/proxy-manager-mode.test.ts b/src/__tests__/proxy-manager-mode.test.ts index 8e4f4fe..dbe8e5e 100644 --- a/src/__tests__/proxy-manager-mode.test.ts +++ b/src/__tests__/proxy-manager-mode.test.ts @@ -26,23 +26,13 @@ describe("proxy deployment separation", () => { isInstalledForRequest.mockReset(); }); - it("routes the Manager root before loading or checking business installation state", async () => { + 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(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(); - }); - - it("serves Manager routes without business installation state", async () => { - process.env.MANAGER_MODE = "true"; - - const response = await handleProxyRequest(request("/manager")); - expect(response.status).toBe(200); + expect(response.headers.get("content-security-policy")).toContain("default-src 'self'"); expect(isInstalledForRequest).not.toHaveBeenCalled(); }); diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts index 04f79dc..86c9f3d 100644 --- a/src/app/api/auth/[...nextauth]/route.ts +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -1,20 +1,3 @@ -/** - * Business Auth.js is intentionally unavailable in the standalone Manager - * deployment. Keeping this check before the dynamic import prevents a stale - * SessionProvider or direct request from initialising Prisma there. - */ -function managerAuthDisabled() { - return process.env.MANAGER_MODE === "true"; -} +import { handlers } from "@/auth"; -export async function GET(request: Request) { - if (managerAuthDisabled()) return new Response(null, { status: 404 }); - const { handlers } = await import("@/auth"); - return handlers.GET(request); -} - -export async function POST(request: Request) { - if (managerAuthDisabled()) return new Response(null, { status: 404 }); - const { handlers } = await import("@/auth"); - return handlers.POST(request); -} +export const { GET, POST } = handlers; diff --git a/src/app/api/cron/process-notifications/route.ts b/src/app/api/cron/process-notifications/route.ts index dbad571..651b3fc 100644 --- a/src/app/api/cron/process-notifications/route.ts +++ b/src/app/api/cron/process-notifications/route.ts @@ -1,5 +1,10 @@ import { NextResponse } from "next/server"; +import { prisma } from "@/lib/db/prisma"; +import { isInstalledForRequest } from "@/lib/installation/installation-state"; import { secretsMatch } from "@/lib/security/secrets-match"; +import { expireOverdueRewards } from "@/services/loyalty/loyalty.service"; +import { getEmailProvider, getPushProvider } from "@/services/notifications/providers"; +import { processDueNotificationDeliveries } from "@/services/notifications/reminder-worker"; /** * The external trigger point for the background notification processor — @@ -25,8 +30,6 @@ import { secretsMatch } from "@/lib/security/secrets-match"; * expected secret. */ export async function POST(request: Request) { - if (process.env.MANAGER_MODE === "true") return NextResponse.json({ error: "Not available on Manager." }, { status: 404 }); - const secret = process.env.CRON_SECRET; if (!secret) { return NextResponse.json({ error: "CRON_SECRET is not configured on this deployment." }, { status: 503 }); @@ -41,18 +44,10 @@ export async function POST(request: Request) { // src/lib/installation/installation-state.ts. A 200 with a no-op result // (not an error) so an external scheduler hitting this on a fixed // interval before day-one setup doesn't page anyone. - const { isInstalledForRequest } = await import("@/lib/installation/installation-state"); if (!(await isInstalledForRequest())) { return NextResponse.json({ skipped: "not_installed" }); } - const [{ prisma }, { expireOverdueRewards }, { getEmailProvider, getPushProvider }, { processDueNotificationDeliveries }] = await Promise.all([ - import("@/lib/db/prisma"), - import("@/services/loyalty/loyalty.service"), - import("@/services/notifications/providers"), - import("@/services/notifications/reminder-worker"), - ]); - const result = await processDueNotificationDeliveries(prisma, { emailProvider: getEmailProvider(), pushProvider: getPushProvider(), diff --git a/src/app/api/cron/sync-google-reviews/route.ts b/src/app/api/cron/sync-google-reviews/route.ts index 64dd29c..5cfec13 100644 --- a/src/app/api/cron/sync-google-reviews/route.ts +++ b/src/app/api/cron/sync-google-reviews/route.ts @@ -1,5 +1,8 @@ import { NextResponse } from "next/server"; +import { prisma } from "@/lib/db/prisma"; +import { isInstalledForRequest } from "@/lib/installation/installation-state"; import { secretsMatch } from "@/lib/security/secrets-match"; +import { syncGoogleReviewsForAllBusinesses } from "@/services/reviews/review-sync.service"; /** * External trigger for the Google Reviews sync — mirrors @@ -16,8 +19,6 @@ import { secretsMatch } from "@/lib/security/secrets-match"; * never duplicates anything. */ export async function POST(request: Request) { - if (process.env.MANAGER_MODE === "true") return NextResponse.json({ error: "Not available on Manager." }, { status: 404 }); - const secret = process.env.CRON_SECRET; if (!secret) { return NextResponse.json({ error: "CRON_SECRET is not configured on this deployment." }, { status: 503 }); @@ -28,15 +29,10 @@ export async function POST(request: Request) { return NextResponse.json({ error: "Unauthorized." }, { status: 401 }); } - const { isInstalledForRequest } = await import("@/lib/installation/installation-state"); if (!(await isInstalledForRequest())) { return NextResponse.json({ skipped: "not_installed" }); } - const [{ prisma }, { syncGoogleReviewsForAllBusinesses }] = await Promise.all([ - import("@/lib/db/prisma"), - import("@/services/reviews/review-sync.service"), - ]); const results = await syncGoogleReviewsForAllBusinesses(prisma); return NextResponse.json({ results }); } diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts index 4910843..665f9da 100644 --- a/src/app/api/health/route.ts +++ b/src/app/api/health/route.ts @@ -1,4 +1,6 @@ import { NextResponse } from "next/server"; +import { prisma } from "@/lib/db/prisma"; +import { getInstallationState } from "@/lib/installation/installation-state"; /** * Docker/orchestrator health check — deliberately never requires @@ -9,14 +11,6 @@ import { NextResponse } from "next/server"; * trace — just enough for a health probe to act on. */ export async function GET() { - if (process.env.MANAGER_MODE === "true") { - return NextResponse.json({ status: "healthy", deployment: "manager" }); - } - - const [{ prisma }, { getInstallationState }] = await Promise.all([ - import("@/lib/db/prisma"), - import("@/lib/installation/installation-state"), - ]); try { await prisma.$queryRaw`SELECT 1`; } catch { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index dd761df..c3a3219 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,6 @@ import type { Metadata, Viewport } from "next"; import { Inter } from "next/font/google"; -import { ManagerProviders } from "@/components/manager-providers"; +import { Providers } from "@/components/providers"; import "./globals.css"; const inter = Inter({ @@ -86,17 +86,8 @@ export default function RootLayout({ > Skip to main content - {process.env.MANAGER_MODE === "true" ? ( - {children} - ) : ( - {children} - )} + {children} ); } - -async function BusinessProviders({ children }: { children: React.ReactNode }) { - const { Providers } = await import("@/components/providers"); - return {children}; -} diff --git a/src/app/manifest.ts b/src/app/manifest.ts index a139e9f..41761ce 100644 --- a/src/app/manifest.ts +++ b/src/app/manifest.ts @@ -1,4 +1,5 @@ import type { MetadataRoute } from "next"; +import { getBranding } from "@/lib/business/branding"; /** * Next.js's manifest file convention — auto-served at /manifest.webmanifest @@ -25,21 +26,6 @@ import type { MetadataRoute } from "next"; export const dynamic = "force-dynamic"; export default async function manifest(): Promise { - if (process.env.MANAGER_MODE === "true") { - return { - name: "NextChair Manager", - short_name: "Manager", - description: "Manage NextChair business deployments.", - start_url: "/manager", - scope: "/", - display: "standalone", - background_color: "#f4f6f4", - theme_color: "#111b15", - orientation: "portrait-primary", - icons: [{ src: "/icons/icon-192.png", sizes: "192x192", type: "image/png" }], - }; - } - const { getBranding } = await import("@/lib/business/branding"); const branding = await getBranding(); return { name: branding.name, diff --git a/src/components/manager-providers.tsx b/src/components/manager-providers.tsx deleted file mode 100644 index 5eef00d..0000000 --- a/src/components/manager-providers.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import type { ReactNode } from "react"; - -/** - * Manager authentication uses its own signed cookie. This provider is kept - * free of next-auth/react so the Manager never polls `/api/auth/session`. - */ -export function ManagerProviders({ children }: { children: ReactNode }) { - return <>{children}; -} diff --git a/src/proxy.ts b/src/proxy.ts index ba7288d..bd6df29 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -91,11 +91,6 @@ export async function handleProxyRequest(req: AuthenticatedNextRequest) { // 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") { - if (pathname === "/") { - const response = NextResponse.redirect(new URL("/manager", req.nextUrl.origin)); - response.headers.set("Content-Security-Policy", csp); - return response; - } const response = NextResponse.next(); response.headers.set("Content-Security-Policy", csp); return response;