535 lines
14 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
} |