"use client"; import { useEffect, useState } from "react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { ClipboardCheck, Flag, ShieldCheck, Gift, Megaphone, Quote, FolderClosed, LayoutDashboard, LayoutGrid, Link2, LogOut, MessageSquare, Users, } from "lucide-react"; import { api, staff, MANAGE_STAFF, type Me, type StaffMe } from "@/lib/api"; import { RequirePermission, StaffProvider } from "@/components/staff-context"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { ConfirmProvider } from "@/components/confirm"; // Each entry names the permission that unlocks it, so the sidebar only shows // what the signed-in role can actually use. const NAV = [ { href: "/", label: "Overview", icon: LayoutDashboard, perm: "stats:read" }, { href: "/users", label: "Users", icon: Users, perm: "users:read" }, { href: "/posts", label: "Posts", icon: LayoutGrid, perm: "posts:moderate" }, { href: "/collections", label: "Collections", icon: FolderClosed, perm: "collections:read", }, { href: "/short-urls", label: "Short URLs", icon: Link2, perm: "short_urls:read", }, { href: "/feedback", label: "Feedback", icon: MessageSquare, perm: "feedback:read", }, { href: "/reports", label: "Reports", icon: Flag, perm: "reports:read" }, { href: "/broadcasts", label: "Announcements", icon: Megaphone, perm: "broadcasts:read", }, { href: "/reward-gifts", label: "Gifts", icon: Gift, perm: "rewards:read" }, { href: "/vani", label: "Vāṇī of the Day", icon: Quote, perm: "vani:read" }, { href: "/quizzes", label: "Quizzes", icon: ClipboardCheck, perm: "quizzes:read", }, { href: "/staff", label: "Staff & roles", icon: ShieldCheck, perm: MANAGE_STAFF, }, ]; export default function DashboardLayout({ children, }: { children: React.ReactNode; }) { const router = useRouter(); const pathname = usePathname(); const [me, setMe] = useState(null); const [staffMe, setStaffMe] = useState(null); const [checked, setChecked] = useState(false); // Staff gate: only an account holding an admin role, and not blocked from // the panel, may see the dashboard. /admin/me answers both at once — it 403s // otherwise. The BFF enforces this on every /admin/* call regardless, so // this is UX, not the security boundary. useEffect(() => { let cancelled = false; Promise.all([api.me(), staff.me()]) .then(([u, s]) => { if (cancelled) return; if (!u || !s) { router.replace("/login"); return; } setMe(u); setStaffMe(s); setChecked(true); }) .catch(() => { if (!cancelled) router.replace("/login"); }); return () => { cancelled = true; }; }, [router]); // A role without stats:read has no Overview — send them to the first // section they can use instead of a page of permission errors. useEffect(() => { if ( !staffMe || pathname !== "/" || staffMe.permissions.includes("stats:read") ) return; const first = NAV.find((n) => staffMe.permissions.includes(n.perm)); if (first) router.replace(first.href); }, [staffMe, pathname, router]); async function signOut() { await api.logout().catch(() => {}); router.replace("/login"); } if (!checked || !me || !staffMe) { return (
Loading…
); } return (
{/* The section's own permission, so a URL typed straight in explains itself instead of failing call by call. */} {(() => { const section = NAV.find( (n) => n.href !== "/" && (pathname === n.href || pathname.startsWith(n.href + "/")), ) ?? (pathname === "/" ? NAV[0] : undefined); return section ? ( {children} ) : ( children ); })()}
); }