diff --git a/src/__tests__/root-page-manager-mode.test.ts b/src/__tests__/root-page-manager-mode.test.ts new file mode 100644 index 0000000..26f57e4 --- /dev/null +++ b/src/__tests__/root-page-manager-mode.test.ts @@ -0,0 +1,32 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +vi.mock("next/navigation", () => ({ + redirect: (location: string) => { + throw new Error(`redirect:${location}`); + }, +})); + +vi.mock("@/lib/db/prisma", () => { + throw new Error("Business Prisma must not load while importing the Manager root."); +}); + +vi.mock("@/features/welcome/pages/business-welcome-page", () => { + throw new Error("Business welcome page must not load while importing the Manager root."); +}); + +import RootPage from "@/app/page"; + +describe("root deployment entry point", () => { + const originalManagerMode = process.env.MANAGER_MODE; + + afterEach(() => { + if (originalManagerMode === undefined) delete process.env.MANAGER_MODE; + else process.env.MANAGER_MODE = originalManagerMode; + }); + + it("redirects Manager root without evaluating business page or Prisma", async () => { + process.env.MANAGER_MODE = "true"; + + await expect(RootPage()).rejects.toThrow("redirect:/manager"); + }); +}); diff --git a/src/app/page.tsx b/src/app/page.tsx index bd28ae5..c88976c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,125 +1,10 @@ -import { Scissors } from "lucide-react"; -import Link from "next/link"; import { redirect } from "next/navigation"; -import { auth } from "@/auth"; -import { Button } from "@/components/ui/button"; -import { getBranding } from "@/lib/business/branding"; -import { getPrimaryBusiness } from "@/lib/business/get-primary-business"; -import { hasPermission, isSuperAdmin } from "@/lib/auth/permissions"; -import { Permission } from "@/generated/prisma/enums"; -import { prisma } from "@/lib/db/prisma"; -import { WelcomeInstallCard } from "@/features/pwa/components/welcome-install-card"; -/** - * The unauthenticated entry point — always shown at `/` before - * authentication, per the brief's "Welcome routing" requirements. - * Authenticated visitors never see it: they're routed straight to their - * role's calendar (barber → /staff, owner/super admin → /owner/calendar) - * or customer home below. This is also where a bare sign-in with no - * `callbackUrl` ends up — see safeRedirectPath's fallback in - * sign-in-form.tsx — so it's the one place that has to get every role - * right, not just staffProfile holders. Unreachable at all before - * installation completes — src/proxy.ts redirects every non-exempt route - * (including this one) to /install until ApplicationInstallation flips - * to INSTALLED. - */ -export default async function WelcomePage() { - const session = await auth(); - if (session?.user) { - const staffProfile = await prisma.staffProfile.findUnique({ where: { userId: session.user.id } }); - if (staffProfile) { - const canManageStaff = await hasPermission(session.user.id, Permission.MANAGE_STAFF); - redirect(canManageStaff ? "/owner" : "/staff"); - } - // A Super Administrator has no StaffProfile by design (see - // isSuperAdmin) — without this check they'd fall through to the - // customer /home below instead of the staff shell + Calendar tab - // their nav role (see lib/navigation/nav-config.ts) actually gives - // them once they land somewhere in it. - if (await isSuperAdmin(session.user.id)) redirect("/owner/calendar"); - redirect("/home"); - } +/** Deployment-mode entry point. Keep this module database-free: Next can + * evaluate the root route module before running the page function. */ +export default async function RootPage() { + if (process.env.MANAGER_MODE === "true") redirect("/manager"); - const [branding, business] = await Promise.all([getBranding(), getPrimaryBusiness()]); - const guestBookingEnabled = business.bookingRule?.allowGuestBooking ?? false; - - // The hero card's photo — the welcome-specific background the owner - // uploaded for this exact screen takes priority (it was purpose-built for - // it, see BrandingInfo.welcomeBackgroundMobileUrl/DesktopUrl); the - // general-purpose cover shot is only a fallback for a business that - // configured a cover image but never set a dedicated welcome background. - // Desktop and mobile keep their own independent images, same as before. - const heroImageMobileUrl = branding.welcomeBackgroundMobileUrl ?? branding.welcomeBackgroundDesktopUrl ?? branding.coverImageUrl; - const heroImageDesktopUrl = branding.welcomeBackgroundDesktopUrl ?? branding.welcomeBackgroundMobileUrl ?? branding.coverImageUrl; - - const hasHeroImage = !!(heroImageMobileUrl || heroImageDesktopUrl); - - return ( -
- {/* Full-bleed background — desktop/mobile are independent images, not one crop stretched two ways. */} - {heroImageMobileUrl && ( -