import { LogOut } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import type { ReactNode } from "react"; import { AdminHubChrome } from "@/components/admin/admin-hub-chrome"; import { AdminMobileWrapper } from "@/components/admin/admin-mobile-wrapper"; import { AdminSidebarNav } from "@/components/admin/admin-sidebar-nav"; import { AdminTopbar } from "@/components/admin/admin-topbar"; import { LanguageSwitcher } from "@/components/language-switcher"; import { ThemeSwitcher } from "@/components/theme-switcher"; import { requireStaff } from "@/lib/admin/guard"; import { collectNavPermissionSlugs } from "@/lib/admin-nav"; import { setCsrfCookie } from "@/lib/foundation/security"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { prisma } from "@/lib/prisma"; import { siteSettings } from "@/lib/services/site-settings"; // Request-time auth: requireStaff, CSRF cookie, and optional 2FA gate cannot be // statically rendered. Child admin pages inherit this — leaf force-dynamic is redundant. export const dynamic = "force-dynamic"; export default async function AdminLayout({ children, }: { children: ReactNode; }) { const staff = await requireStaff(); let csrfToken = ""; try { csrfToken = await setCsrfCookie(); } catch { csrfToken = ""; } if (await siteSettings.getBool("force_staff_2fa", false)) { const u = await prisma.user .findUnique({ where: { id: staff.id }, select: { twoFactorConfirmedAt: true }, }) .catch(() => null); if (!u?.twoFactorConfirmedAt) redirect("/settings/2fa?error=staffrequired"); } return ( <> {csrfToken ? : null} }>
{children}
); } async function Sidebar({ staff, }: { staff: { id: number; username: string; rank: number }; }) { const t = await getTranslations("pages.admin.nav"); const initial = staff.username.charAt(0).toUpperCase(); const { permissions } = await getAdminContext(); const isSuperAdmin = permissions.isSuperAdmin; const hasDashboard = canAccess( permissions, PERMS.ADMIN_DASHBOARD, staff.rank, ); // Super-admins see everything. Everyone else sees items their ACL grants. // If they can open the admin panel but ACL is incomplete (common after HK→ACL // migration gaps), still show *.view nav entries so categories do not vanish. const allowedPermissions = isSuperAdmin ? [] : collectNavPermissionSlugs().filter((slug) => { if (canAccess(permissions, slug, staff.rank)) return true; if (!hasDashboard) return false; return slug.endsWith(".view") || slug === PERMS.ADMIN_DASHBOARD; }); return ( ); }