import { Injectable, Logger, BadRequestException, ForbiddenException, NotFoundException } from '@nestjs/common'; import { KeycloakApiService } from '../../auth/services/keycloak-api.service'; import { CreateUserData, UserRole, KeycloakUser, HubUser, CreateHubUserData, HubUserStats, HubHealthStatus, HubUserActivity, MerchantStats } from '../../auth/services/keycloak-user.model'; // SUPPRIMER import type import { LoginDto, TokenResponse, User } from '../models/hub-user.model'; // ===== SERVICE PRINCIPAL ===== @Injectable() export class HubUsersService { private readonly logger = new Logger(HubUsersService.name); // Définir les rôles Hub autorisés comme constante private readonly HUB_ROLES = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER]; constructor(private readonly keycloakApi: KeycloakApiService) {} // === AUTHENTIFICATION UTILISATEUR === async authenticateUser(loginDto: LoginDto): Promise { return this.keycloakApi.authenticateUser(loginDto.username, loginDto.password); } // ===== VALIDATION ET UTILITAIRES ===== private isValidHubRole(role: UserRole): role is UserRole.DCB_ADMIN | UserRole.DCB_SUPPORT | UserRole.DCB_PARTNER { return this.HUB_ROLES.includes(role); } private validateHubRole(role: UserRole): void { if (!this.isValidHubRole(role)) { throw new BadRequestException( `Invalid hub role: ${role}. Hub roles must be DCB_ADMIN or DCB_SUPPORT` ); } } validateHubRoleFromString(role: string): UserRole.DCB_ADMIN | UserRole.DCB_SUPPORT | UserRole.DCB_PARTNER { // Vérifier si le rôle est valide if (!Object.values(UserRole).includes(role as UserRole)) { throw new BadRequestException(`Invalid role: ${role}`); } const userRole = role as UserRole; if (!this.isValidHubRole(userRole)) { throw new BadRequestException( `Invalid hub role: ${role}. Must be DCB_ADMIN or DCB_SUPPORT` ); } return userRole; } private async validateHubUserAccess(requesterId: string): Promise { const requesterRoles = await this.keycloakApi.getUserClientRoles(requesterId); const isHubAdmin = requesterRoles.some(role => this.HUB_ROLES.includes(role.name as UserRole) ); if (!isHubAdmin) { throw new ForbiddenException('Only hub administrators can manage hub users'); } } private parseKeycloakAttribute(value: string[] | undefined): string | undefined { return value?.[0]; } private parseKeycloakTimestamp(value: string[] | undefined): number | undefined { const strValue = this.parseKeycloakAttribute(value); if (!strValue) return undefined; const timestamp = parseInt(strValue); if (!isNaN(timestamp)) return timestamp; const date = new Date(strValue); return isNaN(date.getTime()) ? undefined : date.getTime(); } private mapToHubUser(user: KeycloakUser, roles: any[]): HubUser { const hubRole = roles.find(role => this.isValidHubRole(role.name as UserRole)); if (!user.id) throw new Error('User ID is required'); if (!user.email) throw new Error('User email is required'); return { id: user.id, username: user.username, email: user.email, firstName: user.firstName || '', lastName: user.lastName || '', role: hubRole?.name as UserRole.DCB_ADMIN | UserRole.DCB_SUPPORT | UserRole.DCB_PARTNER, enabled: user.enabled, emailVerified: user.emailVerified, createdBy: this.parseKeycloakAttribute(user.attributes?.createdBy) || 'unknown', createdByUsername: this.parseKeycloakAttribute(user.attributes?.createdByUsername) || 'unknown', createdTimestamp: user.createdTimestamp || Date.now(), lastLogin: this.parseKeycloakTimestamp(user.attributes?.lastLogin), userType: 'HUB', attributes: user.attributes, }; } // ===== OPÉRATIONS CRUD ===== async createHubUser(creatorId: string, userData: CreateHubUserData): Promise { this.logger.log(`Creating hub user: ${userData.username} with role: ${userData.role}`); this.validateHubRole(userData.role); await this.validateHubUserAccess(creatorId); // Vérifier les doublons this.checkDuplicateHubUser(userData) const keycloakUserData: CreateUserData = { username: userData.username, email: userData.email, firstName: userData.firstName, lastName: userData.lastName, password: userData.password, enabled: userData.enabled ?? true, emailVerified: userData.emailVerified ?? false, merchantPartnerId: undefined, clientRoles: [userData.role], createdBy: creatorId, initialStatus: 'PENDING_ACTIVATION', }; const userId = await this.keycloakApi.createUser(creatorId, keycloakUserData); const createdUser = await this.getHubUserById(userId, creatorId); this.logger.log(`Hub user created successfully: ${userData.username} (ID: ${userId})`); return createdUser; } private async checkDuplicateHubUser(merchantData: CreateHubUserData): Promise { const existingUsers = await this.keycloakApi.findUserByUsername(merchantData.username); if (existingUsers.length > 0) { throw new BadRequestException(`Merchant partner with username ${merchantData.username} already exists`); } const existingEmails = await this.keycloakApi.findUserByEmail(merchantData.email); if (existingEmails.length > 0) { throw new BadRequestException(`Merchant partner with email ${merchantData.email} already exists`); } } // ===== RÉCUPÉRATION DE TOUS LES MERCHANTS (DCB_PARTNER) ===== async getAllMerchants(requesterId: string): Promise { await this.validateHubUserAccess(requesterId); const allUsers = await this.keycloakApi.getAllUsers(); const merchants: HubUser[] = []; for (const user of allUsers) { if (!user.id) continue; try { const userRoles = await this.keycloakApi.getUserClientRoles(user.id); const isMerchant = userRoles.some(role => role.name === UserRole.DCB_PARTNER); if (isMerchant) { merchants.push(this.mapToHubUser(user, userRoles)); } } catch (error) { this.logger.warn(`Could not process merchant ${user.id}: ${error.message}`); } } return merchants; } async suspendMerchantPartner(merchantId: string, reason: string, requesterId: string): Promise { await this.validateHubAccess(requesterId); const dcbPartnerUser = await this.findDcbPartnerByMerchantId(merchantId); // Suspendre le merchant et tous ses utilisateurs await this.keycloakApi.setUserAttributes(dcbPartnerUser.id!, { merchantStatus: ['SUSPENDED'], suspensionReason: [reason], suspendedAt: [new Date().toISOString()], }); // Suspendre tous les utilisateurs du merchant await this.suspendAllMerchantUsers(merchantId, requesterId); this.logger.log(`Merchant partner suspended: ${merchantId}, reason: ${reason}`); } async getMerchantPartnerById(merchantId: string, requesterId: string): Promise { await this.validateMerchantAccess(requesterId, merchantId); const merchantPartner = await this.findDcbPartnerByMerchantId(merchantId); const merchantPartnerRoles = await this.keycloakApi.getUserClientRoles(merchantId); return this.mapToHubUser(merchantPartner, merchantPartnerRoles); } // ===== STATISTIQUES ===== async getMerchantStats(requesterId: string): Promise { await this.validateHubAccess(requesterId); const allMerchants = await this.getAllMerchants(requesterId); const allUsers = await this.keycloakApi.getAllUsers(); let totalUsers = 0; for (const merchant of allMerchants) { const merchantUsers = allUsers.filter(user => user.attributes?.merchantPartnerId?.[0] === merchant.id ); totalUsers += merchantUsers.length; } return { totalMerchants: allMerchants.length, activeMerchants: allMerchants.filter(m => m.enabled).length, suspendedMerchants: allMerchants.filter(m => !m.enabled).length, pendingMerchants: allMerchants.filter(m => m.attributes?.userStatus?.[0] === 'PENDING_ACTIVATION' ).length, totalUsers, }; } private async validateHubAccess(requesterId: string): Promise { const requesterRoles = await this.keycloakApi.getUserClientRoles(requesterId); const isHubAdmin = requesterRoles.some(role => [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT].includes(role.name as UserRole) ); if (!isHubAdmin) { throw new ForbiddenException('Only hub administrators can access this resource'); } } private async validateMerchantAccess(requesterId: string, merchantId?: string): Promise { const requesterRoles = await this.keycloakApi.getUserClientRoles(requesterId); this.logger.debug(`Validating merchant access: requester=${requesterId}, merchant=${merchantId}`); this.logger.debug(`Requester roles: ${requesterRoles.map(r => r.name).join(', ')}`); // Les admins Hub ont accès complet const isHubAdmin = requesterRoles.some(role => [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT].includes(role.name as UserRole) ); if (isHubAdmin) { this.logger.debug('Hub admin access granted'); return; } // CORRECTION: Service account est considéré comme admin hub if (requesterId.includes('service-account')) { this.logger.debug('Service account access granted'); return; } // Les DCB_PARTNER n'ont accès qu'à leur propre merchant if (requesterRoles.some(role => role.name === UserRole.DCB_PARTNER)) { if (merchantId && requesterId === merchantId) { this.logger.debug('DCB_PARTNER access to own merchant granted'); return; // DCB_PARTNER accède à son propre merchant } throw new ForbiddenException('DCB_PARTNER can only access their own merchant partner'); } // Les autres rôles merchants n'ont accès qu'à leur merchant const requesterMerchantId = await this.keycloakApi.getUserMerchantPartnerId(requesterId); if (requesterMerchantId && merchantId && requesterMerchantId === merchantId) { this.logger.debug('Merchant user access to own merchant granted'); return; } throw new ForbiddenException('Insufficient permissions to access this merchant'); } private async validateMerchantManagementPermissions(requesterId: string, merchantId: string): Promise { const requesterRoles = await this.keycloakApi.getUserClientRoles(requesterId); // Seuls les admins Hub peuvent modifier les merchants const isHubAdmin = requesterRoles.some(role => [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT].includes(role.name as UserRole) ); if (!isHubAdmin) { throw new ForbiddenException('Only hub administrators can manage merchant partners'); } } private async findDcbPartnerByMerchantId(merchantId: string): Promise { const users = await this.keycloakApi.findUserByUsername(merchantId); if (users.length === 0) { throw new NotFoundException(`Merchant partner not found: ${merchantId}`); } const user = users[0]; const userRoles = await this.keycloakApi.getUserClientRoles(user.id!); const isDcbPartner = userRoles.some(role => role.name === UserRole.DCB_PARTNER); if (!isDcbPartner) { throw new NotFoundException(`User ${merchantId} is not a DCB_PARTNER`); } return user; } private async suspendAllMerchantUsers(merchantId: string, requesterId: string): Promise { const allUsers = await this.keycloakApi.getAllUsers(); const merchantUsers = allUsers.filter(user => user.attributes?.merchantPartnerId?.[0] === merchantId ); for (const user of merchantUsers) { if (user.id) { try { await this.keycloakApi.updateUser(user.id, { enabled: false }, requesterId); } catch (error) { this.logger.warn(`Could not suspend user ${user.id}: ${error.message}`); } } } } async getHubUserById(userId: string, requesterId: string): Promise { await this.validateHubUserAccess(requesterId); const [user, userRoles] = await Promise.all([ this.keycloakApi.getUserById(userId, requesterId), this.keycloakApi.getUserClientRoles(userId) ]); const isHubUser = userRoles.some(role => this.isValidHubRole(role.name as UserRole)); if (!isHubUser) { throw new BadRequestException(`User ${userId} is not a hub user`); } return this.mapToHubUser(user, userRoles); } async getAllHubUsers(requesterId: string): Promise { await this.validateHubUserAccess(requesterId); const allUsers = await this.keycloakApi.getAllUsers(); const hubUsers: HubUser[] = []; for (const user of allUsers) { if (!user.id) continue; try { const userRoles = await this.keycloakApi.getUserClientRoles(user.id); const hubRole = userRoles.find(role => this.isValidHubRole(role.name as UserRole)); if (hubRole) { hubUsers.push(this.mapToHubUser(user, userRoles)); } } catch (error) { this.logger.warn(`Could not process user ${user.id}: ${error.message}`); } } return hubUsers; } async getHubUsersByRole(role: UserRole.DCB_ADMIN | UserRole.DCB_SUPPORT | UserRole.DCB_PARTNER, requesterId: string): Promise { await this.validateHubUserAccess(requesterId); const allHubUsers = await this.getAllHubUsers(requesterId); return allHubUsers.filter(user => user.role === role); } async updateHubUser( userId: string, updates: Partial<{ firstName: string; lastName: string; email: string; enabled: boolean; }>, requesterId: string ): Promise { await this.validateHubUserAccess(requesterId); await this.getHubUserById(userId, requesterId); await this.keycloakApi.updateUser(userId, updates, requesterId); return this.getHubUserById(userId, requesterId); } async updateHubUserRole( userId: string, newRole: UserRole.DCB_ADMIN | UserRole.DCB_SUPPORT |UserRole.DCB_PARTNER, requesterId: string ): Promise { // Validation implicite via le typage await this.validateHubUserAccess(requesterId); const requesterRoles = await this.keycloakApi.getUserClientRoles(requesterId); const isRequesterAdmin = requesterRoles.some(role => role.name === UserRole.DCB_ADMIN); if (!isRequesterAdmin) { throw new ForbiddenException('Only DCB_ADMIN can change user roles'); } await this.getHubUserById(userId, requesterId); await this.keycloakApi.setClientRoles(userId, [newRole]); return this.getHubUserById(userId, requesterId); } async updateMerchantPartner( merchantId: string, updates: Partial<{ firstName: string; lastName: string; email: string; enabled: boolean; }>, requesterId: string ): Promise { await this.validateHubUserAccess(requesterId); await this.getHubUserById(merchantId, requesterId); await this.validateMerchantManagementPermissions(requesterId, merchantId); return this.getMerchantPartnerById(merchantId, requesterId); } async deleteHubUser(userId: string, requesterId: string): Promise { await this.validateHubUserAccess(requesterId); await this.getHubUserById(userId, requesterId); if (userId === requesterId) { throw new BadRequestException('Cannot delete your own account'); } if (await this.isLastAdmin(userId)) { throw new BadRequestException('Cannot delete the last DCB_ADMIN user'); } await this.keycloakApi.deleteUser(userId, requesterId); this.logger.log(`Hub user deleted: ${userId} by ${requesterId}`); } // ===== GESTION DES MOTS DE PASSE ===== async resetHubUserPassword( userId: string, newPassword: string, temporary: boolean = true, requesterId: string ): Promise { await this.validateHubUserAccess(requesterId); await this.getHubUserById(userId, requesterId); await this.keycloakApi.resetUserPassword(userId, newPassword, temporary); this.logger.log(`Password reset for hub user: ${userId}`); } async sendHubUserPasswordResetEmail(userId: string, requesterId: string): Promise { await this.validateHubUserAccess(requesterId); const user = await this.getHubUserById(userId, requesterId); await this.keycloakApi.sendPasswordResetEmail(user.email); this.logger.log(`Password reset email sent to hub user: ${user.email}`); } // ===== STATISTIQUES ET RAPPORTS ===== async getHubUsersStats(requesterId: string): Promise { await this.validateHubUserAccess(requesterId); const allHubUsers = await this.getAllHubUsers(requesterId); return { totalAdmins: allHubUsers.filter(user => user.role === UserRole.DCB_ADMIN).length, totalSupport: allHubUsers.filter(user => user.role === UserRole.DCB_SUPPORT).length, activeUsers: allHubUsers.filter(user => user.enabled).length, inactiveUsers: allHubUsers.filter(user => !user.enabled).length, pendingActivation: allHubUsers.filter(user => user.attributes?.userStatus?.[0] === 'PENDING_ACTIVATION' ).length, }; } async getHubUserActivity(requesterId: string): Promise { await this.validateHubUserAccess(requesterId); const allHubUsers = await this.getAllHubUsers(requesterId); return allHubUsers.map(user => ({ user, lastLogin: user.lastLogin ? new Date(user.lastLogin) : undefined })); } async getActiveHubSessions(requesterId: string): Promise<{ userId: string; username: string; lastAccess: Date }[]> { const activity = await this.getHubUserActivity(requesterId); return activity .filter(item => item.lastLogin) .map(item => ({ userId: item.user.id, username: item.user.username, lastAccess: item.lastLogin! })); } // ===== SANTÉ ET VALIDATIONS ===== async checkHubUsersHealth(requesterId: string): Promise { await this.validateHubUserAccess(requesterId); const stats = await this.getHubUsersStats(requesterId); const issues: string[] = []; if (stats.totalAdmins === 0) { issues.push('No DCB_ADMIN users found - system is vulnerable'); } if (stats.totalAdmins === 1) { issues.push('Only one DCB_ADMIN user exists - consider creating backup administrators'); } if (stats.inactiveUsers > stats.activeUsers / 2) { issues.push('More than half of hub users are inactive'); } if (stats.pendingActivation > 5) { issues.push('Many users are pending activation - review onboarding process'); } const status = issues.length === 0 ? 'healthy' : issues.length <= 2 ? 'degraded' : 'unhealthy'; return { status, issues, stats }; } private async isLastAdmin(userId: string): Promise { try { const [user, userRoles] = await Promise.all([ this.keycloakApi.getUserById(userId, userId), this.keycloakApi.getUserClientRoles(userId) ]); const isAdmin = userRoles.some(role => role.name === UserRole.DCB_ADMIN); if (!isAdmin) return false; const allHubUsers = await this.getAllHubUsers(userId); const adminCount = allHubUsers.filter(user => user.role === UserRole.DCB_ADMIN).length; return adminCount <= 1; } catch (error) { this.logger.error(`Error checking if user is last admin: ${error.message}`); return false; } } async canUserManageHubUsers(userId: string): Promise { try { const roles = await this.keycloakApi.getUserClientRoles(userId); return roles.some(role => this.HUB_ROLES.includes(role.name as UserRole) ); } catch (error) { return false; } } }