352 lines
12 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
} |