import { Injectable, Logger, BadRequestException, ForbiddenException, NotFoundException, InternalServerErrorException } from '@nestjs/common'; import { KeycloakApiService } from '../../auth/services/keycloak-api.service'; import { CreateUserData as KeycloakCreateUserData, UserRole, KeycloakUser, LoginDto, TokenResponse, KeycloakRole } from '../../auth/services/keycloak-user.model'; import { CreateUserData, User, UserType } from '../models/hub-user.model'; // Configuration Centralisée const SECURITY_CONFIG = { ROLES: { HUB: [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER], MERCHANT: [ UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT, ] }, VALIDATION: { MIN_PASSWORD_LENGTH: 8, MAX_USERNAME_LENGTH: 50, MIN_USERNAME_LENGTH: 3 } }; @Injectable() export class HubUsersService { private readonly logger = new Logger(HubUsersService.name); constructor(private readonly keycloakApi: KeycloakApiService) {} // === PUBLIC INTERFACE === async authenticateUser(loginDto: LoginDto): Promise { return this.keycloakApi.authenticateUser(loginDto.username, loginDto.password); } async getCompleteUserProfile(userId: string, tokenUser: any) { try { const [userDetails, userRoles] = await Promise.all([ this.keycloakApi.getUserById(userId, userId), this.keycloakApi.getUserClientRoles(userId) ]); return this.buildUserProfile(userId, tokenUser, userDetails, userRoles); } catch (error) { throw new InternalServerErrorException('Could not retrieve user profile'); } } // === HUB USERS MANAGEMENT === async getAllHubUsers(requesterId: string): Promise { await this.validateHubUserAccess(requesterId); return this.processUsersByType(await this.keycloakApi.getAllUsers(), UserType.HUB); } /** * Récupère uniquement les utilisateurs DCB_PARTNER */ async getAllDcbPartners(requesterId: string): Promise { await this.validateHubUserAccess(requesterId); const allUsers = await this.keycloakApi.getAllUsers(); const dcbPartners: User[] = []; for (const user of allUsers) { if (!user.id) continue; try { const userRoles = await this.keycloakApi.getUserClientRoles(user.id); // Vérifier si l'utilisateur est un DCB_PARTNER const isDcbPartner = userRoles.some(role => role.name === UserRole.DCB_PARTNER ); if (isDcbPartner) { const mappedUser = this.mapToUser(user, userRoles); dcbPartners.push(mappedUser); } } catch (error) { this.logger.warn(`Could not process user ${user.id} for DCB_PARTNER filter: ${error.message}`); } } this.logger.log(`Retrieved ${dcbPartners.length} DCB_PARTNER users`); return dcbPartners; } async getHubUserById(userId: string, requesterId: string): Promise { await this.validateHubUserAccess(requesterId); return this.getValidatedUser(userId, requesterId, UserType.HUB); } async createHubUser(creatorId: string, userData: CreateUserData): Promise { this.validateUserCreationData(userData, UserType.HUB); await this.validateHubUserAccess(creatorId); await this.validateUserUniqueness(userData.username, userData.email); const keycloakUserData = this.buildKeycloakUserData(userData); const userId = await this.keycloakApi.createUser(creatorId, keycloakUserData); this.logger.log(`Hub user created: ${userData.username}`); return this.getHubUserById(userId, creatorId); } async updateHubUser( userId: string, updates: Partial>, requesterId: string ): Promise { await this.executeWithValidation(userId, requesterId, UserType.HUB, async () => { await this.keycloakApi.updateUser(userId, updates, requesterId); }); return this.getHubUserById(userId, requesterId); } async deleteHubUser(userId: string, requesterId: string): Promise { await this.executeWithValidation(userId, requesterId, UserType.HUB, async () => { await this.keycloakApi.deleteUser(userId, requesterId); }); this.logger.log(`Hub user deleted: ${userId} by ${requesterId}`); } async getHubUsersByRole(role: UserRole, requesterId: string): Promise { await this.validateHubUserAccess(requesterId); const allHubUsers = await this.getAllHubUsers(requesterId); return allHubUsers.filter(user => user.role === role); } async updateHubUserRole(userId: string, newRole: UserRole, requesterId: string): Promise { await this.validateRoleChangePermission(requesterId); await this.executeWithValidation(userId, requesterId, UserType.HUB, async () => { await this.keycloakApi.setClientRoles(userId, [newRole]); }); return this.getHubUserById(userId, requesterId); } // === MERCHANT USERS MANAGEMENT === /** * Vérifie si un utilisateur est Hub Admin ou Support */ async isUserHubAdminOrSupport(userId: string): Promise { try { const userRoles = await this.keycloakApi.getUserClientRoles(userId); const hubAdminSupportRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT]; return userRoles.some(role => hubAdminSupportRoles.includes(role.name as UserRole) ); } catch (error) { this.logger.error(`Error checking Hub Admin/Support status for user ${userId}:`, error); return false; } } /** * Récupère les utilisateurs marchands selon les permissions de l'utilisateur * - Hub Admin/Support: tous les utilisateurs marchands de tous les merchants * - Autres: seulement les utilisateurs de leur propre merchant */ async getMyMerchantUsers(userId: string): Promise { try { // Vérifier si l'utilisateur est un admin ou support Hub const isHubAdminOrSupport = await this.isUserHubAdminOrSupport(userId) if (isHubAdminOrSupport) { // Hub Admin/Support peuvent voir TOUS les utilisateurs marchands return await this.getAllMerchantUsersForHubAdmin(userId); } // Pour les autres utilisateurs (DCB_PARTNER, DCB_PARTNER_ADMIN, etc.) return await this.getMerchantUsersForRegularUser(userId); } catch (error) { this.logger.error(`Error in getMyMerchantUsers for user ${userId}:`, error); throw error; } } /** * Récupère TOUS les utilisateurs marchands pour les Hub Admin/Support */ private async getAllMerchantUsersForHubAdmin(adminUserId: string): Promise { this.logger.log(`Hub Admin/Support ${adminUserId} accessing ALL merchant users`); // Valider que l'utilisateur a bien les droits Hub await this.validateHubUserAccess(adminUserId); // Récupérer tous les utilisateurs du système const allUsers = await this.keycloakApi.getAllUsers(); const merchantUsers: User[] = []; // Filtrer pour ne garder que les utilisateurs marchands for (const user of allUsers) { if (!user.id) continue; try { const userRoles = await this.keycloakApi.getUserClientRoles(user.id); // Vérifier si l'utilisateur a un rôle marchand const hasMerchantRole = userRoles.some(role => [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT] .includes(role.name as UserRole) ); if (hasMerchantRole) { const mappedUser = this.mapToUser(user, userRoles); merchantUsers.push(mappedUser); } } catch (error) { this.logger.warn(`Could not process user ${user.id} for hub admin view: ${error.message}`); continue; } } this.logger.log(`Hub Admin/Support retrieved ${merchantUsers.length} merchant users from all merchants`); return merchantUsers; } /** * Récupère les utilisateurs marchands pour les utilisateurs réguliers (non Hub Admin/Support) */ private async getMerchantUsersForRegularUser(userId: string): Promise { // Récupérer le merchantPartnerId de l'utilisateur const userMerchantId = await this.getUserMerchantPartnerId(userId); if (!userMerchantId) { throw new BadRequestException('Current user is not associated with a merchant partner'); } this.logger.log(`User ${userId} accessing merchant users for partner ${userMerchantId}`); // Utiliser la méthode existante pour récupérer les utilisateurs du merchant spécifique const users = await this.getMerchantUsersByPartner(userMerchantId, userId); this.logger.log(`User ${userId} retrieved ${users.length} merchant users for partner ${userMerchantId}`); return users; } async getMerchantUsersByPartner(merchantPartnerId: string, requesterId: string): Promise { await this.keycloakApi.validateUserAccess(requesterId, merchantPartnerId); const allUsers = await this.keycloakApi.getAllUsers(); const merchantUsers = allUsers.filter(user => user.attributes?.merchantPartnerId?.[0] === merchantPartnerId ); return this.processUsersByType(merchantUsers, UserType.MERCHANT_PARTNER); } async getMerchantUserById(userId: string, requesterId: string): Promise { return this.getValidatedUser(userId, requesterId, UserType.MERCHANT_PARTNER); } async createMerchantUser(creatorId: string, userData: CreateUserData): Promise { this.validateUserCreationData(userData, UserType.MERCHANT_PARTNER); await this.validateMerchantUserCreation(creatorId, userData); await this.validateUserUniqueness(userData.username, userData.email); const keycloakUserData = this.buildKeycloakUserData(userData, userData.merchantPartnerId!); const userId = await this.keycloakApi.createUser(creatorId, keycloakUserData); this.logger.log(`Merchant user created: ${userData.username}`); return this.getMerchantUserById(userId, creatorId); } async updateMerchantUser( userId: string, updates: Partial>, requesterId: string ): Promise { await this.executeWithValidation(userId, requesterId, UserType.MERCHANT_PARTNER, async () => { await this.keycloakApi.updateUser(userId, updates, requesterId); }); return this.getMerchantUserById(userId, requesterId); } async updateMerchantUserRole(userId: string, newRole: UserRole, requesterId: string): Promise { await this.validateRoleChangePermission(requesterId); await this.executeWithValidation(userId, requesterId, UserType.MERCHANT_PARTNER, async () => { await this.keycloakApi.setClientRoles(userId, [newRole]); }); return this.getMerchantUserById(userId, requesterId); } async deleteMerchantUser(userId: string, requesterId: string): Promise { await this.validateSelfDeletion(userId, requesterId); await this.executeWithValidation(userId, requesterId, UserType.MERCHANT_PARTNER, async () => { await this.keycloakApi.deleteUser(userId, requesterId); }); this.logger.log(`Merchant user deleted: ${userId} by ${requesterId}`); } // === COMMON OPERATIONS === async resetUserPassword( userId: string, newPassword: string, temporary: boolean = true, requesterId: string ): Promise { await this.ensureUserExists(userId, requesterId); await this.keycloakApi.resetUserPassword(userId, newPassword, temporary); this.logger.log(`Password reset for user: ${userId}`); } async getUserMerchantPartnerId(userId: string): Promise { return this.keycloakApi.getUserMerchantPartnerId(userId); } // === PRIVATE CORE METHODS === private async getValidatedUser( userId: string, requesterId: string, userType: UserType.HUB | UserType.MERCHANT_PARTNER ): Promise { const [user, userRoles] = await Promise.all([ this.keycloakApi.getUserById(userId, requesterId), this.keycloakApi.getUserClientRoles(userId) ]); this.validateUserType(userRoles, userType, userId); return this.mapToUser(user, userRoles); } private async processUsersByType(users: KeycloakUser[], userType: UserType.HUB | UserType.MERCHANT_PARTNER): Promise { const result: User[] = []; for (const user of users) { if (!user.id) continue; try { const userRoles = await this.keycloakApi.getUserClientRoles(user.id); if (this.isUserType(userRoles, userType)) { result.push(this.mapToUser(user, userRoles)); } } catch (error) { this.logger.warn(`Could not process user ${user.id}: ${error.message}`); } } return result; } private async executeWithValidation( userId: string, requesterId: string, userType: UserType.HUB | UserType.MERCHANT_PARTNER, operation: () => Promise ): Promise { await this.getValidatedUser(userId, requesterId, userType); await operation(); } // === USER MAPPING AND VALIDATION === private mapToUser(user: KeycloakUser, roles: KeycloakRole[]): User { if (!user.id || !user.email) { throw new Error('User ID and email are required'); } const role = this.determineUserRole(roles); const userType = this.determineUserType(roles); return { id: user.id, username: user.username, email: user.email, firstName: user.firstName || '', lastName: user.lastName || '', role, enabled: user.enabled, emailVerified: user.emailVerified, merchantPartnerId: user.attributes?.merchantPartnerId?.[0], createdBy: user.attributes?.createdBy?.[0] || 'unknown', createdByUsername: user.attributes?.createdByUsername?.[0] || 'unknown', createdTimestamp: user.createdTimestamp || Date.now(), lastLogin: this.parseTimestamp(user.attributes?.lastLogin), userType, }; } private determineUserRole(roles: KeycloakRole[]): UserRole { const allRoles = [...SECURITY_CONFIG.ROLES.HUB, ...SECURITY_CONFIG.ROLES.MERCHANT]; const userRole = roles.find(role => allRoles.includes(role.name as UserRole)); if (!userRole) { throw new Error('No valid role found for user'); } return userRole.name as UserRole; } private determineUserType(roles: KeycloakRole[]): UserType { return roles.some(role => SECURITY_CONFIG.ROLES.HUB.includes(role.name as UserRole)) ? UserType.HUB : UserType.MERCHANT_PARTNER; } private isUserType(roles: KeycloakRole[], userType: UserType.HUB | UserType.MERCHANT_PARTNER): boolean { const targetRoles = userType === UserType.HUB ? SECURITY_CONFIG.ROLES.HUB : SECURITY_CONFIG.ROLES.MERCHANT; return roles.some(role => targetRoles.includes(role.name as UserRole)); } private validateUserType(roles: KeycloakRole[], expectedType: UserType.HUB | UserType.MERCHANT_PARTNER, userId: string): void { if (!this.isUserType(roles, expectedType)) { throw new BadRequestException(`User ${userId} is not a ${expectedType.toLowerCase()} user`); } } // === VALIDATION METHODS === private async validateHubUserAccess(requesterId: string): Promise { const requesterRoles = await this.keycloakApi.getUserClientRoles(requesterId); const hasHubAccess = requesterRoles.some(role => SECURITY_CONFIG.ROLES.HUB.includes(role.name as UserRole) ); if (!hasHubAccess) { throw new ForbiddenException('Only hub administrators can manage hub users'); } } private validateUserCreationData(userData: CreateUserData, userType: UserType.HUB | UserType.MERCHANT_PARTNER): void { // 🔍 DEBUG COMPLET this.logger.debug('🔍 === VALIDATION USER CREATION DATA ==='); this.logger.debug('UserType:', userType); this.logger.debug('UserData complet:', JSON.stringify(userData, null, 2)); this.logger.debug('userData.role:', userData.role); this.logger.debug('Type de role:', typeof userData.role); this.logger.debug('Est un tableau?:', Array.isArray(userData.role)); this.logger.debug('Valeur brute role:', userData.role); // Afficher les rôles valides configurés const validRoles = userType === UserType.HUB ? SECURITY_CONFIG.ROLES.HUB : SECURITY_CONFIG.ROLES.MERCHANT; this.logger.debug('Rôles valides pour', userType, ':', validRoles); this.logger.debug('merchantPartnerId:', userData.merchantPartnerId); this.logger.debug('===================================='); // Validation des rôles if (!validRoles.includes(userData.role)) { console.error(`❌ Rôle invalide: ${userData.role} pour le type ${userType}`); console.error(`Rôles autorisés: ${validRoles.join(', ')}`); throw new BadRequestException(`Invalid ${userType.toLowerCase()} role: ${userData.role}`); } // Validation merchantPartnerId pour HUB if (userType === UserType.HUB && userData.merchantPartnerId) { console.error('❌ merchantPartnerId fourni pour un utilisateur HUB'); throw new BadRequestException('merchantPartnerId should not be provided for hub users'); } // Validation merchantPartnerId pour MERCHANT // Vérifier d'abord si role est un tableau ou une valeur simple const isDCBPartner = Array.isArray(userData.role) ? userData.role.includes(UserRole.DCB_PARTNER) : userData.role === UserRole.DCB_PARTNER; this.logger.debug('Est DCB_PARTNER?:', isDCBPartner); if (userType === UserType.MERCHANT_PARTNER && !userData.merchantPartnerId && !isDCBPartner) { console.error('❌ merchantPartnerId manquant pour un utilisateur MERCHANT'); throw new BadRequestException('merchantPartnerId is required for merchant users'); } this.logger.debug('✅ Validation réussie'); } private async validateUserUniqueness(username: string, email: string): Promise { const [existingUsers, existingEmails] = await Promise.all([ this.keycloakApi.findUserByUsername(username), this.keycloakApi.findUserByEmail(email) ]); if (existingUsers.length > 0) { throw new BadRequestException(`User with username ${username} already exists`); } if (existingEmails.length > 0) { throw new BadRequestException(`User with email ${email} already exists`); } } private async validateRoleChangePermission(requesterId: string): Promise { const requesterRoles = await this.keycloakApi.getUserClientRoles(requesterId); const isRequesterAdmin = requesterRoles.some(role => role.name === UserRole.DCB_ADMIN || UserRole.DCB_PARTNER || UserRole.DCB_PARTNER_ADMIN); if (!isRequesterAdmin) { throw new ForbiddenException('Only DCB_ADMIN can change user roles'); } } private async validateSelfDeletion(userId: string, requesterId: string): Promise { if (userId === requesterId) { throw new BadRequestException('Cannot delete your own account'); } } private async ensureUserExists(userId: string, requesterId: string): Promise { try { await this.getHubUserById(userId, requesterId); } catch { try { await this.getMerchantUserById(userId, requesterId); } catch { throw new NotFoundException(`User ${userId} not found`); } } } private buildUserProfile( userId: string, tokenUser: any, userDetails: KeycloakUser, userRoles: KeycloakRole[] ) { return { id: userId, username: tokenUser.preferred_username, email: tokenUser.email, firstName: tokenUser.given_name, lastName: tokenUser.family_name, emailVerified: tokenUser.email_verified, enabled: userDetails.enabled, clientRoles: userRoles.map(role => role.name), merchantPartnerId: userDetails.attributes?.merchantPartnerId?.[0], createdBy: userDetails.attributes?.createdBy?.[0], createdByUsername: userDetails.attributes?.createdByUsername?.[0] }; } private buildKeycloakUserData( userData: CreateUserData, merchantPartnerId?: string ): KeycloakCreateUserData { return { username: userData.username, email: userData.email, firstName: userData.firstName, lastName: userData.lastName, password: userData.password, enabled: userData.enabled ?? true, emailVerified: userData.emailVerified ?? false, merchantPartnerId, clientRoles: [userData.role] }; } private parseTimestamp(value: string[] | undefined): number | undefined { const strValue = value?.[0]; 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 async validateMerchantUserCreation(creatorId: string, userData: CreateUserData): Promise { const creatorRoles = await this.keycloakApi.getUserClientRoles(creatorId); const creationRules = this.getMerchantCreationRules(); for (const rule of creationRules) { if (creatorRoles.some(role => role.name === rule.role)) { await rule.validator(creatorId, userData); return; } } // Vérifier les permissions des administrateurs Hub if (creatorRoles.some(role => [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT].includes(role.name as UserRole))) { return; } throw new ForbiddenException('Insufficient permissions to create merchant users'); } private getMerchantCreationRules() { return [ { role: UserRole.DCB_PARTNER, validator: async (creatorId: string, userData: CreateUserData) => { if (creatorId !== userData.merchantPartnerId) { throw new ForbiddenException('DCB_PARTNER can only create users for their own merchant'); } const allowedRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT]; if (!allowedRoles.includes(userData.role)) { throw new ForbiddenException('DCB_PARTNER can only create MANAGER and SUPPORT roles'); } } }, { role: UserRole.DCB_PARTNER_ADMIN, validator: async (creatorId: string, userData: CreateUserData) => { const creatorMerchantId = await this.keycloakApi.getUserMerchantPartnerId(creatorId); if (creatorMerchantId !== userData.merchantPartnerId) { throw new ForbiddenException('DCB_PARTNER_ADMIN can only create users for their own merchant partner'); } const allowedRoles = [UserRole.DCB_PARTNER_SUPPORT]; if (!allowedRoles.includes(userData.role)) { throw new ForbiddenException('DCB_PARTNER_ADMIN can only create SUPPORT roles'); } } } ]; } }