Files
route-commerce/src/app/admin/layout.tsx
T
tyler 3f731f7739
Deploy to route.crispygoat.com / deploy (push) Successful in 3m9s
fix(admin): wrap post-auth data loads in try/catch to surface and survive SC render errors
The /admin page was throwing a Server Components render error (digest
4266906817) in production. The exact throw point was not visible from
the browser (Next.js hides it for security) and server logs were not
available at debugging time.

The layout and page both call data-loading functions (getActiveBrandId,
listBrandsForAdmin, supabase brands lookup) that could throw on a
transient DB/network failure, and these calls had no try/catch. A
single failed call would crash the entire admin shell.

Wrap each call in try/catch with console.error so:
  1. The page renders with sensible defaults if a call fails
  2. The actual error is logged server-side (visible via the digest in
     the admin error boundary or PM2/Docker logs)
  3. The admin shell stays functional even if a single data source is
     down

No behavior change on the happy path.
2026-06-06 22:35:24 +00:00

101 lines
3.3 KiB
TypeScript

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 "@/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 (
<ToastProvider>
{children}
<ToastContainer />
</ToastProvider>
);
}
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 (
<ToastProviderWrapper>
<AdminSidebar userRole={null} />
<div className="min-h-screen lg:pl-60 admin-section" style={{ backgroundColor: "var(--admin-bg)" }}>
<AdminAccessDenied message={authError} />
</div>
</ToastProviderWrapper>
);
}
// Not authenticated
if (!adminUser) {
return (
<ToastProviderWrapper>
<AdminSidebar userRole={null} />
<div className="min-h-screen lg:pl-60 admin-section" style={{ backgroundColor: "var(--admin-bg)" }}>
<AdminAccessDenied message="Your account does not have admin access." />
</div>
</ToastProviderWrapper>
);
}
// 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<ReturnType<typeof listBrandsForAdmin>> = [];
try {
brands = await listBrandsForAdmin();
} catch (err) {
console.error("[admin/layout] listBrandsForAdmin failed:", err);
}
return (
<ToastProviderWrapper>
<AdminSidebar
userRole={adminUser.role}
brandIds={adminUser.brand_ids}
activeBrandId={activeBrandId}
brands={brands}
/>
<div className="min-h-screen lg:pl-60 admin-section" style={{ backgroundColor: "var(--admin-bg)" }}>
{children}
</div>
</ToastProviderWrapper>
);
}