import { eq } from "drizzle-orm"; import { LogOut } from "lucide-react"; 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 Link from "@/components/link"; import { ThemeSwitcher } from "@/components/theme-switcher"; import { requireStaff } from "@/lib/admin/guard"; import { collectNavPermissionSlugs } from "@/lib/admin-nav"; import { ADMIN_NAV_CONFIG_KEY, parseAdminNavConfig, } from "@/lib/admin-nav-config"; import { db, User } from "@/lib/db"; import { readCsrfCookieToken } from "@/lib/foundation/security"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { siteSettings } from "@/lib/services/site-settings"; // Request-time auth, existing CSRF cookie, and optional 2FA gate cannot be // statically rendered. The root layout's Cache Components opt-out // (instant = false) covers this route tree — leaf opt-outs are redundant. export default async function AdminLayout({ children, }: { children: ReactNode; }) { const staff = await requireStaff(); const csrfToken = await readCsrfCookieToken(); if (await siteSettings.getBool("force_staff_2fa", false)) { const u = await db .select({ twoFactorConfirmedAt: User.twoFactorConfirmedAt }) .from(User) .where(eq(User.id, staff.id)) .limit(1) .then((rows) => rows[0] ?? null) .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; }); const navConfig = parseAdminNavConfig( await siteSettings.get(ADMIN_NAV_CONFIG_KEY, ""), ); return ( ); }