202 lines
6.7 KiB
TypeScript
202 lines
6.7 KiB
TypeScript
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<string>('USER_SERVICE') || 'http://localhost:3001';
|
|
|
|
const keycloakUrl = this.configService.get<string>('KEYCLOAK_SERVER_URL');
|
|
const keycloakRealm = this.configService.get<string>('KEYCLOAK_REALM');
|
|
const clientId = this.configService.get<string>('KEYCLOAK_CLIENT_ID');
|
|
const clientSecret = this.configService.get<string>('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<string> {
|
|
// 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<Record<string, string>> {
|
|
const token = await this.getAccessToken();
|
|
return {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
};
|
|
}
|
|
|
|
async verifyUserExists(userId: string): Promise<boolean> {
|
|
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<UserInfo> {
|
|
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<UserInfo[]> {
|
|
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,
|
|
};
|
|
}
|
|
} |