Files
btpxpress-frontend/contexts/AuthContext.tsx
2025-10-01 01:39:07 +00:00

396 lines
12 KiB
TypeScript

'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<void>;
logout: () => Promise<void>;
refreshAuth: () => Promise<void>;
hasRole: (role: string) => boolean;
hasAnyRole: (roles: string[]) => boolean;
hasPermission: (permission: string) => boolean;
isRoleHigher: (role: string) => boolean;
updateToken: (minValidity?: number) => Promise<boolean>;
}
// Contexte d'authentification
const AuthContext = createContext<AuthContextType | undefined>(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<AuthProviderProps> = ({ children }) => {
const router = useRouter();
// État de l'authentification
const [authState, setAuthState] = useState<AuthState>({
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<UserInfo | null> => {
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<void> => {
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<void> => {
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<void> => {
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<boolean> => {
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 (
<AuthContext.Provider value={contextValue}>
{children}
</AuthContext.Provider>
);
};
export default AuthProvider;