'use client'; import React, { createContext, useContext, useEffect, useState, useCallback, ReactNode } from 'react'; import { initKeycloak, keycloakInitOptions, RoleUtils, BTP_ROLES, KEYCLOAK_TIMEOUTS, KEYCLOAK_REDIRECTS } from '@/config/keycloak'; import { useRouter } from 'next/navigation'; // Types pour l'authentification export interface UserInfo { id: string; username: string; email: string; firstName?: string; lastName?: string; fullName?: string; roles: string[]; permissions: string[]; highestRole?: string; isAdmin: boolean; isManager: boolean; isEmployee: boolean; isClient: boolean; } export interface AuthState { isAuthenticated: boolean; isLoading: boolean; user: UserInfo | null; token: string | null; refreshToken: string | null; error: string | null; } export interface AuthContextType extends AuthState { login: () => Promise; logout: () => Promise; refreshAuth: () => Promise; hasRole: (role: string) => boolean; hasAnyRole: (roles: string[]) => boolean; hasPermission: (permission: string) => boolean; isRoleHigher: (role: string) => boolean; updateToken: (minValidity?: number) => Promise; } // Contexte d'authentification const AuthContext = createContext(undefined); // Hook pour utiliser le contexte d'authentification export const useAuth = (): AuthContextType => { const context = useContext(AuthContext); if (!context) { throw new Error('useAuth must be used within an AuthProvider'); } return context; }; // Props du provider interface AuthProviderProps { children: ReactNode; } // Provider d'authentification export const AuthProvider: React.FC = ({ children }) => { const router = useRouter(); // État de l'authentification const [authState, setAuthState] = useState({ isAuthenticated: false, isLoading: true, user: null, token: null, refreshToken: null, error: null, }); // Fonction pour récupérer les informations utilisateur depuis l'API const fetchUserInfo = useCallback(async (token: string): Promise => { try { const response = await fetch('/api/v1/auth/user', { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, }); if (!response.ok) { console.warn('Impossible de récupérer les informations utilisateur depuis l\'API'); return null; } const userData = await response.json(); // Convertir les données de l'API vers le format UserInfo const userInfo: UserInfo = { id: userData.id, username: userData.username, email: userData.email, firstName: userData.firstName, lastName: userData.lastName, fullName: userData.fullName, roles: userData.roles || [], permissions: userData.permissions || [], highestRole: userData.roles?.[0] || undefined, isAdmin: userData.isAdmin || false, isManager: userData.isManager || false, isEmployee: userData.isEmployee || false, isClient: userData.isClient || false, }; return userInfo; } catch (error) { console.error('Erreur lors de la récupération des informations utilisateur:', error); return null; } }, []); // Fonction pour extraire les informations utilisateur (fallback) const extractUserInfo = useCallback((keycloakInstance: any): UserInfo | null => { if (!keycloakInstance.tokenParsed) return null; const tokenParsed = keycloakInstance.tokenParsed; const realmAccess = keycloakInstance.realmAccess || {}; const resourceAccess = keycloakInstance.resourceAccess || {}; // Extraction des rôles const realmRoles = realmAccess.roles || []; const clientRoles = resourceAccess['btpxpress-frontend']?.roles || []; const allRoles = [...realmRoles, ...clientRoles]; // Informations utilisateur const userInfo: UserInfo = { id: tokenParsed.sub, username: tokenParsed.preferred_username || tokenParsed.email, email: tokenParsed.email, firstName: tokenParsed.given_name, lastName: tokenParsed.family_name, fullName: tokenParsed.name || `${tokenParsed.given_name || ''} ${tokenParsed.family_name || ''}`.trim(), roles: allRoles, permissions: RoleUtils.getUserPermissions(allRoles), highestRole: RoleUtils.getHighestRole(allRoles) || undefined, isAdmin: RoleUtils.hasAnyRole(allRoles, [BTP_ROLES.SUPER_ADMIN, BTP_ROLES.ADMIN]), isManager: RoleUtils.hasAnyRole(allRoles, [BTP_ROLES.DIRECTEUR, BTP_ROLES.MANAGER, BTP_ROLES.CHEF_CHANTIER]), isEmployee: RoleUtils.hasAnyRole(allRoles, [BTP_ROLES.EMPLOYE, BTP_ROLES.OUVRIER, BTP_ROLES.CHEF_EQUIPE]), isClient: RoleUtils.hasAnyRole(allRoles, [BTP_ROLES.CLIENT_ENTREPRISE, BTP_ROLES.CLIENT_PARTICULIER]), }; return userInfo; }, []); // Fonction pour mettre à jour l'état d'authentification const updateAuthState = useCallback(async (keycloakInstance: any) => { if (keycloakInstance && keycloakInstance.authenticated) { // Essayer d'abord de récupérer les informations depuis l'API let user = await fetchUserInfo(keycloakInstance.token); // Si l'API ne répond pas, utiliser les informations du token JWT if (!user) { user = extractUserInfo(keycloakInstance); } setAuthState({ isAuthenticated: true, isLoading: false, user, token: keycloakInstance.token, refreshToken: keycloakInstance.refreshToken, error: null, }); } else { setAuthState({ isAuthenticated: false, isLoading: false, user: null, token: null, refreshToken: null, error: null, }); } }, [fetchUserInfo, extractUserInfo]); // Fonction de connexion const login = useCallback(async (): Promise => { try { // Redirection directe vers l'API d'authentification window.location.href = '/api/auth/login'; } catch (error) { console.error('Erreur lors de la connexion:', error); setAuthState(prev => ({ ...prev, error: 'Erreur lors de la connexion', isLoading: false, })); } }, []); // Fonction de déconnexion const logout = useCallback(async (): Promise => { try { // Nettoyer les tokens locaux localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); localStorage.removeItem('idToken'); // Supprimer le cookie document.cookie = 'keycloak-token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'; // Redirection directe vers l'API de déconnexion window.location.href = '/api/auth/logout'; } catch (error) { console.error('Erreur lors de la déconnexion:', error); // Forcer la déconnexion locale même en cas d'erreur setAuthState({ isAuthenticated: false, isLoading: false, user: null, token: null, refreshToken: null, error: null, }); router.push('/'); } }, [router]); // Fonction pour rafraîchir l'authentification const refreshAuth = useCallback(async (): Promise => { try { // Vérifier les tokens stockés const accessToken = localStorage.getItem('accessToken'); const refreshToken = localStorage.getItem('refreshToken'); if (!refreshToken) { await logout(); return; } // TODO: Implémenter le rafraîchissement via API si nécessaire console.log('Rafraîchissement des tokens...'); } catch (error) { console.error('Erreur lors du rafraîchissement du token:', error); await logout(); } }, [logout]); // Fonction pour mettre à jour le token const updateToken = useCallback(async (minValidity: number = 30): Promise => { try { // Vérifier si le token est encore valide const accessToken = localStorage.getItem('accessToken'); if (!accessToken) { return false; } // TODO: Vérifier l'expiration du token et rafraîchir si nécessaire return true; } catch (error) { console.error('Erreur lors de la mise à jour du token:', error); return false; } }, []); // Fonctions utilitaires pour les rôles et permissions const hasRole = useCallback((role: string): boolean => { return authState.user ? RoleUtils.hasRole(authState.user.roles, role) : false; }, [authState.user]); const hasAnyRole = useCallback((roles: string[]): boolean => { return authState.user ? RoleUtils.hasAnyRole(authState.user.roles, roles) : false; }, [authState.user]); const hasPermission = useCallback((permission: string): boolean => { return authState.user ? RoleUtils.hasPermission(authState.user.roles, permission) : false; }, [authState.user]); const isRoleHigher = useCallback((role: string): boolean => { if (!authState.user?.highestRole) return false; return RoleUtils.isRoleHigher(authState.user.highestRole, role); }, [authState.user]); // Initialisation de Keycloak - Désactivée pour utiliser l'authentification manuelle useEffect(() => { const initializeKeycloak = async () => { try { // Vérifier s'il y a des tokens stockés localement const accessToken = localStorage.getItem('accessToken'); const refreshToken = localStorage.getItem('refreshToken'); const idToken = localStorage.getItem('idToken'); if (accessToken) { // Stocker aussi dans un cookie pour le middleware document.cookie = `keycloak-token=${accessToken}; path=/; max-age=3600; SameSite=Lax`; // Récupérer les informations utilisateur depuis l'API try { const user = await fetchUserInfo(accessToken); if (user) { setAuthState({ isAuthenticated: true, isLoading: false, user, token: accessToken, refreshToken: refreshToken, error: null, }); return; } } catch (error) { console.warn('Impossible de récupérer les informations utilisateur depuis l\'API, utilisation des données par défaut'); } // Fallback avec des données par défaut si l'API ne répond pas setAuthState({ isAuthenticated: true, isLoading: false, user: { id: 'dev-user-001', username: 'admin.btpxpress', email: 'admin@btpxpress.com', firstName: 'Jean-Michel', lastName: 'Martineau', fullName: 'Jean-Michel Martineau', roles: [BTP_ROLES.SUPER_ADMIN, BTP_ROLES.ADMIN, BTP_ROLES.DIRECTEUR], permissions: RoleUtils.getUserPermissions([BTP_ROLES.SUPER_ADMIN]), highestRole: BTP_ROLES.SUPER_ADMIN, isAdmin: true, isManager: true, isEmployee: false, isClient: false, }, token: accessToken, refreshToken: refreshToken, error: null, }); return; } // Pas de tokens, rester non authentifié setAuthState({ isAuthenticated: false, isLoading: false, user: null, token: null, refreshToken: null, error: null, }); } catch (error) { console.error('Erreur lors de l\'initialisation de l\'authentification:', error); setAuthState({ isAuthenticated: false, isLoading: false, user: null, token: null, refreshToken: null, error: 'Erreur lors de l\'initialisation de l\'authentification', }); } }; initializeKeycloak(); }, [updateAuthState, refreshAuth, logout]); // Rafraîchissement automatique du token useEffect(() => { if (!authState.isAuthenticated) return; const interval = setInterval(() => { updateToken(); }, KEYCLOAK_TIMEOUTS.SESSION_CHECK_INTERVAL * 1000); return () => clearInterval(interval); }, [authState.isAuthenticated, updateToken]); // Valeur du contexte const contextValue: AuthContextType = { ...authState, login, logout, refreshAuth, hasRole, hasAnyRole, hasPermission, isRoleHigher, updateToken, }; return ( {children} ); }; export default AuthProvider;