feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature
This commit is contained in:
parent
ad751d96bd
commit
389488bf28
@ -50,7 +50,7 @@ export class StartupService implements OnModuleInit {
|
||||
|
||||
async onModuleInit() {
|
||||
if (process.env.RUN_STARTUP_TESTS === 'true') {
|
||||
this.logger.log('🚀 Starting comprehensive tests (Hub + Merchants with isolation)...');
|
||||
this.logger.log('Starting comprehensive tests (Hub + Merchants with isolation)...');
|
||||
await this.runAllTests();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,298 +0,0 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
Request,
|
||||
HttpStatus,
|
||||
HttpCode,
|
||||
Logger,
|
||||
DefaultValuePipe,
|
||||
ParseIntPipe,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { AuthenticatedUser, Resource, Scopes } from "nest-keycloak-connect";
|
||||
|
||||
import {
|
||||
MerchantPartnersService,
|
||||
MerchantPartner,
|
||||
CreateMerchantPartnerData,
|
||||
MerchantStats,
|
||||
} from '../services/merchant-partners.service';
|
||||
import { RESOURCES } from '../../constants/resources';
|
||||
import { SCOPES } from '../../constants/scopes';
|
||||
|
||||
// DTOs
|
||||
import {
|
||||
CreateMerchantPartnerDto,
|
||||
UpdateMerchantPartnerDto,
|
||||
SuspendMerchantPartnerDto,
|
||||
} from '../dto/merchant-partners.dto';
|
||||
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data?: T;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T = any> {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
@Controller('partners')
|
||||
@Resource(RESOURCES.MERCHANT_USER)
|
||||
export class MerchantPartnersController {
|
||||
private readonly logger = new Logger(MerchantPartnersController.name);
|
||||
|
||||
constructor(private readonly merchantPartnersService: MerchantPartnersService) {}
|
||||
|
||||
private createApiResponse<T>(
|
||||
success: boolean,
|
||||
message: string,
|
||||
data?: T,
|
||||
): ApiResponse<T> {
|
||||
return {
|
||||
success,
|
||||
message,
|
||||
data,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// ===== CRÉATION DE MERCHANT PARTNERS =====
|
||||
@Post()
|
||||
@Scopes(SCOPES.WRITE)
|
||||
async createMerchantPartner(
|
||||
@Body() createMerchantDto: CreateMerchantPartnerDto,
|
||||
@Request() req: any,
|
||||
): Promise<ApiResponse<MerchantPartner>> {
|
||||
this.logger.log(`Creating merchant partner: ${createMerchantDto.name}`);
|
||||
|
||||
const creatorId = req.user.sub;
|
||||
|
||||
const merchantData: CreateMerchantPartnerData = {
|
||||
...createMerchantDto,
|
||||
dcbPartnerOwner: {
|
||||
username: createMerchantDto.dcbPartnerOwnerUsername,
|
||||
email: createMerchantDto.dcbPartnerOwnerEmail,
|
||||
firstName: createMerchantDto.dcbPartnerOwnerFirstName,
|
||||
lastName: createMerchantDto.dcbPartnerOwnerLastName,
|
||||
password: createMerchantDto.dcbPartnerOwnerPassword,
|
||||
},
|
||||
};
|
||||
|
||||
const merchant = await this.merchantPartnersService.createMerchantPartner(creatorId, merchantData);
|
||||
|
||||
this.logger.log(`Merchant partner created successfully: ${merchant.name} (ID: ${merchant.id})`);
|
||||
|
||||
return this.createApiResponse(
|
||||
true,
|
||||
'Merchant partner created successfully',
|
||||
merchant,
|
||||
);
|
||||
}
|
||||
|
||||
// ===== RÉCUPÉRATION DE MERCHANT PARTNERS =====
|
||||
@Get()
|
||||
@Scopes(SCOPES.READ)
|
||||
async getAllMerchantPartners(
|
||||
@Request() req: any,
|
||||
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
|
||||
@Query('limit', new DefaultValuePipe(50), ParseIntPipe) limit: number,
|
||||
@Query('status') status?: string,
|
||||
): Promise<ApiResponse<PaginatedResponse<MerchantPartner>>> {
|
||||
const requesterId = req.user.sub;
|
||||
|
||||
const safeLimit = Math.min(limit, 100);
|
||||
const safePage = Math.max(page, 1);
|
||||
|
||||
let merchants = await this.merchantPartnersService.getAllMerchantPartners(requesterId);
|
||||
|
||||
// Filtrage par statut
|
||||
if (status && ['ACTIVE', 'SUSPENDED', 'PENDING'].includes(status)) {
|
||||
merchants = merchants.filter(merchant => merchant.status === status);
|
||||
}
|
||||
|
||||
// Pagination
|
||||
const startIndex = (safePage - 1) * safeLimit;
|
||||
const endIndex = startIndex + safeLimit;
|
||||
const paginatedMerchants = merchants.slice(startIndex, endIndex);
|
||||
|
||||
const response: PaginatedResponse<MerchantPartner> = {
|
||||
items: paginatedMerchants,
|
||||
total: merchants.length,
|
||||
page: safePage,
|
||||
limit: safeLimit,
|
||||
totalPages: Math.ceil(merchants.length / safeLimit),
|
||||
};
|
||||
|
||||
return this.createApiResponse(
|
||||
true,
|
||||
'Merchant partners retrieved successfully',
|
||||
response,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('stats')
|
||||
@Scopes(SCOPES.READ)
|
||||
async getMerchantStats(
|
||||
@Request() req: any,
|
||||
): Promise<ApiResponse<MerchantStats>> {
|
||||
const requesterId = req.user.sub;
|
||||
const stats = await this.merchantPartnersService.getMerchantStats(requesterId);
|
||||
|
||||
return this.createApiResponse(
|
||||
true,
|
||||
'Merchant statistics retrieved successfully',
|
||||
stats,
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Scopes(SCOPES.READ)
|
||||
async getMerchantPartnerById(
|
||||
@Param('id') merchantId: string,
|
||||
@Request() req: any,
|
||||
): Promise<ApiResponse<MerchantPartner>> {
|
||||
const requesterId = req.user.sub;
|
||||
const merchant = await this.merchantPartnersService.getMerchantPartnerById(merchantId, requesterId);
|
||||
|
||||
return this.createApiResponse(
|
||||
true,
|
||||
'Merchant partner retrieved successfully',
|
||||
merchant,
|
||||
);
|
||||
}
|
||||
|
||||
// ===== MISE À JOUR DE MERCHANT PARTNERS =====
|
||||
@Put(':id')
|
||||
@Scopes(SCOPES.WRITE)
|
||||
async updateMerchantPartner(
|
||||
@Param('id') merchantId: string,
|
||||
@Body() updateMerchantDto: UpdateMerchantPartnerDto,
|
||||
@Request() req: any,
|
||||
): Promise<ApiResponse<MerchantPartner>> {
|
||||
this.logger.log(`Updating merchant partner: ${merchantId}`);
|
||||
|
||||
const requesterId = req.user.sub;
|
||||
const merchant = await this.merchantPartnersService.updateMerchantPartner(
|
||||
merchantId,
|
||||
updateMerchantDto,
|
||||
requesterId,
|
||||
);
|
||||
|
||||
this.logger.log(`Merchant partner updated successfully: ${merchant.name}`);
|
||||
|
||||
return this.createApiResponse(
|
||||
true,
|
||||
'Merchant partner updated successfully',
|
||||
merchant,
|
||||
);
|
||||
}
|
||||
|
||||
@Put(':id/suspend')
|
||||
@Scopes(SCOPES.WRITE)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async suspendMerchantPartner(
|
||||
@Param('id') merchantId: string,
|
||||
@Body() suspendDto: SuspendMerchantPartnerDto,
|
||||
@Request() req: any,
|
||||
): Promise<ApiResponse<void>> {
|
||||
this.logger.log(`Suspending merchant partner: ${merchantId}`);
|
||||
|
||||
const requesterId = req.user.sub;
|
||||
await this.merchantPartnersService.suspendMerchantPartner(
|
||||
merchantId,
|
||||
suspendDto.reason,
|
||||
requesterId,
|
||||
);
|
||||
|
||||
this.logger.log(`Merchant partner suspended successfully: ${merchantId}`);
|
||||
|
||||
return this.createApiResponse(
|
||||
true,
|
||||
'Merchant partner suspended successfully',
|
||||
);
|
||||
}
|
||||
|
||||
@Put(':id/activate')
|
||||
@Scopes(SCOPES.WRITE)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async activateMerchantPartner(
|
||||
@Param('id') merchantId: string,
|
||||
@Request() req: any,
|
||||
): Promise<ApiResponse<MerchantPartner>> {
|
||||
this.logger.log(`Activating merchant partner: ${merchantId}`);
|
||||
|
||||
const requesterId = req.user.sub;
|
||||
const merchant = await this.merchantPartnersService.updateMerchantPartner(
|
||||
merchantId,
|
||||
{ status: 'ACTIVE' },
|
||||
requesterId,
|
||||
);
|
||||
|
||||
this.logger.log(`Merchant partner activated successfully: ${merchant.name}`);
|
||||
|
||||
return this.createApiResponse(
|
||||
true,
|
||||
'Merchant partner activated successfully',
|
||||
merchant,
|
||||
);
|
||||
}
|
||||
|
||||
// ===== SUPPRESSION DE MERCHANT PARTNERS =====
|
||||
@Delete(':id')
|
||||
@Scopes(SCOPES.DELETE)
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async deleteMerchantPartner(
|
||||
@Param('id') merchantId: string,
|
||||
@Request() req: any,
|
||||
): Promise<void> {
|
||||
this.logger.log(`Deleting merchant partner: ${merchantId}`);
|
||||
|
||||
const requesterId = req.user.sub;
|
||||
// Implémentez la logique de suppression si nécessaire
|
||||
// await this.merchantPartnersService.deleteMerchantPartner(merchantId, requesterId);
|
||||
|
||||
this.logger.log(`Merchant partner deletion scheduled: ${merchantId}`);
|
||||
}
|
||||
|
||||
// ===== RECHERCHE ET FILTRES =====
|
||||
@Get('search/by-name')
|
||||
@Scopes(SCOPES.READ)
|
||||
async searchMerchantsByName(
|
||||
@Query('name') name: string,
|
||||
@Request() req: any,
|
||||
): Promise<ApiResponse<MerchantPartner[]>> {
|
||||
if (!name || name.length < 2) {
|
||||
throw new BadRequestException('Name must be at least 2 characters long');
|
||||
}
|
||||
|
||||
this.logger.log(`Searching merchants by name: ${name}`);
|
||||
|
||||
const requesterId = req.user.sub;
|
||||
const allMerchants = await this.merchantPartnersService.getAllMerchantPartners(requesterId);
|
||||
|
||||
const filteredMerchants = allMerchants.filter(merchant =>
|
||||
merchant.name.toLowerCase().includes(name.toLowerCase()),
|
||||
);
|
||||
|
||||
this.logger.log(`Found ${filteredMerchants.length} merchants matching name: ${name}`);
|
||||
|
||||
return this.createApiResponse(
|
||||
true,
|
||||
'Merchants search completed successfully',
|
||||
filteredMerchants,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,96 +0,0 @@
|
||||
import {
|
||||
IsEmail,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MinLength,
|
||||
Matches,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateMerchantPartnerDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
name: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
legalName: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
address?: string;
|
||||
|
||||
// Propriétaire DCB_PARTNER
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@MinLength(3)
|
||||
dcbPartnerOwnerUsername: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsEmail()
|
||||
dcbPartnerOwnerEmail: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
dcbPartnerOwnerFirstName: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
dcbPartnerOwnerLastName: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/, {
|
||||
message: 'Password must contain at least one uppercase letter, one lowercase letter, one number and one special character',
|
||||
})
|
||||
dcbPartnerOwnerPassword: string;
|
||||
}
|
||||
|
||||
export class UpdateMerchantPartnerDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
legalName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
address?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(['ACTIVE', 'SUSPENDED', 'PENDING'])
|
||||
status?: 'ACTIVE' | 'SUSPENDED' | 'PENDING';
|
||||
}
|
||||
|
||||
export class SuspendMerchantPartnerDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@MinLength(5)
|
||||
reason: string;
|
||||
}
|
||||
@ -151,7 +151,8 @@ export class HubUsersService {
|
||||
|
||||
try {
|
||||
const userRoles = await this.keycloakApi.getUserClientRoles(user.id);
|
||||
const isMerchant = userRoles.some(role => role.name === UserRole.DCB_PARTNER);
|
||||
const isMerchant = userRoles.some(
|
||||
role => role.name === UserRole.DCB_PARTNER || UserRole.DCB_PARTNER_ADMIN || UserRole.DCB_PARTNER_MANAGER || UserRole.DCB_PARTNER_SUPPORT);
|
||||
|
||||
if (isMerchant) {
|
||||
merchants.push(this.mapToHubUser(user, userRoles));
|
||||
|
||||
@ -1,353 +0,0 @@
|
||||
import { Injectable, Logger, BadRequestException, ForbiddenException, NotFoundException } from '@nestjs/common';
|
||||
import { KeycloakApiService } from '../../auth/services/keycloak-api.service';
|
||||
import { CreateUserData, UserRole, KeycloakUser } from '../../auth/services/keycloak-user.model';
|
||||
|
||||
export interface MerchantPartner {
|
||||
id: string;
|
||||
name: string;
|
||||
legalName: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
address?: string;
|
||||
status: 'ACTIVE' | 'SUSPENDED' | 'PENDING';
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
dcbPartnerUserId: string; // ID du user DCB_PARTNER propriétaire
|
||||
}
|
||||
|
||||
export interface CreateMerchantPartnerData {
|
||||
name: string;
|
||||
legalName: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
address?: string;
|
||||
dcbPartnerOwner: {
|
||||
username: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MerchantStats {
|
||||
totalMerchants: number;
|
||||
activeMerchants: number;
|
||||
suspendedMerchants: number;
|
||||
pendingMerchants: number;
|
||||
totalUsers: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MerchantPartnersService {
|
||||
private readonly logger = new Logger(MerchantPartnersService.name);
|
||||
|
||||
constructor(private readonly keycloakApi: KeycloakApiService) {}
|
||||
|
||||
// ===== CRÉATION D'UN MERCHANT PARTNER =====
|
||||
async createMerchantPartner(
|
||||
creatorId: string,
|
||||
merchantData: CreateMerchantPartnerData
|
||||
): Promise<MerchantPartner> {
|
||||
this.logger.log(`Creating merchant partner: ${merchantData.name}`);
|
||||
|
||||
// Validation des permissions du créateur
|
||||
await this.validateMerchantCreationPermissions(creatorId);
|
||||
|
||||
// Vérifier les doublons
|
||||
await this.checkDuplicateMerchant(merchantData);
|
||||
|
||||
// 1. Créer le user DCB_PARTNER (propriétaire)
|
||||
const dcbPartnerUserData: CreateUserData = {
|
||||
username: merchantData.dcbPartnerOwner.username,
|
||||
email: merchantData.dcbPartnerOwner.email,
|
||||
firstName: merchantData.dcbPartnerOwner.firstName,
|
||||
lastName: merchantData.dcbPartnerOwner.lastName,
|
||||
password: merchantData.dcbPartnerOwner.password,
|
||||
enabled: true,
|
||||
emailVerified: true,
|
||||
clientRoles: [UserRole.DCB_PARTNER],
|
||||
createdBy: creatorId,
|
||||
};
|
||||
|
||||
const dcbPartnerUserId = await this.keycloakApi.createUser(creatorId, dcbPartnerUserData);
|
||||
|
||||
// 2. Créer l'entité Merchant Partner (dans la base de données ou Keycloak attributes)
|
||||
const merchantPartner: MerchantPartner = {
|
||||
id: merchantData.dcbPartnerOwner.username,
|
||||
name: merchantData.name,
|
||||
legalName: merchantData.legalName,
|
||||
email: merchantData.email,
|
||||
phone: merchantData.phone,
|
||||
address: merchantData.address,
|
||||
status: 'ACTIVE',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
dcbPartnerUserId,
|
||||
};
|
||||
|
||||
// 3. Stocker les infos du merchant dans les attributs du user DCB_PARTNER
|
||||
await this.keycloakApi.setUserAttributes(dcbPartnerUserId, {
|
||||
merchantPartnerName: [merchantData.name],
|
||||
merchantLegalName: [merchantData.legalName],
|
||||
merchantEmail: [merchantData.email],
|
||||
merchantPhone: [merchantData.phone || ''],
|
||||
merchantAddress: [merchantData.address || ''],
|
||||
merchantStatus: ['ACTIVE'],
|
||||
merchantCreatedAt: [new Date().toISOString()],
|
||||
});
|
||||
|
||||
this.logger.log(`Merchant partner created successfully: ${merchantData.name}`);
|
||||
return merchantPartner;
|
||||
}
|
||||
|
||||
// ===== GESTION DES MERCHANTS =====
|
||||
async getAllMerchantPartners(requesterId: string): Promise<MerchantPartner[]> {
|
||||
await this.validateHubAccess(requesterId);
|
||||
|
||||
// Récupérer tous les users DCB_PARTNER
|
||||
const allUsers = await this.keycloakApi.getAllUsers();
|
||||
const merchants: MerchantPartner[] = [];
|
||||
|
||||
for (const user of allUsers) {
|
||||
if (!user.id) continue;
|
||||
|
||||
try {
|
||||
const userRoles = await this.keycloakApi.getUserClientRoles(user.id);
|
||||
const isDcbPartner = userRoles.some(role => role.name === UserRole.DCB_PARTNER);
|
||||
|
||||
if (isDcbPartner && user.attributes?.merchantPartnerName?.[0]) {
|
||||
merchants.push(this.mapToMerchantPartner(user));
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(`Could not process merchant user ${user.id}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return merchants;
|
||||
}
|
||||
|
||||
async getMerchantPartnerById(merchantId: string, requesterId: string): Promise<MerchantPartner> {
|
||||
await this.validateMerchantAccess(requesterId, merchantId);
|
||||
|
||||
// Trouver le user DCB_PARTNER correspondant au merchant
|
||||
const dcbPartnerUser = await this.findDcbPartnerByMerchantId(merchantId);
|
||||
return this.mapToMerchantPartner(dcbPartnerUser);
|
||||
}
|
||||
|
||||
async updateMerchantPartner(
|
||||
merchantId: string,
|
||||
updates: Partial<{
|
||||
name: string;
|
||||
legalName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
address: string;
|
||||
status: 'ACTIVE' | 'SUSPENDED' | 'PENDING';
|
||||
}>,
|
||||
requesterId: string
|
||||
): Promise<MerchantPartner> {
|
||||
await this.validateMerchantManagementPermissions(requesterId, merchantId);
|
||||
|
||||
const dcbPartnerUser = await this.findDcbPartnerByMerchantId(merchantId);
|
||||
|
||||
// Mettre à jour les attributs
|
||||
const attributes: any = {};
|
||||
if (updates.name) attributes.merchantPartnerName = [updates.name];
|
||||
if (updates.legalName) attributes.merchantLegalName = [updates.legalName];
|
||||
if (updates.email) attributes.merchantEmail = [updates.email];
|
||||
if (updates.phone) attributes.merchantPhone = [updates.phone];
|
||||
if (updates.address) attributes.merchantAddress = [updates.address];
|
||||
if (updates.status) attributes.merchantStatus = [updates.status];
|
||||
|
||||
attributes.merchantUpdatedAt = [new Date().toISOString()];
|
||||
|
||||
await this.keycloakApi.setUserAttributes(dcbPartnerUser.id!, attributes);
|
||||
|
||||
return this.getMerchantPartnerById(merchantId, requesterId);
|
||||
}
|
||||
|
||||
async suspendMerchantPartner(merchantId: string, reason: string, requesterId: string): Promise<void> {
|
||||
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}`);
|
||||
}
|
||||
|
||||
// ===== STATISTIQUES =====
|
||||
async getMerchantStats(requesterId: string): Promise<MerchantStats> {
|
||||
await this.validateHubAccess(requesterId);
|
||||
|
||||
const allMerchants = await this.getAllMerchantPartners(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.status === 'ACTIVE').length,
|
||||
suspendedMerchants: allMerchants.filter(m => m.status === 'SUSPENDED').length,
|
||||
pendingMerchants: allMerchants.filter(m => m.status === 'PENDING').length,
|
||||
totalUsers,
|
||||
};
|
||||
}
|
||||
|
||||
// ===== MÉTHODES PRIVÉES =====
|
||||
private async validateMerchantCreationPermissions(creatorId: string): Promise<void> {
|
||||
const creatorRoles = await this.keycloakApi.getUserClientRoles(creatorId);
|
||||
const canCreateMerchant = creatorRoles.some(role =>
|
||||
[UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT].includes(role.name as UserRole)
|
||||
);
|
||||
|
||||
if (!canCreateMerchant) {
|
||||
throw new ForbiddenException('Only hub administrators can create merchant partners');
|
||||
}
|
||||
}
|
||||
|
||||
private async validateHubAccess(requesterId: string): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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 checkDuplicateMerchant(merchantData: CreateMerchantPartnerData): Promise<void> {
|
||||
const existingUsers = await this.keycloakApi.findUserByUsername(merchantData.dcbPartnerOwner.username);
|
||||
if (existingUsers.length > 0) {
|
||||
throw new BadRequestException(`Merchant partner with username ${merchantData.dcbPartnerOwner.username} already exists`);
|
||||
}
|
||||
|
||||
const existingEmails = await this.keycloakApi.findUserByEmail(merchantData.dcbPartnerOwner.email);
|
||||
if (existingEmails.length > 0) {
|
||||
throw new BadRequestException(`Merchant partner with email ${merchantData.dcbPartnerOwner.email} already exists`);
|
||||
}
|
||||
}
|
||||
|
||||
private async findDcbPartnerByMerchantId(merchantId: string): Promise<KeycloakUser> {
|
||||
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 mapToMerchantPartner(user: KeycloakUser): MerchantPartner {
|
||||
return {
|
||||
id: user.username,
|
||||
name: user.attributes?.merchantPartnerName?.[0] || user.username,
|
||||
legalName: user.attributes?.merchantLegalName?.[0] || '',
|
||||
email: user.attributes?.merchantEmail?.[0] || user.email,
|
||||
phone: user.attributes?.merchantPhone?.[0],
|
||||
address: user.attributes?.merchantAddress?.[0],
|
||||
status: (user.attributes?.merchantStatus?.[0] as 'ACTIVE' | 'SUSPENDED' | 'PENDING') || 'ACTIVE',
|
||||
createdAt: user.attributes?.merchantCreatedAt?.[0]
|
||||
? new Date(user.attributes.merchantCreatedAt[0])
|
||||
: new Date(user.createdTimestamp || Date.now()),
|
||||
updatedAt: user.attributes?.merchantUpdatedAt?.[0]
|
||||
? new Date(user.attributes.merchantUpdatedAt[0])
|
||||
: new Date(user.createdTimestamp || Date.now()),
|
||||
dcbPartnerUserId: user.id!,
|
||||
};
|
||||
}
|
||||
|
||||
private async suspendAllMerchantUsers(merchantId: string, requesterId: string): Promise<void> {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user