feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature
This commit is contained in:
parent
389488bf28
commit
fefa5aef42
@ -15,7 +15,7 @@ import { TerminusModule } from '@nestjs/terminus';
|
|||||||
import keycloakConfig, { keycloakConfigValidationSchema } from './config/keycloak.config';
|
import keycloakConfig, { keycloakConfigValidationSchema } from './config/keycloak.config';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { HubUsersModule } from './hub-users/hub-users.module';
|
import { HubUsersModule } from './hub-users/hub-users.module';
|
||||||
import { StartupService } from './auth/services/startup.service';
|
import { StartupServiceInitialization } from './auth/services/startup.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -72,7 +72,7 @@ import { StartupService } from './auth/services/startup.service';
|
|||||||
|
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
StartupService,
|
StartupServiceInitialization,
|
||||||
// Global Authentication Guard
|
// Global Authentication Guard
|
||||||
{
|
{
|
||||||
provide: APP_GUARD,
|
provide: APP_GUARD,
|
||||||
|
|||||||
@ -324,17 +324,8 @@ export class AuthController {
|
|||||||
type: ErrorResponseDto
|
type: ErrorResponseDto
|
||||||
})
|
})
|
||||||
async getProfile(@AuthenticatedUser() user: any) {
|
async getProfile(@AuthenticatedUser() user: any) {
|
||||||
this.logger.log(`Profile requested for user: ${user.preferred_username}`);
|
return this.usersService.getCompleteUserProfile(user.sub, user);
|
||||||
|
|
||||||
return {
|
|
||||||
id: user.sub,
|
|
||||||
username: user.preferred_username,
|
|
||||||
email: user.email,
|
|
||||||
firstName: user.given_name,
|
|
||||||
lastName: user.family_name,
|
|
||||||
roles: user.resource_access?.[this.configService.get('KEYCLOAK_CLIENT_ID')]?.roles || [],
|
|
||||||
emailVerified: user.email_verified,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** -------------------------------
|
/** -------------------------------
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -11,8 +11,6 @@ export interface KeycloakUser {
|
|||||||
createdBy?: string[];
|
createdBy?: string[];
|
||||||
createdByUsername?: string[];
|
createdByUsername?: string[];
|
||||||
userType?: string[];
|
userType?: string[];
|
||||||
userStatus?: string[];
|
|
||||||
lastLogin?: string[];
|
|
||||||
[key: string]: string[] | undefined;
|
[key: string]: string[] | undefined;
|
||||||
};
|
};
|
||||||
createdTimestamp?: number;
|
createdTimestamp?: number;
|
||||||
@ -36,16 +34,22 @@ export interface CreateUserData {
|
|||||||
passwordTemporary?: boolean;
|
passwordTemporary?: boolean;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
emailVerified?: boolean;
|
emailVerified?: boolean;
|
||||||
merchantPartnerId?: string;
|
merchantPartnerId?: string | null;
|
||||||
clientRoles: UserRole[];
|
clientRoles: UserRole[];
|
||||||
createdBy?: string;
|
attributes?: {
|
||||||
createdByUsername?: string;
|
userStatus?: string[];
|
||||||
initialStatus?: string;
|
lastLogin?: string[];
|
||||||
|
merchantPartnerId?: string[];
|
||||||
|
createdBy?: string[];
|
||||||
|
createdByUsername?: string[];
|
||||||
|
userType?: string[];
|
||||||
|
[key: string]: string[] | undefined;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum UserType {
|
export enum UserType {
|
||||||
HUB = 'hub',
|
HUB = 'HUB',
|
||||||
MERCHANT_PARTNER = 'merchant_partner'
|
MERCHANT_PARTNER = 'MERCHANT'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum UserRole {
|
export enum UserRole {
|
||||||
@ -72,8 +76,7 @@ export interface HubUser {
|
|||||||
createdBy: string;
|
createdBy: string;
|
||||||
createdByUsername: string;
|
createdByUsername: string;
|
||||||
createdTimestamp: number;
|
createdTimestamp: number;
|
||||||
lastLogin?: number;
|
userType: string;
|
||||||
userType: 'HUB';
|
|
||||||
attributes?: {
|
attributes?: {
|
||||||
userStatus?: string[];
|
userStatus?: string[];
|
||||||
lastLogin?: string[];
|
lastLogin?: string[];
|
||||||
@ -94,32 +97,57 @@ export interface CreateHubUserData {
|
|||||||
role: UserRole.DCB_ADMIN | UserRole.DCB_SUPPORT | UserRole.DCB_PARTNER;
|
role: UserRole.DCB_ADMIN | UserRole.DCB_SUPPORT | UserRole.DCB_PARTNER;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
emailVerified?: boolean;
|
emailVerified?: boolean;
|
||||||
createdBy: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HubUserStats {
|
export interface HubUserStats {
|
||||||
totalAdmins: number;
|
totalAdmins: number;
|
||||||
totalSupport: number;
|
totalSupports: number;
|
||||||
activeUsers: number;
|
activeUsers: number;
|
||||||
inactiveUsers: number;
|
inactiveUsers: number;
|
||||||
pendingActivation: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MerchantStats {
|
export interface MerchantStats {
|
||||||
totalMerchants: number;
|
totalAdmins: number;
|
||||||
activeMerchants: number;
|
totalManagers: number;
|
||||||
suspendedMerchants: number;
|
totalSupports: number;
|
||||||
pendingMerchants: number;
|
activeUsers: number;
|
||||||
totalUsers: number;
|
inactiveUsers: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HubUserActivity {
|
export interface UserQueryDto {
|
||||||
user: HubUser;
|
page?: number;
|
||||||
lastLogin?: Date;
|
limit?: number;
|
||||||
|
search?: string;
|
||||||
|
userType?: UserType;
|
||||||
|
merchantPartnerId?: string;
|
||||||
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HubHealthStatus {
|
export interface UserResponse {
|
||||||
status: 'healthy' | 'degraded' | 'unhealthy';
|
user: KeycloakUser;
|
||||||
issues: string[];
|
message: string;
|
||||||
stats: HubUserStats;
|
}
|
||||||
|
|
||||||
|
export interface PaginatedUsersResponse {
|
||||||
|
users: KeycloakUser[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pour l'authentification
|
||||||
|
export interface LoginDto {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenResponse {
|
||||||
|
access_token: string;
|
||||||
|
refresh_token?: string;
|
||||||
|
expires_in: number;
|
||||||
|
token_type: string;
|
||||||
|
refresh_expires_in?: number;
|
||||||
|
scope?: string;
|
||||||
}
|
}
|
||||||
@ -1,76 +0,0 @@
|
|||||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
|
||||||
import { KeycloakApiService } from './keycloak-api.service';
|
|
||||||
|
|
||||||
interface TestResults {
|
|
||||||
connection: { [key: string]: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class StartupServiceInitialization implements OnModuleInit {
|
|
||||||
private readonly logger = new Logger(StartupServiceInitialization.name);
|
|
||||||
private isInitialized = false;
|
|
||||||
private initializationError: string | null = null;
|
|
||||||
private testResults: TestResults = {
|
|
||||||
connection: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly keycloakApiService: KeycloakApiService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async onModuleInit() {
|
|
||||||
this.logger.log('🚀 Démarrage des tests de connexion');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.validateKeycloakConnection();
|
|
||||||
|
|
||||||
this.isInitialized = true;
|
|
||||||
this.logger.log('✅ Tests de connexion terminés avec succès');
|
|
||||||
} catch (error: any) {
|
|
||||||
this.initializationError = error.message;
|
|
||||||
this.logger.error(`❌ Échec des tests de connexion: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === VALIDATION CONNEXION KEYCLOAK ===
|
|
||||||
private async validateKeycloakConnection() {
|
|
||||||
this.logger.log('🔌 Test de connexion Keycloak...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const isKeycloakAccessible = await this.keycloakApiService.checkKeycloakAvailability();
|
|
||||||
if (!isKeycloakAccessible) {
|
|
||||||
throw new Error('Keycloak inaccessible');
|
|
||||||
}
|
|
||||||
|
|
||||||
const isServiceConnected = await this.keycloakApiService.checkServiceConnection();
|
|
||||||
if (!isServiceConnected) {
|
|
||||||
throw new Error('Connexion service Keycloak échouée');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.testResults.connection.keycloak = 'SUCCESS';
|
|
||||||
this.logger.log('✅ Connexion Keycloak validée');
|
|
||||||
} catch (error: any) {
|
|
||||||
this.testResults.connection.keycloak = 'FAILED';
|
|
||||||
throw new Error(`Connexion Keycloak échouée: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === METHODES STATUT ===
|
|
||||||
getStatus() {
|
|
||||||
return {
|
|
||||||
status: this.isInitialized ? 'healthy' : 'unhealthy',
|
|
||||||
keycloakConnected: this.isInitialized,
|
|
||||||
testResults: this.testResults,
|
|
||||||
timestamp: new Date(),
|
|
||||||
error: this.initializationError,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
isHealthy(): boolean {
|
|
||||||
return this.isInitialized;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTestResults(): TestResults {
|
|
||||||
return this.testResults;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,710 +0,0 @@
|
|||||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
|
||||||
import { HubUsersService} from '../../hub-users/services/hub-users.service';
|
|
||||||
import { MerchantUsersService, CreateMerchantUserData } from '../../hub-users/services/merchant-users.service';
|
|
||||||
import { KeycloakApiService } from '../../auth/services/keycloak-api.service';
|
|
||||||
import { TokenService } from '../../auth/services/token.service';
|
|
||||||
import { UserRole, CreateHubUserData } from '../../auth/services/keycloak-user.model';
|
|
||||||
|
|
||||||
export interface TestResult {
|
|
||||||
testName: string;
|
|
||||||
success: boolean;
|
|
||||||
duration: number;
|
|
||||||
error?: string;
|
|
||||||
data?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StartupTestSummary {
|
|
||||||
totalTests: number;
|
|
||||||
passedTests: number;
|
|
||||||
failedTests: number;
|
|
||||||
totalDuration: number;
|
|
||||||
results: TestResult[];
|
|
||||||
healthStatus?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
type HubUserRole =
|
|
||||||
| UserRole.DCB_ADMIN
|
|
||||||
| UserRole.DCB_SUPPORT
|
|
||||||
| UserRole.DCB_PARTNER;
|
|
||||||
|
|
||||||
type MerchantUserRole =
|
|
||||||
| UserRole.DCB_PARTNER_ADMIN
|
|
||||||
| UserRole.DCB_PARTNER_MANAGER
|
|
||||||
| UserRole.DCB_PARTNER_SUPPORT;
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class StartupServiceFinal implements OnModuleInit {
|
|
||||||
private readonly logger = new Logger(StartupServiceFinal.name);
|
|
||||||
|
|
||||||
// Stockage des données de test
|
|
||||||
private testUsers: { [key: string]: { id: string; username: string; role: UserRole } } = {};
|
|
||||||
private testMerchants: { [key: string]: { id: string; username: string; role: UserRole } } = {};
|
|
||||||
private testMerchantUsers: { [key: string]: { id: string; username: string; role: UserRole; merchantPartnerId: string } } = {};
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly hubUsersService: HubUsersService,
|
|
||||||
private readonly merchantUsersService: MerchantUsersService,
|
|
||||||
private readonly keycloakApi: KeycloakApiService,
|
|
||||||
private readonly tokenService: TokenService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async onModuleInit() {
|
|
||||||
if (process.env.RUN_STARTUP_TESTS === 'true') {
|
|
||||||
this.logger.log('🚀 Starting comprehensive tests (Hub + Merchants with isolation)...');
|
|
||||||
await this.runAllTests();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// 1. Tests de base
|
|
||||||
await this.testKeycloakConnection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== MÉTHODES DE TEST PRINCIPALES =====
|
|
||||||
async runAllTests(): Promise<StartupTestSummary> {
|
|
||||||
const results: TestResult[] = [];
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. Tests de base
|
|
||||||
results.push(await this.testKeycloakConnection());
|
|
||||||
results.push(await this.testServiceAccountPermissions());
|
|
||||||
|
|
||||||
// 2. Tests de création en parallèle avec isolation
|
|
||||||
const parallelTests = await this.runParallelIsolationTests();
|
|
||||||
results.push(...parallelTests);
|
|
||||||
|
|
||||||
// 3. Tests avancés
|
|
||||||
results.push(await this.testStatsAndReports());
|
|
||||||
results.push(await this.testHealthCheck());
|
|
||||||
results.push(await this.testSecurityValidations());
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('Critical error during startup tests:', error);
|
|
||||||
} finally {
|
|
||||||
await this.cleanupTestUsers();
|
|
||||||
await this.cleanupTestMerchants();
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalDuration = Date.now() - startTime;
|
|
||||||
const passedTests = results.filter(r => r.success).length;
|
|
||||||
const failedTests = results.filter(r => !r.success).length;
|
|
||||||
|
|
||||||
const summary: StartupTestSummary = {
|
|
||||||
totalTests: results.length,
|
|
||||||
passedTests,
|
|
||||||
failedTests,
|
|
||||||
totalDuration,
|
|
||||||
results,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.logTestSummary(summary);
|
|
||||||
return summary;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== TESTS DE BASE =====
|
|
||||||
private async testKeycloakConnection(): Promise<TestResult> {
|
|
||||||
const testName = 'Keycloak Connection Test';
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const token = await this.tokenService.acquireServiceAccountToken();
|
|
||||||
const isValid = await this.tokenService.validateToken(token);
|
|
||||||
|
|
||||||
if (!isValid) {
|
|
||||||
throw new Error('Service account token validation failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.log(`✅ ${testName} - Success (${duration}ms)`);
|
|
||||||
|
|
||||||
return { testName, success: true, duration };
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.error(`❌ ${testName} - Failed: ${error.message}`);
|
|
||||||
return { testName, success: false, duration, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async testServiceAccountPermissions(): Promise<TestResult> {
|
|
||||||
const testName = 'Service Account Permissions Test';
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const serviceToken = await this.tokenService.acquireServiceAccountToken();
|
|
||||||
const decodedToken = this.tokenService.decodeToken(serviceToken);
|
|
||||||
const serviceAccountId = decodedToken.sub;
|
|
||||||
|
|
||||||
if (!serviceAccountId) {
|
|
||||||
throw new Error('Could not extract service account ID from token');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier les rôles du service account
|
|
||||||
const roles = await this.keycloakApi.getUserClientRoles(serviceAccountId);
|
|
||||||
const roleNames = roles.map(r => r.name);
|
|
||||||
|
|
||||||
this.logger.log(`Service account roles: ${roleNames.join(', ')}`);
|
|
||||||
|
|
||||||
// Le service account doit avoir au moins DCB_ADMIN pour créer des utilisateurs
|
|
||||||
const hasRequiredRole = roleNames.some(role =>
|
|
||||||
[UserRole.DCB_ADMIN].includes(role as UserRole)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasRequiredRole) {
|
|
||||||
throw new Error(`Service account missing required roles. Has: ${roleNames.join(', ')}, Needs: ${UserRole.DCB_ADMIN}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1 - Service Account crée un ADMIN DCB-ADMIN
|
|
||||||
const adminData: CreateHubUserData = {
|
|
||||||
username: `test-dcb-admin-${Date.now()}`,
|
|
||||||
email: `test-dcb-admin-${Date.now()}@dcb-test.com`,
|
|
||||||
firstName: 'Test',
|
|
||||||
lastName: 'DCB Admin',
|
|
||||||
password: 'TempPassword123!',
|
|
||||||
role: UserRole.DCB_ADMIN,
|
|
||||||
enabled: true,
|
|
||||||
emailVerified: true,
|
|
||||||
createdBy: 'service-account',
|
|
||||||
};
|
|
||||||
|
|
||||||
const adminUser = await this.hubUsersService.createHubUser(serviceAccountId, adminData);
|
|
||||||
this.testUsers['dcb-admin'] = {
|
|
||||||
id: adminUser.id,
|
|
||||||
username: adminUser.username,
|
|
||||||
role: UserRole.DCB_ADMIN
|
|
||||||
};
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.log(`✅ ${testName} - Success (${duration}ms)`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
testName,
|
|
||||||
success: true,
|
|
||||||
duration,
|
|
||||||
data: {
|
|
||||||
serviceAccountId,
|
|
||||||
roles: roleNames,
|
|
||||||
createdAdmin: adminUser.username
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.error(`❌ ${testName} - Failed: ${error.message}`);
|
|
||||||
return { testName, success: false, duration, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== TESTS PARALLÈLES AVEC ISOLATION =====
|
|
||||||
private async runParallelIsolationTests(): Promise<TestResult[]> {
|
|
||||||
const results: TestResult[] = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Exécuter les tests pour deux merchants différents en parallèle
|
|
||||||
const [teamAResults, teamBResults] = await Promise.all([
|
|
||||||
this.runMerchantTeamTests('TeamA'),
|
|
||||||
this.runMerchantTeamTests('TeamB')
|
|
||||||
]);
|
|
||||||
|
|
||||||
results.push(...teamAResults);
|
|
||||||
results.push(...teamBResults);
|
|
||||||
|
|
||||||
// Test d'isolation entre les deux équipes
|
|
||||||
results.push(await this.testCrossTeamIsolation());
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Parallel isolation tests failed: ${error.message}`);
|
|
||||||
results.push({
|
|
||||||
testName: 'Parallel Isolation Tests',
|
|
||||||
success: false,
|
|
||||||
duration: 0,
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async runMerchantTeamTests(teamName: string): Promise<TestResult[]> {
|
|
||||||
const results: TestResult[] = [];
|
|
||||||
const teamPrefix = teamName.toLowerCase();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 2 - ADMIN DCB-ADMIN crée DCB-SUPPORT et DCB-PARTNER pour cette équipe
|
|
||||||
const dcbAdmin = this.testUsers['dcb-admin'];
|
|
||||||
if (!dcbAdmin) {
|
|
||||||
throw new Error('DCB Admin not found for team tests');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Créer DCB-SUPPORT
|
|
||||||
const supportData: CreateHubUserData = {
|
|
||||||
username: `test-${teamPrefix}-support-${Date.now()}`,
|
|
||||||
email: `test-${teamPrefix}-support-${Date.now()}@dcb-test.com`,
|
|
||||||
firstName: `${teamName}`,
|
|
||||||
lastName: 'Support',
|
|
||||||
password: 'TempPassword123!',
|
|
||||||
role: UserRole.DCB_SUPPORT,
|
|
||||||
enabled: true,
|
|
||||||
emailVerified: true,
|
|
||||||
createdBy: dcbAdmin.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const supportUser = await this.hubUsersService.createHubUser(dcbAdmin.id, supportData);
|
|
||||||
this.testUsers[`${teamPrefix}-support`] = {
|
|
||||||
id: supportUser.id,
|
|
||||||
username: supportUser.username,
|
|
||||||
role: UserRole.DCB_SUPPORT
|
|
||||||
};
|
|
||||||
|
|
||||||
// Créer DCB-PARTNER (Merchant Owner)
|
|
||||||
const partnerData: CreateHubUserData = {
|
|
||||||
username: `test-${teamPrefix}-partner-${Date.now()}`,
|
|
||||||
email: `test-${teamPrefix}-partner-${Date.now()}@dcb-test.com`,
|
|
||||||
firstName: `${teamName}`,
|
|
||||||
lastName: 'Partner',
|
|
||||||
password: 'TempPassword123!',
|
|
||||||
role: UserRole.DCB_PARTNER,
|
|
||||||
enabled: true,
|
|
||||||
emailVerified: true,
|
|
||||||
createdBy: dcbAdmin.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const partnerUser = await this.hubUsersService.createHubUser(dcbAdmin.id, partnerData);
|
|
||||||
this.testMerchants[`${teamPrefix}-partner`] = {
|
|
||||||
id: partnerUser.id,
|
|
||||||
username: partnerUser.username,
|
|
||||||
role: UserRole.DCB_PARTNER
|
|
||||||
};
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
testName: `${teamName} - Admin creates Support and Partner`,
|
|
||||||
success: true,
|
|
||||||
duration: 0,
|
|
||||||
data: {
|
|
||||||
supportUser: supportUser.username,
|
|
||||||
partnerUser: partnerUser.username
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3 - ADMIN DCB-ADMIN crée DCB-PARTNER-ADMIN avec merchantPartnerId du DCB-PARTNER
|
|
||||||
const partnerAdminData: CreateMerchantUserData = {
|
|
||||||
username: `test-${teamPrefix}-partner-admin-${Date.now()}`,
|
|
||||||
email: `test-${teamPrefix}-partner-admin-${Date.now()}@dcb-test.com`,
|
|
||||||
firstName: `${teamName}`,
|
|
||||||
lastName: 'Partner Admin',
|
|
||||||
password: 'TempPassword123!',
|
|
||||||
role: UserRole.DCB_PARTNER_ADMIN,
|
|
||||||
enabled: true,
|
|
||||||
emailVerified: true,
|
|
||||||
merchantPartnerId: partnerUser.id, // Utilise l'ID du DCB-PARTNER
|
|
||||||
createdBy: dcbAdmin.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const partnerAdminUser = await this.merchantUsersService.createMerchantUser(
|
|
||||||
dcbAdmin.id,
|
|
||||||
partnerAdminData
|
|
||||||
);
|
|
||||||
|
|
||||||
this.testMerchantUsers[`${teamPrefix}-partner-admin`] = {
|
|
||||||
id: partnerAdminUser.id,
|
|
||||||
username: partnerAdminUser.username,
|
|
||||||
role: UserRole.DCB_PARTNER_ADMIN,
|
|
||||||
merchantPartnerId: partnerUser.id
|
|
||||||
};
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
testName: `${teamName} - Admin creates Partner Admin`,
|
|
||||||
success: true,
|
|
||||||
duration: 0,
|
|
||||||
data: {
|
|
||||||
partnerAdmin: partnerAdminUser.username,
|
|
||||||
merchantPartnerId: partnerUser.id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4 - DCB-PARTNER crée ses trois types d'utilisateurs
|
|
||||||
const partnerCreatedUsers = await this.testPartnerUserCreation(teamName, partnerUser.id);
|
|
||||||
results.push(...partnerCreatedUsers);
|
|
||||||
|
|
||||||
// 5 - DCB-PARTNER-ADMIN crée un manager
|
|
||||||
const adminCreatedManager = await this.testPartnerAdminCreatesManager(teamName, partnerUser.id);
|
|
||||||
results.push(adminCreatedManager);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
results.push({
|
|
||||||
testName: `${teamName} - Team Tests`,
|
|
||||||
success: false,
|
|
||||||
duration: 0,
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Puis utilisez-le dans votre méthode
|
|
||||||
private async testPartnerUserCreation(teamName: string, partnerId: string): Promise<TestResult[]> {
|
|
||||||
const results: TestResult[] = [];
|
|
||||||
const teamPrefix = teamName.toLowerCase();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const partner = this.testMerchants[`${teamPrefix}-partner`];
|
|
||||||
if (!partner) {
|
|
||||||
throw new Error(`${teamName} Partner not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Types d'utilisateurs à créer par le PARTNER
|
|
||||||
const userTypes: { role: MerchantUserRole; key: string }[] = [
|
|
||||||
{ role: UserRole.DCB_PARTNER_ADMIN, key: 'partner-admin-by-partner' },
|
|
||||||
{ role: UserRole.DCB_PARTNER_MANAGER, key: 'partner-manager-by-partner' },
|
|
||||||
{ role: UserRole.DCB_PARTNER_SUPPORT, key: 'partner-support-by-partner' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const userType of userTypes) {
|
|
||||||
const userData: CreateMerchantUserData = {
|
|
||||||
username: `test-${teamPrefix}-${userType.key}-${Date.now()}`,
|
|
||||||
email: `test-${teamPrefix}-${userType.key}-${Date.now()}@dcb-test.com`,
|
|
||||||
firstName: `${teamName}`,
|
|
||||||
lastName: userType.role.split('_').pop() || 'User',
|
|
||||||
password: 'TempPassword123!',
|
|
||||||
role: userType.role, // Type compatible maintenant
|
|
||||||
enabled: true,
|
|
||||||
emailVerified: true,
|
|
||||||
merchantPartnerId: partnerId,
|
|
||||||
createdBy: partner.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const user = await this.merchantUsersService.createMerchantUser(partner.id, userData);
|
|
||||||
|
|
||||||
this.testMerchantUsers[`${teamPrefix}-${userType.key}`] = {
|
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
role: userType.role,
|
|
||||||
merchantPartnerId: partnerId
|
|
||||||
};
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
testName: `${teamName} - Partner creates ${userType.role}`,
|
|
||||||
success: true,
|
|
||||||
duration: 0,
|
|
||||||
data: {
|
|
||||||
createdUser: user.username,
|
|
||||||
role: userType.role,
|
|
||||||
merchantPartnerId: partnerId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
results.push({
|
|
||||||
testName: `${teamName} - Partner User Creation`,
|
|
||||||
success: false,
|
|
||||||
duration: 0,
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async testPartnerAdminCreatesManager(teamName: string, partnerId: string): Promise<TestResult> {
|
|
||||||
const testName = `${teamName} - Partner Admin creates Manager`;
|
|
||||||
const teamPrefix = teamName.toLowerCase();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const partnerAdmin = this.testMerchantUsers[`${teamPrefix}-partner-admin`];
|
|
||||||
if (!partnerAdmin) {
|
|
||||||
throw new Error(`${teamName} Partner Admin not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5 - DCB-PARTNER-ADMIN crée un manager avec l'ID de son DCB-PARTNER
|
|
||||||
const managerData: CreateMerchantUserData = {
|
|
||||||
username: `test-${teamPrefix}-manager-by-admin-${Date.now()}`,
|
|
||||||
email: `test-${teamPrefix}-manager-by-admin-${Date.now()}@dcb-test.com`,
|
|
||||||
firstName: `${teamName}`,
|
|
||||||
lastName: 'Manager by Admin',
|
|
||||||
password: 'TempPassword123!',
|
|
||||||
role: UserRole.DCB_PARTNER_MANAGER,
|
|
||||||
enabled: true,
|
|
||||||
emailVerified: true,
|
|
||||||
merchantPartnerId: partnerId, // Utilise l'ID du DCB-PARTNER (pas son propre ID)
|
|
||||||
createdBy: partnerAdmin.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const managerUser = await this.merchantUsersService.createMerchantUser(
|
|
||||||
partnerAdmin.id,
|
|
||||||
managerData
|
|
||||||
);
|
|
||||||
|
|
||||||
this.testMerchantUsers[`${teamPrefix}-manager-by-admin`] = {
|
|
||||||
id: managerUser.id,
|
|
||||||
username: managerUser.username,
|
|
||||||
role: UserRole.DCB_PARTNER_MANAGER,
|
|
||||||
merchantPartnerId: partnerId
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
testName,
|
|
||||||
success: true,
|
|
||||||
duration: 0,
|
|
||||||
data: {
|
|
||||||
createdManager: managerUser.username,
|
|
||||||
createdBy: partnerAdmin.username,
|
|
||||||
merchantPartnerId: partnerId
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
testName,
|
|
||||||
success: false,
|
|
||||||
duration: 0,
|
|
||||||
error: error.message
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async testCrossTeamIsolation(): Promise<TestResult> {
|
|
||||||
const testName = 'Cross-Team Isolation Test';
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const teamAPartnerAdmin = this.testMerchantUsers['teama-partner-admin'];
|
|
||||||
const teamBPartner = this.testMerchants['teamb-partner'];
|
|
||||||
|
|
||||||
if (!teamAPartnerAdmin || !teamBPartner) {
|
|
||||||
throw new Error('Team users not found for isolation test');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tenter de créer un utilisateur dans l'autre équipe - devrait échouer
|
|
||||||
try {
|
|
||||||
const crossTeamUserData: CreateMerchantUserData = {
|
|
||||||
username: `test-cross-team-attempt-${Date.now()}`,
|
|
||||||
email: `test-cross-team-attempt-${Date.now()}@dcb-test.com`,
|
|
||||||
firstName: 'Cross',
|
|
||||||
lastName: 'Team Attempt',
|
|
||||||
password: 'TempPassword123!',
|
|
||||||
role: UserRole.DCB_PARTNER_MANAGER,
|
|
||||||
enabled: true,
|
|
||||||
emailVerified: true,
|
|
||||||
merchantPartnerId: teamBPartner.id, // ID d'une autre équipe
|
|
||||||
createdBy: teamAPartnerAdmin.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.merchantUsersService.createMerchantUser(
|
|
||||||
teamAPartnerAdmin.id,
|
|
||||||
crossTeamUserData
|
|
||||||
);
|
|
||||||
|
|
||||||
// Si on arrive ici, l'isolation a échoué
|
|
||||||
throw new Error('Isolation failed - User from TeamA could create user in TeamB');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// Comportement attendu - l'accès doit être refusé
|
|
||||||
if (error.message.includes('Forbidden') ||
|
|
||||||
error.message.includes('Insufficient permissions') ||
|
|
||||||
error.message.includes('not authorized') ||
|
|
||||||
error.message.includes('own merchant')) {
|
|
||||||
// Succès - l'isolation fonctionne
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.log(`✅ ${testName} - Success (${duration}ms)`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
testName,
|
|
||||||
success: true,
|
|
||||||
duration,
|
|
||||||
data: { isolationWorking: true }
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Erreur inattendue
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.error(`❌ ${testName} - Failed: ${error.message}`);
|
|
||||||
return { testName, success: false, duration, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== TESTS AVANCÉS (conservés depuis la version originale) =====
|
|
||||||
private async testStatsAndReports(): Promise<TestResult> {
|
|
||||||
const testName = 'Stats and Reports Test';
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const serviceToken = await this.tokenService.acquireServiceAccountToken();
|
|
||||||
const decodedToken = this.tokenService.decodeToken(serviceToken);
|
|
||||||
const serviceAccountId = decodedToken.sub;
|
|
||||||
|
|
||||||
const stats = await this.hubUsersService.getHubUsersStats(serviceAccountId);
|
|
||||||
const activity = await this.hubUsersService.getHubUserActivity(serviceAccountId);
|
|
||||||
const sessions = await this.hubUsersService.getActiveHubSessions(serviceAccountId);
|
|
||||||
|
|
||||||
// Validation basique des stats
|
|
||||||
if (typeof stats.totalAdmins !== 'number' || typeof stats.totalSupport !== 'number') {
|
|
||||||
throw new Error('Stats validation failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.log(`✅ ${testName} - Success (${duration}ms)`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
testName,
|
|
||||||
success: true,
|
|
||||||
duration,
|
|
||||||
data: {
|
|
||||||
stats,
|
|
||||||
activityCount: activity.length,
|
|
||||||
sessionCount: sessions.length
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.error(`❌ ${testName} - Failed: ${error.message}`);
|
|
||||||
return { testName, success: false, duration, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async testHealthCheck(): Promise<TestResult> {
|
|
||||||
const testName = 'Health Check Test';
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const serviceToken = await this.tokenService.acquireServiceAccountToken();
|
|
||||||
const decodedToken = this.tokenService.decodeToken(serviceToken);
|
|
||||||
const serviceAccountId = decodedToken.sub;
|
|
||||||
|
|
||||||
const health = await this.hubUsersService.checkHubUsersHealth(serviceAccountId);
|
|
||||||
|
|
||||||
if (!health.status || !health.stats || !Array.isArray(health.issues)) {
|
|
||||||
throw new Error('Health check validation failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.log(`✅ ${testName} - Success (${duration}ms)`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
testName,
|
|
||||||
success: true,
|
|
||||||
duration,
|
|
||||||
data: { healthStatus: health.status }
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.error(`❌ ${testName} - Failed: ${error.message}`);
|
|
||||||
return { testName, success: false, duration, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async testSecurityValidations(): Promise<TestResult> {
|
|
||||||
const testName = 'Security Validations Test';
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const serviceToken = await this.tokenService.acquireServiceAccountToken();
|
|
||||||
const decodedToken = this.tokenService.decodeToken(serviceToken);
|
|
||||||
const serviceAccountId = decodedToken.sub;
|
|
||||||
|
|
||||||
// Test de la méthode canUserManageHubUsers
|
|
||||||
const canManage = await this.hubUsersService.canUserManageHubUsers(serviceAccountId);
|
|
||||||
|
|
||||||
if (!canManage) {
|
|
||||||
throw new Error('Service account should be able to manage hub users');
|
|
||||||
}
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.log(`✅ ${testName} - Success (${duration}ms)`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
testName,
|
|
||||||
success: true,
|
|
||||||
duration,
|
|
||||||
data: { canManageHubUsers: canManage }
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.error(`❌ ${testName} - Failed: ${error.message}`);
|
|
||||||
return { testName, success: false, duration, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== NETTOYAGE =====
|
|
||||||
private async cleanupTestUsers(): Promise<void> {
|
|
||||||
this.logger.log('🧹 Cleaning up test users...');
|
|
||||||
|
|
||||||
const serviceToken = await this.tokenService.acquireServiceAccountToken();
|
|
||||||
const decodedToken = this.tokenService.decodeToken(serviceToken);
|
|
||||||
const serviceAccountId = decodedToken.sub;
|
|
||||||
|
|
||||||
// Nettoyer les utilisateurs hub
|
|
||||||
for (const [key, userInfo] of Object.entries(this.testUsers)) {
|
|
||||||
try {
|
|
||||||
await this.hubUsersService.deleteHubUser(userInfo.id, serviceAccountId);
|
|
||||||
this.logger.log(`✅ Deleted test user: ${key} (${userInfo.username})`);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.warn(`⚠️ Could not delete test user ${key}: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.testUsers = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async cleanupTestMerchants(): Promise<void> {
|
|
||||||
this.logger.log('🧹 Cleaning up test merchants...');
|
|
||||||
|
|
||||||
// Implémentez la logique de nettoyage des merchants de test
|
|
||||||
this.testMerchants = {};
|
|
||||||
this.testMerchantUsers = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== LOGGING ET RAPPORTS =====
|
|
||||||
private logTestSummary(summary: StartupTestSummary): void {
|
|
||||||
this.logger.log('='.repeat(60));
|
|
||||||
this.logger.log('🎯 PARALLEL ISOLATION STARTUP TEST SUMMARY');
|
|
||||||
this.logger.log('='.repeat(60));
|
|
||||||
this.logger.log(`📊 Total Tests: ${summary.totalTests}`);
|
|
||||||
this.logger.log(`✅ Passed: ${summary.passedTests}`);
|
|
||||||
this.logger.log(`❌ Failed: ${summary.failedTests}`);
|
|
||||||
this.logger.log(`⏱️ Total Duration: ${summary.totalDuration}ms`);
|
|
||||||
this.logger.log('-'.repeat(60));
|
|
||||||
|
|
||||||
summary.results.forEach(result => {
|
|
||||||
const status = result.success ? '✅' : '❌';
|
|
||||||
this.logger.log(`${status} ${result.testName}: ${result.duration}ms`);
|
|
||||||
if (!result.success) {
|
|
||||||
this.logger.log(` ERROR: ${result.error}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logger.log('='.repeat(60));
|
|
||||||
|
|
||||||
if (summary.failedTests === 0) {
|
|
||||||
this.logger.log('🚀 ALL TESTS PASSED! System is ready with proper isolation.');
|
|
||||||
} else {
|
|
||||||
this.logger.warn(`⚠️ ${summary.failedTests} test(s) failed. Please check the logs above.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== MÉTHODES PUBLIQUES POUR USAGE MANUEL =====
|
|
||||||
async runQuickTest(): Promise<StartupTestSummary> {
|
|
||||||
this.logger.log('🔍 Running quick startup test...');
|
|
||||||
return this.runAllTests();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTestStatus(): Promise<{ status: 'healthy' | 'degraded' | 'unhealthy'; details: string }> {
|
|
||||||
try {
|
|
||||||
const summary = await this.runAllTests();
|
|
||||||
const successRate = (summary.passedTests / summary.totalTests) * 100;
|
|
||||||
|
|
||||||
if (successRate === 100) {
|
|
||||||
return { status: 'healthy', details: 'All tests passed successfully' };
|
|
||||||
} else if (successRate >= 80) {
|
|
||||||
return { status: 'degraded', details: `${summary.failedTests} test(s) failed` };
|
|
||||||
} else {
|
|
||||||
return { status: 'unhealthy', details: 'Multiple test failures detected' };
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return { status: 'unhealthy', details: `Test execution failed: ${error.message}` };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,706 +1,76 @@
|
|||||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
import { HubUsersService} from '../../hub-users/services/hub-users.service';
|
import { KeycloakApiService } from './keycloak-api.service';
|
||||||
import { MerchantUsersService, CreateMerchantUserData } from '../../hub-users/services/merchant-users.service';
|
|
||||||
import { KeycloakApiService } from '../../auth/services/keycloak-api.service';
|
|
||||||
import { TokenService } from '../../auth/services/token.service';
|
|
||||||
import { UserRole, CreateHubUserData } from '../../auth/services/keycloak-user.model';
|
|
||||||
|
|
||||||
export interface TestResult {
|
interface TestResults {
|
||||||
testName: string;
|
connection: { [key: string]: string };
|
||||||
success: boolean;
|
|
||||||
duration: number;
|
|
||||||
error?: string;
|
|
||||||
data?: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StartupTestSummary {
|
|
||||||
totalTests: number;
|
|
||||||
passedTests: number;
|
|
||||||
failedTests: number;
|
|
||||||
totalDuration: number;
|
|
||||||
results: TestResult[];
|
|
||||||
healthStatus?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
type HubUserRole =
|
|
||||||
| UserRole.DCB_ADMIN
|
|
||||||
| UserRole.DCB_SUPPORT
|
|
||||||
| UserRole.DCB_PARTNER;
|
|
||||||
|
|
||||||
type MerchantUserRole =
|
|
||||||
| UserRole.DCB_PARTNER_ADMIN
|
|
||||||
| UserRole.DCB_PARTNER_MANAGER
|
|
||||||
| UserRole.DCB_PARTNER_SUPPORT;
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StartupService implements OnModuleInit {
|
export class StartupServiceInitialization implements OnModuleInit {
|
||||||
private readonly logger = new Logger(StartupService.name);
|
private readonly logger = new Logger(StartupServiceInitialization.name);
|
||||||
|
private isInitialized = false;
|
||||||
// Stockage des données de test
|
private initializationError: string | null = null;
|
||||||
private testUsers: { [key: string]: { id: string; username: string; role: UserRole } } = {};
|
private testResults: TestResults = {
|
||||||
private testMerchants: { [key: string]: { id: string; username: string; role: UserRole } } = {};
|
connection: {},
|
||||||
private testMerchantUsers: { [key: string]: { id: string; username: string; role: UserRole; merchantPartnerId: string } } = {};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly hubUsersService: HubUsersService,
|
private readonly keycloakApiService: KeycloakApiService,
|
||||||
private readonly merchantUsersService: MerchantUsersService,
|
|
||||||
private readonly keycloakApi: KeycloakApiService,
|
|
||||||
private readonly tokenService: TokenService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
if (process.env.RUN_STARTUP_TESTS === 'true') {
|
this.logger.log('🚀 Démarrage des tests de connexion');
|
||||||
this.logger.log('Starting comprehensive tests (Hub + Merchants with isolation)...');
|
|
||||||
await this.runAllTests();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== MÉTHODES DE TEST PRINCIPALES =====
|
|
||||||
async runAllTests(): Promise<StartupTestSummary> {
|
|
||||||
const results: TestResult[] = [];
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Tests de base
|
await this.validateKeycloakConnection();
|
||||||
results.push(await this.testKeycloakConnection());
|
|
||||||
results.push(await this.testServiceAccountPermissions());
|
|
||||||
|
|
||||||
// 2. Tests de création en parallèle avec isolation
|
this.isInitialized = true;
|
||||||
const parallelTests = await this.runParallelIsolationTests();
|
this.logger.log('✅ Tests de connexion terminés avec succès');
|
||||||
results.push(...parallelTests);
|
} catch (error: any) {
|
||||||
|
this.initializationError = error.message;
|
||||||
// 3. Tests avancés
|
this.logger.error(`❌ Échec des tests de connexion: ${error.message}`);
|
||||||
results.push(await this.testStatsAndReports());
|
}
|
||||||
results.push(await this.testHealthCheck());
|
|
||||||
results.push(await this.testSecurityValidations());
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('Critical error during startup tests:', error);
|
|
||||||
} finally {
|
|
||||||
await this.cleanupTestUsers();
|
|
||||||
await this.cleanupTestMerchants();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalDuration = Date.now() - startTime;
|
// === VALIDATION CONNEXION KEYCLOAK ===
|
||||||
const passedTests = results.filter(r => r.success).length;
|
private async validateKeycloakConnection() {
|
||||||
const failedTests = results.filter(r => !r.success).length;
|
this.logger.log('🔌 Test de connexion Keycloak...');
|
||||||
|
|
||||||
const summary: StartupTestSummary = {
|
|
||||||
totalTests: results.length,
|
|
||||||
passedTests,
|
|
||||||
failedTests,
|
|
||||||
totalDuration,
|
|
||||||
results,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.logTestSummary(summary);
|
|
||||||
return summary;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== TESTS DE BASE =====
|
|
||||||
private async testKeycloakConnection(): Promise<TestResult> {
|
|
||||||
const testName = 'Keycloak Connection Test';
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = await this.tokenService.acquireServiceAccountToken();
|
const isKeycloakAccessible = await this.keycloakApiService.checkKeycloakAvailability();
|
||||||
const isValid = await this.tokenService.validateToken(token);
|
if (!isKeycloakAccessible) {
|
||||||
|
throw new Error('Keycloak inaccessible');
|
||||||
if (!isValid) {
|
|
||||||
throw new Error('Service account token validation failed');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
const isServiceConnected = await this.keycloakApiService.checkServiceConnection();
|
||||||
this.logger.log(`✅ ${testName} - Success (${duration}ms)`);
|
if (!isServiceConnected) {
|
||||||
|
throw new Error('Connexion service Keycloak échouée');
|
||||||
|
}
|
||||||
|
|
||||||
return { testName, success: true, duration };
|
this.testResults.connection.keycloak = 'SUCCESS';
|
||||||
} catch (error) {
|
this.logger.log('✅ Connexion Keycloak validée');
|
||||||
const duration = Date.now() - startTime;
|
} catch (error: any) {
|
||||||
this.logger.error(`❌ ${testName} - Failed: ${error.message}`);
|
this.testResults.connection.keycloak = 'FAILED';
|
||||||
return { testName, success: false, duration, error: error.message };
|
throw new Error(`Connexion Keycloak échouée: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async testServiceAccountPermissions(): Promise<TestResult> {
|
// === METHODES STATUT ===
|
||||||
const testName = 'Service Account Permissions Test';
|
getStatus() {
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const serviceToken = await this.tokenService.acquireServiceAccountToken();
|
|
||||||
const decodedToken = this.tokenService.decodeToken(serviceToken);
|
|
||||||
const serviceAccountId = decodedToken.sub;
|
|
||||||
|
|
||||||
if (!serviceAccountId) {
|
|
||||||
throw new Error('Could not extract service account ID from token');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier les rôles du service account
|
|
||||||
const roles = await this.keycloakApi.getUserClientRoles(serviceAccountId);
|
|
||||||
const roleNames = roles.map(r => r.name);
|
|
||||||
|
|
||||||
this.logger.log(`Service account roles: ${roleNames.join(', ')}`);
|
|
||||||
|
|
||||||
// Le service account doit avoir au moins DCB_ADMIN pour créer des utilisateurs
|
|
||||||
const hasRequiredRole = roleNames.some(role =>
|
|
||||||
[UserRole.DCB_ADMIN].includes(role as UserRole)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasRequiredRole) {
|
|
||||||
throw new Error(`Service account missing required roles. Has: ${roleNames.join(', ')}, Needs: ${UserRole.DCB_ADMIN}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1 - Service Account crée un ADMIN DCB-ADMIN
|
|
||||||
const adminData: CreateHubUserData = {
|
|
||||||
username: `test-dcb-admin-${Date.now()}`,
|
|
||||||
email: `test-dcb-admin-${Date.now()}@dcb-test.com`,
|
|
||||||
firstName: 'Test',
|
|
||||||
lastName: 'DCB Admin',
|
|
||||||
password: 'TempPassword123!',
|
|
||||||
role: UserRole.DCB_ADMIN,
|
|
||||||
enabled: true,
|
|
||||||
emailVerified: true,
|
|
||||||
createdBy: 'service-account',
|
|
||||||
};
|
|
||||||
|
|
||||||
const adminUser = await this.hubUsersService.createHubUser(serviceAccountId, adminData);
|
|
||||||
this.testUsers['dcb-admin'] = {
|
|
||||||
id: adminUser.id,
|
|
||||||
username: adminUser.username,
|
|
||||||
role: UserRole.DCB_ADMIN
|
|
||||||
};
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.log(`✅ ${testName} - Success (${duration}ms)`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
testName,
|
status: this.isInitialized ? 'healthy' : 'unhealthy',
|
||||||
success: true,
|
keycloakConnected: this.isInitialized,
|
||||||
duration,
|
testResults: this.testResults,
|
||||||
data: {
|
timestamp: new Date(),
|
||||||
serviceAccountId,
|
error: this.initializationError,
|
||||||
roles: roleNames,
|
|
||||||
createdAdmin: adminUser.username
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.error(`❌ ${testName} - Failed: ${error.message}`);
|
|
||||||
return { testName, success: false, duration, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== TESTS PARALLÈLES AVEC ISOLATION =====
|
|
||||||
private async runParallelIsolationTests(): Promise<TestResult[]> {
|
|
||||||
const results: TestResult[] = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Exécuter les tests pour deux merchants différents en parallèle
|
|
||||||
const [teamAResults, teamBResults] = await Promise.all([
|
|
||||||
this.runMerchantTeamTests('TeamA'),
|
|
||||||
this.runMerchantTeamTests('TeamB')
|
|
||||||
]);
|
|
||||||
|
|
||||||
results.push(...teamAResults);
|
|
||||||
results.push(...teamBResults);
|
|
||||||
|
|
||||||
// Test d'isolation entre les deux équipes
|
|
||||||
results.push(await this.testCrossTeamIsolation());
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Parallel isolation tests failed: ${error.message}`);
|
|
||||||
results.push({
|
|
||||||
testName: 'Parallel Isolation Tests',
|
|
||||||
success: false,
|
|
||||||
duration: 0,
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async runMerchantTeamTests(teamName: string): Promise<TestResult[]> {
|
|
||||||
const results: TestResult[] = [];
|
|
||||||
const teamPrefix = teamName.toLowerCase();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 2 - ADMIN DCB-ADMIN crée DCB-SUPPORT et DCB-PARTNER pour cette équipe
|
|
||||||
const dcbAdmin = this.testUsers['dcb-admin'];
|
|
||||||
if (!dcbAdmin) {
|
|
||||||
throw new Error('DCB Admin not found for team tests');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Créer DCB-SUPPORT
|
|
||||||
const supportData: CreateHubUserData = {
|
|
||||||
username: `test-${teamPrefix}-support-${Date.now()}`,
|
|
||||||
email: `test-${teamPrefix}-support-${Date.now()}@dcb-test.com`,
|
|
||||||
firstName: `${teamName}`,
|
|
||||||
lastName: 'Support',
|
|
||||||
password: 'TempPassword123!',
|
|
||||||
role: UserRole.DCB_SUPPORT,
|
|
||||||
enabled: true,
|
|
||||||
emailVerified: true,
|
|
||||||
createdBy: dcbAdmin.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const supportUser = await this.hubUsersService.createHubUser(dcbAdmin.id, supportData);
|
|
||||||
this.testUsers[`${teamPrefix}-support`] = {
|
|
||||||
id: supportUser.id,
|
|
||||||
username: supportUser.username,
|
|
||||||
role: UserRole.DCB_SUPPORT
|
|
||||||
};
|
|
||||||
|
|
||||||
// Créer DCB-PARTNER (Merchant Owner)
|
|
||||||
const partnerData: CreateHubUserData = {
|
|
||||||
username: `test-${teamPrefix}-partner-${Date.now()}`,
|
|
||||||
email: `test-${teamPrefix}-partner-${Date.now()}@dcb-test.com`,
|
|
||||||
firstName: `${teamName}`,
|
|
||||||
lastName: 'Partner',
|
|
||||||
password: 'TempPassword123!',
|
|
||||||
role: UserRole.DCB_PARTNER,
|
|
||||||
enabled: true,
|
|
||||||
emailVerified: true,
|
|
||||||
createdBy: dcbAdmin.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const partnerUser = await this.hubUsersService.createHubUser(dcbAdmin.id, partnerData);
|
|
||||||
this.testMerchants[`${teamPrefix}-partner`] = {
|
|
||||||
id: partnerUser.id,
|
|
||||||
username: partnerUser.username,
|
|
||||||
role: UserRole.DCB_PARTNER
|
|
||||||
};
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
testName: `${teamName} - Admin creates Support and Partner`,
|
|
||||||
success: true,
|
|
||||||
duration: 0,
|
|
||||||
data: {
|
|
||||||
supportUser: supportUser.username,
|
|
||||||
partnerUser: partnerUser.username
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3 - ADMIN DCB-ADMIN crée DCB-PARTNER-ADMIN avec merchantPartnerId du DCB-PARTNER
|
|
||||||
const partnerAdminData: CreateMerchantUserData = {
|
|
||||||
username: `test-${teamPrefix}-partner-admin-${Date.now()}`,
|
|
||||||
email: `test-${teamPrefix}-partner-admin-${Date.now()}@dcb-test.com`,
|
|
||||||
firstName: `${teamName}`,
|
|
||||||
lastName: 'Partner Admin',
|
|
||||||
password: 'TempPassword123!',
|
|
||||||
role: UserRole.DCB_PARTNER_ADMIN,
|
|
||||||
enabled: true,
|
|
||||||
emailVerified: true,
|
|
||||||
merchantPartnerId: partnerUser.id, // Utilise l'ID du DCB-PARTNER
|
|
||||||
createdBy: dcbAdmin.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const partnerAdminUser = await this.merchantUsersService.createMerchantUser(
|
|
||||||
dcbAdmin.id,
|
|
||||||
partnerAdminData
|
|
||||||
);
|
|
||||||
|
|
||||||
this.testMerchantUsers[`${teamPrefix}-partner-admin`] = {
|
|
||||||
id: partnerAdminUser.id,
|
|
||||||
username: partnerAdminUser.username,
|
|
||||||
role: UserRole.DCB_PARTNER_ADMIN,
|
|
||||||
merchantPartnerId: partnerUser.id
|
|
||||||
};
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
testName: `${teamName} - Admin creates Partner Admin`,
|
|
||||||
success: true,
|
|
||||||
duration: 0,
|
|
||||||
data: {
|
|
||||||
partnerAdmin: partnerAdminUser.username,
|
|
||||||
merchantPartnerId: partnerUser.id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4 - DCB-PARTNER crée ses trois types d'utilisateurs
|
|
||||||
const partnerCreatedUsers = await this.testPartnerUserCreation(teamName, partnerUser.id);
|
|
||||||
results.push(...partnerCreatedUsers);
|
|
||||||
|
|
||||||
// 5 - DCB-PARTNER-ADMIN crée un manager
|
|
||||||
const adminCreatedManager = await this.testPartnerAdminCreatesManager(teamName, partnerUser.id);
|
|
||||||
results.push(adminCreatedManager);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
results.push({
|
|
||||||
testName: `${teamName} - Team Tests`,
|
|
||||||
success: false,
|
|
||||||
duration: 0,
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Puis utilisez-le dans votre méthode
|
|
||||||
private async testPartnerUserCreation(teamName: string, partnerId: string): Promise<TestResult[]> {
|
|
||||||
const results: TestResult[] = [];
|
|
||||||
const teamPrefix = teamName.toLowerCase();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const partner = this.testMerchants[`${teamPrefix}-partner`];
|
|
||||||
if (!partner) {
|
|
||||||
throw new Error(`${teamName} Partner not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Types d'utilisateurs à créer par le PARTNER
|
|
||||||
const userTypes: { role: MerchantUserRole; key: string }[] = [
|
|
||||||
{ role: UserRole.DCB_PARTNER_ADMIN, key: 'partner-admin-by-partner' },
|
|
||||||
{ role: UserRole.DCB_PARTNER_MANAGER, key: 'partner-manager-by-partner' },
|
|
||||||
{ role: UserRole.DCB_PARTNER_SUPPORT, key: 'partner-support-by-partner' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const userType of userTypes) {
|
|
||||||
const userData: CreateMerchantUserData = {
|
|
||||||
username: `test-${teamPrefix}-${userType.key}-${Date.now()}`,
|
|
||||||
email: `test-${teamPrefix}-${userType.key}-${Date.now()}@dcb-test.com`,
|
|
||||||
firstName: `${teamName}`,
|
|
||||||
lastName: userType.role.split('_').pop() || 'User',
|
|
||||||
password: 'TempPassword123!',
|
|
||||||
role: userType.role, // Type compatible maintenant
|
|
||||||
enabled: true,
|
|
||||||
emailVerified: true,
|
|
||||||
merchantPartnerId: partnerId,
|
|
||||||
createdBy: partner.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const user = await this.merchantUsersService.createMerchantUser(partner.id, userData);
|
|
||||||
|
|
||||||
this.testMerchantUsers[`${teamPrefix}-${userType.key}`] = {
|
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
role: userType.role,
|
|
||||||
merchantPartnerId: partnerId
|
|
||||||
};
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
testName: `${teamName} - Partner creates ${userType.role}`,
|
|
||||||
success: true,
|
|
||||||
duration: 0,
|
|
||||||
data: {
|
|
||||||
createdUser: user.username,
|
|
||||||
role: userType.role,
|
|
||||||
merchantPartnerId: partnerId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
results.push({
|
|
||||||
testName: `${teamName} - Partner User Creation`,
|
|
||||||
success: false,
|
|
||||||
duration: 0,
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async testPartnerAdminCreatesManager(teamName: string, partnerId: string): Promise<TestResult> {
|
|
||||||
const testName = `${teamName} - Partner Admin creates Manager`;
|
|
||||||
const teamPrefix = teamName.toLowerCase();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const partnerAdmin = this.testMerchantUsers[`${teamPrefix}-partner-admin`];
|
|
||||||
if (!partnerAdmin) {
|
|
||||||
throw new Error(`${teamName} Partner Admin not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5 - DCB-PARTNER-ADMIN crée un manager avec l'ID de son DCB-PARTNER
|
|
||||||
const managerData: CreateMerchantUserData = {
|
|
||||||
username: `test-${teamPrefix}-manager-by-admin-${Date.now()}`,
|
|
||||||
email: `test-${teamPrefix}-manager-by-admin-${Date.now()}@dcb-test.com`,
|
|
||||||
firstName: `${teamName}`,
|
|
||||||
lastName: 'Manager by Admin',
|
|
||||||
password: 'TempPassword123!',
|
|
||||||
role: UserRole.DCB_PARTNER_MANAGER,
|
|
||||||
enabled: true,
|
|
||||||
emailVerified: true,
|
|
||||||
merchantPartnerId: partnerId, // Utilise l'ID du DCB-PARTNER (pas son propre ID)
|
|
||||||
createdBy: partnerAdmin.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const managerUser = await this.merchantUsersService.createMerchantUser(
|
|
||||||
partnerAdmin.id,
|
|
||||||
managerData
|
|
||||||
);
|
|
||||||
|
|
||||||
this.testMerchantUsers[`${teamPrefix}-manager-by-admin`] = {
|
|
||||||
id: managerUser.id,
|
|
||||||
username: managerUser.username,
|
|
||||||
role: UserRole.DCB_PARTNER_MANAGER,
|
|
||||||
merchantPartnerId: partnerId
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
testName,
|
|
||||||
success: true,
|
|
||||||
duration: 0,
|
|
||||||
data: {
|
|
||||||
createdManager: managerUser.username,
|
|
||||||
createdBy: partnerAdmin.username,
|
|
||||||
merchantPartnerId: partnerId
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
testName,
|
|
||||||
success: false,
|
|
||||||
duration: 0,
|
|
||||||
error: error.message
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isHealthy(): boolean {
|
||||||
|
return this.isInitialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async testCrossTeamIsolation(): Promise<TestResult> {
|
getTestResults(): TestResults {
|
||||||
const testName = 'Cross-Team Isolation Test';
|
return this.testResults;
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const teamAPartnerAdmin = this.testMerchantUsers['teama-partner-admin'];
|
|
||||||
const teamBPartner = this.testMerchants['teamb-partner'];
|
|
||||||
|
|
||||||
if (!teamAPartnerAdmin || !teamBPartner) {
|
|
||||||
throw new Error('Team users not found for isolation test');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tenter de créer un utilisateur dans l'autre équipe - devrait échouer
|
|
||||||
try {
|
|
||||||
const crossTeamUserData: CreateMerchantUserData = {
|
|
||||||
username: `test-cross-team-attempt-${Date.now()}`,
|
|
||||||
email: `test-cross-team-attempt-${Date.now()}@dcb-test.com`,
|
|
||||||
firstName: 'Cross',
|
|
||||||
lastName: 'Team Attempt',
|
|
||||||
password: 'TempPassword123!',
|
|
||||||
role: UserRole.DCB_PARTNER_MANAGER,
|
|
||||||
enabled: true,
|
|
||||||
emailVerified: true,
|
|
||||||
merchantPartnerId: teamBPartner.id, // ID d'une autre équipe
|
|
||||||
createdBy: teamAPartnerAdmin.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.merchantUsersService.createMerchantUser(
|
|
||||||
teamAPartnerAdmin.id,
|
|
||||||
crossTeamUserData
|
|
||||||
);
|
|
||||||
|
|
||||||
// Si on arrive ici, l'isolation a échoué
|
|
||||||
throw new Error('Isolation failed - User from TeamA could create user in TeamB');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// Comportement attendu - l'accès doit être refusé
|
|
||||||
if (error.message.includes('Forbidden') ||
|
|
||||||
error.message.includes('Insufficient permissions') ||
|
|
||||||
error.message.includes('not authorized') ||
|
|
||||||
error.message.includes('own merchant')) {
|
|
||||||
// Succès - l'isolation fonctionne
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.log(`✅ ${testName} - Success (${duration}ms)`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
testName,
|
|
||||||
success: true,
|
|
||||||
duration,
|
|
||||||
data: { isolationWorking: true }
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Erreur inattendue
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.error(`❌ ${testName} - Failed: ${error.message}`);
|
|
||||||
return { testName, success: false, duration, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== TESTS AVANCÉS (conservés depuis la version originale) =====
|
|
||||||
private async testStatsAndReports(): Promise<TestResult> {
|
|
||||||
const testName = 'Stats and Reports Test';
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const serviceToken = await this.tokenService.acquireServiceAccountToken();
|
|
||||||
const decodedToken = this.tokenService.decodeToken(serviceToken);
|
|
||||||
const serviceAccountId = decodedToken.sub;
|
|
||||||
|
|
||||||
const stats = await this.hubUsersService.getHubUsersStats(serviceAccountId);
|
|
||||||
const activity = await this.hubUsersService.getHubUserActivity(serviceAccountId);
|
|
||||||
const sessions = await this.hubUsersService.getActiveHubSessions(serviceAccountId);
|
|
||||||
|
|
||||||
// Validation basique des stats
|
|
||||||
if (typeof stats.totalAdmins !== 'number' || typeof stats.totalSupport !== 'number') {
|
|
||||||
throw new Error('Stats validation failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.log(`✅ ${testName} - Success (${duration}ms)`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
testName,
|
|
||||||
success: true,
|
|
||||||
duration,
|
|
||||||
data: {
|
|
||||||
stats,
|
|
||||||
activityCount: activity.length,
|
|
||||||
sessionCount: sessions.length
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.error(`❌ ${testName} - Failed: ${error.message}`);
|
|
||||||
return { testName, success: false, duration, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async testHealthCheck(): Promise<TestResult> {
|
|
||||||
const testName = 'Health Check Test';
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const serviceToken = await this.tokenService.acquireServiceAccountToken();
|
|
||||||
const decodedToken = this.tokenService.decodeToken(serviceToken);
|
|
||||||
const serviceAccountId = decodedToken.sub;
|
|
||||||
|
|
||||||
const health = await this.hubUsersService.checkHubUsersHealth(serviceAccountId);
|
|
||||||
|
|
||||||
if (!health.status || !health.stats || !Array.isArray(health.issues)) {
|
|
||||||
throw new Error('Health check validation failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.log(`✅ ${testName} - Success (${duration}ms)`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
testName,
|
|
||||||
success: true,
|
|
||||||
duration,
|
|
||||||
data: { healthStatus: health.status }
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.error(`❌ ${testName} - Failed: ${error.message}`);
|
|
||||||
return { testName, success: false, duration, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async testSecurityValidations(): Promise<TestResult> {
|
|
||||||
const testName = 'Security Validations Test';
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const serviceToken = await this.tokenService.acquireServiceAccountToken();
|
|
||||||
const decodedToken = this.tokenService.decodeToken(serviceToken);
|
|
||||||
const serviceAccountId = decodedToken.sub;
|
|
||||||
|
|
||||||
// Test de la méthode canUserManageHubUsers
|
|
||||||
const canManage = await this.hubUsersService.canUserManageHubUsers(serviceAccountId);
|
|
||||||
|
|
||||||
if (!canManage) {
|
|
||||||
throw new Error('Service account should be able to manage hub users');
|
|
||||||
}
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.log(`✅ ${testName} - Success (${duration}ms)`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
testName,
|
|
||||||
success: true,
|
|
||||||
duration,
|
|
||||||
data: { canManageHubUsers: canManage }
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.error(`❌ ${testName} - Failed: ${error.message}`);
|
|
||||||
return { testName, success: false, duration, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== NETTOYAGE =====
|
|
||||||
private async cleanupTestUsers(): Promise<void> {
|
|
||||||
this.logger.log('🧹 Cleaning up test users...');
|
|
||||||
|
|
||||||
const serviceToken = await this.tokenService.acquireServiceAccountToken();
|
|
||||||
const decodedToken = this.tokenService.decodeToken(serviceToken);
|
|
||||||
const serviceAccountId = decodedToken.sub;
|
|
||||||
|
|
||||||
// Nettoyer les utilisateurs hub
|
|
||||||
for (const [key, userInfo] of Object.entries(this.testUsers)) {
|
|
||||||
try {
|
|
||||||
await this.hubUsersService.deleteHubUser(userInfo.id, serviceAccountId);
|
|
||||||
this.logger.log(`✅ Deleted test user: ${key} (${userInfo.username})`);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.warn(`⚠️ Could not delete test user ${key}: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.testUsers = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async cleanupTestMerchants(): Promise<void> {
|
|
||||||
this.logger.log('🧹 Cleaning up test merchants...');
|
|
||||||
|
|
||||||
// Implémentez la logique de nettoyage des merchants de test
|
|
||||||
this.testMerchants = {};
|
|
||||||
this.testMerchantUsers = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== LOGGING ET RAPPORTS =====
|
|
||||||
private logTestSummary(summary: StartupTestSummary): void {
|
|
||||||
this.logger.log('='.repeat(60));
|
|
||||||
this.logger.log('🎯 PARALLEL ISOLATION STARTUP TEST SUMMARY');
|
|
||||||
this.logger.log('='.repeat(60));
|
|
||||||
this.logger.log(`📊 Total Tests: ${summary.totalTests}`);
|
|
||||||
this.logger.log(`✅ Passed: ${summary.passedTests}`);
|
|
||||||
this.logger.log(`❌ Failed: ${summary.failedTests}`);
|
|
||||||
this.logger.log(`⏱️ Total Duration: ${summary.totalDuration}ms`);
|
|
||||||
this.logger.log('-'.repeat(60));
|
|
||||||
|
|
||||||
summary.results.forEach(result => {
|
|
||||||
const status = result.success ? '✅' : '❌';
|
|
||||||
this.logger.log(`${status} ${result.testName}: ${result.duration}ms`);
|
|
||||||
if (!result.success) {
|
|
||||||
this.logger.log(` ERROR: ${result.error}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logger.log('='.repeat(60));
|
|
||||||
|
|
||||||
if (summary.failedTests === 0) {
|
|
||||||
this.logger.log('🚀 ALL TESTS PASSED! System is ready with proper isolation.');
|
|
||||||
} else {
|
|
||||||
this.logger.warn(`⚠️ ${summary.failedTests} test(s) failed. Please check the logs above.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== MÉTHODES PUBLIQUES POUR USAGE MANUEL =====
|
|
||||||
async runQuickTest(): Promise<StartupTestSummary> {
|
|
||||||
this.logger.log('🔍 Running quick startup test...');
|
|
||||||
return this.runAllTests();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTestStatus(): Promise<{ status: 'healthy' | 'degraded' | 'unhealthy'; details: string }> {
|
|
||||||
try {
|
|
||||||
const summary = await this.runAllTests();
|
|
||||||
const successRate = (summary.passedTests / summary.totalTests) * 100;
|
|
||||||
|
|
||||||
if (successRate === 100) {
|
|
||||||
return { status: 'healthy', details: 'All tests passed successfully' };
|
|
||||||
} else if (successRate >= 80) {
|
|
||||||
return { status: 'degraded', details: `${summary.failedTests} test(s) failed` };
|
|
||||||
} else {
|
|
||||||
return { status: 'unhealthy', details: 'Multiple test failures detected' };
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return { status: 'unhealthy', details: `Test execution failed: ${error.message}` };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -23,9 +23,6 @@ export interface DecodedToken {
|
|||||||
realm_access?: { roles: string[] };
|
realm_access?: { roles: string[] };
|
||||||
resource_access?: { [key: string]: { roles: string[] } };
|
resource_access?: { [key: string]: { roles: string[] } };
|
||||||
merchantPartnerId?: string;
|
merchantPartnerId?: string;
|
||||||
// Ajout des claims personnalisés
|
|
||||||
'merchant-partner-id'?: string;
|
|
||||||
'user-type'?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -6,12 +6,12 @@ import {
|
|||||||
Delete,
|
Delete,
|
||||||
Body,
|
Body,
|
||||||
Param,
|
Param,
|
||||||
Query,
|
|
||||||
UseGuards,
|
|
||||||
Request,
|
Request,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
ParseUUIDPipe,
|
ParseUUIDPipe,
|
||||||
|
ForbiddenException,
|
||||||
|
Logger,
|
||||||
BadRequestException
|
BadRequestException
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
@ -20,110 +20,134 @@ import {
|
|||||||
ApiResponse,
|
ApiResponse,
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
ApiParam,
|
ApiParam,
|
||||||
ApiQuery,
|
ApiProperty,
|
||||||
ApiProperty
|
getSchemaPath
|
||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
IsEmail,
|
||||||
|
IsEnum,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsBoolean,
|
||||||
|
MinLength,
|
||||||
|
IsString,
|
||||||
|
ValidateIf
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
import { HubUsersService } from '../services/hub-users.service';
|
import { HubUsersService } from '../services/hub-users.service';
|
||||||
import { UserRole, HubUser, CreateHubUserData, HubUserStats, HubHealthStatus, HubUserActivity, MerchantStats } from '../../auth/services/keycloak-user.model';
|
import { UserRole, UserType } from '../../auth/services/keycloak-user.model';
|
||||||
import { JwtAuthGuard } from '../../auth/guards/jwt.guard';
|
|
||||||
import { RESOURCES } from '../../constants/resources';
|
import { RESOURCES } from '../../constants/resources';
|
||||||
import { SCOPES } from '../../constants/scopes';
|
import { SCOPES } from '../../constants/scopes';
|
||||||
import { Resource, Scopes } from 'nest-keycloak-connect';
|
import { Resource, Scopes } from 'nest-keycloak-connect';
|
||||||
|
import { CreateUserData, User } from '../models/hub-user.model';
|
||||||
|
|
||||||
export class LoginDto {
|
// ===== DTO SPÉCIFIQUES AUX HUB USERS =====
|
||||||
@ApiProperty({ description: 'Username' })
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Password' })
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TokenResponseDto {
|
|
||||||
@ApiProperty({ description: 'Access token' })
|
|
||||||
access_token: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Refresh token' })
|
|
||||||
refresh_token?: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Token type' })
|
|
||||||
token_type: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Expires in (seconds)' })
|
|
||||||
expires_in: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Refresh expires in (seconds)' })
|
|
||||||
refresh_expires_in?: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Scope' })
|
|
||||||
scope?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// DTOs pour les utilisateurs Hub
|
|
||||||
export class CreateHubUserDto {
|
export class CreateHubUserDto {
|
||||||
@ApiProperty({ description: 'Username for the hub user' })
|
@ApiProperty({ description: 'Username for the user' })
|
||||||
|
@IsNotEmpty({ message: 'Username is required' })
|
||||||
|
@IsString()
|
||||||
|
@MinLength(3, { message: 'Username must be at least 3 characters' })
|
||||||
username: string;
|
username: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Email address' })
|
@ApiProperty({ description: 'Email address' })
|
||||||
|
@IsNotEmpty({ message: 'Email is required' })
|
||||||
|
@IsEmail({}, { message: 'Invalid email format' })
|
||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'First name' })
|
@ApiProperty({ description: 'First name' })
|
||||||
|
@IsNotEmpty({ message: 'First name is required' })
|
||||||
|
@IsString()
|
||||||
firstName: string;
|
firstName: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Last name' })
|
@ApiProperty({ description: 'Last name' })
|
||||||
|
@IsNotEmpty({ message: 'Last name is required' })
|
||||||
|
@IsString()
|
||||||
lastName: string;
|
lastName: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Password for the user' })
|
@ApiProperty({ description: 'Password for the user' })
|
||||||
|
@IsNotEmpty({ message: 'Password is required' })
|
||||||
|
@IsString()
|
||||||
|
@MinLength(8, { message: 'Password must be at least 8 characters' })
|
||||||
password: string;
|
password: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
enum: [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER],
|
enum: UserRole,
|
||||||
description: 'Role for the hub user'
|
description: 'Role for the user',
|
||||||
|
examples: [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER]
|
||||||
})
|
})
|
||||||
role: UserRole.DCB_ADMIN | UserRole.DCB_SUPPORT | UserRole.DCB_PARTNER;
|
@IsEnum(UserRole, { message: 'Invalid role' })
|
||||||
|
@IsNotEmpty({ message: 'Role is required' })
|
||||||
|
role: UserRole;
|
||||||
|
|
||||||
@ApiProperty({ required: false, default: true })
|
@ApiProperty({ required: false, default: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean({ message: 'Enabled must be a boolean' })
|
||||||
enabled?: boolean = true;
|
enabled?: boolean = true;
|
||||||
|
|
||||||
@ApiProperty({ required: false, default: false })
|
@ApiProperty({ required: false, default: true })
|
||||||
emailVerified?: boolean = false;
|
@IsOptional()
|
||||||
|
@IsBoolean({ message: 'EmailVerified must be a boolean' })
|
||||||
|
emailVerified?: boolean = true;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
enum: UserType,
|
||||||
|
description: 'Type of user',
|
||||||
|
example: UserType.HUB
|
||||||
|
})
|
||||||
|
@IsEnum(UserType, { message: 'Invalid user type' })
|
||||||
|
@IsNotEmpty({ message: 'User type is required' })
|
||||||
|
userType: UserType;
|
||||||
|
|
||||||
|
// Pas de merchantPartnerId pour les hub users
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateHubUserDto {
|
export class UpdateHubUserDto {
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
|
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
|
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail()
|
||||||
email?: string;
|
email?: string;
|
||||||
|
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateUserRoleDto {
|
export class ResetHubUserPasswordDto {
|
||||||
|
@ApiProperty({ description: 'New password' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
@MinLength(8)
|
||||||
|
newPassword: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, default: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
temporary?: boolean = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateHubUserRoleDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
enum: [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER],
|
enum: [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER],
|
||||||
description: 'New role for the user'
|
description: 'New role for the user'
|
||||||
})
|
})
|
||||||
role: UserRole.DCB_ADMIN | UserRole.DCB_SUPPORT | UserRole.DCB_PARTNER;
|
@IsEnum(UserRole, { message: 'Invalid role' })
|
||||||
|
@IsNotEmpty({ message: 'Role is required' })
|
||||||
|
role: UserRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ResetPasswordDto {
|
|
||||||
@ApiProperty({ description: 'New password' })
|
|
||||||
newPassword: string;
|
|
||||||
|
|
||||||
@ApiProperty({ required: false, default: true })
|
|
||||||
temporary?: boolean = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SuspendMerchantDto {
|
|
||||||
@ApiProperty({ description: 'Reason for suspension' })
|
|
||||||
reason: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// DTOs pour les réponses
|
|
||||||
export class HubUserResponse {
|
export class HubUserResponse {
|
||||||
@ApiProperty({ description: 'User ID' })
|
@ApiProperty({ description: 'User ID' })
|
||||||
id: string;
|
id: string;
|
||||||
@ -144,7 +168,7 @@ export class HubUserResponse {
|
|||||||
enum: [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER],
|
enum: [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER],
|
||||||
description: 'User role'
|
description: 'User role'
|
||||||
})
|
})
|
||||||
role: UserRole.DCB_ADMIN | UserRole.DCB_SUPPORT | UserRole.DCB_PARTNER;
|
role: UserRole;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Whether the user is enabled' })
|
@ApiProperty({ description: 'Whether the user is enabled' })
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -158,95 +182,50 @@ export class HubUserResponse {
|
|||||||
@ApiProperty({ description: 'User creator username' })
|
@ApiProperty({ description: 'User creator username' })
|
||||||
createdByUsername: string;
|
createdByUsername: string;
|
||||||
|
|
||||||
|
@ApiProperty({ enum: ['HUB'], description: 'User type' })
|
||||||
|
userType: UserType;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Creation timestamp' })
|
@ApiProperty({ description: 'Creation timestamp' })
|
||||||
createdTimestamp: number;
|
createdTimestamp: number;
|
||||||
|
|
||||||
@ApiProperty({ required: false, description: 'Last login timestamp' })
|
@ApiProperty({ required: false, description: 'Last login timestamp' })
|
||||||
lastLogin?: number;
|
lastLogin?: number;
|
||||||
|
|
||||||
@ApiProperty({ enum: ['HUB'], description: 'User type' })
|
|
||||||
userType: 'HUB';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HubUsersStatsResponse {
|
export class HubUserProfileResponse {
|
||||||
@ApiProperty({ description: 'Total admin users' })
|
|
||||||
totalAdmins: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Total support users' })
|
|
||||||
totalSupport: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Active users count' })
|
|
||||||
activeUsers: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Inactive users count' })
|
|
||||||
inactiveUsers: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Users pending activation' })
|
|
||||||
pendingActivation: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MerchantStatsResponse {
|
|
||||||
@ApiProperty({ description: 'Total merchants' })
|
|
||||||
totalMerchants: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Active merchants count' })
|
|
||||||
activeMerchants: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Suspended merchants count' })
|
|
||||||
suspendedMerchants: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Pending merchants count' })
|
|
||||||
pendingMerchants: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Total merchant users' })
|
|
||||||
totalUsers: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class HealthStatusResponse {
|
|
||||||
@ApiProperty({ enum: ['healthy', 'degraded', 'unhealthy'] })
|
|
||||||
status: string;
|
|
||||||
|
|
||||||
@ApiProperty({ type: [String], description: 'Health issues detected' })
|
|
||||||
issues: string[];
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'System statistics' })
|
|
||||||
stats: HubUsersStatsResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UserActivityResponse {
|
|
||||||
@ApiProperty({ description: 'User information' })
|
|
||||||
user: HubUserResponse;
|
|
||||||
|
|
||||||
@ApiProperty({ required: false, description: 'Last login date' })
|
|
||||||
lastLogin?: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SessionResponse {
|
|
||||||
@ApiProperty({ description: 'User ID' })
|
@ApiProperty({ description: 'User ID' })
|
||||||
userId: string;
|
id: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Username' })
|
@ApiProperty({ description: 'Username' })
|
||||||
username: string;
|
username: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Last access date' })
|
@ApiProperty({ description: 'Email address' })
|
||||||
lastAccess: Date;
|
email: string;
|
||||||
}
|
|
||||||
|
|
||||||
export class PermissionResponse {
|
@ApiProperty({ description: 'First name' })
|
||||||
@ApiProperty({ description: 'Whether user can manage hub users' })
|
firstName: string;
|
||||||
canManageHubUsers: boolean;
|
|
||||||
}
|
@ApiProperty({ description: 'Last name' })
|
||||||
|
lastName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Whether the email is verified' })
|
||||||
|
emailVerified: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Whether the user is enabled' })
|
||||||
|
enabled: boolean;
|
||||||
|
|
||||||
export class AvailableRolesResponse {
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
type: [Object],
|
description: 'Client roles',
|
||||||
description: 'Available roles'
|
type: [String],
|
||||||
|
enum: [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER]
|
||||||
})
|
})
|
||||||
roles: Array<{
|
clientRoles: string[];
|
||||||
value: UserRole;
|
|
||||||
label: string;
|
@ApiProperty({ required: false, description: 'User creator ID' })
|
||||||
description: string;
|
createdBy?: string;
|
||||||
}>;
|
|
||||||
|
@ApiProperty({ required: false, description: 'User creator username' })
|
||||||
|
createdByUsername?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MessageResponse {
|
export class MessageResponse {
|
||||||
@ -255,39 +234,50 @@ export class MessageResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mapper functions
|
// Mapper functions
|
||||||
function mapToHubUserResponse(hubUser: HubUser): HubUserResponse {
|
function mapToHubUserResponse(user: User): HubUserResponse {
|
||||||
return {
|
return {
|
||||||
id: hubUser.id,
|
id: user.id,
|
||||||
username: hubUser.username,
|
username: user.username,
|
||||||
email: hubUser.email,
|
email: user.email,
|
||||||
firstName: hubUser.firstName,
|
firstName: user.firstName,
|
||||||
lastName: hubUser.lastName,
|
lastName: user.lastName,
|
||||||
role: hubUser.role,
|
role: user.role,
|
||||||
enabled: hubUser.enabled,
|
enabled: user.enabled,
|
||||||
emailVerified: hubUser.emailVerified,
|
emailVerified: user.emailVerified,
|
||||||
createdBy: hubUser.createdBy,
|
createdBy: user.createdBy,
|
||||||
createdByUsername: hubUser.createdByUsername,
|
createdByUsername: user.createdByUsername,
|
||||||
createdTimestamp: hubUser.createdTimestamp,
|
userType: user.userType,
|
||||||
lastLogin: hubUser.lastLogin,
|
createdTimestamp: user.createdTimestamp,
|
||||||
userType: hubUser.userType,
|
lastLogin: user.lastLogin,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapToUserActivityResponse(activity: HubUserActivity): UserActivityResponse {
|
function mapToHubUserProfileResponse(profile: any): HubUserProfileResponse {
|
||||||
return {
|
return {
|
||||||
user: mapToHubUserResponse(activity.user),
|
id: profile.id,
|
||||||
lastLogin: activity.lastLogin
|
username: profile.username,
|
||||||
|
email: profile.email,
|
||||||
|
firstName: profile.firstName,
|
||||||
|
lastName: profile.lastName,
|
||||||
|
emailVerified: profile.emailVerified,
|
||||||
|
enabled: profile.enabled,
|
||||||
|
clientRoles: profile.clientRoles,
|
||||||
|
createdBy: profile.createdBy,
|
||||||
|
createdByUsername: profile.createdByUsername,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== CONTROLLER POUR LES UTILISATEURS HUB =====
|
||||||
|
|
||||||
@ApiTags('Hub Users')
|
@ApiTags('Hub Users')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@Controller('hub-users')
|
@Controller('hub-users')
|
||||||
@Resource(RESOURCES.HUB_USER || RESOURCES.MERCHANT_USER)
|
@Resource(RESOURCES.HUB_USER)
|
||||||
export class HubUsersController {
|
export class HubUsersController {
|
||||||
constructor(private readonly hubUsersService: HubUsersService) {}
|
constructor(private readonly usersService: HubUsersService) {}
|
||||||
|
private readonly logger = new Logger(HubUsersController.name);
|
||||||
|
|
||||||
// ===== GESTION DES UTILISATEURS HUB =====
|
// ===== ROUTES SANS PARAMÈTRES =====
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
@ -299,56 +289,30 @@ export class HubUsersController {
|
|||||||
description: 'Hub users retrieved successfully',
|
description: 'Hub users retrieved successfully',
|
||||||
type: [HubUserResponse]
|
type: [HubUserResponse]
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 403, description: 'Forbidden - insufficient permissions' })
|
|
||||||
|
|
||||||
@Scopes(SCOPES.READ)
|
@Scopes(SCOPES.READ)
|
||||||
async getAllHubUsers(@Request() req): Promise<HubUserResponse[]> {
|
async getAllHubUsers(@Request() req): Promise<HubUserResponse[]> {
|
||||||
const userId = req.user.sub;
|
const userId = req.user.sub;
|
||||||
const users = await this.hubUsersService.getAllHubUsers(userId);
|
const users = await this.usersService.getAllHubUsers(userId);
|
||||||
return users.map(mapToHubUserResponse);
|
return users.map(mapToHubUserResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('role/:role')
|
@Get('partners/dcb-partners')
|
||||||
@ApiOperation({ summary: 'Get hub users by role' })
|
@ApiOperation({
|
||||||
|
summary: 'Get all DCB_PARTNER users only',
|
||||||
|
description: 'Returns only DCB_PARTNER users (excludes DCB_ADMIN and DCB_SUPPORT)'
|
||||||
|
})
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
description: 'Hub users retrieved successfully',
|
description: 'DCB_PARTNER users retrieved successfully',
|
||||||
type: [HubUserResponse]
|
type: [HubUserResponse]
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 400, description: 'Invalid role' })
|
|
||||||
@ApiParam({ name: 'role', enum: UserRole, description: 'User role' })
|
|
||||||
|
|
||||||
@Scopes(SCOPES.READ)
|
@Scopes(SCOPES.READ)
|
||||||
async getHubUsersByRole(
|
async getAllDcbPartners(@Request() req): Promise<HubUserResponse[]> {
|
||||||
@Param('role') role: UserRole,
|
|
||||||
@Request() req
|
|
||||||
): Promise<HubUserResponse[]> {
|
|
||||||
const userId = req.user.sub;
|
const userId = req.user.sub;
|
||||||
const validRole = this.hubUsersService.validateHubRoleFromString(role);
|
const users = await this.usersService.getAllDcbPartners(userId);
|
||||||
const users = await this.hubUsersService.getHubUsersByRole(validRole, userId);
|
|
||||||
return users.map(mapToHubUserResponse);
|
return users.map(mapToHubUserResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
|
||||||
@ApiOperation({ summary: 'Get hub user by ID' })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 200,
|
|
||||||
description: 'Hub user retrieved successfully',
|
|
||||||
type: HubUserResponse
|
|
||||||
})
|
|
||||||
@ApiResponse({ status: 404, description: 'Hub user not found' })
|
|
||||||
@ApiParam({ name: 'id', description: 'User ID' })
|
|
||||||
|
|
||||||
@Scopes(SCOPES.READ)
|
|
||||||
async getHubUserById(
|
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
|
||||||
@Request() req
|
|
||||||
): Promise<HubUserResponse> {
|
|
||||||
const userId = req.user.sub;
|
|
||||||
const user = await this.hubUsersService.getHubUserById(id, userId);
|
|
||||||
return mapToHubUserResponse(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Create a new hub user',
|
summary: 'Create a new hub user',
|
||||||
@ -359,22 +323,162 @@ export class HubUsersController {
|
|||||||
description: 'Hub user created successfully',
|
description: 'Hub user created successfully',
|
||||||
type: HubUserResponse
|
type: HubUserResponse
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 400, description: 'Bad request - invalid data or duplicate user' })
|
|
||||||
@ApiResponse({ status: 403, description: 'Forbidden - insufficient permissions' })
|
|
||||||
|
|
||||||
@Scopes(SCOPES.WRITE)
|
@Scopes(SCOPES.WRITE)
|
||||||
async createHubUser(
|
async createHubUser(
|
||||||
@Body() createHubUserDto: CreateHubUserDto,
|
@Body() createUserDto: CreateHubUserDto,
|
||||||
|
@Request() req
|
||||||
|
): Promise<HubUserResponse> {
|
||||||
|
|
||||||
|
// Debug complet
|
||||||
|
this.logger.debug('🔍 === CONTROLLER - CREATE HUB USER ===');
|
||||||
|
this.logger.debug('Request headers:', req.headers);
|
||||||
|
this.logger.debug('Content-Type:', req.headers['content-type']);
|
||||||
|
this.logger.debug('Raw body exists:', !!req.body);
|
||||||
|
this.logger.debug('CreateHubUserDto received:', createUserDto);
|
||||||
|
this.logger.debug('DTO structure:', {
|
||||||
|
username: createUserDto.username,
|
||||||
|
email: createUserDto.email,
|
||||||
|
firstName: createUserDto.firstName,
|
||||||
|
lastName: createUserDto.lastName,
|
||||||
|
role: createUserDto.role,
|
||||||
|
userType: createUserDto.userType,
|
||||||
|
});
|
||||||
|
this.logger.debug('====================================');
|
||||||
|
|
||||||
|
// Validation manuelle renforcée
|
||||||
|
const requiredFields = ['username', 'email', 'firstName', 'lastName', 'password', 'role', 'userType'];
|
||||||
|
const missingFields = requiredFields.filter(field => !createUserDto[field]);
|
||||||
|
|
||||||
|
if (missingFields.length > 0) {
|
||||||
|
throw new BadRequestException(`Missing required fields: ${missingFields.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createUserDto.userType !== UserType.HUB) {
|
||||||
|
throw new BadRequestException('User type must be HUB for hub users');
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = req.user.sub;
|
||||||
|
|
||||||
|
const userData: CreateUserData = {
|
||||||
|
...createUserDto,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logger.debug('UserData passed to service:', userData);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await this.usersService.createHubUser(userId, userData);
|
||||||
|
return mapToHubUserResponse(user);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error creating hub user:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ===== ROUTES AVEC PARAMÈTRES STATIQUES =====
|
||||||
|
|
||||||
|
@Get('all-users')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Get global users overview',
|
||||||
|
description: 'Returns hub users and all merchant users (Admin only)'
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Global overview retrieved successfully',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
hubUsers: { type: 'array', items: { $ref: getSchemaPath(HubUserResponse) } },
|
||||||
|
merchantUsers: { type: 'array', items: { $ref: getSchemaPath(HubUserResponse) } },
|
||||||
|
statistics: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
totalHubUsers: { type: 'number' },
|
||||||
|
totalMerchantUsers: { type: 'number' },
|
||||||
|
totalUsers: { type: 'number' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
@Scopes(SCOPES.READ)
|
||||||
|
async getGlobalUsersOverview(@Request() req): Promise<any> {
|
||||||
|
const userId = req.user.sub;
|
||||||
|
|
||||||
|
const isAdmin = await this.usersService.isUserHubAdminOrSupport(userId);
|
||||||
|
if (!isAdmin) {
|
||||||
|
throw new ForbiddenException('Only Hub administrators can access global overview');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hubUsers = await this.usersService.getAllHubUsers(userId);
|
||||||
|
const merchantUsers = await this.usersService.getMyMerchantUsers(userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hubUsers: hubUsers.map(mapToHubUserResponse),
|
||||||
|
merchantUsers: merchantUsers.map(mapToHubUserResponse),
|
||||||
|
statistics: {
|
||||||
|
totalHubUsers: hubUsers.length,
|
||||||
|
totalMerchantUsers: merchantUsers.length,
|
||||||
|
totalUsers: hubUsers.length + merchantUsers.length
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('profile/:id')
|
||||||
|
@ApiOperation({ summary: 'Get complete user profile' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'User profile retrieved successfully',
|
||||||
|
type: HubUserProfileResponse
|
||||||
|
})
|
||||||
|
@ApiParam({ name: 'id', description: 'User ID' })
|
||||||
|
@Scopes(SCOPES.READ)
|
||||||
|
async getCompleteUserProfile(
|
||||||
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
|
@Request() req
|
||||||
|
): Promise<HubUserProfileResponse> {
|
||||||
|
const tokenUser = req.user;
|
||||||
|
const profile = await this.usersService.getCompleteUserProfile(id, tokenUser);
|
||||||
|
return mapToHubUserProfileResponse(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('role/:role')
|
||||||
|
@ApiOperation({ summary: 'Get hub users by role' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Hub users retrieved successfully',
|
||||||
|
type: [HubUserResponse]
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'role',
|
||||||
|
enum: [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER],
|
||||||
|
description: 'User role'
|
||||||
|
})
|
||||||
|
@Scopes(SCOPES.READ)
|
||||||
|
async getHubUsersByRole(
|
||||||
|
@Param('role') role: UserRole,
|
||||||
|
@Request() req
|
||||||
|
): Promise<HubUserResponse[]> {
|
||||||
|
const userId = req.user.sub;
|
||||||
|
const users = await this.usersService.getHubUsersByRole(role, userId);
|
||||||
|
return users.map(mapToHubUserResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== ROUTES AVEC PARAMÈTRES DYNAMIQUES =====
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: 'Get hub user by ID' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Hub user retrieved successfully',
|
||||||
|
type: HubUserResponse
|
||||||
|
})
|
||||||
|
@ApiParam({ name: 'id', description: 'User ID' })
|
||||||
|
@Scopes(SCOPES.READ)
|
||||||
|
async getHubUserById(
|
||||||
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
@Request() req
|
@Request() req
|
||||||
): Promise<HubUserResponse> {
|
): Promise<HubUserResponse> {
|
||||||
const userId = req.user.sub;
|
const userId = req.user.sub;
|
||||||
|
const user = await this.usersService.getHubUserById(id, userId);
|
||||||
const userData: CreateHubUserData = {
|
|
||||||
...createHubUserDto,
|
|
||||||
createdBy: userId,
|
|
||||||
};
|
|
||||||
|
|
||||||
const user = await this.hubUsersService.createHubUser(userId, userData);
|
|
||||||
return mapToHubUserResponse(user);
|
return mapToHubUserResponse(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -385,20 +489,32 @@ export class HubUsersController {
|
|||||||
description: 'Hub user updated successfully',
|
description: 'Hub user updated successfully',
|
||||||
type: HubUserResponse
|
type: HubUserResponse
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 404, description: 'Hub user not found' })
|
|
||||||
@ApiParam({ name: 'id', description: 'User ID' })
|
@ApiParam({ name: 'id', description: 'User ID' })
|
||||||
|
|
||||||
@Scopes(SCOPES.WRITE)
|
@Scopes(SCOPES.WRITE)
|
||||||
async updateHubUser(
|
async updateHubUser(
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
@Body() updateHubUserDto: UpdateHubUserDto,
|
@Body() updateUserDto: UpdateHubUserDto,
|
||||||
@Request() req
|
@Request() req
|
||||||
): Promise<HubUserResponse> {
|
): Promise<HubUserResponse> {
|
||||||
const userId = req.user.sub;
|
const userId = req.user.sub;
|
||||||
const user = await this.hubUsersService.updateHubUser(id, updateHubUserDto, userId);
|
const user = await this.usersService.updateHubUser(id, updateUserDto, userId);
|
||||||
return mapToHubUserResponse(user);
|
return mapToHubUserResponse(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@ApiOperation({ summary: 'Delete a hub user' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Hub user deleted successfully' })
|
||||||
|
@ApiParam({ name: 'id', description: 'User ID' })
|
||||||
|
@Scopes(SCOPES.DELETE)
|
||||||
|
async deleteHubUser(
|
||||||
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
|
@Request() req
|
||||||
|
): Promise<MessageResponse> {
|
||||||
|
const userId = req.user.sub;
|
||||||
|
await this.usersService.deleteHubUser(id, userId);
|
||||||
|
return { message: 'Hub user deleted successfully' };
|
||||||
|
}
|
||||||
|
|
||||||
@Put(':id/role')
|
@Put(':id/role')
|
||||||
@ApiOperation({ summary: 'Update hub user role' })
|
@ApiOperation({ summary: 'Update hub user role' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
@ -406,54 +522,31 @@ export class HubUsersController {
|
|||||||
description: 'User role updated successfully',
|
description: 'User role updated successfully',
|
||||||
type: HubUserResponse
|
type: HubUserResponse
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 403, description: 'Forbidden - only DCB_ADMIN can change roles' })
|
|
||||||
@ApiParam({ name: 'id', description: 'User ID' })
|
@ApiParam({ name: 'id', description: 'User ID' })
|
||||||
|
|
||||||
@Scopes(SCOPES.WRITE)
|
@Scopes(SCOPES.WRITE)
|
||||||
async updateHubUserRole(
|
async updateHubUserRole(
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
@Body() updateRoleDto: UpdateUserRoleDto,
|
@Body() updateRoleDto: UpdateHubUserRoleDto,
|
||||||
@Request() req
|
@Request() req
|
||||||
): Promise<HubUserResponse> {
|
): Promise<HubUserResponse> {
|
||||||
const userId = req.user.sub;
|
const userId = req.user.sub;
|
||||||
const user = await this.hubUsersService.updateHubUserRole(id, updateRoleDto.role, userId);
|
const user = await this.usersService.updateHubUserRole(id, updateRoleDto.role, userId);
|
||||||
return mapToHubUserResponse(user);
|
return mapToHubUserResponse(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
|
||||||
@ApiOperation({ summary: 'Delete a hub user' })
|
|
||||||
@ApiResponse({ status: 200, description: 'Hub user deleted successfully' })
|
|
||||||
@ApiResponse({ status: 400, description: 'Cannot delete own account or last admin' })
|
|
||||||
@ApiResponse({ status: 404, description: 'Hub user not found' })
|
|
||||||
@ApiParam({ name: 'id', description: 'User ID' })
|
|
||||||
|
|
||||||
@Scopes(SCOPES.DELETE)
|
|
||||||
async deleteHubUser(
|
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
|
||||||
@Request() req
|
|
||||||
): Promise<MessageResponse> {
|
|
||||||
const userId = req.user.sub;
|
|
||||||
await this.hubUsersService.deleteHubUser(id, userId);
|
|
||||||
return { message: 'Hub user deleted successfully' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== GESTION DES MOTS DE PASSE =====
|
|
||||||
|
|
||||||
@Post(':id/reset-password')
|
@Post(':id/reset-password')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: 'Reset hub user password' })
|
@ApiOperation({ summary: 'Reset hub user password' })
|
||||||
@ApiResponse({ status: 200, description: 'Password reset successfully' })
|
@ApiResponse({ status: 200, description: 'Password reset successfully' })
|
||||||
@ApiResponse({ status: 404, description: 'Hub user not found' })
|
|
||||||
@ApiParam({ name: 'id', description: 'User ID' })
|
@ApiParam({ name: 'id', description: 'User ID' })
|
||||||
|
|
||||||
@Scopes(SCOPES.WRITE)
|
@Scopes(SCOPES.WRITE)
|
||||||
async resetHubUserPassword(
|
async resetHubUserPassword(
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
@Body() resetPasswordDto: ResetPasswordDto,
|
@Body() resetPasswordDto: ResetHubUserPasswordDto,
|
||||||
@Request() req
|
@Request() req
|
||||||
): Promise<MessageResponse> {
|
): Promise<MessageResponse> {
|
||||||
const userId = req.user.sub;
|
const userId = req.user.sub;
|
||||||
await this.hubUsersService.resetHubUserPassword(
|
await this.usersService.resetUserPassword(
|
||||||
id,
|
id,
|
||||||
resetPasswordDto.newPassword,
|
resetPasswordDto.newPassword,
|
||||||
resetPasswordDto.temporary,
|
resetPasswordDto.temporary,
|
||||||
@ -461,213 +554,4 @@ export class HubUsersController {
|
|||||||
);
|
);
|
||||||
return { message: 'Password reset successfully' };
|
return { message: 'Password reset successfully' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/send-reset-email')
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@ApiOperation({ summary: 'Send password reset email to hub user' })
|
|
||||||
@ApiResponse({ status: 200, description: 'Password reset email sent successfully' })
|
|
||||||
@ApiResponse({ status: 404, description: 'Hub user not found' })
|
|
||||||
@ApiParam({ name: 'id', description: 'User ID' })
|
|
||||||
|
|
||||||
@Scopes(SCOPES.WRITE)
|
|
||||||
async sendHubUserPasswordResetEmail(
|
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
|
||||||
@Request() req
|
|
||||||
): Promise<MessageResponse> {
|
|
||||||
const userId = req.user.sub;
|
|
||||||
await this.hubUsersService.sendHubUserPasswordResetEmail(id, userId);
|
|
||||||
return { message: 'Password reset email sent successfully' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== GESTION DES MERCHANTS (DCB_PARTNER) =====
|
|
||||||
|
|
||||||
@Get('merchants/all')
|
|
||||||
@ApiOperation({ summary: 'Get all merchant partners' })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 200,
|
|
||||||
description: 'Merchant partners retrieved successfully',
|
|
||||||
type: [HubUserResponse]
|
|
||||||
})
|
|
||||||
|
|
||||||
@Scopes(SCOPES.READ)
|
|
||||||
async getAllMerchants(@Request() req): Promise<HubUserResponse[]> {
|
|
||||||
const userId = req.user.sub;
|
|
||||||
const merchants = await this.hubUsersService.getAllMerchants(userId);
|
|
||||||
return merchants.map(mapToHubUserResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('merchants/:merchantId')
|
|
||||||
@ApiOperation({ summary: 'Get merchant partner by ID' })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 200,
|
|
||||||
description: 'Merchant partner retrieved successfully',
|
|
||||||
type: HubUserResponse
|
|
||||||
})
|
|
||||||
@ApiResponse({ status: 404, description: 'Merchant partner not found' })
|
|
||||||
@ApiParam({ name: 'merchantId', description: 'Merchant Partner ID' })
|
|
||||||
|
|
||||||
@Scopes(SCOPES.READ)
|
|
||||||
async getMerchantPartnerById(
|
|
||||||
@Param('merchantId', ParseUUIDPipe) merchantId: string,
|
|
||||||
@Request() req
|
|
||||||
): Promise<HubUserResponse> {
|
|
||||||
const userId = req.user.sub;
|
|
||||||
const merchant = await this.hubUsersService.getMerchantPartnerById(merchantId, userId);
|
|
||||||
return mapToHubUserResponse(merchant);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Put('merchants/:merchantId')
|
|
||||||
@ApiOperation({ summary: 'Update a merchant partner' })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 200,
|
|
||||||
description: 'Merchant partner updated successfully',
|
|
||||||
type: HubUserResponse
|
|
||||||
})
|
|
||||||
@ApiResponse({ status: 404, description: 'Merchant partner not found' })
|
|
||||||
@ApiParam({ name: 'merchantId', description: 'Merchant Partner ID' })
|
|
||||||
|
|
||||||
@Scopes(SCOPES.WRITE)
|
|
||||||
async updateMerchantPartner(
|
|
||||||
@Param('merchantId', ParseUUIDPipe) merchantId: string,
|
|
||||||
@Body() updateHubUserDto: UpdateHubUserDto,
|
|
||||||
@Request() req
|
|
||||||
): Promise<HubUserResponse> {
|
|
||||||
const userId = req.user.sub;
|
|
||||||
const merchant = await this.hubUsersService.updateMerchantPartner(merchantId, updateHubUserDto, userId);
|
|
||||||
return mapToHubUserResponse(merchant);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('merchants/:merchantId/suspend')
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@ApiOperation({ summary: 'Suspend a merchant partner and all its users' })
|
|
||||||
@ApiResponse({ status: 200, description: 'Merchant partner suspended successfully' })
|
|
||||||
@ApiResponse({ status: 404, description: 'Merchant partner not found' })
|
|
||||||
@ApiParam({ name: 'merchantId', description: 'Merchant Partner ID' })
|
|
||||||
|
|
||||||
@Scopes(SCOPES.WRITE)
|
|
||||||
async suspendMerchantPartner(
|
|
||||||
@Param('merchantId', ParseUUIDPipe) merchantId: string,
|
|
||||||
@Body() suspendMerchantDto: SuspendMerchantDto,
|
|
||||||
@Request() req
|
|
||||||
): Promise<MessageResponse> {
|
|
||||||
const userId = req.user.sub;
|
|
||||||
await this.hubUsersService.suspendMerchantPartner(merchantId, suspendMerchantDto.reason, userId);
|
|
||||||
return { message: 'Merchant partner suspended successfully' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== STATISTIQUES ET RAPPORTS =====
|
|
||||||
|
|
||||||
@Get('stats/overview')
|
|
||||||
@ApiOperation({ summary: 'Get hub users statistics overview' })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 200,
|
|
||||||
description: 'Statistics retrieved successfully',
|
|
||||||
type: HubUsersStatsResponse
|
|
||||||
})
|
|
||||||
|
|
||||||
@Scopes(SCOPES.READ)
|
|
||||||
async getHubUsersStats(@Request() req): Promise<HubUsersStatsResponse> {
|
|
||||||
const userId = req.user.sub;
|
|
||||||
return this.hubUsersService.getHubUsersStats(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('stats/merchants')
|
|
||||||
@ApiOperation({ summary: 'Get merchants statistics' })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 200,
|
|
||||||
description: 'Merchants statistics retrieved successfully',
|
|
||||||
type: MerchantStatsResponse
|
|
||||||
})
|
|
||||||
|
|
||||||
@Scopes(SCOPES.READ)
|
|
||||||
async getMerchantStats(@Request() req): Promise<MerchantStatsResponse> {
|
|
||||||
const userId = req.user.sub;
|
|
||||||
return this.hubUsersService.getMerchantStats(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('activity/recent')
|
|
||||||
@ApiOperation({ summary: 'Get recent hub user activity' })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 200,
|
|
||||||
description: 'Activity retrieved successfully',
|
|
||||||
type: [UserActivityResponse]
|
|
||||||
})
|
|
||||||
|
|
||||||
@Scopes(SCOPES.READ)
|
|
||||||
async getHubUserActivity(@Request() req): Promise<UserActivityResponse[]> {
|
|
||||||
const userId = req.user.sub;
|
|
||||||
const activities = await this.hubUsersService.getHubUserActivity(userId);
|
|
||||||
return activities.map(mapToUserActivityResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('sessions/active')
|
|
||||||
@ApiOperation({ summary: 'Get active hub sessions' })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 200,
|
|
||||||
description: 'Active sessions retrieved successfully',
|
|
||||||
type: [SessionResponse]
|
|
||||||
})
|
|
||||||
|
|
||||||
@Scopes(SCOPES.READ)
|
|
||||||
async getActiveHubSessions(@Request() req): Promise<SessionResponse[]> {
|
|
||||||
const userId = req.user.sub;
|
|
||||||
const sessions = await this.hubUsersService.getActiveHubSessions(userId);
|
|
||||||
return sessions.map(session => ({
|
|
||||||
userId: session.userId,
|
|
||||||
username: session.username,
|
|
||||||
lastAccess: session.lastAccess
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== SANTÉ ET UTILITAIRES =====
|
|
||||||
|
|
||||||
@Get('health/status')
|
|
||||||
@ApiOperation({ summary: 'Get hub users health status' })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 200,
|
|
||||||
description: 'Health status retrieved successfully',
|
|
||||||
type: HealthStatusResponse
|
|
||||||
})
|
|
||||||
|
|
||||||
@Scopes(SCOPES.READ)
|
|
||||||
async checkHubUsersHealth(@Request() req): Promise<HealthStatusResponse> {
|
|
||||||
const userId = req.user.sub;
|
|
||||||
return this.hubUsersService.checkHubUsersHealth(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('me/permissions')
|
|
||||||
@ApiOperation({ summary: 'Check if current user can manage hub users' })
|
|
||||||
@ApiResponse({ status: 200, description: 'Permissions check completed' })
|
|
||||||
async canUserManageHubUsers(@Request() req): Promise<PermissionResponse> {
|
|
||||||
const userId = req.user.sub;
|
|
||||||
const canManage = await this.hubUsersService.canUserManageHubUsers(userId);
|
|
||||||
return { canManageHubUsers: canManage };
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('roles/available')
|
|
||||||
@ApiOperation({ summary: 'Get available hub roles' })
|
|
||||||
@ApiResponse({ status: 200, description: 'Available roles retrieved successfully' })
|
|
||||||
|
|
||||||
@Scopes(SCOPES.READ)
|
|
||||||
async getAvailableHubRoles(): Promise<AvailableRolesResponse> {
|
|
||||||
const roles = [
|
|
||||||
{
|
|
||||||
value: UserRole.DCB_ADMIN,
|
|
||||||
label: 'DCB Admin',
|
|
||||||
description: 'Full administrative access to the entire system'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: UserRole.DCB_SUPPORT,
|
|
||||||
label: 'DCB Support',
|
|
||||||
description: 'Support access with limited administrative capabilities'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: UserRole.DCB_PARTNER,
|
|
||||||
label: 'DCB Partner',
|
|
||||||
description: 'Merchant partner with access to their own merchant ecosystem'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return { roles };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -6,13 +6,14 @@ import {
|
|||||||
Delete,
|
Delete,
|
||||||
Body,
|
Body,
|
||||||
Param,
|
Param,
|
||||||
Query,
|
|
||||||
UseGuards,
|
|
||||||
Request,
|
Request,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
ParseUUIDPipe,
|
ParseUUIDPipe,
|
||||||
BadRequestException
|
ForbiddenException,
|
||||||
|
Logger,
|
||||||
|
BadRequestException,
|
||||||
|
InternalServerErrorException
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
ApiTags,
|
ApiTags,
|
||||||
@ -20,68 +21,136 @@ import {
|
|||||||
ApiResponse,
|
ApiResponse,
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
ApiParam,
|
ApiParam,
|
||||||
ApiQuery,
|
|
||||||
ApiProperty
|
ApiProperty
|
||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
import { MerchantUsersService, MerchantUser, CreateMerchantUserData } from '../services/merchant-users.service';
|
|
||||||
import { JwtAuthGuard } from '../../auth/guards/jwt.guard';
|
import {
|
||||||
import { UserRole } from '../../auth/services/keycloak-user.model';
|
IsEmail,
|
||||||
|
IsEnum,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsBoolean,
|
||||||
|
MinLength,
|
||||||
|
IsString,
|
||||||
|
ValidateIf
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
import { HubUsersService } from '../services/hub-users.service';
|
||||||
|
import { UserRole, UserType } from '../../auth/services/keycloak-user.model';
|
||||||
|
|
||||||
import { RESOURCES } from '../../constants/resources';
|
import { RESOURCES } from '../../constants/resources';
|
||||||
import { SCOPES } from '../../constants/scopes';
|
import { SCOPES } from '../../constants/scopes';
|
||||||
import { Resource, Scopes } from 'nest-keycloak-connect';
|
import { Resource, Scopes } from 'nest-keycloak-connect';
|
||||||
|
import { CreateUserData, User } from '../models/hub-user.model';
|
||||||
|
|
||||||
|
// ===== DTO SPÉCIFIQUES AUX MERCHANT USERS =====
|
||||||
|
|
||||||
export class CreateMerchantUserDto {
|
export class CreateMerchantUserDto {
|
||||||
@ApiProperty({ description: 'Username for the merchant user' })
|
@ApiProperty({ description: 'Username for the user' })
|
||||||
|
@IsNotEmpty({ message: 'Username is required' })
|
||||||
|
@IsString()
|
||||||
|
@MinLength(3, { message: 'Username must be at least 3 characters' })
|
||||||
username: string;
|
username: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Email address' })
|
@ApiProperty({ description: 'Email address' })
|
||||||
|
@IsNotEmpty({ message: 'Email is required' })
|
||||||
|
@IsEmail({}, { message: 'Invalid email format' })
|
||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'First name' })
|
@ApiProperty({ description: 'First name' })
|
||||||
|
@IsNotEmpty({ message: 'First name is required' })
|
||||||
|
@IsString()
|
||||||
firstName: string;
|
firstName: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Last name' })
|
@ApiProperty({ description: 'Last name' })
|
||||||
|
@IsNotEmpty({ message: 'Last name is required' })
|
||||||
|
@IsString()
|
||||||
lastName: string;
|
lastName: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Password for the user' })
|
@ApiProperty({ description: 'Password for the user' })
|
||||||
|
@IsNotEmpty({ message: 'Password is required' })
|
||||||
|
@IsString()
|
||||||
|
@MinLength(8, { message: 'Password must be at least 8 characters' })
|
||||||
password: string;
|
password: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
enum: [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT],
|
enum: UserRole,
|
||||||
description: 'Role for the merchant user'
|
description: 'Role for the user',
|
||||||
|
examples: [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT]
|
||||||
})
|
})
|
||||||
role: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT;
|
@IsEnum(UserRole, { message: 'Invalid role' })
|
||||||
|
@IsNotEmpty({ message: 'Role is required' })
|
||||||
|
role: UserRole;
|
||||||
|
|
||||||
@ApiProperty({ required: false, default: true })
|
@ApiProperty({ required: false, default: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean({ message: 'Enabled must be a boolean' })
|
||||||
enabled?: boolean = true;
|
enabled?: boolean = true;
|
||||||
|
|
||||||
@ApiProperty({ required: false, default: false })
|
@ApiProperty({ required: false, default: true })
|
||||||
emailVerified?: boolean = false;
|
@IsOptional()
|
||||||
|
@IsBoolean({ message: 'EmailVerified must be a boolean' })
|
||||||
|
emailVerified?: boolean = true;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Merchant partner ID' })
|
@ApiProperty({
|
||||||
merchantPartnerId: string;
|
enum: UserType,
|
||||||
|
description: 'Type of user',
|
||||||
|
example: UserType.MERCHANT_PARTNER
|
||||||
|
})
|
||||||
|
@IsEnum(UserType, { message: 'Invalid user type' })
|
||||||
|
@IsNotEmpty({ message: 'User type is required' })
|
||||||
|
userType: UserType;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateIf((o) => o.userType === UserType.MERCHANT_PARTNER && o.role !== UserRole.DCB_PARTNER)
|
||||||
|
@IsString({ message: 'Merchant partner ID must be a string' })
|
||||||
|
merchantPartnerId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ResetMerchantUserPasswordDto {
|
||||||
|
@ApiProperty({ description: 'New password' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
@MinLength(8)
|
||||||
|
newPassword: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, default: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
temporary?: boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateMerchantUserDto {
|
export class UpdateMerchantUserDto {
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
|
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
|
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail()
|
||||||
email?: string;
|
email?: string;
|
||||||
|
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ResetPasswordDto {
|
export class UpdateMerchantUserRoleDto {
|
||||||
@ApiProperty({ description: 'New password' })
|
@ApiProperty({
|
||||||
newPassword: string;
|
enum: [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER],
|
||||||
|
description: 'New role for the user'
|
||||||
@ApiProperty({ required: false, default: true })
|
})
|
||||||
temporary?: boolean = true;
|
@IsEnum(UserRole, { message: 'Invalid role' })
|
||||||
|
@IsNotEmpty({ message: 'Role is required' })
|
||||||
|
role: UserRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MerchantUserResponse {
|
export class MerchantUserResponse {
|
||||||
@ -101,10 +170,10 @@ export class MerchantUserResponse {
|
|||||||
lastName: string;
|
lastName: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
enum: [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT],
|
enum: [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER, UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT],
|
||||||
description: 'User role'
|
description: 'User role'
|
||||||
})
|
})
|
||||||
role: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT;
|
role: UserRole;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Whether the user is enabled' })
|
@ApiProperty({ description: 'Whether the user is enabled' })
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -112,8 +181,8 @@ export class MerchantUserResponse {
|
|||||||
@ApiProperty({ description: 'Whether the email is verified' })
|
@ApiProperty({ description: 'Whether the email is verified' })
|
||||||
emailVerified: boolean;
|
emailVerified: boolean;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Merchant partner ID' })
|
@ApiProperty({ required: false, description: 'Merchant partner ID' })
|
||||||
merchantPartnerId: string;
|
merchantPartnerId?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'User creator ID' })
|
@ApiProperty({ description: 'User creator ID' })
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
@ -121,150 +190,130 @@ export class MerchantUserResponse {
|
|||||||
@ApiProperty({ description: 'User creator username' })
|
@ApiProperty({ description: 'User creator username' })
|
||||||
createdByUsername: string;
|
createdByUsername: string;
|
||||||
|
|
||||||
|
@ApiProperty({ enum: ['HUB', 'MERCHANT'], description: 'User type' })
|
||||||
|
userType: 'HUB' | 'MERCHANT';
|
||||||
|
|
||||||
@ApiProperty({ description: 'Creation timestamp' })
|
@ApiProperty({ description: 'Creation timestamp' })
|
||||||
createdTimestamp: number;
|
createdTimestamp: number;
|
||||||
|
|
||||||
@ApiProperty({ required: false, description: 'Last login timestamp' })
|
@ApiProperty({ required: false, description: 'Last login timestamp' })
|
||||||
lastLogin?: number;
|
lastLogin?: number;
|
||||||
|
|
||||||
@ApiProperty({ enum: ['MERCHANT'], description: 'User type' })
|
|
||||||
userType: 'MERCHANT';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MerchantUsersStatsResponse {
|
export class UserProfileResponse {
|
||||||
@ApiProperty({ description: 'Total admin users' })
|
@ApiProperty({ description: 'User ID' })
|
||||||
totalAdmins: number;
|
id: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Total manager users' })
|
@ApiProperty({ description: 'Username' })
|
||||||
totalManagers: number;
|
username: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Total support users' })
|
@ApiProperty({ description: 'Email address' })
|
||||||
totalSupport: number;
|
email: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Total users' })
|
@ApiProperty({ description: 'First name' })
|
||||||
totalUsers: number;
|
firstName: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Active users count' })
|
@ApiProperty({ description: 'Last name' })
|
||||||
activeUsers: number;
|
lastName: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Inactive users count' })
|
@ApiProperty({ description: 'Whether the email is verified' })
|
||||||
inactiveUsers: number;
|
emailVerified: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Whether the user is enabled' })
|
||||||
|
enabled: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Client roles', type: [String] })
|
||||||
|
clientRoles: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, description: 'Merchant partner ID' })
|
||||||
|
merchantPartnerId?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, description: 'User creator ID' })
|
||||||
|
createdBy?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, description: 'User creator username' })
|
||||||
|
createdByUsername?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AvailableRolesResponse {
|
export class MessageResponse {
|
||||||
@ApiProperty({
|
@ApiProperty({ description: 'Response message' })
|
||||||
type: [Object],
|
message: string;
|
||||||
description: 'Available roles with permissions'
|
|
||||||
})
|
|
||||||
roles: Array<{
|
|
||||||
value: UserRole;
|
|
||||||
label: string;
|
|
||||||
description: string;
|
|
||||||
allowedForCreation: boolean;
|
|
||||||
}>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mapper function pour convertir MerchantUser en MerchantUserResponse
|
// Mapper functions
|
||||||
function mapToMerchantUserResponse(merchantUser: MerchantUser): MerchantUserResponse {
|
function mapToMerchantUserResponse(user: User): MerchantUserResponse {
|
||||||
return {
|
return {
|
||||||
id: merchantUser.id,
|
id: user.id,
|
||||||
username: merchantUser.username,
|
username: user.username,
|
||||||
email: merchantUser.email,
|
email: user.email,
|
||||||
firstName: merchantUser.firstName,
|
firstName: user.firstName,
|
||||||
lastName: merchantUser.lastName,
|
lastName: user.lastName,
|
||||||
role: merchantUser.role,
|
role: user.role,
|
||||||
enabled: merchantUser.enabled,
|
enabled: user.enabled,
|
||||||
emailVerified: merchantUser.emailVerified,
|
emailVerified: user.emailVerified,
|
||||||
merchantPartnerId: merchantUser.merchantPartnerId,
|
merchantPartnerId: user.merchantPartnerId,
|
||||||
createdBy: merchantUser.createdBy,
|
createdBy: user.createdBy,
|
||||||
createdByUsername: merchantUser.createdByUsername,
|
createdByUsername: user.createdByUsername,
|
||||||
createdTimestamp: merchantUser.createdTimestamp,
|
userType: user.userType,
|
||||||
lastLogin: merchantUser.lastLogin,
|
createdTimestamp: user.createdTimestamp,
|
||||||
userType: merchantUser.userType,
|
lastLogin: user.lastLogin,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapToUserProfileResponse(profile: any): UserProfileResponse {
|
||||||
|
return {
|
||||||
|
id: profile.id,
|
||||||
|
username: profile.username,
|
||||||
|
email: profile.email,
|
||||||
|
firstName: profile.firstName,
|
||||||
|
lastName: profile.lastName,
|
||||||
|
emailVerified: profile.emailVerified,
|
||||||
|
enabled: profile.enabled,
|
||||||
|
clientRoles: profile.clientRoles,
|
||||||
|
merchantPartnerId: profile.merchantPartnerId,
|
||||||
|
createdBy: profile.createdBy,
|
||||||
|
createdByUsername: profile.createdByUsername,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== CONTROLLER POUR LES UTILISATEURS MERCHANT =====
|
||||||
|
|
||||||
@ApiTags('Merchant Users')
|
@ApiTags('Merchant Users')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@Controller('merchant-users')
|
@Controller('merchant-users')
|
||||||
@Resource(RESOURCES.MERCHANT_USER)
|
@Resource(RESOURCES.MERCHANT_USER)
|
||||||
export class MerchantUsersController {
|
export class MerchantUsersController {
|
||||||
constructor(private readonly merchantUsersService: MerchantUsersService) {}
|
constructor(private readonly usersService: HubUsersService) {}
|
||||||
|
|
||||||
// ===== RÉCUPÉRATION D'UTILISATEURS =====
|
// ===== ROUTES SANS PARAMÈTRES D'ABORD =====
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Get merchant users for current user merchant',
|
summary: 'Get merchant users for current user merchant',
|
||||||
description: 'Returns merchant users based on the current user merchant partner ID'
|
description: 'Returns merchant users. Hub admins/support see all merchants users, others see only their own merchant users.'
|
||||||
})
|
})
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
description: 'Merchant users retrieved successfully',
|
description: 'Merchant users retrieved successfully',
|
||||||
type: [MerchantUserResponse]
|
type: [MerchantUserResponse]
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 403, description: 'Forbidden - insufficient permissions' })
|
|
||||||
@Resource(RESOURCES.MERCHANT_USER)
|
|
||||||
@Scopes(SCOPES.READ)
|
@Scopes(SCOPES.READ)
|
||||||
async getMyMerchantUsers(@Request() req): Promise<MerchantUserResponse[]> {
|
async getMyMerchantUsers(@Request() req): Promise<MerchantUserResponse[]> {
|
||||||
const userId = req.user.sub;
|
const userId = req.user.sub;
|
||||||
|
|
||||||
// Récupérer le merchantPartnerId de l'utilisateur courant
|
try {
|
||||||
const userMerchantId = await this.getUserMerchantPartnerId(userId);
|
const users = await this.usersService.getMyMerchantUsers(userId);
|
||||||
if (!userMerchantId) {
|
|
||||||
throw new BadRequestException('Current user is not associated with a merchant partner');
|
|
||||||
}
|
|
||||||
|
|
||||||
const users = await this.merchantUsersService.getMerchantUsersByPartner(userMerchantId, userId);
|
|
||||||
return users.map(mapToMerchantUserResponse);
|
return users.map(mapToMerchantUserResponse);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof BadRequestException || error instanceof ForbiddenException) {
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('partner/:partnerId')
|
throw new InternalServerErrorException('Could not retrieve merchant users');
|
||||||
@ApiOperation({
|
|
||||||
summary: 'Get merchant users by partner ID',
|
|
||||||
description: 'Returns all merchant users for a specific merchant partner'
|
|
||||||
})
|
|
||||||
@ApiResponse({
|
|
||||||
status: 200,
|
|
||||||
description: 'Merchant users retrieved successfully',
|
|
||||||
type: [MerchantUserResponse]
|
|
||||||
})
|
|
||||||
@ApiResponse({ status: 403, description: 'Forbidden - insufficient permissions' })
|
|
||||||
@ApiResponse({ status: 404, description: 'Merchant partner not found' })
|
|
||||||
@ApiParam({ name: 'partnerId', description: 'Merchant Partner ID' })
|
|
||||||
@Resource(RESOURCES.MERCHANT_USER)
|
|
||||||
@Scopes(SCOPES.READ)
|
|
||||||
async getMerchantUsersByPartner(
|
|
||||||
@Param('partnerId', ParseUUIDPipe) partnerId: string,
|
|
||||||
@Request() req
|
|
||||||
): Promise<MerchantUserResponse[]> {
|
|
||||||
const userId = req.user.sub;
|
|
||||||
const users = await this.merchantUsersService.getMerchantUsersByPartner(partnerId, userId);
|
|
||||||
return users.map(mapToMerchantUserResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
|
||||||
@ApiOperation({ summary: 'Get merchant user by ID' })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 200,
|
|
||||||
description: 'Merchant user retrieved successfully',
|
|
||||||
type: MerchantUserResponse
|
|
||||||
})
|
|
||||||
@ApiResponse({ status: 404, description: 'Merchant user not found' })
|
|
||||||
@ApiParam({ name: 'id', description: 'Merchant User ID' })
|
|
||||||
@Resource(RESOURCES.MERCHANT_USER)
|
|
||||||
@Scopes(SCOPES.READ)
|
|
||||||
async getMerchantUserById(
|
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
|
||||||
@Request() req
|
|
||||||
): Promise<MerchantUserResponse> {
|
|
||||||
const userId = req.user.sub;
|
|
||||||
const user = await this.merchantUsersService.getMerchantUserById(id, userId);
|
|
||||||
return mapToMerchantUserResponse(user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== CRÉATION D'UTILISATEURS =====
|
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Create a new merchant user',
|
summary: 'Create a new merchant user',
|
||||||
@ -275,26 +324,85 @@ export class MerchantUsersController {
|
|||||||
description: 'Merchant user created successfully',
|
description: 'Merchant user created successfully',
|
||||||
type: MerchantUserResponse
|
type: MerchantUserResponse
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 400, description: 'Bad request - invalid data or duplicate user' })
|
|
||||||
@ApiResponse({ status: 403, description: 'Forbidden - insufficient permissions' })
|
|
||||||
@Resource(RESOURCES.MERCHANT_USER)
|
|
||||||
@Scopes(SCOPES.WRITE)
|
@Scopes(SCOPES.WRITE)
|
||||||
async createMerchantUser(
|
async createMerchantUser(
|
||||||
@Body() createMerchantUserDto: CreateMerchantUserDto,
|
@Body() createUserDto: CreateMerchantUserDto,
|
||||||
@Request() req
|
@Request() req
|
||||||
): Promise<MerchantUserResponse> {
|
): Promise<MerchantUserResponse> {
|
||||||
const userId = req.user.sub;
|
const userId = req.user.sub;
|
||||||
|
|
||||||
const userData: CreateMerchantUserData = {
|
if (!createUserDto.merchantPartnerId && !createUserDto.role.includes(UserRole.DCB_PARTNER)) {
|
||||||
...createMerchantUserDto,
|
throw new BadRequestException('merchantPartnerId is required for merchant users except DCB_PARTNER');
|
||||||
createdBy: userId,
|
}
|
||||||
|
|
||||||
|
const userData: CreateUserData = {
|
||||||
|
...createUserDto,
|
||||||
};
|
};
|
||||||
|
|
||||||
const user = await this.merchantUsersService.createMerchantUser(userId, userData);
|
const user = await this.usersService.createMerchantUser(userId, userData);
|
||||||
return mapToMerchantUserResponse(user);
|
return mapToMerchantUserResponse(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== MISE À JOUR D'UTILISATEURS =====
|
// ===== ROUTES AVEC PARAMÈTRES STATIQUES AVANT LES PARAMÈTRES DYNAMIQUES =====
|
||||||
|
|
||||||
|
@Get('profile/:id')
|
||||||
|
@ApiOperation({ summary: 'Get complete user profile' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'User profile retrieved successfully',
|
||||||
|
type: UserProfileResponse
|
||||||
|
})
|
||||||
|
@ApiParam({ name: 'id', description: 'User ID' })
|
||||||
|
@Scopes(SCOPES.READ)
|
||||||
|
async getCompleteUserProfile(
|
||||||
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
|
@Request() req
|
||||||
|
): Promise<UserProfileResponse> {
|
||||||
|
const tokenUser = req.user;
|
||||||
|
const profile = await this.usersService.getCompleteUserProfile(id, tokenUser);
|
||||||
|
return mapToUserProfileResponse(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('merchant-partner/:userId')
|
||||||
|
@ApiOperation({ summary: 'Get merchant partner ID for a user' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Merchant partner ID retrieved successfully',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
merchantPartnerId: { type: 'string', nullable: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
@ApiParam({ name: 'userId', description: 'User ID' })
|
||||||
|
@Scopes(SCOPES.READ)
|
||||||
|
async getUserMerchantPartnerId(
|
||||||
|
@Param('userId', ParseUUIDPipe) userId: string
|
||||||
|
): Promise<{ merchantPartnerId: string | null }> {
|
||||||
|
const merchantPartnerId = await this.usersService.getUserMerchantPartnerId(userId);
|
||||||
|
return { merchantPartnerId };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== ROUTES AVEC PARAMÈTRES DYNAMIQUES EN DERNIER =====
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: 'Get merchant user by ID' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Merchant user retrieved successfully',
|
||||||
|
type: MerchantUserResponse
|
||||||
|
})
|
||||||
|
@ApiParam({ name: 'id', description: 'User ID' })
|
||||||
|
@Scopes(SCOPES.READ)
|
||||||
|
async getMerchantUserById(
|
||||||
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
|
@Request() req
|
||||||
|
): Promise<MerchantUserResponse> {
|
||||||
|
const userId = req.user.sub;
|
||||||
|
const user = await this.usersService.getMerchantUserById(id, userId);
|
||||||
|
return mapToMerchantUserResponse(user);
|
||||||
|
}
|
||||||
|
|
||||||
@Put(':id')
|
@Put(':id')
|
||||||
@ApiOperation({ summary: 'Update a merchant user' })
|
@ApiOperation({ summary: 'Update a merchant user' })
|
||||||
@ -303,212 +411,69 @@ export class MerchantUsersController {
|
|||||||
description: 'Merchant user updated successfully',
|
description: 'Merchant user updated successfully',
|
||||||
type: MerchantUserResponse
|
type: MerchantUserResponse
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 404, description: 'Merchant user not found' })
|
@ApiParam({ name: 'id', description: 'User ID' })
|
||||||
@ApiParam({ name: 'id', description: 'Merchant User ID' })
|
|
||||||
@Resource(RESOURCES.MERCHANT_USER)
|
|
||||||
@Scopes(SCOPES.WRITE)
|
@Scopes(SCOPES.WRITE)
|
||||||
async updateMerchantUser(
|
async updateMerchantUser(
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
@Body() updateMerchantUserDto: UpdateMerchantUserDto,
|
@Body() updateUserDto: UpdateMerchantUserDto,
|
||||||
@Request() req
|
@Request() req
|
||||||
): Promise<MerchantUserResponse> {
|
): Promise<MerchantUserResponse> {
|
||||||
const userId = req.user.sub;
|
const userId = req.user.sub;
|
||||||
|
const user = await this.usersService.updateMerchantUser(id, updateUserDto, userId);
|
||||||
// Pour l'instant, on suppose que la mise à jour se fait via Keycloak
|
return mapToMerchantUserResponse(user);
|
||||||
// Vous devrez implémenter updateMerchantUser dans le service
|
|
||||||
throw new BadRequestException('Update merchant user not implemented yet');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== SUPPRESSION D'UTILISATEURS =====
|
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@ApiOperation({ summary: 'Delete a merchant user' })
|
@ApiOperation({ summary: 'Delete a merchant user' })
|
||||||
@ApiResponse({ status: 200, description: 'Merchant user deleted successfully' })
|
@ApiResponse({ status: 200, description: 'Merchant user deleted successfully' })
|
||||||
@ApiResponse({ status: 404, description: 'Merchant user not found' })
|
@ApiParam({ name: 'id', description: 'User ID' })
|
||||||
@ApiParam({ name: 'id', description: 'Merchant User ID' })
|
|
||||||
@Resource(RESOURCES.MERCHANT_USER)
|
|
||||||
@Scopes(SCOPES.DELETE)
|
@Scopes(SCOPES.DELETE)
|
||||||
async deleteMerchantUser(
|
async deleteMerchantUser(
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
@Request() req
|
@Request() req
|
||||||
): Promise<{ message: string }> {
|
): Promise<MessageResponse> {
|
||||||
const userId = req.user.sub;
|
const userId = req.user.sub;
|
||||||
|
await this.usersService.deleteMerchantUser(id, userId);
|
||||||
// Vous devrez implémenter deleteMerchantUser dans le service
|
return { message: 'Merchant user deleted successfully' };
|
||||||
throw new BadRequestException('Delete merchant user not implemented yet');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== GESTION DES MOTS DE PASSE =====
|
@Put(':id/role')
|
||||||
|
@ApiOperation({ summary: 'Update merchant user role' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'User role updated successfully',
|
||||||
|
type: MerchantUserResponse
|
||||||
|
})
|
||||||
|
@ApiParam({ name: 'id', description: 'User ID' })
|
||||||
|
@Scopes(SCOPES.WRITE)
|
||||||
|
async updateMerchantUserRole(
|
||||||
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
|
@Body() updateRoleDto: UpdateMerchantUserRoleDto,
|
||||||
|
@Request() req
|
||||||
|
): Promise<MerchantUserResponse> {
|
||||||
|
const userId = req.user.sub;
|
||||||
|
const user = await this.usersService.updateMerchantUserRole(id, updateRoleDto.role, userId);
|
||||||
|
return mapToMerchantUserResponse(user);
|
||||||
|
}
|
||||||
|
|
||||||
@Post(':id/reset-password')
|
@Post(':id/reset-password')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: 'Reset merchant user password' })
|
@ApiOperation({ summary: 'Reset merchant user password' })
|
||||||
@ApiResponse({ status: 200, description: 'Password reset successfully' })
|
@ApiResponse({ status: 200, description: 'Password reset successfully' })
|
||||||
@ApiResponse({ status: 404, description: 'Merchant user not found' })
|
@ApiParam({ name: 'id', description: 'User ID' })
|
||||||
@ApiParam({ name: 'id', description: 'Merchant User ID' })
|
|
||||||
@Resource(RESOURCES.MERCHANT_USER)
|
|
||||||
@Scopes(SCOPES.WRITE)
|
@Scopes(SCOPES.WRITE)
|
||||||
async resetMerchantUserPassword(
|
async resetMerchantUserPassword(
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
@Body() resetPasswordDto: ResetPasswordDto,
|
@Body() resetPasswordDto: ResetMerchantUserPasswordDto,
|
||||||
@Request() req
|
@Request() req
|
||||||
): Promise<{ message: string }> {
|
): Promise<MessageResponse> {
|
||||||
const userId = req.user.sub;
|
const userId = req.user.sub;
|
||||||
|
await this.usersService.resetUserPassword(
|
||||||
// Vous devrez implémenter resetMerchantUserPassword dans le service
|
id,
|
||||||
throw new BadRequestException('Reset merchant user password not implemented yet');
|
resetPasswordDto.newPassword,
|
||||||
}
|
resetPasswordDto.temporary,
|
||||||
|
userId
|
||||||
// ===== STATISTIQUES ET RAPPORTS =====
|
|
||||||
|
|
||||||
@Get('stats/overview')
|
|
||||||
@ApiOperation({ summary: 'Get merchant users statistics overview' })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 200,
|
|
||||||
description: 'Statistics retrieved successfully',
|
|
||||||
type: MerchantUsersStatsResponse
|
|
||||||
})
|
|
||||||
@Resource(RESOURCES.MERCHANT_USER)
|
|
||||||
@Scopes(SCOPES.READ)
|
|
||||||
async getMerchantUsersStats(@Request() req): Promise<MerchantUsersStatsResponse> {
|
|
||||||
const userId = req.user.sub;
|
|
||||||
|
|
||||||
// Récupérer le merchantPartnerId de l'utilisateur courant
|
|
||||||
const userMerchantId = await this.getUserMerchantPartnerId(userId);
|
|
||||||
if (!userMerchantId) {
|
|
||||||
throw new BadRequestException('Current user is not associated with a merchant partner');
|
|
||||||
}
|
|
||||||
|
|
||||||
const users = await this.merchantUsersService.getMerchantUsersByPartner(userMerchantId, userId);
|
|
||||||
|
|
||||||
const stats: MerchantUsersStatsResponse = {
|
|
||||||
totalAdmins: users.filter(user => user.role === UserRole.DCB_PARTNER_ADMIN).length,
|
|
||||||
totalManagers: users.filter(user => user.role === UserRole.DCB_PARTNER_MANAGER).length,
|
|
||||||
totalSupport: users.filter(user => user.role === UserRole.DCB_PARTNER_SUPPORT).length,
|
|
||||||
totalUsers: users.length,
|
|
||||||
activeUsers: users.filter(user => user.enabled).length,
|
|
||||||
inactiveUsers: users.filter(user => !user.enabled).length,
|
|
||||||
};
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('search')
|
|
||||||
@ApiOperation({ summary: 'Search merchant users' })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 200,
|
|
||||||
description: 'Search results retrieved successfully',
|
|
||||||
type: [MerchantUserResponse]
|
|
||||||
})
|
|
||||||
@ApiQuery({ name: 'query', required: false, description: 'Search query (username, email, first name, last name)' })
|
|
||||||
@ApiQuery({ name: 'role', required: false, enum: UserRole, description: 'Filter by role' })
|
|
||||||
@ApiQuery({ name: 'enabled', required: false, type: Boolean, description: 'Filter by enabled status' })
|
|
||||||
@Resource(RESOURCES.MERCHANT_USER)
|
|
||||||
@Scopes(SCOPES.READ)
|
|
||||||
async searchMerchantUsers(
|
|
||||||
@Request() req,
|
|
||||||
@Query('query') query?: string,
|
|
||||||
@Query('role') role?: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT,
|
|
||||||
@Query('enabled') enabled?: boolean
|
|
||||||
): Promise<MerchantUserResponse[]> {
|
|
||||||
const userId = req.user.sub;
|
|
||||||
|
|
||||||
// Récupérer le merchantPartnerId de l'utilisateur courant
|
|
||||||
const userMerchantId = await this.getUserMerchantPartnerId(userId);
|
|
||||||
if (!userMerchantId) {
|
|
||||||
throw new BadRequestException('Current user is not associated with a merchant partner');
|
|
||||||
}
|
|
||||||
|
|
||||||
let users = await this.merchantUsersService.getMerchantUsersByPartner(userMerchantId, userId);
|
|
||||||
|
|
||||||
// Appliquer les filtres
|
|
||||||
if (query) {
|
|
||||||
const lowerQuery = query.toLowerCase();
|
|
||||||
users = users.filter(user =>
|
|
||||||
user.username.toLowerCase().includes(lowerQuery) ||
|
|
||||||
user.email.toLowerCase().includes(lowerQuery) ||
|
|
||||||
user.firstName.toLowerCase().includes(lowerQuery) ||
|
|
||||||
user.lastName.toLowerCase().includes(lowerQuery)
|
|
||||||
);
|
);
|
||||||
}
|
return { message: 'Password reset successfully' };
|
||||||
|
|
||||||
if (role) {
|
|
||||||
users = users.filter(user => user.role === role);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enabled !== undefined) {
|
|
||||||
users = users.filter(user => user.enabled === enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
return users.map(mapToMerchantUserResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== UTILITAIRES =====
|
|
||||||
|
|
||||||
@Get('roles/available')
|
|
||||||
@ApiOperation({ summary: 'Get available merchant roles' })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 200,
|
|
||||||
description: 'Available roles retrieved successfully',
|
|
||||||
type: AvailableRolesResponse
|
|
||||||
})
|
|
||||||
@Resource(RESOURCES.MERCHANT_USER)
|
|
||||||
@Scopes(SCOPES.READ)
|
|
||||||
async getAvailableMerchantRoles(@Request() req): Promise<AvailableRolesResponse> {
|
|
||||||
const userId = req.user.sub;
|
|
||||||
const userRoles = await this.getUserRoles(userId);
|
|
||||||
|
|
||||||
const isPartner = userRoles.includes(UserRole.DCB_PARTNER);
|
|
||||||
const isPartnerAdmin = userRoles.includes(UserRole.DCB_PARTNER_ADMIN);
|
|
||||||
const isHubAdmin = userRoles.some(role =>
|
|
||||||
[UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT].includes(role)
|
|
||||||
);
|
|
||||||
|
|
||||||
const roles = [
|
|
||||||
{
|
|
||||||
value: UserRole.DCB_PARTNER_ADMIN,
|
|
||||||
label: 'Partner Admin',
|
|
||||||
description: 'Full administrative access within the merchant partner',
|
|
||||||
allowedForCreation: isPartner || isHubAdmin
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: UserRole.DCB_PARTNER_MANAGER,
|
|
||||||
label: 'Partner Manager',
|
|
||||||
description: 'Manager access with limited administrative capabilities',
|
|
||||||
allowedForCreation: isPartner || isPartnerAdmin || isHubAdmin
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: UserRole.DCB_PARTNER_SUPPORT,
|
|
||||||
label: 'Partner Support',
|
|
||||||
description: 'Support role with read-only and basic operational access',
|
|
||||||
allowedForCreation: isPartner || isPartnerAdmin || isHubAdmin
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return { roles };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== MÉTHODES PRIVÉES D'ASSISTANCE =====
|
|
||||||
|
|
||||||
private async getUserMerchantPartnerId(userId: string): Promise<string | null> {
|
|
||||||
// Implémentez cette méthode pour récupérer le merchantPartnerId de l'utilisateur
|
|
||||||
// Cela dépend de votre implémentation Keycloak
|
|
||||||
try {
|
|
||||||
// Exemple - à adapter selon votre implémentation
|
|
||||||
const user = await this.merchantUsersService['keycloakApi'].getUserById(userId, userId);
|
|
||||||
return user.attributes?.merchantPartnerId?.[0] || null;
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getUserRoles(userId: string): Promise<UserRole[]> {
|
|
||||||
// Implémentez cette méthode pour récupérer les rôles de l'utilisateur
|
|
||||||
try {
|
|
||||||
const roles = await this.merchantUsersService['keycloakApi'].getUserClientRoles(userId);
|
|
||||||
return roles.map(role => role.name as UserRole);
|
|
||||||
} catch (error) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,105 +0,0 @@
|
|||||||
// dto/hub-users.dto.ts
|
|
||||||
import {
|
|
||||||
IsEmail,
|
|
||||||
IsEnum,
|
|
||||||
IsNotEmpty,
|
|
||||||
IsOptional,
|
|
||||||
IsBoolean,
|
|
||||||
IsString,
|
|
||||||
MinLength,
|
|
||||||
Matches,
|
|
||||||
IsUUID,
|
|
||||||
} from 'class-validator';
|
|
||||||
import { UserRole } from '../../auth/services/keycloak-user.model';
|
|
||||||
|
|
||||||
// Utiliser directement UserRole au lieu de créer un enum local
|
|
||||||
export class CreateHubUserDto {
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsString()
|
|
||||||
@MinLength(3)
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsEmail()
|
|
||||||
email: string;
|
|
||||||
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsString()
|
|
||||||
@MinLength(2)
|
|
||||||
firstName: string;
|
|
||||||
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsString()
|
|
||||||
@MinLength(2)
|
|
||||||
lastName: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@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',
|
|
||||||
})
|
|
||||||
password?: string;
|
|
||||||
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsEnum([UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT], {
|
|
||||||
message: 'Role must be either DCB_ADMIN or DCB_SUPPORT',
|
|
||||||
})
|
|
||||||
role: UserRole.DCB_ADMIN | UserRole.DCB_SUPPORT;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
enabled?: boolean;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
emailVerified?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UpdateHubUserDto {
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@MinLength(2)
|
|
||||||
firstName?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@MinLength(2)
|
|
||||||
lastName?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsEmail()
|
|
||||||
email?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
enabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UpdateHubUserRoleDto {
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsEnum([UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT], {
|
|
||||||
message: 'Role must be either DCB_ADMIN or DCB_SUPPORT',
|
|
||||||
})
|
|
||||||
role: UserRole.DCB_ADMIN | UserRole.DCB_SUPPORT;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ResetPasswordDto {
|
|
||||||
@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',
|
|
||||||
})
|
|
||||||
password: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
temporary?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UserIdParamDto {
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsUUID()
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
import {
|
|
||||||
IsEmail,
|
|
||||||
IsEnum,
|
|
||||||
IsNotEmpty,
|
|
||||||
IsOptional,
|
|
||||||
IsBoolean,
|
|
||||||
IsString,
|
|
||||||
MinLength,
|
|
||||||
Matches,
|
|
||||||
IsUUID,
|
|
||||||
} from 'class-validator';
|
|
||||||
import { UserRole } from '../../auth/services/keycloak-user.model';
|
|
||||||
|
|
||||||
export class CreateMerchantUserDto {
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsString()
|
|
||||||
@MinLength(3)
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsEmail()
|
|
||||||
email: string;
|
|
||||||
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsString()
|
|
||||||
@MinLength(2)
|
|
||||||
firstName: string;
|
|
||||||
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsString()
|
|
||||||
@MinLength(2)
|
|
||||||
lastName: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@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',
|
|
||||||
})
|
|
||||||
password?: string;
|
|
||||||
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsEnum([UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT])
|
|
||||||
role: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
enabled?: boolean;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
emailVerified?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UpdateMerchantUserDto {
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@MinLength(2)
|
|
||||||
firstName?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@MinLength(2)
|
|
||||||
lastName?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsEmail()
|
|
||||||
email?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
enabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ResetMerchantPasswordDto {
|
|
||||||
@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',
|
|
||||||
})
|
|
||||||
password: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
temporary?: boolean;
|
|
||||||
}
|
|
||||||
@ -4,7 +4,6 @@ import { HttpModule } from '@nestjs/axios';
|
|||||||
import { TokenService } from '../auth/services/token.service'
|
import { TokenService } from '../auth/services/token.service'
|
||||||
import { HubUsersService } from './services/hub-users.service'
|
import { HubUsersService } from './services/hub-users.service'
|
||||||
import { HubUsersController } from './controllers/hub-users.controller'
|
import { HubUsersController } from './controllers/hub-users.controller'
|
||||||
import { MerchantUsersService } from './services/merchant-users.service'
|
|
||||||
import { MerchantUsersController } from './controllers/merchant-users.controller'
|
import { MerchantUsersController } from './controllers/merchant-users.controller'
|
||||||
import { KeycloakApiService } from '../auth/services/keycloak-api.service';
|
import { KeycloakApiService } from '../auth/services/keycloak-api.service';
|
||||||
|
|
||||||
@ -14,9 +13,9 @@ import { KeycloakApiService } from '../auth/services/keycloak-api.service';
|
|||||||
HttpModule,
|
HttpModule,
|
||||||
JwtModule.register({}),
|
JwtModule.register({}),
|
||||||
],
|
],
|
||||||
providers: [HubUsersService, MerchantUsersService, KeycloakApiService, TokenService],
|
providers: [HubUsersService, KeycloakApiService, TokenService],
|
||||||
controllers: [HubUsersController, MerchantUsersController],
|
controllers: [HubUsersController, MerchantUsersController ],
|
||||||
exports: [HubUsersService, MerchantUsersService, KeycloakApiService, TokenService, JwtModule],
|
exports: [HubUsersService, KeycloakApiService, TokenService, JwtModule],
|
||||||
})
|
})
|
||||||
export class HubUsersModule {}
|
export class HubUsersModule {}
|
||||||
|
|
||||||
|
|||||||
@ -1,23 +1,37 @@
|
|||||||
// user.models.ts
|
// user.models.ts
|
||||||
|
// Interfaces et Constantes Centralisées
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
|
role: UserRole;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
emailVerified: boolean;
|
emailVerified: boolean;
|
||||||
userType: UserType;
|
|
||||||
merchantPartnerId?: string;
|
merchantPartnerId?: string;
|
||||||
clientRoles: UserRole[];
|
createdBy: string;
|
||||||
createdBy?: string;
|
createdByUsername: string;
|
||||||
createdByUsername?: string;
|
|
||||||
createdTimestamp: number;
|
createdTimestamp: number;
|
||||||
|
lastLogin?: number;
|
||||||
|
userType: UserType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateUserData {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
password?: string;
|
||||||
|
role: UserRole;
|
||||||
|
enabled?: boolean;
|
||||||
|
emailVerified?: boolean;
|
||||||
|
merchantPartnerId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum UserType {
|
export enum UserType {
|
||||||
HUB = 'hub',
|
HUB = 'HUB',
|
||||||
MERCHANT_PARTNER = 'merchant_partner'
|
MERCHANT_PARTNER = 'MERCHANT'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum UserRole {
|
export enum UserRole {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,192 +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 MerchantUser {
|
|
||||||
id: string;
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
role: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT;
|
|
||||||
enabled: boolean;
|
|
||||||
emailVerified: boolean;
|
|
||||||
merchantPartnerId: string;
|
|
||||||
createdBy: string;
|
|
||||||
createdByUsername: string;
|
|
||||||
createdTimestamp: number;
|
|
||||||
lastLogin?: number;
|
|
||||||
userType: 'MERCHANT';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateMerchantUserData {
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
password?: string;
|
|
||||||
role: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT;
|
|
||||||
enabled?: boolean;
|
|
||||||
emailVerified?: boolean;
|
|
||||||
merchantPartnerId: string;
|
|
||||||
createdBy: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class MerchantUsersService {
|
|
||||||
private readonly logger = new Logger(MerchantUsersService.name);
|
|
||||||
|
|
||||||
private readonly MERCHANT_ROLES = [
|
|
||||||
UserRole.DCB_PARTNER_ADMIN,
|
|
||||||
UserRole.DCB_PARTNER_MANAGER,
|
|
||||||
UserRole.DCB_PARTNER_SUPPORT,
|
|
||||||
];
|
|
||||||
|
|
||||||
constructor(private readonly keycloakApi: KeycloakApiService) {}
|
|
||||||
|
|
||||||
// ===== CRÉATION D'UTILISATEURS MERCHANT =====
|
|
||||||
async createMerchantUser(creatorId: string, userData: CreateMerchantUserData): Promise<MerchantUser> {
|
|
||||||
this.logger.log(`Creating merchant user: ${userData.username} for merchant: ${userData.merchantPartnerId}`);
|
|
||||||
|
|
||||||
// Validation des permissions et du merchant
|
|
||||||
await this.validateMerchantUserCreation(creatorId, userData);
|
|
||||||
|
|
||||||
// Vérifier les doublons
|
|
||||||
const existingUsers = await this.keycloakApi.findUserByUsername(userData.username);
|
|
||||||
if (existingUsers.length > 0) {
|
|
||||||
throw new BadRequestException(`User with username ${userData.username} already exists`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const keycloakUserData: CreateUserData = {
|
|
||||||
username: userData.username,
|
|
||||||
email: userData.email,
|
|
||||||
firstName: userData.firstName,
|
|
||||||
lastName: userData.lastName,
|
|
||||||
password: userData.password,
|
|
||||||
enabled: userData.enabled ?? true,
|
|
||||||
emailVerified: userData.emailVerified ?? false,
|
|
||||||
merchantPartnerId: userData.merchantPartnerId,
|
|
||||||
clientRoles: [userData.role],
|
|
||||||
createdBy: creatorId,
|
|
||||||
};
|
|
||||||
|
|
||||||
const userId = await this.keycloakApi.createUser(creatorId, keycloakUserData);
|
|
||||||
const createdUser = await this.getMerchantUserById(userId, creatorId);
|
|
||||||
|
|
||||||
this.logger.log(`Merchant user created successfully: ${userData.username}`);
|
|
||||||
return createdUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== RÉCUPÉRATION D'UTILISATEURS MERCHANT =====
|
|
||||||
async getMerchantUsersByPartner(merchantPartnerId: string, requesterId: string): Promise<MerchantUser[]> {
|
|
||||||
await this.validateMerchantAccess(requesterId, merchantPartnerId);
|
|
||||||
|
|
||||||
const allUsers = await this.keycloakApi.getAllUsers();
|
|
||||||
const merchantUsers: MerchantUser[] = [];
|
|
||||||
|
|
||||||
for (const user of allUsers) {
|
|
||||||
if (!user.id) continue;
|
|
||||||
|
|
||||||
const userMerchantId = user.attributes?.merchantPartnerId?.[0];
|
|
||||||
if (userMerchantId === merchantPartnerId) {
|
|
||||||
try {
|
|
||||||
const userRoles = await this.keycloakApi.getUserClientRoles(user.id);
|
|
||||||
const merchantRole = userRoles.find(role => this.MERCHANT_ROLES.includes(role.name as UserRole));
|
|
||||||
|
|
||||||
if (merchantRole) {
|
|
||||||
merchantUsers.push(this.mapToMerchantUser(user, userRoles));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.warn(`Could not process merchant user ${user.id}: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return merchantUsers;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getMerchantUserById(userId: string, requesterId: string): Promise<MerchantUser> {
|
|
||||||
const user = await this.keycloakApi.getUserById(userId, requesterId);
|
|
||||||
const userRoles = await this.keycloakApi.getUserClientRoles(userId);
|
|
||||||
|
|
||||||
const merchantRole = userRoles.find(role => this.MERCHANT_ROLES.includes(role.name as UserRole));
|
|
||||||
if (!merchantRole) {
|
|
||||||
throw new BadRequestException(`User ${userId} is not a merchant user`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const merchantPartnerId = user.attributes?.merchantPartnerId?.[0];
|
|
||||||
if (!merchantPartnerId) {
|
|
||||||
throw new BadRequestException(`User ${userId} has no merchant partner association`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.validateMerchantAccess(requesterId, merchantPartnerId);
|
|
||||||
|
|
||||||
return this.mapToMerchantUser(user, userRoles);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== VALIDATIONS =====
|
|
||||||
private async validateMerchantUserCreation(creatorId: string, userData: CreateMerchantUserData): Promise<void> {
|
|
||||||
const creatorRoles = await this.keycloakApi.getUserClientRoles(creatorId);
|
|
||||||
|
|
||||||
// DCB_PARTNER peut créer des utilisateurs
|
|
||||||
if (creatorRoles.some(role => role.name === UserRole.DCB_PARTNER)) {
|
|
||||||
if (creatorId !== userData.merchantPartnerId) {
|
|
||||||
throw new ForbiddenException('DCB_PARTNER can only create users for their own ');
|
|
||||||
}
|
|
||||||
// DCB_PARTNER ne peut créer que certains rôles
|
|
||||||
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 DCB_PARTNER_ADMIN, MANAGER, or SUPPORT roles');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// DCB_PARTNER_ADMIN peut créer des utilisateurs pour son merchant
|
|
||||||
if (creatorRoles.some(role => role.name === UserRole.DCB_PARTNER_ADMIN)) {
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
// DCB_PARTNER_ADMIN ne peut créer que certains rôles
|
|
||||||
const allowedRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT];
|
|
||||||
if (!allowedRoles.includes(userData.role)) {
|
|
||||||
throw new ForbiddenException('DCB_PARTNER_ADMIN can only create DCB_PARTNER_ADMIN, DCB_PARTNER_MANAGER or SUPPORT roles');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Les admins Hub peuvent créer pour n'importe quel merchant
|
|
||||||
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 async validateMerchantAccess(requesterId: string, merchantPartnerId: string): Promise<void> {
|
|
||||||
await this.keycloakApi.validateUserAccess(requesterId, merchantPartnerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private mapToMerchantUser(user: KeycloakUser, roles: any[]): MerchantUser {
|
|
||||||
const merchantRole = roles.find(role => this.MERCHANT_ROLES.includes(role.name as UserRole));
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: user.id!,
|
|
||||||
username: user.username,
|
|
||||||
email: user.email,
|
|
||||||
firstName: user.firstName,
|
|
||||||
lastName: user.lastName,
|
|
||||||
role: merchantRole?.name as UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT,
|
|
||||||
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: user.attributes?.lastLogin?.[0] ? parseInt(user.attributes.lastLogin[0]) : undefined,
|
|
||||||
userType: 'MERCHANT',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
41
src/main.ts
41
src/main.ts
@ -1,23 +1,56 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { ValidationPipe, Logger } from '@nestjs/common';
|
import { ValidationPipe, Logger, BadRequestException } from '@nestjs/common';
|
||||||
import helmet from 'helmet';
|
import helmet from 'helmet';
|
||||||
import { KeycloakExceptionFilter } from './filters/keycloak-exception.filter';
|
import { KeycloakExceptionFilter } from './filters/keycloak-exception.filter';
|
||||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||||
|
import { useContainer } from 'class-validator';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
const logger = new Logger('dcb-user-service');
|
const logger = new Logger('dcb-user-service');
|
||||||
|
|
||||||
|
useContainer(app.select(AppModule), { fallbackOnErrors: true });
|
||||||
|
|
||||||
// Middlewares de sécurité
|
// Middlewares de sécurité
|
||||||
app.use(helmet());
|
app.use(helmet());
|
||||||
app.enableCors({ origin: '*' });
|
app.enableCors({
|
||||||
|
origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization']
|
||||||
|
});
|
||||||
|
|
||||||
// Gestion globale des erreurs et validation
|
// Gestion globale des erreurs et validation
|
||||||
app.useGlobalFilters(new KeycloakExceptionFilter());
|
app.useGlobalFilters(new KeycloakExceptionFilter());
|
||||||
|
|
||||||
|
// ValidationPipe CORRIGÉ
|
||||||
app.useGlobalPipes(new ValidationPipe({
|
app.useGlobalPipes(new ValidationPipe({
|
||||||
whitelist: true,
|
whitelist: true,
|
||||||
transform: true
|
forbidNonWhitelisted: true,
|
||||||
|
transform: true,
|
||||||
|
transformOptions: {
|
||||||
|
enableImplicitConversion: true,
|
||||||
|
},
|
||||||
|
exceptionFactory: (errors) => {
|
||||||
|
const messages = errors.map(error => {
|
||||||
|
// Détails complets de l'erreur
|
||||||
|
const constraints = error.constraints ? Object.values(error.constraints) : ['Unknown validation error'];
|
||||||
|
return {
|
||||||
|
field: error.property,
|
||||||
|
errors: constraints,
|
||||||
|
value: error.value,
|
||||||
|
children: error.children
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('🔴 VALIDATION ERRORS:', JSON.stringify(messages, null, 2));
|
||||||
|
|
||||||
|
return new BadRequestException({
|
||||||
|
message: 'Validation failed',
|
||||||
|
errors: messages,
|
||||||
|
details: 'Check the errors array for specific field validation issues'
|
||||||
|
});
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Préfixe global de l'API
|
// Préfixe global de l'API
|
||||||
@ -43,4 +76,4 @@ async function bootstrap() {
|
|||||||
logger.log(`Application running on http://localhost:${port}`);
|
logger.log(`Application running on http://localhost:${port}`);
|
||||||
logger.log(`Swagger documentation available at http://localhost:${port}/api-docs`);
|
logger.log(`Swagger documentation available at http://localhost:${port}/api-docs`);
|
||||||
}
|
}
|
||||||
bootstrap()
|
bootstrap();
|
||||||
Loading…
Reference in New Issue
Block a user