dcb-user-service/src/users/services/merchant-team.service.ts

352 lines
12 KiB
TypeScript

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<MerchantUserResponse> {
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<MerchantUserResponse[]> {
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<MerchantUserResponse> {
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<void> {
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<boolean> {
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;
}
}
}