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

552 lines
20 KiB
TypeScript

import { Injectable, Logger, BadRequestException, ForbiddenException, NotFoundException } from '@nestjs/common';
import { KeycloakApiService } from '../../auth/services/keycloak-api.service';
import { CreateUserData, UserRole, KeycloakUser, HubUser, CreateHubUserData, HubUserStats, HubHealthStatus, HubUserActivity, MerchantStats } from '../../auth/services/keycloak-user.model'; // SUPPRIMER import type
import { LoginDto, TokenResponse, User } from '../models/hub-user.model';
// ===== SERVICE PRINCIPAL =====
@Injectable()
export class HubUsersService {
private readonly logger = new Logger(HubUsersService.name);
// Définir les rôles Hub autorisés comme constante
private readonly HUB_ROLES = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER];
constructor(private readonly keycloakApi: KeycloakApiService) {}
// === AUTHENTIFICATION UTILISATEUR ===
async authenticateUser(loginDto: LoginDto): Promise<TokenResponse> {
return this.keycloakApi.authenticateUser(loginDto.username, loginDto.password);
}
// ===== VALIDATION ET UTILITAIRES =====
private isValidHubRole(role: UserRole): role is UserRole.DCB_ADMIN | UserRole.DCB_SUPPORT | UserRole.DCB_PARTNER {
return this.HUB_ROLES.includes(role);
}
private validateHubRole(role: UserRole): void {
if (!this.isValidHubRole(role)) {
throw new BadRequestException(
`Invalid hub role: ${role}. Hub roles must be DCB_ADMIN or DCB_SUPPORT`
);
}
}
validateHubRoleFromString(role: string): UserRole.DCB_ADMIN | UserRole.DCB_SUPPORT | UserRole.DCB_PARTNER {
// Vérifier si le rôle est valide
if (!Object.values(UserRole).includes(role as UserRole)) {
throw new BadRequestException(`Invalid role: ${role}`);
}
const userRole = role as UserRole;
if (!this.isValidHubRole(userRole)) {
throw new BadRequestException(
`Invalid hub role: ${role}. Must be DCB_ADMIN or DCB_SUPPORT`
);
}
return userRole;
}
private async validateHubUserAccess(requesterId: string): Promise<void> {
const requesterRoles = await this.keycloakApi.getUserClientRoles(requesterId);
const isHubAdmin = requesterRoles.some(role =>
this.HUB_ROLES.includes(role.name as UserRole)
);
if (!isHubAdmin) {
throw new ForbiddenException('Only hub administrators can manage hub users');
}
}
private parseKeycloakAttribute(value: string[] | undefined): string | undefined {
return value?.[0];
}
private parseKeycloakTimestamp(value: string[] | undefined): number | undefined {
const strValue = this.parseKeycloakAttribute(value);
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 mapToHubUser(user: KeycloakUser, roles: any[]): HubUser {
const hubRole = roles.find(role => this.isValidHubRole(role.name as UserRole));
if (!user.id) throw new Error('User ID is required');
if (!user.email) throw new Error('User email is required');
return {
id: user.id,
username: user.username,
email: user.email,
firstName: user.firstName || '',
lastName: user.lastName || '',
role: hubRole?.name as UserRole.DCB_ADMIN | UserRole.DCB_SUPPORT | UserRole.DCB_PARTNER,
enabled: user.enabled,
emailVerified: user.emailVerified,
createdBy: this.parseKeycloakAttribute(user.attributes?.createdBy) || 'unknown',
createdByUsername: this.parseKeycloakAttribute(user.attributes?.createdByUsername) || 'unknown',
createdTimestamp: user.createdTimestamp || Date.now(),
lastLogin: this.parseKeycloakTimestamp(user.attributes?.lastLogin),
userType: 'HUB',
attributes: user.attributes,
};
}
// ===== OPÉRATIONS CRUD =====
async createHubUser(creatorId: string, userData: CreateHubUserData): Promise<HubUser> {
this.logger.log(`Creating hub user: ${userData.username} with role: ${userData.role}`);
this.validateHubRole(userData.role);
await this.validateHubUserAccess(creatorId);
// Vérifier les doublons
this.checkDuplicateHubUser(userData)
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: undefined,
clientRoles: [userData.role],
createdBy: creatorId,
initialStatus: 'PENDING_ACTIVATION',
};
const userId = await this.keycloakApi.createUser(creatorId, keycloakUserData);
const createdUser = await this.getHubUserById(userId, creatorId);
this.logger.log(`Hub user created successfully: ${userData.username} (ID: ${userId})`);
return createdUser;
}
private async checkDuplicateHubUser(merchantData: CreateHubUserData): Promise<void> {
const existingUsers = await this.keycloakApi.findUserByUsername(merchantData.username);
if (existingUsers.length > 0) {
throw new BadRequestException(`Merchant partner with username ${merchantData.username} already exists`);
}
const existingEmails = await this.keycloakApi.findUserByEmail(merchantData.email);
if (existingEmails.length > 0) {
throw new BadRequestException(`Merchant partner with email ${merchantData.email} already exists`);
}
}
// ===== RÉCUPÉRATION DE TOUS LES MERCHANTS (DCB_PARTNER) =====
async getAllMerchants(requesterId: string): Promise<HubUser[]> {
await this.validateHubUserAccess(requesterId);
const allUsers = await this.keycloakApi.getAllUsers();
const merchants: HubUser[] = [];
for (const user of allUsers) {
if (!user.id) continue;
try {
const userRoles = await this.keycloakApi.getUserClientRoles(user.id);
const isMerchant = userRoles.some(role => role.name === UserRole.DCB_PARTNER);
if (isMerchant) {
merchants.push(this.mapToHubUser(user, userRoles));
}
} catch (error) {
this.logger.warn(`Could not process merchant ${user.id}: ${error.message}`);
}
}
return merchants;
}
async suspendMerchantPartner(merchantId: string, reason: string, requesterId: string): Promise<void> {
await this.validateHubAccess(requesterId);
const dcbPartnerUser = await this.findDcbPartnerByMerchantId(merchantId);
// Suspendre le merchant et tous ses utilisateurs
await this.keycloakApi.setUserAttributes(dcbPartnerUser.id!, {
merchantStatus: ['SUSPENDED'],
suspensionReason: [reason],
suspendedAt: [new Date().toISOString()],
});
// Suspendre tous les utilisateurs du merchant
await this.suspendAllMerchantUsers(merchantId, requesterId);
this.logger.log(`Merchant partner suspended: ${merchantId}, reason: ${reason}`);
}
async getMerchantPartnerById(merchantId: string, requesterId: string): Promise<HubUser> {
await this.validateMerchantAccess(requesterId, merchantId);
const merchantPartner = await this.findDcbPartnerByMerchantId(merchantId);
const merchantPartnerRoles = await this.keycloakApi.getUserClientRoles(merchantId);
return this.mapToHubUser(merchantPartner, merchantPartnerRoles);
}
// ===== STATISTIQUES =====
async getMerchantStats(requesterId: string): Promise<MerchantStats> {
await this.validateHubAccess(requesterId);
const allMerchants = await this.getAllMerchants(requesterId);
const allUsers = await this.keycloakApi.getAllUsers();
let totalUsers = 0;
for (const merchant of allMerchants) {
const merchantUsers = allUsers.filter(user =>
user.attributes?.merchantPartnerId?.[0] === merchant.id
);
totalUsers += merchantUsers.length;
}
return {
totalMerchants: allMerchants.length,
activeMerchants: allMerchants.filter(m => m.enabled).length,
suspendedMerchants: allMerchants.filter(m => !m.enabled).length,
pendingMerchants: allMerchants.filter(m => m.attributes?.userStatus?.[0] === 'PENDING_ACTIVATION'
).length,
totalUsers,
};
}
private async validateHubAccess(requesterId: string): Promise<void> {
const requesterRoles = await this.keycloakApi.getUserClientRoles(requesterId);
const isHubAdmin = requesterRoles.some(role =>
[UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT].includes(role.name as UserRole)
);
if (!isHubAdmin) {
throw new ForbiddenException('Only hub administrators can access this resource');
}
}
private async validateMerchantAccess(requesterId: string, merchantId?: string): Promise<void> {
const requesterRoles = await this.keycloakApi.getUserClientRoles(requesterId);
this.logger.debug(`Validating merchant access: requester=${requesterId}, merchant=${merchantId}`);
this.logger.debug(`Requester roles: ${requesterRoles.map(r => r.name).join(', ')}`);
// Les admins Hub ont accès complet
const isHubAdmin = requesterRoles.some(role =>
[UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT].includes(role.name as UserRole)
);
if (isHubAdmin) {
this.logger.debug('Hub admin access granted');
return;
}
// CORRECTION: Service account est considéré comme admin hub
if (requesterId.includes('service-account')) {
this.logger.debug('Service account access granted');
return;
}
// Les DCB_PARTNER n'ont accès qu'à leur propre merchant
if (requesterRoles.some(role => role.name === UserRole.DCB_PARTNER)) {
if (merchantId && requesterId === merchantId) {
this.logger.debug('DCB_PARTNER access to own merchant granted');
return; // DCB_PARTNER accède à son propre merchant
}
throw new ForbiddenException('DCB_PARTNER can only access their own merchant partner');
}
// Les autres rôles merchants n'ont accès qu'à leur merchant
const requesterMerchantId = await this.keycloakApi.getUserMerchantPartnerId(requesterId);
if (requesterMerchantId && merchantId && requesterMerchantId === merchantId) {
this.logger.debug('Merchant user access to own merchant granted');
return;
}
throw new ForbiddenException('Insufficient permissions to access this merchant');
}
private async validateMerchantManagementPermissions(requesterId: string, merchantId: string): Promise<void> {
const requesterRoles = await this.keycloakApi.getUserClientRoles(requesterId);
// Seuls les admins Hub peuvent modifier les merchants
const isHubAdmin = requesterRoles.some(role =>
[UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT].includes(role.name as UserRole)
);
if (!isHubAdmin) {
throw new ForbiddenException('Only hub administrators can manage merchant partners');
}
}
private async findDcbPartnerByMerchantId(merchantId: string): Promise<KeycloakUser> {
const users = await this.keycloakApi.findUserByUsername(merchantId);
if (users.length === 0) {
throw new NotFoundException(`Merchant partner not found: ${merchantId}`);
}
const user = users[0];
const userRoles = await this.keycloakApi.getUserClientRoles(user.id!);
const isDcbPartner = userRoles.some(role => role.name === UserRole.DCB_PARTNER);
if (!isDcbPartner) {
throw new NotFoundException(`User ${merchantId} is not a DCB_PARTNER`);
}
return user;
}
private async suspendAllMerchantUsers(merchantId: string, requesterId: string): Promise<void> {
const allUsers = await this.keycloakApi.getAllUsers();
const merchantUsers = allUsers.filter(user =>
user.attributes?.merchantPartnerId?.[0] === merchantId
);
for (const user of merchantUsers) {
if (user.id) {
try {
await this.keycloakApi.updateUser(user.id, { enabled: false }, requesterId);
} catch (error) {
this.logger.warn(`Could not suspend user ${user.id}: ${error.message}`);
}
}
}
}
async getHubUserById(userId: string, requesterId: string): Promise<HubUser> {
await this.validateHubUserAccess(requesterId);
const [user, userRoles] = await Promise.all([
this.keycloakApi.getUserById(userId, requesterId),
this.keycloakApi.getUserClientRoles(userId)
]);
const isHubUser = userRoles.some(role => this.isValidHubRole(role.name as UserRole));
if (!isHubUser) {
throw new BadRequestException(`User ${userId} is not a hub user`);
}
return this.mapToHubUser(user, userRoles);
}
async getAllHubUsers(requesterId: string): Promise<HubUser[]> {
await this.validateHubUserAccess(requesterId);
const allUsers = await this.keycloakApi.getAllUsers();
const hubUsers: HubUser[] = [];
for (const user of allUsers) {
if (!user.id) continue;
try {
const userRoles = await this.keycloakApi.getUserClientRoles(user.id);
const hubRole = userRoles.find(role => this.isValidHubRole(role.name as UserRole));
if (hubRole) {
hubUsers.push(this.mapToHubUser(user, userRoles));
}
} catch (error) {
this.logger.warn(`Could not process user ${user.id}: ${error.message}`);
}
}
return hubUsers;
}
async getHubUsersByRole(role: UserRole.DCB_ADMIN | UserRole.DCB_SUPPORT | UserRole.DCB_PARTNER, requesterId: string): Promise<HubUser[]> {
await this.validateHubUserAccess(requesterId);
const allHubUsers = await this.getAllHubUsers(requesterId);
return allHubUsers.filter(user => user.role === role);
}
async updateHubUser(
userId: string,
updates: Partial<{
firstName: string;
lastName: string;
email: string;
enabled: boolean;
}>,
requesterId: string
): Promise<HubUser> {
await this.validateHubUserAccess(requesterId);
await this.getHubUserById(userId, requesterId);
await this.keycloakApi.updateUser(userId, updates, requesterId);
return this.getHubUserById(userId, requesterId);
}
async updateHubUserRole(
userId: string,
newRole: UserRole.DCB_ADMIN | UserRole.DCB_SUPPORT |UserRole.DCB_PARTNER,
requesterId: string
): Promise<HubUser> {
// Validation implicite via le typage
await this.validateHubUserAccess(requesterId);
const requesterRoles = await this.keycloakApi.getUserClientRoles(requesterId);
const isRequesterAdmin = requesterRoles.some(role => role.name === UserRole.DCB_ADMIN);
if (!isRequesterAdmin) {
throw new ForbiddenException('Only DCB_ADMIN can change user roles');
}
await this.getHubUserById(userId, requesterId);
await this.keycloakApi.setClientRoles(userId, [newRole]);
return this.getHubUserById(userId, requesterId);
}
async updateMerchantPartner(
merchantId: string,
updates: Partial<{
firstName: string;
lastName: string;
email: string;
enabled: boolean;
}>,
requesterId: string
): Promise<HubUser> {
await this.validateHubUserAccess(requesterId);
await this.getHubUserById(merchantId, requesterId);
await this.validateMerchantManagementPermissions(requesterId, merchantId);
return this.getMerchantPartnerById(merchantId, requesterId);
}
async deleteHubUser(userId: string, requesterId: string): Promise<void> {
await this.validateHubUserAccess(requesterId);
await this.getHubUserById(userId, requesterId);
if (userId === requesterId) {
throw new BadRequestException('Cannot delete your own account');
}
if (await this.isLastAdmin(userId)) {
throw new BadRequestException('Cannot delete the last DCB_ADMIN user');
}
await this.keycloakApi.deleteUser(userId, requesterId);
this.logger.log(`Hub user deleted: ${userId} by ${requesterId}`);
}
// ===== GESTION DES MOTS DE PASSE =====
async resetHubUserPassword(
userId: string,
newPassword: string,
temporary: boolean = true,
requesterId: string
): Promise<void> {
await this.validateHubUserAccess(requesterId);
await this.getHubUserById(userId, requesterId);
await this.keycloakApi.resetUserPassword(userId, newPassword, temporary);
this.logger.log(`Password reset for hub user: ${userId}`);
}
async sendHubUserPasswordResetEmail(userId: string, requesterId: string): Promise<void> {
await this.validateHubUserAccess(requesterId);
const user = await this.getHubUserById(userId, requesterId);
await this.keycloakApi.sendPasswordResetEmail(user.email);
this.logger.log(`Password reset email sent to hub user: ${user.email}`);
}
// ===== STATISTIQUES ET RAPPORTS =====
async getHubUsersStats(requesterId: string): Promise<HubUserStats> {
await this.validateHubUserAccess(requesterId);
const allHubUsers = await this.getAllHubUsers(requesterId);
return {
totalAdmins: allHubUsers.filter(user => user.role === UserRole.DCB_ADMIN).length,
totalSupport: allHubUsers.filter(user => user.role === UserRole.DCB_SUPPORT).length,
activeUsers: allHubUsers.filter(user => user.enabled).length,
inactiveUsers: allHubUsers.filter(user => !user.enabled).length,
pendingActivation: allHubUsers.filter(user =>
user.attributes?.userStatus?.[0] === 'PENDING_ACTIVATION'
).length,
};
}
async getHubUserActivity(requesterId: string): Promise<HubUserActivity[]> {
await this.validateHubUserAccess(requesterId);
const allHubUsers = await this.getAllHubUsers(requesterId);
return allHubUsers.map(user => ({
user,
lastLogin: user.lastLogin ? new Date(user.lastLogin) : undefined
}));
}
async getActiveHubSessions(requesterId: string): Promise<{ userId: string; username: string; lastAccess: Date }[]> {
const activity = await this.getHubUserActivity(requesterId);
return activity
.filter(item => item.lastLogin)
.map(item => ({
userId: item.user.id,
username: item.user.username,
lastAccess: item.lastLogin!
}));
}
// ===== SANTÉ ET VALIDATIONS =====
async checkHubUsersHealth(requesterId: string): Promise<HubHealthStatus> {
await this.validateHubUserAccess(requesterId);
const stats = await this.getHubUsersStats(requesterId);
const issues: string[] = [];
if (stats.totalAdmins === 0) {
issues.push('No DCB_ADMIN users found - system is vulnerable');
}
if (stats.totalAdmins === 1) {
issues.push('Only one DCB_ADMIN user exists - consider creating backup administrators');
}
if (stats.inactiveUsers > stats.activeUsers / 2) {
issues.push('More than half of hub users are inactive');
}
if (stats.pendingActivation > 5) {
issues.push('Many users are pending activation - review onboarding process');
}
const status = issues.length === 0 ? 'healthy' : issues.length <= 2 ? 'degraded' : 'unhealthy';
return { status, issues, stats };
}
private async isLastAdmin(userId: string): Promise<boolean> {
try {
const [user, userRoles] = await Promise.all([
this.keycloakApi.getUserById(userId, userId),
this.keycloakApi.getUserClientRoles(userId)
]);
const isAdmin = userRoles.some(role => role.name === UserRole.DCB_ADMIN);
if (!isAdmin) return false;
const allHubUsers = await this.getAllHubUsers(userId);
const adminCount = allHubUsers.filter(user => user.role === UserRole.DCB_ADMIN).length;
return adminCount <= 1;
} catch (error) {
this.logger.error(`Error checking if user is last admin: ${error.message}`);
return false;
}
}
async canUserManageHubUsers(userId: string): Promise<boolean> {
try {
const roles = await this.keycloakApi.getUserClientRoles(userId);
return roles.some(role =>
this.HUB_ROLES.includes(role.name as UserRole)
);
} catch (error) {
return false;
}
}
}