espace-paie-odentas/app/(app)/layout.tsx
2025-10-12 17:05:46 +02:00

328 lines
11 KiB
TypeScript

import Header from "@/components/Header";
import Sidebar from "@/components/Sidebar";
import { MaintenanceButton } from "@/components/MaintenanceButton";
import { ReactNode } from "react";
import { redirect } from "next/navigation";
// import { getAccessToken } from "@/lib/auth"; // Supprimé, on utilise directement Supabase
import { cookies, headers } from "next/headers";
import { createSbServer } from "@/lib/supabaseServer";
// app/layout.tsx
import "@/styles/cmdk.css";
import { detectDemoModeFromHeaders } from "@/lib/demo-detector";
import { DEMO_USER, DEMO_ORGANIZATION } from "@/lib/demo-data";
import { DemoBanner } from "@/components/DemoBanner";
import { DemoModeProvider } from "@/hooks/useDemoMode";
type ClientInfo = {
id: string;
name: string;
api_name?: string;
user?: {
id: string;
email: string;
display_name?: string;
first_name?: string;
} | null;
} | null;
export default async function AppLayout({ children }: { children: ReactNode }) {
const c = cookies();
const h = headers();
// 🎭 Vérification du mode démo AVANT tout traitement d'auth
const isDemoMode = detectDemoModeFromHeaders(h);
if (isDemoMode) {
// Mode démo : utiliser des données fictives sans authentification
const demoClientInfo: ClientInfo = {
id: DEMO_ORGANIZATION.id,
name: DEMO_ORGANIZATION.name,
api_name: DEMO_ORGANIZATION.api_name,
user: {
id: DEMO_USER.id,
email: DEMO_USER.email,
display_name: DEMO_USER.display_name,
first_name: DEMO_USER.first_name
}
};
console.log("🎭 [LAYOUT] Mode démo actif - Utilisation des données fictives");
return (
<DemoModeProvider forceDemoMode={true}>
<div className="min-h-screen">
{/* Demo Banner */}
<DemoBanner isDemoMode={true} isPublicDemo={process.env.NODE_ENV === 'production'} />
<div className="grid md:grid-cols-[calc(var(--sidebar-w)_+_8px)_1fr] min-h-screen">
{/* Sidebar flush left */}
<aside className="hidden md:block sticky top-0 h-screen overflow-y-auto overflow-x-hidden border-r bg-background">
<Sidebar clientInfo={demoClientInfo} isStaff={false} />
</aside>
{/* Main column (header + content) */}
<div className="flex flex-col min-h-screen">
{/* Header aligned with content column */}
<header className="m-0 p-0">
<Header clientInfo={demoClientInfo} isStaff={false} />
<div className="flex items-center justify-end gap-3 mt-2">
<MaintenanceButton isStaff={false} />
</div>
</header>
{/* Main content area */}
<main className="p-4">
{children}
</main>
</div>
</div>
</div>
</DemoModeProvider>
);
}
// ⚠️ DEV ONLY : bypass auth si AUTH_BYPASS=1
if (process.env.NODE_ENV === "development" && process.env.AUTH_BYPASS === "1") {
// En mode bypass, essayons quand même de récupérer les vraies infos si un utilisateur est connecté
let realClientInfo: ClientInfo = null;
let mockIsStaff = false; // Déclarer ici pour qu'elle soit accessible dans tout le bloc
try {
const sb = createSbServer();
const { data: { user } } = await sb.auth.getUser();
if (user) {
// Infos utilisateur (pour afficher le prénom)
const userInfo = {
id: user.id,
email: user.email || '',
display_name: user.user_metadata?.display_name || user.user_metadata?.full_name || null,
first_name: user.user_metadata?.first_name || user.user_metadata?.display_name?.split(' ')[0] || null,
};
// Vérifier si l'utilisateur est staff
const { data: staffData } = await sb
.from("staff_users")
.select("is_staff")
.eq("user_id", user.id)
.maybeSingle();
const isStaff = !!staffData?.is_staff;
mockIsStaff = isStaff; // Utilise la vraie valeur staff pour le mock
if (isStaff) {
// Staff : récupérer l'org sélectionnée si disponible
const activeOrgId = c.get("active_org_id")?.value;
if (activeOrgId) {
const { data: org } = await sb
.from("organizations")
.select("id, name, structure_api")
.eq("id", activeOrgId)
.maybeSingle();
if (org) {
realClientInfo = {
id: org.id,
name: org.name,
api_name: org.structure_api,
user: userInfo,
};
}
}
// Même si aucune organisation active n'est sélectionnée, on veut au moins le prénom
if (!realClientInfo) {
realClientInfo = {
id: "dev-org-id",
name: "Organisation",
api_name: undefined,
user: userInfo,
};
}
} else {
// Client : récupérer son organisation unique via requête directe
const { data: memberData, error } = await sb
.from("organization_members")
.select(`
organization:organizations(id, name, structure_api)
`)
.eq("user_id", user.id)
.eq("revoked", false)
.maybeSingle();
if (!error && memberData?.organization) {
const org = memberData.organization as any;
realClientInfo = {
id: org.id,
name: org.name,
api_name: org.structure_api,
user: userInfo,
};
} else {
// Pas d'organisation associée trouvée, mais on garde le prénom
realClientInfo = {
id: "dev-org-id",
name: "Organisation",
api_name: undefined,
user: userInfo,
};
}
}
}
} catch (error) {
console.warn("Impossible de récupérer les vraies infos en mode bypass:", error);
}
// Utiliser les vraies infos si disponibles, sinon fallback sur le mock
const mockClientInfo: ClientInfo = realClientInfo || {
id: "dev-org-id",
name: "Dev Company",
api_name: "dev-api",
user: realClientInfo ? (realClientInfo as any).user : null,
};
return (
<div className="grid md:grid-cols-[calc(var(--sidebar-w)_+_8px)_1fr] min-h-screen">
{/* Sidebar flush left */}
<aside className="hidden md:block sticky top-0 h-screen overflow-y-auto overflow-x-hidden border-r bg-background">
<Sidebar clientInfo={mockClientInfo} isStaff={mockIsStaff} />
</aside>
{/* Main column (header + content) */}
<div className="flex flex-col min-h-screen">
{/* Header aligned with content column */}
<header className="m-0 p-0">
<Header clientInfo={mockClientInfo} isStaff={mockIsStaff} />
<div className="flex items-center justify-end gap-3 mt-2">
<MaintenanceButton isStaff={mockIsStaff} />
</div>
</header>
{/* Main content area */}
<main className="p-4">
{children}
</main>
</div>
</div>
);
}
// Authentification directe via Supabase, sans cookie personnalisé
const sb = createSbServer();
const { data: { user } } = await sb.auth.getUser();
if (!user) {
redirect("/signin");
}
// Récupération DIRECTE des infos utilisateur et organisation - 100% server-side
let isStaff = false;
let clientInfo: ClientInfo = null;
let staffOrgInfo: ClientInfo = null;
// Extraire les infos utilisateur pour le partage
const userInfo = {
id: user.id,
email: user.email || '',
display_name: user.user_metadata?.display_name || user.user_metadata?.full_name || null,
first_name: user.user_metadata?.first_name || user.user_metadata?.display_name?.split(' ')[0] || null,
};
try {
// 1. Vérifier si l'utilisateur est staff
const { data: staffData } = await sb
.from("staff_users")
.select("is_staff")
.eq("user_id", user.id)
.maybeSingle();
isStaff = !!staffData?.is_staff;
if (isStaff) {
// Staff : récupérer l'org sélectionnée si disponible
const activeOrgId = c.get("active_org_id")?.value;
if (activeOrgId) {
const { data: org } = await sb
.from("organizations")
.select("id, name, structure_api")
.eq("id", activeOrgId)
.maybeSingle();
if (org) {
staffOrgInfo = {
id: org.id,
name: org.name,
api_name: org.structure_api,
user: userInfo,
};
}
}
// Si aucune organisation sélectionnée, on passe quand même l'utilisateur pour l'affichage du prénom
if (!staffOrgInfo) {
staffOrgInfo = {
id: "unknown",
name: "Organisation",
api_name: undefined,
user: userInfo,
};
}
} else {
// Client : récupérer son organisation unique via requête directe
const { data: memberData, error } = await sb
.from("organization_members")
.select(`
organization:organizations(id, name, structure_api)
`)
.eq("user_id", user.id)
.eq("revoked", false)
.maybeSingle();
if (!error && memberData?.organization) {
const org = memberData.organization as any;
clientInfo = {
id: org.id,
name: org.name,
api_name: org.structure_api,
user: userInfo,
};
} else {
// Pas d'organisation récupérée, on passe au moins l'utilisateur pour afficher le prénom
clientInfo = {
id: "unknown",
name: "Organisation",
api_name: undefined,
user: userInfo,
};
}
}
} catch (error) {
console.error("Erreur lors de la récupération des infos organisation:", error);
// En cas d'erreur, on continue avec des infos nulles
}
// Déterminer quelle info afficher
const displayInfo = isStaff ? staffOrgInfo : clientInfo;
return (
<div className="grid md:grid-cols-[calc(var(--sidebar-w)_+_8px)_1fr] min-h-screen">
{/* Sidebar flush left */}
<aside className="hidden md:block sticky top-0 h-screen overflow-y-auto overflow-x-hidden border-r bg-background">
<Sidebar clientInfo={displayInfo} isStaff={isStaff} />
</aside>
{/* Main column (header + content) */}
<div className="flex flex-col min-h-screen">
{/* Header aligned with content column */}
<header className="m-0 p-0 sticky top-0 z-40">
<Header clientInfo={displayInfo} isStaff={isStaff} />
<div className="flex items-center justify-end gap-3 mt-2">
<MaintenanceButton isStaff={isStaff} />
</div>
</header>
{/* Main content area */}
<main className="p-4">
{children}
</main>
</div>
</div>
);
}