import AdminSidebar from "@/components/admin/AdminSidebar"; import AdminAccessDenied from "@/components/admin/AdminAccessDenied"; import { getAdminUser } from "@/lib/admin-permissions"; import { getActiveBrandId } from "@/lib/brand-scope"; import { listBrandsForAdmin } from "@/actions/brands"; import { redirect } from "next/navigation"; import { getSession } from "@/lib/auth"; import "@/styles/admin-design-system.css"; import { ToastProvider } from "@/components/admin/Toast"; import { ToastContainer } from "@/components/admin/ToastContainer"; // Admin layout calls getAdminUser() which reads cookies(). Without this, // Next.js tries to prerender the entire /admin/* tree statically and the // first page that hits cookies() aborts the build with DYNAMIC_SERVER_USAGE. export const dynamic = "force-dynamic"; // Toast provider wrapper component function ToastProviderWrapper({ children }: { children: React.ReactNode }) { return ( {children} ); } export default async function AdminLayout({ children }: { children: React.ReactNode }) { let adminUser = null; let authError: string | null = null; // Robust auth with try-catch to prevent crashes try { adminUser = await getAdminUser(); } catch (error) { console.error("Admin auth error:", error); authError = "Failed to verify authentication. Please try again."; } // Auth verification failed if (authError) { return (
); } // Not authenticated / not provisioned if (!adminUser) { // Best-effort: surface the Neon Auth identity so the user (or support) knows // which account was checked. getAdminUser already logged details. let attemptedEmail: string | null = null; try { const { data: session } = await getSession(); attemptedEmail = session?.user?.email ?? null; } catch { // ignore } const message = attemptedEmail ? `Your account (${attemptedEmail}) does not have admin access. Contact a platform administrator to be provisioned.` : "Your account does not have admin access."; return (
); } // Must change password if (adminUser.must_change_password) { redirect("/change-password"); } // Resolve the active brand (URL > cookie > legacy > first of brand_ids). // Wrapped in try/catch so a transient brand-resolution failure can't // crash the whole admin shell — we fall back to null (no active brand). let activeBrandId: string | null = null; try { activeBrandId = await getActiveBrandId(adminUser); } catch (err) { console.error("[admin/layout] getActiveBrandId failed:", err); } // Fetch accessible brands for the sidebar BrandSelector. Wrapped in // try/catch so the sidebar renders empty rather than crashing the page // if the brands query fails. let brands: Awaited> = []; try { brands = await listBrandsForAdmin(); } catch (err) { console.error("[admin/layout] listBrandsForAdmin failed:", err); } return (
{children}
); }