Files
btpxpress-frontend/middleware.ts
dahoud a8825a058b Fix: Corriger toutes les erreurs de build du frontend
- Correction des erreurs TypeScript dans userService.ts et workflowTester.ts
- Ajout des propriétés manquantes aux objets User mockés
- Conversion des dates de string vers objets Date
- Correction des appels asynchrones et des types incompatibles
- Ajout de dynamic rendering pour résoudre les erreurs useSearchParams
- Enveloppement de useSearchParams dans Suspense boundary
- Configuration de force-dynamic au niveau du layout principal

Build réussi: 126 pages générées avec succès

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 13:23:08 +00:00

226 lines
7.6 KiB
TypeScript

/**
* Middleware Next.js pour la protection des routes avec Keycloak
* Gère l'authentification et l'autorisation au niveau des routes
*/
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify, JWTPayload } from 'jose';
// Configuration des routes protégées
const PROTECTED_ROUTES = [
'/dashboard',
'/chantiers',
'/clients',
'/devis',
'/factures',
'/materiels',
'/employes',
'/equipes',
'/planning',
'/reports',
'/admin',
'/profile',
] as const;
// Configuration des routes publiques (toujours accessibles)
const PUBLIC_ROUTES = [
'/',
'/api/health',
'/api/auth/login',
'/api/auth/logout',
'/api/auth/token',
] as const;
// Configuration des routes par rôle
const ROLE_BASED_ROUTES = {
'/admin': ['super_admin', 'admin'],
'/reports': ['super_admin', 'admin', 'directeur', 'manager'],
'/employes': ['super_admin', 'admin', 'directeur', 'manager', 'chef_chantier'],
'/equipes': ['super_admin', 'admin', 'directeur', 'manager', 'chef_chantier'],
'/materiels/manage': ['super_admin', 'admin', 'logisticien'],
'/clients/manage': ['super_admin', 'admin', 'commercial'],
'/devis/manage': ['super_admin', 'admin', 'commercial'],
'/factures/manage': ['super_admin', 'admin', 'comptable'],
} as const;
// Interface pour le token JWT décodé
interface KeycloakToken extends JWTPayload {
preferred_username?: string;
email?: string;
given_name?: string;
family_name?: string;
realm_access?: {
roles: string[];
};
resource_access?: {
[key: string]: {
roles: string[];
};
};
}
// Fonction pour vérifier si une route est protégée
function isProtectedRoute(pathname: string): boolean {
return PROTECTED_ROUTES.some(route => pathname.startsWith(route));
}
// Fonction pour vérifier si une route est publique
function isPublicRoute(pathname: string): boolean {
return PUBLIC_ROUTES.some(route => pathname === route || pathname.startsWith(route));
}
// Fonction pour extraire les rôles du token
function extractRoles(token: KeycloakToken): string[] {
const realmRoles = token.realm_access?.roles || [];
const clientRoles = token.resource_access?.['btpxpress-frontend']?.roles || [];
return [...realmRoles, ...clientRoles];
}
// Fonction pour vérifier les permissions de rôle
function hasRequiredRole(userRoles: string[], requiredRoles: string[]): boolean {
return requiredRoles.some(role => userRoles.includes(role));
}
// Fonction pour vérifier et décoder le token JWT
async function verifyToken(token: string): Promise<KeycloakToken | null> {
try {
// Configuration de la clé publique Keycloak
const keycloakUrl = process.env.NEXT_PUBLIC_KEYCLOAK_URL || 'https://security.lions.dev';
const realm = process.env.NEXT_PUBLIC_KEYCLOAK_REALM || 'btpxpress';
// Récupérer la clé publique depuis Keycloak
const jwksUrl = `${keycloakUrl}/realms/${realm}/protocol/openid_connect/certs`;
// Pour la vérification côté serveur, nous utilisons une approche simplifiée
// En production, il faudrait implémenter une vérification complète avec JWKS
// Décoder le token sans vérification pour le middleware (vérification côté client)
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
// Vérifier l'expiration
if (payload.exp && payload.exp < Date.now() / 1000) {
return null;
}
return payload as KeycloakToken;
} catch (error) {
console.error('Erreur lors de la vérification du token:', error);
return null;
}
}
// Fonction principale du middleware
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Ignorer les fichiers statiques et les API routes internes
if (
pathname.startsWith('/_next') ||
pathname.startsWith('/api') ||
pathname.includes('.') ||
pathname.startsWith('/favicon')
) {
return NextResponse.next();
}
// Ignorer les requêtes vers /dashboard qui contiennent un code d'autorisation
// Cela permet à la page de traiter le code sans que le middleware ne redirige
if (pathname === '/dashboard' && request.nextUrl.searchParams.has('code')) {
console.log('🔓 Middleware: Autorisant /dashboard avec code d\'autorisation');
return NextResponse.next();
}
// Permettre l'accès aux routes publiques
if (isPublicRoute(pathname)) {
return NextResponse.next();
}
// Vérifier si la route nécessite une authentification
if (isProtectedRoute(pathname)) {
// Récupérer le token depuis les cookies ou headers
const authHeader = request.headers.get('authorization');
const tokenFromCookie = request.cookies.get('keycloak-token')?.value;
const pkceVerifier = request.cookies.get('pkce_code_verifier')?.value;
console.log(`🔍 Middleware: Vérification de ${pathname}:`, {
hasAuthHeader: !!authHeader,
hasTokenCookie: !!tokenFromCookie,
hasPkceVerifier: !!pkceVerifier,
hasCode: request.nextUrl.searchParams.has('code')
});
let token: string | null = null;
if (authHeader && authHeader.startsWith('Bearer ')) {
token = authHeader.substring(7);
} else if (tokenFromCookie) {
token = tokenFromCookie;
}
// Si pas de token, vérifier si un processus d'authentification est en cours
if (!token) {
// Si on a un code verifier PKCE, cela signifie qu'un processus d'authentification est en cours
// Autoriser l'accès pour permettre à la page de terminer l'échange du code
if (pkceVerifier && pathname === '/dashboard') {
console.log('🔓 Middleware: Autorisant /dashboard avec PKCE verifier (authentification en cours)');
return NextResponse.next();
}
console.log(`🔒 Middleware: Redirection vers /api/auth/login pour ${pathname} (pas de token ni de PKCE verifier)`);
const loginUrl = new URL('/api/auth/login', request.url);
loginUrl.searchParams.set('redirect', pathname);
return NextResponse.redirect(loginUrl);
}
// Vérifier et décoder le token
const decodedToken = await verifyToken(token);
if (!decodedToken) {
// Token invalide ou expiré
const loginUrl = new URL('/api/auth/login', request.url);
loginUrl.searchParams.set('redirect', pathname);
return NextResponse.redirect(loginUrl);
}
// Extraire les rôles de l'utilisateur
const userRoles = extractRoles(decodedToken);
// Vérifier les permissions basées sur les rôles pour des routes spécifiques
for (const [route, requiredRoles] of Object.entries(ROLE_BASED_ROUTES)) {
if (pathname.startsWith(route)) {
if (!hasRequiredRole(userRoles, [...requiredRoles])) {
// Utilisateur n'a pas les rôles requis
return NextResponse.redirect(new URL('/api/auth/login', request.url));
}
break;
}
}
// Ajouter les informations utilisateur aux headers pour les composants
const response = NextResponse.next();
response.headers.set('x-user-id', decodedToken.sub || '');
response.headers.set('x-user-email', decodedToken.email || '');
response.headers.set('x-user-roles', JSON.stringify(userRoles));
return response;
}
// Par défaut, permettre l'accès
return NextResponse.next();
}
// Configuration du matcher pour spécifier quelles routes le middleware doit traiter
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - public files (images, etc.)
*/
'/((?!api|_next/static|_next/image|favicon.ico|.*\\..*|public).*)',
],
};