import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; import { firstValueFrom } from 'rxjs'; import { UserInfo, UserServiceClient } from '../interfaces/user.service.interface'; @Injectable() export class HttpUserServiceClient implements UserServiceClient { private readonly baseUrl: string; private readonly keycloakUrl: string; private readonly keycloakRealm: string; private readonly clientId: string; private readonly clientSecret: string; private accessToken: string | null = null; private tokenExpiry: number = 0; constructor( private readonly httpService: HttpService, private readonly configService: ConfigService, ) { this.baseUrl = this.configService.get('USER_SERVICE') || 'http://localhost:3001'; const keycloakUrl = this.configService.get('KEYCLOAK_SERVER_URL'); const keycloakRealm = this.configService.get('KEYCLOAK_REALM'); const clientId = this.configService.get('KEYCLOAK_CLIENT_ID'); const clientSecret = this.configService.get('KEYCLOAK_CLIENT_SECRET'); if (!keycloakUrl || !keycloakRealm || !clientId || !clientSecret) { throw new Error('Missing required Keycloak configuration'); } this.keycloakUrl = keycloakUrl; this.keycloakRealm = keycloakRealm; this.clientId = clientId; this.clientSecret = clientSecret; } private async getAccessToken(): Promise { // Vérifier si le token est encore valide (avec une marge de 30 secondes) if (this.accessToken !== null && Date.now() < this.tokenExpiry - 30000) { return this.accessToken; } try { const tokenUrl = `${this.keycloakUrl}/realms/${this.keycloakRealm}/protocol/openid-connect/token`; const params = new URLSearchParams(); params.append('grant_type', 'client_credentials'); params.append('client_id', this.clientId); params.append('client_secret', this.clientSecret); const response = await firstValueFrom( this.httpService.post(tokenUrl, params.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }), ); this.accessToken = response.data.access_token; // Calculer l'expiration du token (expires_in est en secondes) this.tokenExpiry = Date.now() + (response.data.expires_in * 1000); return this.accessToken || ''; } catch (error) { throw new HttpException( 'Failed to authenticate with Keycloak', HttpStatus.UNAUTHORIZED, ); } } private async getAuthHeaders(): Promise> { const token = await this.getAccessToken(); return { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }; } async verifyUserExists(userId: string): Promise { try { console.log(`🔍 [verifyUserExists] Vérification de l'utilisateur: ${userId}`); console.log(` Type de userId: ${typeof userId}`); console.log(` Valeur de userId: "${userId}"`); const headers = await this.getAuthHeaders(); const url = `${this.baseUrl}/merchant-users/${userId}`; const response = await firstValueFrom( this.httpService.get(url, { headers }), ); console.log(`✅ [verifyUserExists] Réponse complète:`, JSON.stringify(response.data, null, 2)); // Vérifier si on a reçu une réponse valide if (!response.data) { console.log(` ❌ Aucune donnée dans la réponse`); return false; } // L'utilisateur existe si on a reçu une réponse 200 avec des données const exists = response.data && response.data.id === userId; console.log(` Résultat: ${exists ? '✅ Utilisateur existe' : '❌ Utilisateur non trouvé'}`); return exists; } catch (error) { console.error(`❌ [verifyUserExists] Erreur détaillée:`, { name: error.name, message: error.message, status: error.response?.status, statusText: error.response?.statusText, data: error.response?.data, code: error.code }); if (error.response?.status === 404) { console.log(` 📭 Utilisateur ${userId} non trouvé (404)`); return false; } if (error.response?.status === 401) { console.log(` 🔄 Token invalide (401), rafraîchissement...`); this.accessToken = null; await new Promise(resolve => setTimeout(resolve, 1000)); return this.verifyUserExists(userId); } // Autres erreurs HTTP if (error.response?.status) { console.log(` ⚠️ Erreur HTTP ${error.response.status}: ${error.response.statusText}`); return false; } // Erreur réseau ou autre console.log(` 🚨 Erreur non-HTTP: ${error.message}`); throw new HttpException( `Failed to verify user existence: ${error.message}`, HttpStatus.SERVICE_UNAVAILABLE, ); } } async getUserInfo(userId: string): Promise { try { const headers = await this.getAuthHeaders(); const response = await firstValueFrom( this.httpService.get(`${this.baseUrl}/users/${userId}`, { headers }), ); return this.mapToUserInfo(response.data); } catch (error) { if (error.response?.status === 404) { throw new HttpException(`User ${userId} not found`, HttpStatus.NOT_FOUND); } if (error.response?.status === 401) { this.accessToken = null; return this.getUserInfo(userId); } throw new HttpException( 'Failed to get user information', HttpStatus.SERVICE_UNAVAILABLE, ); } } async getUsersInfo(userIds: string[]): Promise { if (userIds.length === 0) { return []; } try { const headers = await this.getAuthHeaders(); const response = await firstValueFrom( this.httpService.post(`${this.baseUrl}/users/batch`, { userIds }, { headers }), ); return response.data.map(user => this.mapToUserInfo(user)); } catch (error) { if (error.response?.status === 401) { this.accessToken = null; return this.getUsersInfo(userIds); } throw new HttpException( 'Failed to get users information', HttpStatus.SERVICE_UNAVAILABLE, ); } } private mapToUserInfo(data: any): UserInfo { return { id: data.id || data.userId, email: data.email, name: data.name || `${data.firstName} ${data.lastName}`.trim(), firstName: data.firstName, lastName: data.lastName, ...data, }; } }