3f731f7739
Deploy to route.crispygoat.com / deploy (push) Successful in 3m9s
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.
101 lines
3.3 KiB
TypeScript
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>
|
|
);
|
|
}
|