328 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|