import { Injectable, Logger, NotFoundException, BadRequestException, ForbiddenException, } from '@nestjs/common'; import { KeycloakApiService } from '../../auth/services/keycloak-api.service'; import { UsersService } from './users.service'; import { CreateMerchantUserDto, UpdateMerchantUserDto, MerchantUserResponse, PaginatedMerchantUserResponse, ResetPasswordDto, MerchantUserQueryDto, LoginDto, TokenResponse, MerchantRole } from '../models/merchant'; @Injectable() export class MerchantTeamService { private readonly logger = new Logger(MerchantTeamService.name); // Rôles spécifiques aux équipes de marchands private readonly availableMerchantRoles = [ 'merchant-admin', 'merchant-manager', 'merchant-support', 'merchant-user' ]; constructor( private readonly keycloakApi: KeycloakApiService, private readonly usersService: UsersService, ) {} // === VALIDATION DES ROLES MARCHAND === private validateMerchantRole(role: string): MerchantRole { const validRoles: MerchantRole[] = ['merchant-admin', 'merchant-manager', 'merchant-support']; if (!validRoles.includes(role as MerchantRole)) { throw new BadRequestException(`Invalid client role: ${role}. Valid roles are: ${validRoles.join(', ')}`); } return role as MerchantRole; } private validateMerchantRoles(roles: string[]): MerchantRole[] { return roles.map(role => this.validateMerchantRole(role)); } // === VERIFICATION DES DROITS D'ACCÈS === private async validateMerchantAccess(targetUserId: string | null, requestingUserId: string): Promise<{ success: boolean; error?: string }> { try { const requestingUserRoles = await this.usersService.getUserClientRoles(requestingUserId); // Les admins peuvent tout faire if (requestingUserRoles.includes('admin')) { return { success: true }; } // Vérifier que le demandeur est un marchand if (!requestingUserRoles.includes('merchant')) { return { success: false, error: 'Access denied: Merchant role required' }; } // Pour les opérations sur un utilisateur spécifique if (targetUserId) { const targetOwnerId = await this.usersService.getMerchantOwner(targetUserId); // Si l'utilisateur cible n'a pas de merchantOwnerId, c'est un marchand principal if (!targetOwnerId) { return { success: false, error: 'Access denied: Cannot modify principal merchant accounts' }; } // Vérifier que le demandeur est bien le propriétaire if (targetOwnerId !== requestingUserId) { return { success: false, error: 'Access denied: Can only manage your own team members' }; } } return { success: true }; } catch (error: any) { return { success: false, error: `Failed to validate access: ${error.message}` }; } } // === CREATION D'UTILISATEUR MARCHAND === async createMerchantUser( merchantData: CreateMerchantUserDto, createdByUserId: string ): Promise { try { await this.validateMerchantAccess(null, createdByUserId); const validatedMerchantRoles = this.validateMerchantRoles(merchantData.merchantRoles); const userData = { username: merchantData.username, email: merchantData.email, firstName: merchantData.firstName, lastName: merchantData.lastName, password: merchantData.password, enabled: merchantData.enabled ?? true, clientRoles: ['merchant', ...validatedMerchantRoles] as any, attributes: { merchantOwnerId: [createdByUserId] } }; const createdUser = await this.usersService.createUser(userData); return { id: createdUser.id, username: createdUser.username, email: createdUser.email, firstName: createdUser.firstName, lastName: createdUser.lastName, merchantRoles: validatedMerchantRoles, emailVerified: true, enabled: createdUser.enabled, createdTimestamp: createdUser.createdTimestamp, merchantOwnerId: createdByUserId }; } catch (error: any) { this.logger.error(`Failed to create merchant user: ${error.message}`); throw error; } } // === OBTENIR L'EQUIPE DU MARCHAND === async getMerchantTeam(merchantUserId: string): Promise { try { // Vérifier les droits await this.validateMerchantAccess(null, merchantUserId); // Obtenir tous les utilisateurs const allUsers = await this.usersService.findAllUsers({ limit: 1000, page: 1 }); const teamMembers = await Promise.all( allUsers.users.map(async (user) => { try { const roles = await this.usersService.getUserClientRoles(user.id); const merchantRoles = roles.filter(role => role.startsWith('merchant-') && role !== 'merchant' ); // Récupérer l'owner ID const merchantOwnerId = user.attributes?.merchantOwnerId?.[0]; // Filtrer : ne retourner que les utilisateurs de CE marchand if (merchantRoles.length > 0 && merchantOwnerId === merchantUserId) { return { id: user.id, username: user.username, email: user.email, firstName: user.firstName, lastName: user.lastName, merchantRoles, enabled: user.enabled, createdTimestamp: user.createdTimestamp, merchantOwnerId }; } return null; } catch (error) { this.logger.warn(`Failed to process user ${user.id}: ${error.message}`); return null; } }) ); return teamMembers.filter((member): member is MerchantUserResponse => member !== null); } catch (error: any) { this.logger.error(`Failed to fetch merchant team: ${error.message}`); throw new BadRequestException('Failed to fetch merchant team'); } } // === METTRE A JOUR UN MEMBRE === async updateMerchantUser( userId: string, updateData: UpdateMerchantUserDto, updatedByUserId: string ): Promise { try { // Vérifier les droits await this.validateMerchantAccess(userId, updatedByUserId); // Préparer les données de mise à jour const updatePayload: any = {}; if (updateData.firstName) updatePayload.firstName = updateData.firstName; if (updateData.lastName) updatePayload.lastName = updateData.lastName; if (updateData.email) updatePayload.email = updateData.email; if (updateData.enabled !== undefined) updatePayload.enabled = updateData.enabled; // Mettre à jour les rôles si fournis if (updateData.merchantRoles) { const validatedRoles = this.validateMerchantRoles(updateData.merchantRoles); const finalRoles = ['merchant', ...validatedRoles]; await this.usersService.assignClientRoles(userId, finalRoles); } // Mettre à jour les autres informations if (Object.keys(updatePayload).length > 0) { await this.usersService.updateUser(userId, updatePayload); } // Récupérer l'utilisateur mis à jour const updatedUser = await this.usersService.getUserById(userId); const userRoles = await this.usersService.getUserClientRoles(userId); const merchantRoles = userRoles.filter(role => role.startsWith('merchant-') && role !== 'merchant' ); const merchantOwnerId = await this.usersService.getMerchantOwner(userId); return { id: updatedUser.id, username: updatedUser.username, email: updatedUser.email, firstName: updatedUser.firstName, lastName: updatedUser.lastName, merchantRoles, enabled: updatedUser.enabled, emailVerified: true, createdTimestamp: updatedUser.createdTimestamp, merchantOwnerId: merchantOwnerId ?? '', }; } catch (error: any) { this.logger.error(`Failed to update merchant user ${userId}: ${error.message}`); throw error; } } // === SUPPRIMER UN MEMBRE (retirer les rôles marchands) === async removeMerchantUser(userId: string, removedByUserId: string): Promise { try { await this.validateMerchantAccess(userId, removedByUserId); // Retirer les rôles marchands mais garder l'utilisateur const userRoles = await this.usersService.getUserClientRoles(userId); const nonMerchantRoles = userRoles.filter(role => !role.startsWith('merchant-') && role !== 'merchant' ); await this.usersService.assignClientRoles(userId, nonMerchantRoles); // Optionnel: supprimer l'attribut merchantOwnerId await this.keycloakApi.setUserAttributes(userId, { merchantOwnerId: [] }); } catch (error: any) { this.logger.error(`Failed to remove merchant user ${userId}: ${error.message}`); throw error; } } // === OBTENIR LES ROLES DISPONIBLES === getAvailableMerchantRoles(): string[] { return [...this.availableMerchantRoles]; } // === AJOUTER UN ROLE === async addMerchantRole( userId: string, role: string, addedByUserId: string ): Promise<{ message: string }> { try { await this.validateMerchantAccess(userId, addedByUserId); const validatedRole = this.validateMerchantRole(role); const currentRoles = await this.usersService.getUserClientRoles(userId); if (!currentRoles.includes('merchant')) { currentRoles.push('merchant'); } if (!currentRoles.includes(validatedRole)) { currentRoles.push(validatedRole); await this.usersService.assignClientRoles(userId, currentRoles as any); // 🔥 CAST ICI } return { message: `Role ${validatedRole} added successfully` }; } catch (error: any) { this.logger.error(`Failed to add merchant role to user ${userId}: ${error.message}`); throw error; } } // === RETIRER UN ROLE === async removeMerchantRole( userId: string, role: string, removedByUserId: string ): Promise<{ message: string }> { try { await this.validateMerchantAccess(userId, removedByUserId); const currentRoles = await this.usersService.getUserClientRoles(userId); const updatedRoles = currentRoles.filter(r => r !== role); const hasOtherMerchantRoles = updatedRoles.some(r => r.startsWith('merchant-') && r !== 'merchant' ); if (!hasOtherMerchantRoles) { const finalRoles = updatedRoles.filter(r => r !== 'merchant'); await this.usersService.assignClientRoles(userId, finalRoles as any); // 🔥 CAST ICI } else { await this.usersService.assignClientRoles(userId, updatedRoles as any); // 🔥 CAST ICI } return { message: `Role ${role} removed successfully` }; } catch (error: any) { this.logger.error(`Failed to remove merchant role from user ${userId}: ${error.message}`); throw error; } } // === VERIFIER SI UN UTILISATEUR EST DANS L'EQUIPE === async isUserInMerchantTeam(userId: string, merchantUserId: string): Promise { try { const merchantOwnerId = await this.usersService.getMerchantOwner(userId); return merchantOwnerId === merchantUserId; } catch (error) { this.logger.warn(`Failed to check if user ${userId} is in merchant ${merchantUserId} team: ${error.message}`); return false; } } }