espace-paie-odentas/middleware.ts

190 lines
7.2 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();
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|.*\\..*).*)",
],
};