dcb-backoffice/src/app/core/services/auth.service.ts

535 lines
14 KiB
TypeScript

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<boolean>(this.isAuthenticated());
private userProfile$ = new BehaviorSubject<User | null>(null);
private initialized$ = new BehaviorSubject<boolean>(false);
// === INITIALISATION DE L'APPLICATION ===
/**
* Initialise l'authentification au démarrage de l'application
*/
async initialize(): Promise<boolean> {
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<boolean> {
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<boolean> {
return this.initialized$.asObservable();
}
// === MÉTHODES D'AUTHENTIFICATION ===
/**
* Connexion utilisateur
*/
login(credentials: LoginDto): Observable<LoginResponseDto> {
return this.http.post<LoginResponseDto>(
`${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<LoginResponseDto> {
const refreshToken = this.getRefreshToken();
if (!refreshToken) {
return throwError(() => new Error('No refresh token available'));
}
return this.http.post<LoginResponseDto>(
`${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<LogoutResponseDto> {
return this.http.post<LogoutResponseDto>(
`${environment.iamApiUrl}/auth/logout`,
{}
).pipe(
tap(() => this.clearAuthData()),
catchError(error => {
this.clearAuthData();
return throwError(() => error);
})
);
}
/**
* Chargement du profil utilisateur
*/
loadUserProfile(): Observable<User> {
return this.http.get<any>(
`${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<TokenValidationResponseDto> {
return this.http.get<TokenValidationResponseDto>(
`${environment.iamApiUrl}/auth/validate`
);
}
// === OBSERVABLES POUR COMPOSANTS ===
getAuthState(): Observable<boolean> {
return this.authState$.asObservable();
}
getUserProfile(): Observable<User | null> {
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<boolean> {
return this.authState$.asObservable();
}
getProfile(): Observable<User | null> {
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<never> {
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;
}
}
}