src/app/admin/layout.tsx
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>
);
}
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>
);
}