dcb-user-service/src/hub-users/services/hub-users.service.ts

616 lines
22 KiB
TypeScript

import { Injectable, Logger, BadRequestException, ForbiddenException, NotFoundException, InternalServerErrorException } from '@nestjs/common';
import { KeycloakApiService } from '../../auth/services/keycloak-api.service';
import { CreateUserData as KeycloakCreateUserData, UserRole, KeycloakUser, LoginDto, TokenResponse, KeycloakRole } from '../../auth/services/keycloak-user.model';
import { CreateUserData, User, UserType } from '../models/hub-user.model';
// Configuration Centralisée
const SECURITY_CONFIG = {
ROLES: {
HUB: [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER],
MERCHANT: [
UserRole.DCB_PARTNER_ADMIN,
UserRole.DCB_PARTNER_MANAGER,
UserRole.DCB_PARTNER_SUPPORT,
]
},
VALIDATION: {
MIN_PASSWORD_LENGTH: 8,
MAX_USERNAME_LENGTH: 50,
MIN_USERNAME_LENGTH: 3
}
};
@Injectable()
export class HubUsersService {
private readonly logger = new Logger(HubUsersService.name);
constructor(private readonly keycloakApi: KeycloakApiService) {}
// === PUBLIC INTERFACE ===
async authenticateUser(loginDto: LoginDto): Promise<TokenResponse> {
return this.keycloakApi.authenticateUser(loginDto.username, loginDto.password);
}
async getCompleteUserProfile(userId: string, tokenUser: any) {
try {
const [userDetails, userRoles] = await Promise.all([
this.keycloakApi.getUserById(userId, userId),
this.keycloakApi.getUserClientRoles(userId)
]);
return this.buildUserProfile(userId, tokenUser, userDetails, userRoles);
} catch (error) {
throw new InternalServerErrorException('Could not retrieve user profile');
}
}
// === HUB USERS MANAGEMENT ===
async getAllHubUsers(requesterId: string): Promise<User[]> {
await this.validateHubUserAccess(requesterId);
return this.processUsersByType(await this.keycloakApi.getAllUsers(), UserType.HUB);
}
/**
* Récupère uniquement les utilisateurs DCB_PARTNER
*/
async getAllDcbPartners(requesterId: string): Promise<User[]> {
await this.validateHubUserAccess(requesterId);
const allUsers = await this.keycloakApi.getAllUsers();
const dcbPartners: User[] = [];
for (const user of allUsers) {
if (!user.id) continue;
try {
const userRoles = await this.keycloakApi.getUserClientRoles(user.id);
// Vérifier si l'utilisateur est un DCB_PARTNER
const isDcbPartner = userRoles.some(role =>
role.name === UserRole.DCB_PARTNER
);
if (isDcbPartner) {
const mappedUser = this.mapToUser(user, userRoles);
dcbPartners.push(mappedUser);
}
} catch (error) {
this.logger.warn(`Could not process user ${user.id} for DCB_PARTNER filter: ${error.message}`);
}
}
this.logger.log(`Retrieved ${dcbPartners.length} DCB_PARTNER users`);
return dcbPartners;
}
async getHubUserById(userId: string, requesterId: string): Promise<User> {
await this.validateHubUserAccess(requesterId);
return this.getValidatedUser(userId, requesterId, UserType.HUB);
}
async createHubUser(creatorId: string, userData: CreateUserData): Promise<User> {
this.validateUserCreationData(userData, UserType.HUB);
await this.validateHubUserAccess(creatorId);
await this.validateUserUniqueness(userData.username, userData.email);
const keycloakUserData = this.buildKeycloakUserData(userData);
const userId = await this.keycloakApi.createUser(creatorId, keycloakUserData);
this.logger.log(`Hub user created: ${userData.username}`);
return this.getHubUserById(userId, creatorId);
}
async updateHubUser(
userId: string,
updates: Partial<Pick<User, 'firstName' | 'lastName' | 'email' | 'enabled'>>,
requesterId: string
): Promise<User> {
await this.executeWithValidation(userId, requesterId, UserType.HUB, async () => {
await this.keycloakApi.updateUser(userId, updates, requesterId);
});
return this.getHubUserById(userId, requesterId);
}
async deleteHubUser(userId: string, requesterId: string): Promise<void> {
await this.executeWithValidation(userId, requesterId, UserType.HUB, async () => {
await this.keycloakApi.deleteUser(userId, requesterId);
});
this.logger.log(`Hub user deleted: ${userId} by ${requesterId}`);
}
async getHubUsersByRole(role: UserRole, requesterId: string): Promise<User[]> {
await this.validateHubUserAccess(requesterId);
const allHubUsers = await this.getAllHubUsers(requesterId);
return allHubUsers.filter(user => user.role === role);
}
async updateHubUserRole(userId: string, newRole: UserRole, requesterId: string): Promise<User> {
await this.validateRoleChangePermission(requesterId);
await this.executeWithValidation(userId, requesterId, UserType.HUB, async () => {
await this.keycloakApi.setClientRoles(userId, [newRole]);
});
return this.getHubUserById(userId, requesterId);
}
// === MERCHANT USERS MANAGEMENT ===
/**
* Vérifie si un utilisateur est Hub Admin ou Support
*/
async isUserHubAdminOrSupport(userId: string): Promise<boolean> {
try {
const userRoles = await this.keycloakApi.getUserClientRoles(userId);
const hubAdminSupportRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT];
return userRoles.some(role =>
hubAdminSupportRoles.includes(role.name as UserRole)
);
} catch (error) {
this.logger.error(`Error checking Hub Admin/Support status for user ${userId}:`, error);
return false;
}
}
/**
* Récupère les utilisateurs marchands selon les permissions de l'utilisateur
* - Hub Admin/Support: tous les utilisateurs marchands de tous les merchants
* - Autres: seulement les utilisateurs de leur propre merchant
*/
async getMyMerchantUsers(userId: string): Promise<User[]> {
try {
// Vérifier si l'utilisateur est un admin ou support Hub
const isHubAdminOrSupport = await this.isUserHubAdminOrSupport(userId)
if (isHubAdminOrSupport) {
// Hub Admin/Support peuvent voir TOUS les utilisateurs marchands
return await this.getAllMerchantUsersForHubAdmin(userId);
}
// Pour les autres utilisateurs (DCB_PARTNER, DCB_PARTNER_ADMIN, etc.)
return await this.getMerchantUsersForRegularUser(userId);
} catch (error) {
this.logger.error(`Error in getMyMerchantUsers for user ${userId}:`, error);
throw error;
}
}
/**
* Récupère TOUS les utilisateurs marchands pour les Hub Admin/Support
*/
private async getAllMerchantUsersForHubAdmin(adminUserId: string): Promise<User[]> {
this.logger.log(`Hub Admin/Support ${adminUserId} accessing ALL merchant users`);
// Valider que l'utilisateur a bien les droits Hub
await this.validateHubUserAccess(adminUserId);
// Récupérer tous les utilisateurs du système
const allUsers = await this.keycloakApi.getAllUsers();
const merchantUsers: User[] = [];
// Filtrer pour ne garder que les utilisateurs marchands
for (const user of allUsers) {
if (!user.id) continue;
try {
const userRoles = await this.keycloakApi.getUserClientRoles(user.id);
// Vérifier si l'utilisateur a un rôle marchand
const hasMerchantRole = userRoles.some(role =>
[UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT]
.includes(role.name as UserRole)
);
if (hasMerchantRole) {
const mappedUser = this.mapToUser(user, userRoles);
merchantUsers.push(mappedUser);
}
} catch (error) {
this.logger.warn(`Could not process user ${user.id} for hub admin view: ${error.message}`);
continue;
}
}
this.logger.log(`Hub Admin/Support retrieved ${merchantUsers.length} merchant users from all merchants`);
return merchantUsers;
}
/**
* Récupère les utilisateurs marchands pour les utilisateurs réguliers (non Hub Admin/Support)
*/
private async getMerchantUsersForRegularUser(userId: string): Promise<User[]> {
// Récupérer le merchantPartnerId de l'utilisateur
const userMerchantId = await this.getUserMerchantPartnerId(userId);
if (!userMerchantId) {
throw new BadRequestException('Current user is not associated with a merchant partner');
}
this.logger.log(`User ${userId} accessing merchant users for partner ${userMerchantId}`);
// Utiliser la méthode existante pour récupérer les utilisateurs du merchant spécifique
const users = await this.getMerchantUsersByPartner(userMerchantId, userId);
this.logger.log(`User ${userId} retrieved ${users.length} merchant users for partner ${userMerchantId}`);
return users;
}
async getMerchantUsersByPartner(merchantPartnerId: string, requesterId: string): Promise<User[]> {
await this.keycloakApi.validateUserAccess(requesterId, merchantPartnerId);
const allUsers = await this.keycloakApi.getAllUsers();
const merchantUsers = allUsers.filter(user =>
user.attributes?.merchantPartnerId?.[0] === merchantPartnerId
);
return this.processUsersByType(merchantUsers, UserType.MERCHANT_PARTNER);
}
async getMerchantUserById(userId: string, requesterId: string): Promise<User> {
return this.getValidatedUser(userId, requesterId, UserType.MERCHANT_PARTNER);
}
async createMerchantUser(creatorId: string, userData: CreateUserData): Promise<User> {
this.validateUserCreationData(userData, UserType.MERCHANT_PARTNER);
await this.validateMerchantUserCreation(creatorId, userData);
await this.validateUserUniqueness(userData.username, userData.email);
const keycloakUserData = this.buildKeycloakUserData(userData, userData.merchantPartnerId!);
const userId = await this.keycloakApi.createUser(creatorId, keycloakUserData);
this.logger.log(`Merchant user created: ${userData.username}`);
return this.getMerchantUserById(userId, creatorId);
}
async updateMerchantUser(
userId: string,
updates: Partial<Pick<User, 'firstName' | 'lastName' | 'email' | 'enabled'>>,
requesterId: string
): Promise<User> {
await this.executeWithValidation(userId, requesterId, UserType.MERCHANT_PARTNER, async () => {
await this.keycloakApi.updateUser(userId, updates, requesterId);
});
return this.getMerchantUserById(userId, requesterId);
}
async updateMerchantUserRole(userId: string, newRole: UserRole, requesterId: string): Promise<User> {
await this.validateRoleChangePermission(requesterId);
await this.executeWithValidation(userId, requesterId, UserType.MERCHANT_PARTNER, async () => {
await this.keycloakApi.setClientRoles(userId, [newRole]);
});
return this.getMerchantUserById(userId, requesterId);
}
async deleteMerchantUser(userId: string, requesterId: string): Promise<void> {
await this.validateSelfDeletion(userId, requesterId);
await this.executeWithValidation(userId, requesterId, UserType.MERCHANT_PARTNER, async () => {
await this.keycloakApi.deleteUser(userId, requesterId);
});
this.logger.log(`Merchant user deleted: ${userId} by ${requesterId}`);
}
// === COMMON OPERATIONS ===
async resetUserPassword(
userId: string,
newPassword: string,
temporary: boolean = true,
requesterId: string
): Promise<void> {
await this.ensureUserExists(userId, requesterId);
await this.keycloakApi.resetUserPassword(userId, newPassword, temporary);
this.logger.log(`Password reset for user: ${userId}`);
}
async getUserMerchantPartnerId(userId: string): Promise<string | null> {
return this.keycloakApi.getUserMerchantPartnerId(userId);
}
// === PRIVATE CORE METHODS ===
private async getValidatedUser(
userId: string,
requesterId: string,
userType: UserType.HUB | UserType.MERCHANT_PARTNER
): Promise<User> {
const [user, userRoles] = await Promise.all([
this.keycloakApi.getUserById(userId, requesterId),
this.keycloakApi.getUserClientRoles(userId)
]);
this.validateUserType(userRoles, userType, userId);
return this.mapToUser(user, userRoles);
}
private async processUsersByType(users: KeycloakUser[], userType: UserType.HUB | UserType.MERCHANT_PARTNER): Promise<User[]> {
const result: User[] = [];
for (const user of users) {
if (!user.id) continue;
try {
const userRoles = await this.keycloakApi.getUserClientRoles(user.id);
if (this.isUserType(userRoles, userType)) {
result.push(this.mapToUser(user, userRoles));
}
} catch (error) {
this.logger.warn(`Could not process user ${user.id}: ${error.message}`);
}
}
return result;
}
private async executeWithValidation(
userId: string,
requesterId: string,
userType: UserType.HUB | UserType.MERCHANT_PARTNER,
operation: () => Promise<void>
): Promise<void> {
await this.getValidatedUser(userId, requesterId, userType);
await operation();
}
// === USER MAPPING AND VALIDATION ===
private mapToUser(user: KeycloakUser, roles: KeycloakRole[]): User {
if (!user.id || !user.email) {
throw new Error('User ID and email are required');
}
const role = this.determineUserRole(roles);
const userType = this.determineUserType(roles);
return {
id: user.id,
username: user.username,
email: user.email,
firstName: user.firstName || '',
lastName: user.lastName || '',
role,
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: this.parseTimestamp(user.attributes?.lastLogin),
userType,
};
}
private determineUserRole(roles: KeycloakRole[]): UserRole {
const allRoles = [...SECURITY_CONFIG.ROLES.HUB, ...SECURITY_CONFIG.ROLES.MERCHANT];
const userRole = roles.find(role => allRoles.includes(role.name as UserRole));
if (!userRole) {
throw new Error('No valid role found for user');
}
return userRole.name as UserRole;
}
private determineUserType(roles: KeycloakRole[]): UserType {
return roles.some(role => SECURITY_CONFIG.ROLES.HUB.includes(role.name as UserRole))
? UserType.HUB
: UserType.MERCHANT_PARTNER;
}
private isUserType(roles: KeycloakRole[], userType: UserType.HUB | UserType.MERCHANT_PARTNER): boolean {
const targetRoles = userType === UserType.HUB
? SECURITY_CONFIG.ROLES.HUB
: SECURITY_CONFIG.ROLES.MERCHANT;
return roles.some(role => targetRoles.includes(role.name as UserRole));
}
private validateUserType(roles: KeycloakRole[], expectedType: UserType.HUB | UserType.MERCHANT_PARTNER, userId: string): void {
if (!this.isUserType(roles, expectedType)) {
throw new BadRequestException(`User ${userId} is not a ${expectedType.toLowerCase()} user`);
}
}
// === VALIDATION METHODS ===
private async validateHubUserAccess(requesterId: string): Promise<void> {
const requesterRoles = await this.keycloakApi.getUserClientRoles(requesterId);
const hasHubAccess = requesterRoles.some(role =>
SECURITY_CONFIG.ROLES.HUB.includes(role.name as UserRole)
);
if (!hasHubAccess) {
throw new ForbiddenException('Only hub administrators can manage hub users');
}
}
private validateUserCreationData(userData: CreateUserData, userType: UserType.HUB | UserType.MERCHANT_PARTNER): void {
// 🔍 DEBUG COMPLET
this.logger.debug('🔍 === VALIDATION USER CREATION DATA ===');
this.logger.debug('UserType:', userType);
this.logger.debug('UserData complet:', JSON.stringify(userData, null, 2));
this.logger.debug('userData.role:', userData.role);
this.logger.debug('Type de role:', typeof userData.role);
this.logger.debug('Est un tableau?:', Array.isArray(userData.role));
this.logger.debug('Valeur brute role:', userData.role);
// Afficher les rôles valides configurés
const validRoles = userType === UserType.HUB
? SECURITY_CONFIG.ROLES.HUB
: SECURITY_CONFIG.ROLES.MERCHANT;
this.logger.debug('Rôles valides pour', userType, ':', validRoles);
this.logger.debug('merchantPartnerId:', userData.merchantPartnerId);
this.logger.debug('====================================');
// Validation des rôles
if (!validRoles.includes(userData.role)) {
console.error(`❌ Rôle invalide: ${userData.role} pour le type ${userType}`);
console.error(`Rôles autorisés: ${validRoles.join(', ')}`);
throw new BadRequestException(`Invalid ${userType.toLowerCase()} role: ${userData.role}`);
}
// Validation merchantPartnerId pour HUB
if (userType === UserType.HUB && userData.merchantPartnerId) {
console.error('❌ merchantPartnerId fourni pour un utilisateur HUB');
throw new BadRequestException('merchantPartnerId should not be provided for hub users');
}
// Validation merchantPartnerId pour MERCHANT
// Vérifier d'abord si role est un tableau ou une valeur simple
const isDCBPartner = Array.isArray(userData.role)
? userData.role.includes(UserRole.DCB_PARTNER)
: userData.role === UserRole.DCB_PARTNER;
this.logger.debug('Est DCB_PARTNER?:', isDCBPartner);
if (userType === UserType.MERCHANT_PARTNER && !userData.merchantPartnerId && !isDCBPartner) {
console.error('❌ merchantPartnerId manquant pour un utilisateur MERCHANT');
throw new BadRequestException('merchantPartnerId is required for merchant users');
}
this.logger.debug('✅ Validation réussie');
}
private async validateUserUniqueness(username: string, email: string): Promise<void> {
const [existingUsers, existingEmails] = await Promise.all([
this.keycloakApi.findUserByUsername(username),
this.keycloakApi.findUserByEmail(email)
]);
if (existingUsers.length > 0) {
throw new BadRequestException(`User with username ${username} already exists`);
}
if (existingEmails.length > 0) {
throw new BadRequestException(`User with email ${email} already exists`);
}
}
private async validateRoleChangePermission(requesterId: string): Promise<void> {
const requesterRoles = await this.keycloakApi.getUserClientRoles(requesterId);
const isRequesterAdmin = requesterRoles.some(role => role.name === UserRole.DCB_ADMIN || UserRole.DCB_PARTNER || UserRole.DCB_PARTNER_ADMIN);
if (!isRequesterAdmin) {
throw new ForbiddenException('Only DCB_ADMIN can change user roles');
}
}
private async validateSelfDeletion(userId: string, requesterId: string): Promise<void> {
if (userId === requesterId) {
throw new BadRequestException('Cannot delete your own account');
}
}
private async ensureUserExists(userId: string, requesterId: string): Promise<void> {
try {
await this.getHubUserById(userId, requesterId);
} catch {
try {
await this.getMerchantUserById(userId, requesterId);
} catch {
throw new NotFoundException(`User ${userId} not found`);
}
}
}
private buildUserProfile(
userId: string,
tokenUser: any,
userDetails: KeycloakUser,
userRoles: KeycloakRole[]
) {
return {
id: userId,
username: tokenUser.preferred_username,
email: tokenUser.email,
firstName: tokenUser.given_name,
lastName: tokenUser.family_name,
emailVerified: tokenUser.email_verified,
enabled: userDetails.enabled,
clientRoles: userRoles.map(role => role.name),
merchantPartnerId: userDetails.attributes?.merchantPartnerId?.[0],
createdBy: userDetails.attributes?.createdBy?.[0],
createdByUsername: userDetails.attributes?.createdByUsername?.[0]
};
}
private buildKeycloakUserData(
userData: CreateUserData,
merchantPartnerId?: string
): KeycloakCreateUserData {
return {
username: userData.username,
email: userData.email,
firstName: userData.firstName,
lastName: userData.lastName,
password: userData.password,
enabled: userData.enabled ?? true,
emailVerified: userData.emailVerified ?? false,
merchantPartnerId,
clientRoles: [userData.role]
};
}
private parseTimestamp(value: string[] | undefined): number | undefined {
const strValue = value?.[0];
if (!strValue) return undefined;
const timestamp = parseInt(strValue);
if (!isNaN(timestamp)) return timestamp;
const date = new Date(strValue);
return isNaN(date.getTime()) ? undefined : date.getTime();
}
private async validateMerchantUserCreation(creatorId: string, userData: CreateUserData): Promise<void> {
const creatorRoles = await this.keycloakApi.getUserClientRoles(creatorId);
const creationRules = this.getMerchantCreationRules();
for (const rule of creationRules) {
if (creatorRoles.some(role => role.name === rule.role)) {
await rule.validator(creatorId, userData);
return;
}
}
// Vérifier les permissions des administrateurs Hub
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 getMerchantCreationRules() {
return [
{
role: UserRole.DCB_PARTNER,
validator: async (creatorId: string, userData: CreateUserData) => {
if (creatorId !== userData.merchantPartnerId) {
throw new ForbiddenException('DCB_PARTNER can only create users for their own merchant');
}
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 MANAGER and SUPPORT roles');
}
}
},
{
role: UserRole.DCB_PARTNER_ADMIN,
validator: async (creatorId: string, userData: CreateUserData) => {
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');
}
const allowedRoles = [UserRole.DCB_PARTNER_SUPPORT];
if (!allowedRoles.includes(userData.role)) {
throw new ForbiddenException('DCB_PARTNER_ADMIN can only create SUPPORT roles');
}
}
}
];
}
}