TypeScript 50.6%
JavaScript 46.1%
SQL 2.1%
Shell 0.6%
CSS 0.5%
1import { redirect } from "next/navigation";2import Link from "next/link";3import { headers } from "next/headers";4import { currentUser } from "@/lib/auth/session";5import { pgAvailable } from "@/db/client";6import { Empty } from "@/components/ui";7import { isFreshMfa } from "@/lib/admin/mfa";8export const dynamic = "force-dynamic";9/** Console d'administration applicative : rôle ADMIN/MODERATOR + TOTP récent. Non indexée ; à exposer de préférence via VPN/Tailscale. */10export default async function AdminLayout({ children }: { children: React.ReactNode }) {11 if (!pgAvailable()) return <div className="mx-auto max-w-3xl px-4 py-10"><Empty title="Indisponible" /></div>;12 const u = await currentUser(); const path = (await headers()).get("x-pathname") ?? "/admin";13 if (!u) redirect(`/connexion?next=${encodeURIComponent(path)}`);14 if (!u.roles.includes("ADMIN") && !u.roles.includes("MODERATOR")) redirect("/membre");15 const fresh = isFreshMfa(u.mfaAt);16 if (!fresh && !path.startsWith("/admin/mfa")) redirect(`/admin/mfa?next=${encodeURIComponent(path)}`);17 const nav = [["/admin", "Tableau de bord"], ["/admin/annonces", "Annonces à vérifier"], ["/admin/signalements", "Signalements"], ...(u.roles.includes("ADMIN") ? [["/admin/utilisateurs", "Utilisateurs"]] : []), ["/admin/journal", "Journal d'audit"]];18 return (19 <div className="mx-auto max-w-7xl px-4 py-6">20 <div className="flex flex-wrap items-center gap-3 mb-5"><span className="tv-badge bg-ink text-white">Administration</span><nav className="flex gap-1 overflow-x-auto no-scrollbar">{nav.map(([h, l]) => <Link key={h} href={h} className="tv-chip">{l}</Link>)}</nav><span className="ml-auto text-xs text-ink-3">{u.email} · {u.roles.join(", ")}{fresh ? " · TOTP ✓" : ""}</span></div>21 {children}22 </div>23 );24}25