213 lines
7.9 KiB
TypeScript
213 lines
7.9 KiB
TypeScript
// middleware.ts
|
|
import { NextResponse } from "next/server";
|
|
import type { NextRequest } from "next/server";
|
|
import { createMiddlewareClient } from "@supabase/auth-helpers-nextjs";
|
|
import { detectDemoMode, logDemoMode } from "@/lib/demo-detector";
|
|
|
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL || "";
|
|
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY || "";
|
|
|
|
// Pages de l'Espace Paie nécessitant une session authentifiée
|
|
const PROTECTED = [
|
|
"/contrats",
|
|
"/contrats-multi",
|
|
"/contrats-rg",
|
|
"/salaries",
|
|
"/cotisations",
|
|
"/facturation",
|
|
"/virements-salaires",
|
|
"/vos-documents",
|
|
"/vos-acces",
|
|
"/informations",
|
|
"/compte",
|
|
"/support",
|
|
"/signatures-electroniques",
|
|
"/staff",
|
|
"/debug",
|
|
];
|
|
|
|
export async function middleware(req: NextRequest) {
|
|
// Toujours créer une réponse et rafraîchir la session en amont
|
|
const res = NextResponse.next();
|
|
|
|
if (!supabaseUrl || !supabaseAnonKey) {
|
|
console.error("[Middleware] Supabase env manquantes", {
|
|
hasUrl: Boolean(supabaseUrl),
|
|
hasAnonKey: Boolean(supabaseAnonKey),
|
|
note: "Vérifie .env.local (à la racine du projet) et redémarre le serveur"
|
|
});
|
|
// On retourne la réponse sans tenter de lire la session (évite le crash en dev)
|
|
return res;
|
|
}
|
|
|
|
// 1) Rafraîchit les cookies de session si nécessaire (very important)
|
|
const supabase = createMiddlewareClient({ req, res }, { supabaseUrl, supabaseKey: supabaseAnonKey });
|
|
const { data: { session } } = await supabase.auth.getSession();
|
|
|
|
// 1.5) Maintenir les cookies persistants si rememberMe est activé
|
|
const rememberMeCookie = req.cookies.get("remember_me");
|
|
if (session && rememberMeCookie?.value === "true") {
|
|
// Prolonger la durée des cookies Supabase à chaque requête
|
|
const cookieOptions = {
|
|
maxAge: 60 * 60 * 24 * 30, // 30 jours
|
|
path: "/",
|
|
sameSite: "lax" as const,
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === "production",
|
|
};
|
|
|
|
// Récupérer tous les cookies Supabase et les renouveler
|
|
req.cookies.getAll().forEach((cookie) => {
|
|
if (cookie.name.startsWith("sb-")) {
|
|
res.cookies.set(cookie.name, cookie.value, cookieOptions);
|
|
}
|
|
});
|
|
|
|
// Renouveler aussi le cookie remember_me
|
|
res.cookies.set("remember_me", "true", cookieOptions);
|
|
}
|
|
|
|
const path = req.nextUrl.pathname;
|
|
const isStaticFile = /\.[^/]+$/.test(path); // ex: /odentas-logo.png, /robots.txt, etc.
|
|
|
|
// 2) Vérification du mode maintenance
|
|
// Éviter de vérifier sur les API calls, assets, la page maintenance elle-même, la page de connexion,
|
|
// et les pages publiques (auto-declaration, dl-contrat-signe, signature-salarie, politique-confidentialite, mentions-legales)
|
|
const isApiOrAssets = path.startsWith('/api') || path.startsWith('/_next') ||
|
|
path.startsWith('/favicon') || path.startsWith('/public') || isStaticFile;
|
|
const isMaintenancePage = path === '/maintenance';
|
|
const isSigninPage = path === '/signin';
|
|
const isPublicPage = path.startsWith('/auto-declaration') ||
|
|
path.startsWith('/dl-contrat-signe') ||
|
|
path.startsWith('/signature-salarie') ||
|
|
path === '/politique-confidentialite' ||
|
|
path === '/mentions-legales';
|
|
|
|
// Ne pas impacter l'environnement local/dev par le mode maintenance
|
|
const hostname = req.nextUrl.hostname || '';
|
|
const isDevEnv = process.env.NODE_ENV === 'development';
|
|
const isLocalHost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname.endsWith('.local');
|
|
// Optionnel: IP privée (LAN) → considérer comme non-prod
|
|
const isPrivateIp = /^10\./.test(hostname) || /^192\.168\./.test(hostname) || /^172\.(1[6-9]|2\d|3[0-1])\./.test(hostname);
|
|
const skipMaintenance = isDevEnv || isLocalHost || isPrivateIp;
|
|
|
|
if (!skipMaintenance && !isApiOrAssets && !isMaintenancePage && !isSigninPage && !isPublicPage) {
|
|
try {
|
|
// Vérifier le statut de maintenance
|
|
const { data: maintenanceStatus } = await supabase
|
|
.from("maintenance_status")
|
|
.select("is_maintenance_mode")
|
|
.single();
|
|
|
|
if (maintenanceStatus?.is_maintenance_mode) {
|
|
// Vérifier si l'utilisateur est staff
|
|
let isStaff = false;
|
|
if (session?.user) {
|
|
const { data: staffData } = await supabase
|
|
.from("staff_users")
|
|
.select("is_staff")
|
|
.eq("user_id", session.user.id)
|
|
.maybeSingle();
|
|
|
|
isStaff = !!staffData?.is_staff;
|
|
}
|
|
|
|
// Si pas staff, rediriger vers la page maintenance
|
|
if (!isStaff) {
|
|
const url = req.nextUrl.clone();
|
|
url.pathname = "/maintenance";
|
|
return NextResponse.redirect(url);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Erreur lors de la vérification de maintenance:", error);
|
|
// En cas d'erreur, on continue sans bloquer
|
|
}
|
|
}
|
|
|
|
// 3) Si on essaie d'aller sur /maintenance mais qu'on n'est pas en maintenance
|
|
if (isMaintenancePage && !skipMaintenance) {
|
|
try {
|
|
const { data: maintenanceStatus } = await supabase
|
|
.from("maintenance_status")
|
|
.select("is_maintenance_mode")
|
|
.single();
|
|
|
|
// Si pas en maintenance, rediriger vers l'accueil
|
|
if (!maintenanceStatus?.is_maintenance_mode) {
|
|
const url = req.nextUrl.clone();
|
|
url.pathname = "/";
|
|
return NextResponse.redirect(url);
|
|
}
|
|
} catch (error) {
|
|
console.error("Erreur lors de la vérification de maintenance:", error);
|
|
}
|
|
}
|
|
|
|
// 4) Staff status verification (DB-first, then fallback to metadata like /api/me)
|
|
let isStaff = false;
|
|
if (session?.user?.id) {
|
|
try {
|
|
// Try DB first (same logic as /api/me)
|
|
const { data } = await supabase
|
|
.from("staff_users")
|
|
.select("is_staff")
|
|
.eq("user_id", session.user.id)
|
|
.maybeSingle();
|
|
isStaff = !!data?.is_staff;
|
|
} catch (e) {
|
|
// fallback to token metadata
|
|
isStaff = Boolean(
|
|
(session.user.user_metadata && (session.user.user_metadata.is_staff === true || session.user.user_metadata.role === 'staff')) ||
|
|
(session.user.app_metadata && Array.isArray((session.user.app_metadata as any).roles) && (session.user.app_metadata as any).roles.includes('staff'))
|
|
);
|
|
}
|
|
}
|
|
|
|
// 4.1) Guard: restrict access to table entry route to staff only (temporary feature flag)
|
|
if (path.startsWith('/contrats/nouveau/saisie-tableau') && !isStaff) {
|
|
const url = req.nextUrl.clone();
|
|
url.pathname = '/contrats';
|
|
return NextResponse.redirect(url);
|
|
}
|
|
|
|
if (!isStaff) {
|
|
// Si un cookie staff "active_org_id" existe, purge-le pour éviter les fuites d'org côté client
|
|
const hasLegacy = req.cookies.get('active_org_id');
|
|
if (hasLegacy) {
|
|
res.cookies.set('active_org_id', '', { path: '/', maxAge: 0 });
|
|
res.cookies.set('active_org_id', '', { path: '/staff', maxAge: 0 });
|
|
}
|
|
}
|
|
|
|
// 5) Vérification du mode démo avant l'authentification
|
|
const isDemoMode = detectDemoMode(req);
|
|
|
|
// Si en mode démo, ajouter un header pour indiquer au layout de contourner l'auth
|
|
if (isDemoMode) {
|
|
res.headers.set('x-demo-mode', 'true');
|
|
logDemoMode(true, 'MIDDLEWARE', req);
|
|
}
|
|
|
|
// 6) Gate des pages protégées (laisse passer API & assets)
|
|
const needsAuth = PROTECTED.some((p) => path.startsWith(p));
|
|
|
|
// Contourner l'authentification si en mode démo
|
|
if (needsAuth && !session && !isDemoMode) {
|
|
const url = req.nextUrl.clone();
|
|
url.pathname = "/signin";
|
|
url.searchParams.set("next", path);
|
|
return NextResponse.redirect(url);
|
|
}
|
|
|
|
// Important : retourner *res* (celui modifié par auth-helpers)
|
|
return res;
|
|
}
|
|
|
|
// Évite d'exécuter le middleware sur les assets Next
|
|
export const config = {
|
|
matcher: [
|
|
// Exclure: assets Next, favicon et tout fichier avec extension (images, css, js, etc.)
|
|
"/((?!_next/static|_next/image|favicon.ico|public|.*\\..*).*)",
|
|
],
|
|
};
|