diff --git a/apps/web/app/(dashboard)/layout.tsx b/apps/web/app/(dashboard)/layout.tsx index 6484011..23d6775 100644 --- a/apps/web/app/(dashboard)/layout.tsx +++ b/apps/web/app/(dashboard)/layout.tsx @@ -1,39 +1,42 @@ -"use client"; - -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { useSession } from "@/lib/auth-client"; +import { redirect } from "next/navigation"; +import { getServerSession } from "@/lib/server-auth"; import DashboardShell from "@/components/dashboard/DashboardShell"; -import { Loader2 } from "lucide-react"; -export default function DashboardLayout({ +/** + * Server-side gate for every /dashboard route. + * + * This was previously a client component that called `useSession()` and + * redirected from a `useEffect`. That is not protection. A client-side + * check runs only after the server has already sent the HTML and the + * browser has downloaded, parsed and hydrated the whole dashboard bundle + * — so an unauthenticated visitor received the entire screen, saw a + * spinner, and was then bounced. The route's existence, its layout, its + * navigation and its feature set all leaked, and a user whose session had + * expired got a flash of the UI before the redirect. + * + * Resolving the session here means an unauthenticated request never gets + * a dashboard response at all — it gets a redirect, before any of this + * subtree renders. + * + * Customer data was never exposed by the old version: every /api/dashboard + * route proxies through `forwardToCP`, which returns 401 when the + * `vls_session` cookie is absent and otherwise hands the cookie to the + * control plane to validate. The leak was the shell, not the contents. + * That is why this is a real defect and not an incident. + * + * This is a coarse gate. Per-page checks are still the standard — a + * route's protection should be readable in the route's own file — and + * every page under here is currently a client component, so they cannot + * do it yet. Converting them is tracked separately; this closes the hole + * in the meantime. + */ +export default async function DashboardLayout({ children, }: { children: React.ReactNode; }) { - const { data: session, isPending } = useSession(); - const router = useRouter(); - - useEffect(() => { - if (!isPending && !session) { - router.push("/login"); - } - }, [isPending, session, router]); - - if (isPending) { - return ( -
Loading...
-