SPB Git forge

spb/toveli

Public
14commits 1branches 0releases
1.1 MBsize
maindefault branch
3 h agolast push
TypeScript 50.6% JavaScript 46.1% SQL 2.1% Shell 0.6% CSS 0.5%
1.8 KB · 25 lines tsx
Raw Blame History
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