src/app/admin/layout.tsx

component·app·3.7 KB · 106 lignes· Voir l'itinéraire
Annotation non disponible

Lance npm run annotate (nécessite ANTHROPIC_API_KEY dans .env.local) pour générer une annotation française par Claude Haiku 4.5.

1 export

default

Code source· tsx

import { headers, cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getSession } from "@/lib/auth/session";
import { prisma } from "@/lib/prisma/client";
import AdminShell from "@/components/admin/admin-shell";

// Admin Sprint A — Layout admin refondu (revu drawer hamburger mobile 2026-05-12)
// - Pages auth (/admin/login, /admin/reset-mdp, /admin/setup) rendues sans shell
// - Pages protégées wrappées dans AdminShell (sidebar desktop + drawer mobile)

// Routes auth qui n'ont pas besoin du shell (login + reset-mdp + setup wizard).
// startsWith pour gérer les sous-routes (ex: /admin/setup/email Sprint 3).
const AUTH_ROUTE_PREFIXES = ["/admin/login", "/admin/reset-mdp", "/admin/setup"];
function isAuth(pathname: string) {
  return AUTH_ROUTE_PREFIXES.some((p) => pathname.startsWith(p));
}

async function getCurrentPath(): Promise<string> {
  // En App Router server component, on lit le path via les headers Next.js
  // (header `x-invoke-path` ou `referer` selon la version). Pour Next 16, on a
  // accès au pathname via le middleware/headers seulement.
  // Approche robuste : on utilise `x-pathname` injecté par notre middleware
  // (fallback : on regarde le referer ou on assume route protégée).
  const h = await headers();
  return h.get("x-pathname") ?? h.get("x-invoke-path") ?? "";
}

export default async function AdminLayout({ children }: { children: React.ReactNode }) {
  const session = await getSession();
  const pathname = await getCurrentPath();
  const isAuthRoute = isAuth(pathname);
  const cookieStore = await cookies();
  const initialTheme: "light" | "dark" =
    cookieStore.get("wari-admin-theme")?.value === "dark" ? "dark" : "light";

  // Pages auth : rendre les children seuls, pas de shell (même si l'user est déjà loggé).
  // On wrap quand même dans un div .admin-dark pour que les classes dark: fonctionnent
  // sur la page login (qui peut être en mode sombre).
  if (isAuthRoute) {
    return (
      <div
        className={`min-h-screen bg-gray-50 dark:bg-zinc-950 ${
          initialTheme === "dark" ? "admin-dark" : ""
        }`}
      >
        {children}
      </div>
    );
  }

  // Pas de session valide : rendre children (la page protégée fera sa propre redirection)
  if (!session || session.role !== "TENANT_ADMIN" || !session.tenantId) {
    return (
      <div
        className={`min-h-screen bg-gray-50 dark:bg-zinc-950 ${
          initialTheme === "dark" ? "admin-dark" : ""
        }`}
      >
        {children}
      </div>
    );
  }

  const tenant = await prisma.tenant.findUnique({
    where: { id: session.tenantId },
    include: {
      modules: { where: { actif: true } },
      prefs: true,
    },
  });

  if (!tenant || !tenant.actif) {
    return <div className="min-h-screen bg-gray-50">{children}</div>;
  }

  // Sprint Login 4 — Bascule seuil : si onboarding pas terminé, rediriger
  // vers le wizard config vitrine. Le code est encore valide (codeAcces
  // non null tant que onboardingStep != DONE).
  if (tenant.onboardingStep !== "DONE" && tenant.codeAcces) {
    redirect(`/admin/setup/vitrine?code=${encodeURIComponent(tenant.codeAcces)}`);
  }

  // Récupérer email user pour avatar/menu
  const user = await prisma.user.findFirst({
    where: { tenantId: session.tenantId, id: session.userId },
    select: { email: true },
  });

  const modulesActifs = tenant.modules.map((m) => m.nom);
  const couleurAccent = tenant.couleurAccent ?? "#C9A227";

  return (
    <AdminShell
      modulesActifs={modulesActifs}
      couleurAccent={couleurAccent}
      tenantNom={tenant.nom}
      tenantSubdomain={tenant.subdomain}
      tenantLogoUrl={tenant.logoUrl}
      userEmail={user?.email ?? "admin"}
      initialTheme={initialTheme}
    >
      {children}
    </AdminShell>
  );
}