import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Router } from '@angular/router'; import { environment } from '@environments/environment'; import { BehaviorSubject, Observable, throwError, tap, catchError } from 'rxjs'; import { firstValueFrom } from 'rxjs'; import { User, UserType, UserRole, } from '@core/models/dcb-bo-hub-user.model'; // === INTERFACES DTO AUTH === export interface LoginDto { username: string; password: string; } export interface RefreshTokenDto { refresh_token: string; } export interface LoginResponseDto { access_token: string; refresh_token: string; expires_in: number; token_type: string; } export interface LogoutResponseDto { message: string; } export interface AuthStatusResponseDto { authenticated: boolean; status: string; } export interface TokenValidationResponseDto { valid: boolean; user: { id: string; username: string; email: string; firstName: string; lastName: string; roles: string[]; }; expires_in: number; } @Injectable({ providedIn: 'root' }) export class AuthService { private readonly http = inject(HttpClient); private readonly tokenKey = 'access_token'; private readonly refreshTokenKey = 'refresh_token'; private authState$ = new BehaviorSubject(this.isAuthenticated()); private userProfile$ = new BehaviorSubject(null); private initialized$ = new BehaviorSubject(false); // === INITIALISATION DE L'APPLICATION === /** * Initialise l'authentification au démarrage de l'application */ async initialize(): Promise { await new Promise(resolve => setTimeout(resolve, 0)); try { const token = this.getAccessToken(); if (!token) { setTimeout(() => { this.initialized$.next(true); }); return false; } if (this.isTokenExpired(token)) { const refreshSuccess = await this.tryRefreshToken(); setTimeout(() => { this.initialized$.next(true); }); return refreshSuccess; } // Token valide : charger le profil utilisateur await firstValueFrom(this.loadUserProfile()); setTimeout(() => { this.authState$.next(true); this.initialized$.next(true); }); return true; } catch (error) { this.clearAuthData(); setTimeout(() => { this.initialized$.next(true); }); return false; } } /** * Tente de rafraîchir le token de manière synchrone */ private async tryRefreshToken(): Promise { const refreshToken = this.getRefreshToken(); if (!refreshToken) { return false; } try { await firstValueFrom(this.refreshAccessToken()); await firstValueFrom(this.loadUserProfile()); this.authState$.next(true); return true; } catch (error) { this.clearAuthData(); return false; } } /** * Observable pour suivre l'état d'initialisation */ getInitializedState(): Observable { return this.initialized$.asObservable(); } // === MÉTHODES D'AUTHENTIFICATION === /** * Connexion utilisateur */ login(credentials: LoginDto): Observable { return this.http.post( `${environment.iamApiUrl}/auth/login`, credentials ).pipe( tap(response => { this.handleLoginSuccess(response); this.loadUserProfile().subscribe(); }), catchError(error => this.handleLoginError(error)) ); } /** * Rafraîchissement du token d'accès */ refreshAccessToken(): Observable { const refreshToken = this.getRefreshToken(); if (!refreshToken) { return throwError(() => new Error('No refresh token available')); } return this.http.post( `${environment.iamApiUrl}/auth/refresh`, { refresh_token: refreshToken } ).pipe( tap(response => this.handleLoginSuccess(response)), catchError(error => { this.clearAuthData(); return throwError(() => error); }) ); } /** * Déconnexion utilisateur */ logout(): Observable { return this.http.post( `${environment.iamApiUrl}/auth/logout`, {} ).pipe( tap(() => this.clearAuthData()), catchError(error => { this.clearAuthData(); return throwError(() => error); }) ); } /** * Chargement du profil utilisateur */ loadUserProfile(): Observable { return this.http.get( `${environment.iamApiUrl}/auth/profile` ).pipe( tap(apiResponse => { // Déterminer le type d'utilisateur const userType = this.determineUserType(apiResponse); // Mapper vers le modèle User const userProfile = this.mapToUserModel(apiResponse, userType); this.userProfile$.next(userProfile); }), catchError(error => { console.error('❌ Erreur chargement profil:', error); return throwError(() => error); }) ); } /** * Détermine le type d'utilisateur basé sur la réponse API */ private determineUserType(apiUser: any): UserType { const hubRoles = [UserRole.DCB_ADMIN || UserRole.DCB_SUPPORT]; const merchantRoles = [UserRole.DCB_PARTNER || UserRole.DCB_PARTNER_ADMIN || UserRole.DCB_PARTNER_MANAGER || UserRole.DCB_PARTNER_SUPPORT]; // Logique pour déterminer le type d'utilisateur if (apiUser.clientRoles?.[0].includes(merchantRoles)) { return UserType.MERCHANT_PARTNER; } else if (apiUser.clientRoles?.[0].includes(hubRoles)) { return UserType.HUB; } else { console.warn('Type d\'utilisateur non reconnu, rôle:', apiUser.clientRoles?.[0]); return UserType.HUB; // Fallback } } private mapToUserModel(apiUser: any, userType: UserType): User { const mappedUser: User = { id: apiUser.id || apiUser.userId || '', username: apiUser.username || apiUser.userName || '', email: apiUser.email || '', firstName: apiUser.firstName || apiUser.firstname || apiUser.given_name || '', lastName: apiUser.lastName || apiUser.lastname || apiUser.family_name || '', enabled: apiUser.enabled ?? apiUser.active ?? true, emailVerified: apiUser.emailVerified ?? apiUser.email_verified ?? false, userType: userType, merchantPartnerId: apiUser.merchantPartnerId || apiUser.partnerId || apiUser.merchantId || null, role: apiUser.clientRoles || apiUser.clientRoles?.[0] || '', // Gérer rôle unique ou tableau createdBy: apiUser.createdBy || apiUser.creatorId || null, createdByUsername: apiUser.createdByUsername || apiUser.creatorUsername || null, createdTimestamp: apiUser.createdTimestamp || apiUser.createdAt || apiUser.creationDate || Date.now(), lastLogin: apiUser.lastLogin || apiUser.lastLoginAt || apiUser.lastConnection || null }; console.log('✅ Utilisateur mappé:', mappedUser); return mappedUser; } // === GESTION DE SESSION === private handleLoginSuccess(response: LoginResponseDto): void { if (response.access_token) { localStorage.setItem(this.tokenKey, response.access_token); if (response.refresh_token) { localStorage.setItem(this.refreshTokenKey, response.refresh_token); } this.authState$.next(true); } } private clearAuthData(): void { localStorage.removeItem(this.tokenKey); localStorage.removeItem(this.refreshTokenKey); this.authState$.next(false); this.userProfile$.next(null); } // === VALIDATION DU TOKEN === validateToken(): Observable { return this.http.get( `${environment.iamApiUrl}/auth/validate` ); } // === OBSERVABLES POUR COMPOSANTS === getAuthState(): Observable { return this.authState$.asObservable(); } getUserProfile(): Observable { return this.userProfile$.asObservable(); } // === GESTION DES RÔLES ET TYPES === getCurrentUserRoles(): UserRole[] { const token = this.getAccessToken(); if (!token) return []; try { const payload = JSON.parse(atob(token.split('.')[1])); // Mapping des rôles Keycloak vers vos rôles DCB const roleMappings: { [key: string]: UserRole } = { // Rôles administrateur 'admin': UserRole.DCB_ADMIN, 'dcb-admin': UserRole.DCB_ADMIN, 'administrator': UserRole.DCB_ADMIN, // Rôles support 'support': UserRole.DCB_SUPPORT, 'dcb-support': UserRole.DCB_SUPPORT, // Rôles partenaire 'partner': UserRole.DCB_PARTNER, 'dcb-partner': UserRole.DCB_PARTNER, // Rôles admin partenaire 'partner-admin': UserRole.DCB_PARTNER_ADMIN, 'dcb-partner-admin': UserRole.DCB_PARTNER_ADMIN, // Rôles manager partenaire 'partner-manager': UserRole.DCB_PARTNER_MANAGER, 'dcb-partner-manager': UserRole.DCB_PARTNER_MANAGER, // Rôles support partenaire 'partner-support': UserRole.DCB_PARTNER_SUPPORT, 'dcb-partner-support': UserRole.DCB_PARTNER_SUPPORT, }; let allRoles: string[] = []; // Collecter tous les rôles du token if (payload.resource_access) { Object.values(payload.resource_access).forEach((client: any) => { if (client?.roles) { allRoles = allRoles.concat(client.roles); } }); } if (payload.realm_access?.roles) { allRoles = allRoles.concat(payload.realm_access.roles); } const mappedRoles = allRoles .map(role => roleMappings[role.toLowerCase()]) .filter(role => role !== undefined); return mappedRoles; } catch (error) { console.error('❌ Error:', error); return []; } } /** * Récupère le rôle principal de l'utilisateur courant */ getCurrentUserRole(): UserRole | null { const roles = this.getCurrentUserRoles(); return roles.length > 0 ? roles[0] : null; } /** * Récupère le type d'utilisateur courant */ getCurrentUserType(): UserType | null { const role = this.getCurrentUserRole(); if (!role) return null; // Déterminer le type d'utilisateur basé sur le rôle const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER]; const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT]; if (hubRoles.includes(role)) { return UserType.HUB; } else if (merchantRoles.includes(role)) { return UserType.MERCHANT_PARTNER; } return null; } /** * Récupère les clientRoles du profil utilisateur */ getCurrentUserClientRoles(): UserRole | null { const profile = this.userProfile$.value; return profile?.role || null; } /** * Récupère le rôle principal du profil utilisateur */ getCurrentUserPrimaryRole(): UserRole | null { const clientRoles = this.getCurrentUserClientRoles(); return clientRoles || null; } /** * Vérifie si l'utilisateur courant est un utilisateur Hub */ isHubUser(): boolean { const profile = this.userProfile$.value; return profile?.userType === UserType.HUB; } /** * Vérifie si l'utilisateur courant est un utilisateur Marchand */ isMerchantUser(): boolean { const profile = this.userProfile$.value; return profile?.userType === UserType.MERCHANT_PARTNER; } /** * Vérifie si l'utilisateur courant a un rôle spécifique */ hasRole(role: UserRole): boolean { const clientRoles = this.getCurrentUserClientRoles(); return clientRoles === role; } /** * Vérifie si l'utilisateur courant a au moins un des rôles spécifiés */ hasAnyRole(role: UserRole): boolean { const userRoles = this.getCurrentUserClientRoles(); return userRoles === role; } /** * Vérifie si l'utilisateur courant peut gérer les utilisateurs Hub */ canManageHubUsers(): boolean { return this.hasAnyRole(UserRole.DCB_ADMIN) || this.hasAnyRole(UserRole.DCB_SUPPORT); } /** * Vérifie si l'utilisateur courant peut gérer les utilisateurs Marchands */ canManageMerchantUsers(): boolean { return this.hasAnyRole(UserRole.DCB_ADMIN) || this.hasAnyRole(UserRole.DCB_PARTNER); } // === MÉTHODES UTILITAIRES === onAuthState(): Observable { return this.authState$.asObservable(); } getProfile(): Observable { return this.getUserProfile(); } /** * Récupère l'ID de l'utilisateur courant */ getCurrentUserId(): string | null { const profile = this.userProfile$.value; return profile?.id || null; } /** * Récupère le merchantPartnerId de l'utilisateur courant (si marchand) */ getCurrentMerchantPartnerId(): string | null { const profile = this.userProfile$.value; return profile?.merchantPartnerId || null; } /** * Vérifie si le profil fourni est celui de l'utilisateur courant */ isCurrentUserProfile(userId: string): boolean { const currentUserId = this.getCurrentUserId(); return currentUserId === userId; } /** * Vérifie si l'utilisateur peut visualiser tous les marchands */ canViewAllMerchants(): boolean { return this.hasAnyRole(UserRole.DCB_ADMIN) || this.hasAnyRole(UserRole.DCB_PARTNER); } // === TOKENS === getAccessToken(): string | null { return localStorage.getItem(this.tokenKey); } getRefreshToken(): string | null { return localStorage.getItem(this.refreshTokenKey); } // === GESTION DES ERREURS === private handleLoginError(error: HttpErrorResponse): Observable { let errorMessage = 'Login failed'; if (error.status === 401) { errorMessage = 'Invalid username or password'; } else if (error.status === 403) { errorMessage = 'Account is disabled or not fully set up'; } else if (error.error?.message) { errorMessage = error.error.message; } return throwError(() => new Error(errorMessage)); } // === VERIFICATIONS === isAuthenticated(): boolean { const token = this.getAccessToken(); return !!token && !this.isTokenExpired(token); } private isTokenExpired(token: string): boolean { try { const payload = JSON.parse(atob(token.split('.')[1])); const expiry = payload.exp; return (Math.floor((new Date).getTime() / 1000)) >= expiry; } catch { return true; } } }