feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature
This commit is contained in:
parent
950bf4397b
commit
fd43877036
@ -15,8 +15,8 @@ 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 { ApiModule } from './api/api.module';
|
import { ApiModule } from './api/api.module';
|
||||||
import { HealthModule } from './health/health.module';
|
|
||||||
import { UsersModule } from './users/users.module';
|
import { UsersModule } from './users/users.module';
|
||||||
|
import { StartupService } from './auth/services/startup.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -70,10 +70,11 @@ import { UsersModule } from './users/users.module';
|
|||||||
// Feature Modules
|
// Feature Modules
|
||||||
AuthModule,
|
AuthModule,
|
||||||
ApiModule,
|
ApiModule,
|
||||||
HealthModule,
|
|
||||||
UsersModule,
|
UsersModule,
|
||||||
|
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
StartupService,
|
||||||
// Global Authentication Guard
|
// Global Authentication Guard
|
||||||
{
|
{
|
||||||
provide: APP_GUARD,
|
provide: APP_GUARD,
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { HttpModule } from '@nestjs/axios';
|
import { HttpModule } from '@nestjs/axios';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { StartupService } from './services/startup.service';
|
|
||||||
import { TokenService } from './services/token.service';
|
import { TokenService } from './services/token.service';
|
||||||
import { KeycloakApiService } from './services/keycloak-api.service';
|
import { KeycloakApiService } from './services/keycloak-api.service';
|
||||||
import { AuthController } from './controllers/auth.controller';
|
import { AuthController } from './controllers/auth.controller';
|
||||||
import { HealthController } from '../health/health.controller';
|
|
||||||
import { UsersService } from '../users/services/users.service';
|
import { UsersService } from '../users/services/users.service';
|
||||||
import { KeycloakJwtStrategy } from './services/keycloak.strategy';
|
import { MerchantTeamService } from 'src/users/services/merchant-team.service';
|
||||||
import { JwtAuthGuard } from './guards/jwt.guard';
|
import { JwtAuthGuard } from './guards/jwt.guard';
|
||||||
|
|
||||||
|
|
||||||
@ -17,15 +15,14 @@ import { JwtAuthGuard } from './guards/jwt.guard';
|
|||||||
HttpModule,
|
HttpModule,
|
||||||
JwtModule.register({}),
|
JwtModule.register({}),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
KeycloakJwtStrategy,
|
|
||||||
JwtAuthGuard,
|
JwtAuthGuard,
|
||||||
StartupService,
|
|
||||||
TokenService,
|
TokenService,
|
||||||
KeycloakApiService,
|
KeycloakApiService,
|
||||||
UsersService
|
UsersService,
|
||||||
|
MerchantTeamService
|
||||||
],
|
],
|
||||||
controllers: [AuthController, HealthController],
|
controllers: [AuthController],
|
||||||
exports: [JwtAuthGuard, StartupService, TokenService, KeycloakApiService, UsersService, JwtModule],
|
exports: [JwtAuthGuard, TokenService, KeycloakApiService, UsersService, MerchantTeamService, JwtModule],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
|||||||
33
src/auth/guards/merchant-owner.guard.ts
Normal file
33
src/auth/guards/merchant-owner.guard.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
|
||||||
|
import { MerchantTeamService } from '../../users/services/merchant-team.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MerchantOwnerGuard implements CanActivate {
|
||||||
|
constructor(private readonly merchantTeamService: MerchantTeamService) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const user = request.user;
|
||||||
|
const targetUserId = request.params.userId;
|
||||||
|
|
||||||
|
if (!user || !targetUserId) {
|
||||||
|
throw new ForbiddenException('Invalid request');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Vérifier que l'utilisateur cible appartient bien au marchand
|
||||||
|
const isInTeam = await this.merchantTeamService.isUserInMerchantTeam(
|
||||||
|
targetUserId,
|
||||||
|
user.sub
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isInTeam) {
|
||||||
|
throw new ForbiddenException('Access denied: User not in your team');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
throw new ForbiddenException('Failed to verify team membership');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/auth/guards/merchant.guard.ts
Normal file
29
src/auth/guards/merchant.guard.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
|
||||||
|
import { UsersService } from '../../users/services/users.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MerchantGuard implements CanActivate {
|
||||||
|
constructor(private readonly usersService: UsersService) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const user = request.user;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new ForbiddenException('User not authenticated');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userRoles = await this.usersService.getUserClientRoles(user.sub);
|
||||||
|
|
||||||
|
// Autoriser les admins et les marchands
|
||||||
|
if (userRoles.includes('admin') || userRoles.includes('merchant')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ForbiddenException('Merchant access required');
|
||||||
|
} catch (error) {
|
||||||
|
throw new ForbiddenException('Failed to verify merchant permissions');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@ import { ConfigService } from '@nestjs/config';
|
|||||||
import { HttpService } from '@nestjs/axios';
|
import { HttpService } from '@nestjs/axios';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { firstValueFrom, Observable, timeout as rxjsTimeout } from 'rxjs';
|
import { firstValueFrom, Observable, timeout as rxjsTimeout } from 'rxjs';
|
||||||
import { TokenService } from './token.service'; // Import du TokenService
|
import { TokenService } from './token.service';
|
||||||
|
|
||||||
export interface KeycloakUser {
|
export interface KeycloakUser {
|
||||||
id?: string;
|
id?: string;
|
||||||
@ -13,7 +13,8 @@ export interface KeycloakUser {
|
|||||||
lastName?: string;
|
lastName?: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
emailVerified: boolean;
|
emailVerified: boolean;
|
||||||
attributes?: Record<string, any>;
|
attributes?: Record<string, string[]>;
|
||||||
|
createdTimestamp?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KeycloakRole {
|
export interface KeycloakRole {
|
||||||
@ -22,7 +23,7 @@ export interface KeycloakRole {
|
|||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ClientRole = 'admin' | 'merchant' | 'support';
|
export type ClientRole = 'admin' | 'merchant' | 'support' | 'merchant-admin' | 'merchant-manager' | 'merchant-support' | 'merchant-user';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class KeycloakApiService {
|
export class KeycloakApiService {
|
||||||
@ -34,7 +35,7 @@ export class KeycloakApiService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly httpService: HttpService,
|
private readonly httpService: HttpService,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
private readonly tokenService: TokenService, // Injection du TokenService
|
private readonly tokenService: TokenService,
|
||||||
) {
|
) {
|
||||||
this.keycloakBaseUrl = this.configService.get<string>('KEYCLOAK_SERVER_URL') || 'http://localhost:8080';
|
this.keycloakBaseUrl = this.configService.get<string>('KEYCLOAK_SERVER_URL') || 'http://localhost:8080';
|
||||||
this.realm = this.configService.get<string>('KEYCLOAK_REALM') || 'master';
|
this.realm = this.configService.get<string>('KEYCLOAK_REALM') || 'master';
|
||||||
@ -46,7 +47,7 @@ export class KeycloakApiService {
|
|||||||
return this.tokenService.acquireUserToken(username, password);
|
return this.tokenService.acquireUserToken(username, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== CORE REQUEST METHOD (pour opérations admin) =====
|
// ===== CORE REQUEST METHOD =====
|
||||||
private async request<T>(
|
private async request<T>(
|
||||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||||
path: string,
|
path: string,
|
||||||
@ -100,6 +101,7 @@ export class KeycloakApiService {
|
|||||||
|
|
||||||
this.logger.error(`Keycloak API error in ${context}: ${error.message}`, {
|
this.logger.error(`Keycloak API error in ${context}: ${error.message}`, {
|
||||||
status: error.response?.status,
|
status: error.response?.status,
|
||||||
|
data: error.response?.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
@ -111,50 +113,105 @@ export class KeycloakApiService {
|
|||||||
// ===== USER CRUD OPERATIONS =====
|
// ===== USER CRUD OPERATIONS =====
|
||||||
async createUser(userData: {
|
async createUser(userData: {
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email?: string;
|
||||||
firstName: string;
|
firstName?: string;
|
||||||
lastName: string;
|
lastName?: string;
|
||||||
password: string;
|
password?: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
emailVerified?: boolean;
|
||||||
|
attributes?: Record<string, string[]>;
|
||||||
|
credentials?: any[];
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
const userPayload = {
|
|
||||||
|
this.logger.debug(`🔍 CREATE USER - Input data:`, {
|
||||||
|
username: userData.username,
|
||||||
|
hasPassword: !!userData.password,
|
||||||
|
hasCredentials: !!userData.credentials,
|
||||||
|
credentialsLength: userData.credentials ? userData.credentials.length : 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const userPayload: any = {
|
||||||
username: userData.username,
|
username: userData.username,
|
||||||
email: userData.email,
|
email: userData.email,
|
||||||
firstName: userData.firstName,
|
firstName: userData.firstName,
|
||||||
lastName: userData.lastName,
|
lastName: userData.lastName,
|
||||||
enabled: userData.enabled ?? true,
|
enabled: userData.enabled ?? true,
|
||||||
emailVerified: true,
|
emailVerified: userData.emailVerified ?? true,
|
||||||
credentials: [{
|
};
|
||||||
|
|
||||||
|
if (userData.password) {
|
||||||
|
// Format direct : password field
|
||||||
|
userPayload.credentials = [{
|
||||||
type: 'password',
|
type: 'password',
|
||||||
value: userData.password,
|
value: userData.password,
|
||||||
temporary: false,
|
temporary: false,
|
||||||
}],
|
}];
|
||||||
};
|
} else if (userData.credentials && userData.credentials.length > 0) {
|
||||||
|
// Format credentials array (venant de UsersService)
|
||||||
await this.request('POST', `/admin/realms/${this.realm}/users`, userPayload);
|
userPayload.credentials = userData.credentials;
|
||||||
|
} else {
|
||||||
const users = await this.findUserByUsername(userData.username);
|
throw new BadRequestException('Password is required');
|
||||||
if (users.length === 0) {
|
}
|
||||||
throw new Error('Failed to create user');
|
|
||||||
|
this.logger.debug(`🔍 CREATE USER - Final Keycloak payload:`, JSON.stringify(userPayload, null, 2));
|
||||||
|
|
||||||
|
// Format correct des attributs
|
||||||
|
if (userData.attributes) {
|
||||||
|
const formattedAttributes: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(userData.attributes)) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
formattedAttributes[key] = value;
|
||||||
|
} else {
|
||||||
|
formattedAttributes[key] = [String(value)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userPayload.attributes = formattedAttributes;
|
||||||
|
this.logger.debug(`🔍 CREATE USER - Formatted attributes:`, formattedAttributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.logger.log(`Creating user in Keycloak: ${userData.username}`);
|
||||||
|
await this.request('POST', `/admin/realms/${this.realm}/users`, userPayload);
|
||||||
|
|
||||||
|
// Récupérer l'ID
|
||||||
|
const users = await this.findUserByUsername(userData.username);
|
||||||
|
if (users.length === 0) {
|
||||||
|
throw new Error('User not found after creation');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`User created successfully with ID: ${users[0].id}`);
|
||||||
|
return users[0].id!;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`❌ FAILED to create user in Keycloak: ${error.message}`);
|
||||||
|
if (error.response?.data) {
|
||||||
|
this.logger.error(`❌ Keycloak error response: ${JSON.stringify(error.response.data)}`);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return users[0].id!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserById(userId: string): Promise<KeycloakUser> {
|
async getUserById(userId: string): Promise<KeycloakUser> {
|
||||||
return this.request('GET', `/admin/realms/${this.realm}/users/${userId}`);
|
return this.request<KeycloakUser>('GET', `/admin/realms/${this.realm}/users/${userId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllUsers(): Promise<KeycloakUser[]> {
|
async getAllUsers(): Promise<KeycloakUser[]> {
|
||||||
return this.request('GET', `/admin/realms/${this.realm}/users`);
|
return this.request<KeycloakUser[]>('GET', `/admin/realms/${this.realm}/users`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findUserByUsername(username: string): Promise<KeycloakUser[]> {
|
async findUserByUsername(username: string): Promise<KeycloakUser[]> {
|
||||||
return this.request('GET', `/admin/realms/${this.realm}/users?username=${encodeURIComponent(username)}`);
|
return this.request<KeycloakUser[]>(
|
||||||
|
'GET',
|
||||||
|
`/admin/realms/${this.realm}/users?username=${encodeURIComponent(username)}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findUserByEmail(email: string): Promise<KeycloakUser[]> {
|
async findUserByEmail(email: string): Promise<KeycloakUser[]> {
|
||||||
return this.request('GET', `/admin/realms/${this.realm}/users?email=${encodeURIComponent(email)}`);
|
return this.request<KeycloakUser[]>(
|
||||||
|
'GET',
|
||||||
|
`/admin/realms/${this.realm}/users?email=${encodeURIComponent(email)}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUser(userId: string, userData: Partial<KeycloakUser>): Promise<void> {
|
async updateUser(userId: string, userData: Partial<KeycloakUser>): Promise<void> {
|
||||||
@ -165,35 +222,196 @@ export class KeycloakApiService {
|
|||||||
return this.request('DELETE', `/admin/realms/${this.realm}/users/${userId}`);
|
return this.request('DELETE', `/admin/realms/${this.realm}/users/${userId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== ATTRIBUTES MANAGEMENT =====
|
||||||
|
async setUserAttributes(userId: string, attributes: Record<string, string[]>): Promise<void> {
|
||||||
|
try {
|
||||||
|
const user = await this.getUserById(userId);
|
||||||
|
const updatedUser = {
|
||||||
|
...user,
|
||||||
|
attributes: {
|
||||||
|
...user.attributes,
|
||||||
|
...attributes
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.updateUser(userId, updatedUser);
|
||||||
|
this.logger.log(`Attributes set for user ${userId}: ${Object.keys(attributes).join(', ')}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to set attributes for user ${userId}: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUserAttributes(userId: string, attributes: Record<string, string[]>): Promise<void> {
|
||||||
|
try {
|
||||||
|
const user = await this.getUserById(userId);
|
||||||
|
const currentAttributes = user.attributes || {};
|
||||||
|
|
||||||
|
const updatedUser = {
|
||||||
|
...user,
|
||||||
|
attributes: {
|
||||||
|
...currentAttributes,
|
||||||
|
...attributes
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.updateUser(userId, updatedUser);
|
||||||
|
this.logger.log(`Attributes updated for user ${userId}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to update attributes for user ${userId}: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeUserAttribute(userId: string, attributeName: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const user = await this.getUserById(userId);
|
||||||
|
const currentAttributes = { ...user.attributes };
|
||||||
|
|
||||||
|
if (currentAttributes && currentAttributes[attributeName]) {
|
||||||
|
delete currentAttributes[attributeName];
|
||||||
|
|
||||||
|
const updatedUser = {
|
||||||
|
...user,
|
||||||
|
attributes: currentAttributes
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.updateUser(userId, updatedUser);
|
||||||
|
this.logger.log(`Attribute ${attributeName} removed from user ${userId}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to remove attribute ${attributeName} from user ${userId}: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserAttribute(userId: string, attributeName: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const user = await this.getUserById(userId);
|
||||||
|
const attributes = user.attributes || {};
|
||||||
|
return attributes[attributeName]?.[0] || null;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to get attribute ${attributeName} for user ${userId}: ${error.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserAttributes(userId: string): Promise<Record<string, string[]>> {
|
||||||
|
try {
|
||||||
|
const user = await this.getUserById(userId);
|
||||||
|
return user.attributes || {};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to get attributes for user ${userId}: ${error.message}`);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ===== CLIENT ROLE OPERATIONS =====
|
// ===== CLIENT ROLE OPERATIONS =====
|
||||||
async getUserClientRoles(userId: string): Promise<KeycloakRole[]> {
|
async getUserClientRoles(userId: string): Promise<KeycloakRole[]> {
|
||||||
const clients = await this.getClient();
|
try {
|
||||||
return this.request('GET', `/admin/realms/${this.realm}/users/${userId}/role-mappings/clients/${clients[0].id}`);
|
const clients = await this.getClient();
|
||||||
|
return await this.request<KeycloakRole[]>(
|
||||||
|
'GET',
|
||||||
|
`/admin/realms/${this.realm}/users/${userId}/role-mappings/clients/${clients[0].id}`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to get client roles for user ${userId}: ${error.message}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async assignClientRole(userId: string, role: ClientRole): Promise<void> {
|
async assignClientRole(userId: string, role: ClientRole): Promise<void> {
|
||||||
const clients = await this.getClient();
|
try {
|
||||||
const targetRole = await this.getRole(role, clients[0].id);
|
const clients = await this.getClient();
|
||||||
|
const targetRole = await this.getRole(role, clients[0].id);
|
||||||
return this.request('POST', `/admin/realms/${this.realm}/users/${userId}/role-mappings/clients/${clients[0].id}`, [targetRole]);
|
|
||||||
|
await this.request(
|
||||||
|
'POST',
|
||||||
|
`/admin/realms/${this.realm}/users/${userId}/role-mappings/clients/${clients[0].id}`,
|
||||||
|
[targetRole]
|
||||||
|
);
|
||||||
|
this.logger.log(`Role ${role} assigned to user ${userId}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to assign role ${role} to user ${userId}: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeClientRole(userId: string, role: ClientRole): Promise<void> {
|
async removeClientRole(userId: string, role: ClientRole): Promise<void> {
|
||||||
const clients = await this.getClient();
|
try {
|
||||||
const targetRole = await this.getRole(role, clients[0].id);
|
const clients = await this.getClient();
|
||||||
|
const targetRole = await this.getRole(role, clients[0].id);
|
||||||
return this.request('DELETE', `/admin/realms/${this.realm}/users/${userId}/role-mappings/clients/${clients[0].id}`, [targetRole]);
|
|
||||||
|
await this.request(
|
||||||
|
'DELETE',
|
||||||
|
`/admin/realms/${this.realm}/users/${userId}/role-mappings/clients/${clients[0].id}`,
|
||||||
|
[targetRole]
|
||||||
|
);
|
||||||
|
this.logger.log(`Role ${role} removed from user ${userId}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to remove role ${role} from user ${userId}: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setClientRoles(userId: string, roles: ClientRole[]): Promise<void> {
|
async setClientRoles(userId: string, roles: ClientRole[]): Promise<void> {
|
||||||
const currentRoles = await this.getUserClientRoles(userId);
|
try {
|
||||||
if (currentRoles.length > 0) {
|
|
||||||
const clients = await this.getClient();
|
const clients = await this.getClient();
|
||||||
await this.request('DELETE', `/admin/realms/${this.realm}/users/${userId}/role-mappings/clients/${clients[0].id}`, currentRoles);
|
const clientId = clients[0].id;
|
||||||
}
|
|
||||||
|
// Récupérer les rôles actuels
|
||||||
|
const currentRoles = await this.getUserClientRoles(userId);
|
||||||
|
|
||||||
|
// Supprimer tous les rôles actuels si nécessaire
|
||||||
|
if (currentRoles.length > 0) {
|
||||||
|
await this.request(
|
||||||
|
'DELETE',
|
||||||
|
`/admin/realms/${this.realm}/users/${userId}/role-mappings/clients/${clientId}`,
|
||||||
|
currentRoles
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
for (const role of roles) {
|
// Ajouter les nouveaux rôles
|
||||||
await this.assignClientRole(userId, role);
|
if (roles.length > 0) {
|
||||||
|
const targetRoles = await Promise.all(
|
||||||
|
roles.map(role => this.getRole(role, clientId))
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.request(
|
||||||
|
'POST',
|
||||||
|
`/admin/realms/${this.realm}/users/${userId}/role-mappings/clients/${clientId}`,
|
||||||
|
targetRoles
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Client roles set for user ${userId}: ${roles.join(', ')}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to set client roles for user ${userId}: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addClientRoles(userId: string, roles: ClientRole[]): Promise<void> {
|
||||||
|
try {
|
||||||
|
for (const role of roles) {
|
||||||
|
await this.assignClientRole(userId, role);
|
||||||
|
}
|
||||||
|
this.logger.log(`Client roles added to user ${userId}: ${roles.join(', ')}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to add client roles to user ${userId}: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeClientRoles(userId: string, roles: ClientRole[]): Promise<void> {
|
||||||
|
try {
|
||||||
|
for (const role of roles) {
|
||||||
|
await this.removeClientRole(userId, role);
|
||||||
|
}
|
||||||
|
this.logger.log(`Client roles removed from user ${userId}: ${roles.join(', ')}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to remove client roles from user ${userId}: ${error.message}`);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,10 +427,12 @@ export class KeycloakApiService {
|
|||||||
|
|
||||||
async enableUser(userId: string): Promise<void> {
|
async enableUser(userId: string): Promise<void> {
|
||||||
await this.updateUser(userId, { enabled: true });
|
await this.updateUser(userId, { enabled: true });
|
||||||
|
this.logger.log(`User ${userId} enabled`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async disableUser(userId: string): Promise<void> {
|
async disableUser(userId: string): Promise<void> {
|
||||||
await this.updateUser(userId, { enabled: false });
|
await this.updateUser(userId, { enabled: false });
|
||||||
|
this.logger.log(`User ${userId} disabled`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async resetPassword(userId: string, newPassword: string): Promise<void> {
|
async resetPassword(userId: string, newPassword: string): Promise<void> {
|
||||||
@ -222,14 +442,69 @@ export class KeycloakApiService {
|
|||||||
temporary: false,
|
temporary: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.request('PUT', `/admin/realms/${this.realm}/users/${userId}/reset-password`, credentials);
|
await this.request('PUT', `/admin/realms/${this.realm}/users/${userId}/reset-password`, credentials);
|
||||||
|
this.logger.log(`Password reset for user ${userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUsersByAttribute(attributeName: string, attributeValue: string): Promise<KeycloakUser[]> {
|
||||||
|
try {
|
||||||
|
const allUsers = await this.getAllUsers();
|
||||||
|
return allUsers.filter(user =>
|
||||||
|
user.attributes &&
|
||||||
|
user.attributes[attributeName] &&
|
||||||
|
user.attributes[attributeName].includes(attributeValue)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to get users by attribute ${attributeName}: ${error.message}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== HEALTH CHECKS =====
|
||||||
|
async checkKeycloakAvailability(): Promise<boolean> {
|
||||||
|
const url = `${this.keycloakBaseUrl}/realms/${this.realm}`;
|
||||||
|
try {
|
||||||
|
await firstValueFrom(
|
||||||
|
this.httpService.get(url).pipe(rxjsTimeout(5000)),
|
||||||
|
);
|
||||||
|
this.logger.log(`Keycloak disponible à l'adresse : ${url}`);
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Keycloak indisponible : ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkServiceConnection(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const token = await this.tokenService.acquireServiceAccountToken();
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('Aucun token de service retourné');
|
||||||
|
}
|
||||||
|
|
||||||
|
const testUrl = `${this.keycloakBaseUrl}/admin/realms/${this.realm}/users`;
|
||||||
|
const config = {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
timeout: 5000,
|
||||||
|
};
|
||||||
|
|
||||||
|
await firstValueFrom(
|
||||||
|
this.httpService.get(testUrl, config).pipe(rxjsTimeout(5000)),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.log('Connexion du service à Keycloak réussie');
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Échec de la connexion du service : ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== PRIVATE HELPERS =====
|
// ===== PRIVATE HELPERS =====
|
||||||
private async getClient(): Promise<any[]> {
|
private async getClient(): Promise<any[]> {
|
||||||
const clients = await this.request<any[]>('GET', `/admin/realms/${this.realm}/clients?clientId=${this.clientId}`);
|
const clients = await this.request<any[]>('GET', `/admin/realms/${this.realm}/clients?clientId=${this.clientId}`);
|
||||||
if (!clients || clients.length === 0) {
|
if (!clients || clients.length === 0) {
|
||||||
throw new Error('Client not found');
|
throw new Error(`Client '${this.clientId}' not found in realm '${this.realm}'`);
|
||||||
}
|
}
|
||||||
return clients;
|
return clients;
|
||||||
}
|
}
|
||||||
@ -239,8 +514,53 @@ export class KeycloakApiService {
|
|||||||
const targetRole = roles.find(r => r.name === role);
|
const targetRole = roles.find(r => r.name === role);
|
||||||
|
|
||||||
if (!targetRole) {
|
if (!targetRole) {
|
||||||
throw new BadRequestException(`Role '${role}' not found`);
|
throw new BadRequestException(`Role '${role}' not found in client '${this.clientId}'`);
|
||||||
}
|
}
|
||||||
return targetRole;
|
return targetRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== MÉTHODES POUR LE STARTUP SERVICE (compatibilité) =====
|
||||||
|
async checkHealth(username: string, password: string): Promise<{ status: string }> {
|
||||||
|
try {
|
||||||
|
const isAvailable = await this.checkKeycloakAvailability();
|
||||||
|
const isConnected = await this.checkServiceConnection();
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: isAvailable && isConnected ? 'healthy' : 'unhealthy'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { status: 'unhealthy' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRealmClients(realm: string, username: string, password: string): Promise<any[]> {
|
||||||
|
return this.request<any[]>('GET', `/admin/realms/${realm}/clients`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRealmInfo(realm: string, username: string, password: string): Promise<any> {
|
||||||
|
return this.request<any>('GET', `/admin/realms/${realm}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUsers(realm: string, username: string, password: string, options?: any): Promise<KeycloakUser[]> {
|
||||||
|
let url = `/admin/realms/${realm}/users`;
|
||||||
|
if (options?.max) {
|
||||||
|
url += `?max=${options.max}`;
|
||||||
|
}
|
||||||
|
if (options?.username) {
|
||||||
|
url += `${url.includes('?') ? '&' : '?'}username=${encodeURIComponent(options.username)}`;
|
||||||
|
}
|
||||||
|
return this.request<KeycloakUser[]>(url.includes('?') ? 'GET' : 'GET', url);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserProfile(realm: string, token: string): Promise<any> {
|
||||||
|
const config = {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = `${this.keycloakBaseUrl}/realms/${realm}/protocol/openid-connect/userinfo`;
|
||||||
|
const response = await firstValueFrom(this.httpService.get(url, config));
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... autres méthodes de compatibilité pour le StartupService
|
||||||
}
|
}
|
||||||
@ -1,56 +0,0 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
|
||||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
|
||||||
import * as jwksRsa from 'jwks-rsa';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class KeycloakJwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
|
||||||
private readonly logger = new Logger(KeycloakJwtStrategy.name);
|
|
||||||
|
|
||||||
constructor(private configService: ConfigService) {
|
|
||||||
const jwksUri = configService.get<string>('KEYCLOAK_JWKS_URI');
|
|
||||||
const issuer = configService.get<string>('KEYCLOAK_ISSUER');
|
|
||||||
|
|
||||||
if (!jwksUri || !issuer) {
|
|
||||||
throw new Error('Missing Keycloak configuration (KEYCLOAK_JWKS_URI / KEYCLOAK_ISSUER)');
|
|
||||||
}
|
|
||||||
|
|
||||||
super({
|
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
||||||
secretOrKeyProvider: jwksRsa.passportJwtSecret({
|
|
||||||
cache: true,
|
|
||||||
rateLimit: true,
|
|
||||||
jwksRequestsPerMinute: 5,
|
|
||||||
jwksUri,
|
|
||||||
}),
|
|
||||||
issuer,
|
|
||||||
algorithms: ['RS256'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async validate(payload: any) {
|
|
||||||
// Récupération des rôles du realm
|
|
||||||
const realmRoles: string[] = payload.realm_access?.roles || [];
|
|
||||||
|
|
||||||
// Récupération des rôles du client dans resource_access
|
|
||||||
const clientId = payload.client_id;
|
|
||||||
const resourceRoles: string[] =
|
|
||||||
payload.resource_access?.[clientId]?.roles || [];
|
|
||||||
|
|
||||||
// Fusion et suppression des doublons
|
|
||||||
const roles = Array.from(new Set([...realmRoles, ...resourceRoles]));
|
|
||||||
|
|
||||||
this.logger.verbose(`User ${payload.preferred_username} roles: ${JSON.stringify(roles)}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
sub: payload.sub,
|
|
||||||
preferred_username: payload.preferred_username,
|
|
||||||
email: payload.email,
|
|
||||||
client_id: clientId,
|
|
||||||
roles,
|
|
||||||
realmRoles,
|
|
||||||
resourceRoles,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,184 +1,42 @@
|
|||||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
import { TokenService } from './token.service';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { KeycloakApiService } from './keycloak-api.service';
|
import { KeycloakApiService } from './keycloak-api.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StartupService implements OnModuleInit {
|
export class StartupService implements OnModuleInit {
|
||||||
private readonly logger = new Logger(StartupService.name);
|
private readonly logger = new Logger(StartupService.name);
|
||||||
private isInitialized = false;
|
private initialized = false;
|
||||||
private initializationError: string | null = null;
|
private error: string | null = null;
|
||||||
private userToken: string | null = null;
|
|
||||||
|
|
||||||
constructor(
|
constructor(private readonly keycloakApiService: KeycloakApiService) {}
|
||||||
private readonly tokenService: TokenService,
|
|
||||||
private readonly keycloakApiService: KeycloakApiService,
|
|
||||||
private readonly configService: ConfigService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
this.logger.log('Starting Keycloak connection tests...');
|
this.logger.log('Vérification de la disponibilité de Keycloak...');
|
||||||
|
|
||||||
const username = this.configService.get<string>('KEYCLOAK_TEST_USER_ADMIN');
|
|
||||||
const password = this.configService.get<string>('KEYCLOAK_TEST_PASSWORD_ADMIN');
|
|
||||||
|
|
||||||
if (!username || !password) {
|
|
||||||
this.initializationError = 'Test username/password not configured';
|
|
||||||
this.logger.error(this.initializationError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Test d'authentification utilisateur
|
const available = await this.keycloakApiService.checkKeycloakAvailability();
|
||||||
const tokenResponse = await this.keycloakApiService.authenticateUser(username, password);
|
if (!available) throw new Error('Keycloak non accessible');
|
||||||
this.userToken = tokenResponse.access_token;
|
|
||||||
this.logger.log('✅ User authentication test passed');
|
|
||||||
|
|
||||||
// 2. Test des opérations CRUD admin
|
const serviceConnected = await this.keycloakApiService.checkServiceConnection();
|
||||||
//await this.testAdminOperations();
|
if (!serviceConnected) throw new Error('Échec de la connexion du service à Keycloak');
|
||||||
|
|
||||||
// 3. Test des opérations avec le nouveau mot de passe
|
this.initialized = true;
|
||||||
//await this.testUserOperationsWithNewPassword();
|
this.logger.log('Keycloak disponible et connexion du service réussie');
|
||||||
|
} catch (err: any) {
|
||||||
this.isInitialized = true;
|
this.error = err.message;
|
||||||
this.logger.log('✅ All CRUD operations tested successfully');
|
this.logger.error('Échec de la vérification de Keycloak', err);
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
this.initializationError = error.message || error;
|
|
||||||
this.logger.error('❌ Keycloak connection or method test failed', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async testAdminOperations(): Promise<void> {
|
|
||||||
this.logger.log('Testing admin CRUD operations...');
|
|
||||||
|
|
||||||
let testUserId = '';
|
|
||||||
const usernameTest = 'startup-test-user';
|
|
||||||
|
|
||||||
// Vérifier si l'utilisateur existe
|
|
||||||
let testUser = await this.keycloakApiService.findUserByUsername(usernameTest);
|
|
||||||
if (!testUser || testUser.length === 0) {
|
|
||||||
// Créer l'utilisateur si inexistant
|
|
||||||
const newUser = {
|
|
||||||
username: usernameTest,
|
|
||||||
email: 'startup-test@example.com',
|
|
||||||
firstName: 'Startup',
|
|
||||||
lastName: 'Test',
|
|
||||||
password: 'TempPassword123!',
|
|
||||||
enabled: true,
|
|
||||||
};
|
|
||||||
testUserId = await this.keycloakApiService.createUser(newUser);
|
|
||||||
this.logger.log(`✅ Test user "${usernameTest}" created with ID: ${testUserId}`);
|
|
||||||
} else {
|
|
||||||
testUserId = testUser[0].id!;
|
|
||||||
this.logger.log(`✅ Using existing test user ID: ${testUserId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!testUserId) {
|
|
||||||
throw new Error('Unable to create or fetch test user');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer l'utilisateur par ID
|
|
||||||
const userById = await this.keycloakApiService.getUserById(testUserId);
|
|
||||||
this.logger.log(`✅ User fetched by ID: ${userById.username}`);
|
|
||||||
|
|
||||||
// Modifier l'utilisateur
|
|
||||||
await this.keycloakApiService.updateUser(testUserId, { firstName: 'UpdatedStartup' });
|
|
||||||
this.logger.log(`✅ User updated: ${testUserId}`);
|
|
||||||
|
|
||||||
// Opérations sur les rôles
|
|
||||||
const userRoles = await this.keycloakApiService.getUserClientRoles(testUserId);
|
|
||||||
this.logger.log(`✅ User client roles: ${userRoles.length}`);
|
|
||||||
|
|
||||||
// Assigner un rôle client
|
|
||||||
await this.keycloakApiService.assignClientRole(testUserId, 'merchant');
|
|
||||||
this.logger.log(`✅ Assigned merchant role to user: ${testUserId}`);
|
|
||||||
|
|
||||||
// Vérifier les rôles après assignation
|
|
||||||
const rolesAfterAssign = await this.keycloakApiService.getUserClientRoles(testUserId);
|
|
||||||
this.logger.log(`✅ User roles after assignment: ${rolesAfterAssign.map(r => r.name).join(', ')}`);
|
|
||||||
|
|
||||||
// Retirer le rôle
|
|
||||||
await this.keycloakApiService.removeClientRole(testUserId, 'merchant');
|
|
||||||
this.logger.log(`✅ Removed merchant role from user: ${testUserId}`);
|
|
||||||
|
|
||||||
// Tester l'assignation multiple de rôles
|
|
||||||
await this.keycloakApiService.setClientRoles(testUserId, ['admin', 'support']);
|
|
||||||
this.logger.log(`✅ Set multiple roles for user: admin, support`);
|
|
||||||
|
|
||||||
// Vérifier les rôles après assignation multiple
|
|
||||||
const finalRoles = await this.keycloakApiService.getUserClientRoles(testUserId);
|
|
||||||
this.logger.log(`✅ Final user roles: ${finalRoles.map(r => r.name).join(', ')}`);
|
|
||||||
|
|
||||||
// Réinitialisation du mot de passe
|
|
||||||
const newPassword = 'NewStartupPass123!';
|
|
||||||
await this.keycloakApiService.resetPassword(testUserId, newPassword);
|
|
||||||
this.logger.log(`✅ Password reset for user: ${testUserId}`);
|
|
||||||
|
|
||||||
// Tester enable/disable user
|
|
||||||
await this.keycloakApiService.disableUser(testUserId);
|
|
||||||
this.logger.log(`✅ User disabled: ${testUserId}`);
|
|
||||||
|
|
||||||
// Vérifier que l'utilisateur est désactivé
|
|
||||||
const disabledUser = await this.keycloakApiService.getUserById(testUserId);
|
|
||||||
this.logger.log(`✅ User enabled status: ${disabledUser.enabled}`);
|
|
||||||
|
|
||||||
// Réactiver l'utilisateur
|
|
||||||
await this.keycloakApiService.enableUser(testUserId);
|
|
||||||
this.logger.log(`✅ User enabled: ${testUserId}`);
|
|
||||||
|
|
||||||
// Tester la recherche d'utilisateurs
|
|
||||||
const userByEmail = await this.keycloakApiService.findUserByEmail('startup-test@example.com');
|
|
||||||
this.logger.log(`✅ User found by email: ${userByEmail ? userByEmail[0]?.username : 'none'}`);
|
|
||||||
|
|
||||||
// Vérifier si l'utilisateur existe
|
|
||||||
const userExists = await this.keycloakApiService.userExists(usernameTest);
|
|
||||||
this.logger.log(`✅ User exists check: ${userExists}`);
|
|
||||||
|
|
||||||
// Récupérer tous les utilisateurs
|
|
||||||
const allUsers = await this.keycloakApiService.getAllUsers();
|
|
||||||
this.logger.log(`✅ All users count: ${allUsers.length}`);
|
|
||||||
|
|
||||||
// Cleanup optionnel - Supprimer l'utilisateur de test
|
|
||||||
// await this.keycloakApiService.deleteUser(testUserId);
|
|
||||||
// this.logger.log(`✅ Test user deleted: ${testUserId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async testUserOperationsWithNewPassword(): Promise<void> {
|
|
||||||
this.logger.log('Testing user operations with new password...');
|
|
||||||
|
|
||||||
const usernameTest = 'startup-test-user';
|
|
||||||
const newPassword = 'NewStartupPass123!';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Tester la reconnexion avec le nouveau mot de passe
|
|
||||||
const newTokenResponse = await this.keycloakApiService.authenticateUser(usernameTest, newPassword);
|
|
||||||
this.logger.log(`✅ Test user reconnected successfully with new password`);
|
|
||||||
this.logger.log(`✅ New token acquired for test user`);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.warn(`⚠️ Test user reconnection failed: ${error.message}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatus() {
|
getStatus() {
|
||||||
return {
|
return {
|
||||||
status: this.isInitialized ? 'healthy' : 'unhealthy',
|
status: this.initialized ? 'healthy' : 'unhealthy',
|
||||||
keycloak: {
|
keycloakConnected: this.initialized,
|
||||||
connected: this.isInitialized,
|
|
||||||
realm: this.configService.get('KEYCLOAK_REALM'),
|
|
||||||
serverUrl: this.configService.get('KEYCLOAK_SERVER_URL'),
|
|
||||||
},
|
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
error: this.initializationError,
|
error: this.error,
|
||||||
userToken: this.userToken ? 'available' : 'none',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
isHealthy(): boolean {
|
isHealthy(): boolean {
|
||||||
return this.isInitialized;
|
return this.initialized;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
getUserToken(): string | null {
|
|
||||||
return this.userToken;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,268 +0,0 @@
|
|||||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { HttpService } from '@nestjs/axios';
|
|
||||||
import { firstValueFrom, timeout as rxjsTimeout } from 'rxjs';
|
|
||||||
|
|
||||||
export interface KeycloakTokenResponse {
|
|
||||||
access_token: string;
|
|
||||||
expires_in: number;
|
|
||||||
refresh_token?: string;
|
|
||||||
token_type: string;
|
|
||||||
scope?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class TokenService implements OnModuleInit {
|
|
||||||
private readonly logger = new Logger(TokenService.name);
|
|
||||||
|
|
||||||
private currentToken: string | null = null;
|
|
||||||
private tokenExpiry: Date | null = null;
|
|
||||||
private acquiringTokenPromise: Promise<string> | null = null;
|
|
||||||
|
|
||||||
// Mapping access_token → refresh_token pour gérer les logouts utilisateurs
|
|
||||||
private userRefreshTokens = new Map<string, string>();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly configService: ConfigService,
|
|
||||||
private readonly httpService: HttpService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async onModuleInit() {
|
|
||||||
try {
|
|
||||||
await this.acquireClientToken();
|
|
||||||
} catch {
|
|
||||||
this.logger.warn('Initial Keycloak client token acquisition failed (will retry on demand).');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getTokenUrl(): string {
|
|
||||||
const serverUrl = this.configService.get<string>('keycloak.serverUrl');
|
|
||||||
const realm = this.configService.get<string>('keycloak.realm');
|
|
||||||
|
|
||||||
if (!serverUrl || !realm) {
|
|
||||||
throw new Error('Keycloak serverUrl or realm not configured');
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${serverUrl}/realms/${realm}/protocol/openid-connect/token`;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAccessTokenFromHeader(request: Request): string | null {
|
|
||||||
const authHeader = request.headers['authorization'];
|
|
||||||
if (!authHeader) return null;
|
|
||||||
const [type, token] = authHeader.split(' ');
|
|
||||||
return type === 'Bearer' ? token : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getLogoutUrl(): string {
|
|
||||||
const serverUrl = this.configService.get<string>('keycloak.serverUrl');
|
|
||||||
const realm = this.configService.get<string>('keycloak.realm');
|
|
||||||
return `${serverUrl}/realms/${realm}/protocol/openid-connect/logout`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private validateClientConfig(): { clientId: string; clientSecret: string } {
|
|
||||||
const clientId = this.configService.get<string>('keycloak.clientId');
|
|
||||||
const clientSecret = this.configService.get<string>('keycloak.clientSecret');
|
|
||||||
|
|
||||||
if (!clientId) throw new Error('KEYCLOAK_CLIENT_ID is not configured');
|
|
||||||
if (!clientSecret) throw new Error('KEYCLOAK_CLIENT_SECRET is not configured');
|
|
||||||
|
|
||||||
return { clientId, clientSecret };
|
|
||||||
}
|
|
||||||
|
|
||||||
private validateUserClientConfig(): { userClientId: string; userClientSecret: string } {
|
|
||||||
const userClientId = this.configService.get<string>('keycloak.userClientId');
|
|
||||||
const userClientSecret = this.configService.get<string>('keycloak.userClientSecret');
|
|
||||||
|
|
||||||
if (!userClientId) throw new Error('KEYCLOAK_CLIENT_ID is not configured');
|
|
||||||
if (!userClientSecret) throw new Error('KEYCLOAK_CLIENT_SECRET is not configured');
|
|
||||||
|
|
||||||
return { userClientId, userClientSecret };
|
|
||||||
}
|
|
||||||
|
|
||||||
/** -------------------------------
|
|
||||||
* CLIENT CREDENTIALS TOKEN
|
|
||||||
* ------------------------------- */
|
|
||||||
async acquireClientToken(): Promise<string> {
|
|
||||||
if (this.acquiringTokenPromise) return this.acquiringTokenPromise;
|
|
||||||
|
|
||||||
this.acquiringTokenPromise = (async () => {
|
|
||||||
try {
|
|
||||||
const tokenUrl = this.getTokenUrl();
|
|
||||||
const { clientId, clientSecret } = this.validateClientConfig();
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append('grant_type', 'client_credentials');
|
|
||||||
params.append('client_id', clientId);
|
|
||||||
params.append('client_secret', clientSecret);
|
|
||||||
|
|
||||||
const response = await firstValueFrom(
|
|
||||||
this.httpService
|
|
||||||
.post<KeycloakTokenResponse>(tokenUrl, params.toString(), {
|
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
||||||
})
|
|
||||||
.pipe(rxjsTimeout(10000)),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response?.data?.access_token) throw new Error('No access_token in Keycloak response');
|
|
||||||
|
|
||||||
this.currentToken = response.data.access_token;
|
|
||||||
const expiresIn = response.data.expires_in ?? 60;
|
|
||||||
const buffer = this.configService.get<number>('keycloak.tokenBufferSeconds') ?? 30;
|
|
||||||
|
|
||||||
const expiry = new Date();
|
|
||||||
expiry.setSeconds(expiry.getSeconds() + Math.max(0, expiresIn - buffer));
|
|
||||||
this.tokenExpiry = expiry;
|
|
||||||
|
|
||||||
this.logger.log(`Acquired Keycloak client token (expires in ${expiresIn}s)`);
|
|
||||||
return this.currentToken;
|
|
||||||
} finally {
|
|
||||||
this.acquiringTokenPromise = null;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
return this.acquiringTokenPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** -------------------------------
|
|
||||||
* USER PASSWORD TOKEN (ROPC)
|
|
||||||
* ------------------------------- */
|
|
||||||
async acquireUserToken(username: string, password: string): Promise<KeycloakTokenResponse> {
|
|
||||||
const tokenUrl = this.getTokenUrl();
|
|
||||||
const { userClientId, userClientSecret } = this.validateUserClientConfig();
|
|
||||||
|
|
||||||
if (!username || !password) throw new Error('Username and password are required');
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append('grant_type', 'password');
|
|
||||||
params.append('client_id', userClientId);
|
|
||||||
params.append('client_secret', userClientSecret);
|
|
||||||
params.append('username', username);
|
|
||||||
params.append('password', password);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await firstValueFrom(
|
|
||||||
this.httpService
|
|
||||||
.post<KeycloakTokenResponse>(tokenUrl, params.toString(), {
|
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
||||||
})
|
|
||||||
.pipe(rxjsTimeout(10000)),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response?.data?.access_token) throw new Error('No access_token in Keycloak response');
|
|
||||||
|
|
||||||
// 🔹 Stocke le refresh_token associé
|
|
||||||
if (response.data.refresh_token) {
|
|
||||||
this.userRefreshTokens.set(response.data.access_token, response.data.refresh_token);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(`User token acquired for "${username}"`);
|
|
||||||
return response.data;
|
|
||||||
} catch (err: any) {
|
|
||||||
this.logger.error(`Failed to acquire Keycloak user token: ${err?.message ?? err}`);
|
|
||||||
throw new Error('Invalid username or password');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** -------------------------------
|
|
||||||
* REFRESH TOKEN
|
|
||||||
* ------------------------------- */
|
|
||||||
async refreshToken(refreshToken: string): Promise<KeycloakTokenResponse> {
|
|
||||||
const tokenUrl = this.getTokenUrl();
|
|
||||||
|
|
||||||
const { userClientId, userClientSecret } = this.validateUserClientConfig();
|
|
||||||
|
|
||||||
if (!refreshToken) throw new Error('Refresh token is required');
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append('grant_type', 'refresh_token');
|
|
||||||
params.append('client_id', userClientId);
|
|
||||||
params.append('client_secret', userClientSecret);
|
|
||||||
params.append('refresh_token', refreshToken);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await firstValueFrom(
|
|
||||||
this.httpService
|
|
||||||
.post<KeycloakTokenResponse>(tokenUrl, params.toString(), {
|
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
||||||
})
|
|
||||||
.pipe(rxjsTimeout(10000))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response?.data?.access_token) throw new Error('No access_token in Keycloak response');
|
|
||||||
|
|
||||||
// Met à jour le mapping
|
|
||||||
if (response.data.refresh_token) {
|
|
||||||
this.userRefreshTokens.set(response.data.access_token, response.data.refresh_token);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log('Token refreshed successfully');
|
|
||||||
return response.data;
|
|
||||||
} catch (error: any) {
|
|
||||||
this.logger.error(`Failed to refresh token: ${error.message}`);
|
|
||||||
throw new Error('Failed to refresh token');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** -------------------------------
|
|
||||||
* REVOKE TOKEN (LOGOUT)
|
|
||||||
* ------------------------------- */
|
|
||||||
async revokeToken(refreshToken: string): Promise<void> {
|
|
||||||
const logoutUrl = this.getLogoutUrl();
|
|
||||||
const clientId = this.configService.get<string>('keycloak.userClientId');
|
|
||||||
const clientSecret = this.configService.get<string>('keycloak.userClientSecret');
|
|
||||||
|
|
||||||
if (!clientId || !clientSecret) throw new Error('ClientId or ClientSecret not configured');
|
|
||||||
if (!refreshToken) throw new Error('Refresh token is required');
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append('client_id', clientId);
|
|
||||||
params.append('client_secret', clientSecret);
|
|
||||||
params.append('refresh_token', refreshToken);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await firstValueFrom(
|
|
||||||
this.httpService
|
|
||||||
.post(logoutUrl, params.toString(), {
|
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
||||||
})
|
|
||||||
.pipe(rxjsTimeout(10000)),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.logger.log(`Refresh token revoked successfully`);
|
|
||||||
} catch (error: any) {
|
|
||||||
this.logger.warn(`Failed to revoke refresh token: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** -------------------------------
|
|
||||||
* HELPER METHODS
|
|
||||||
* ------------------------------- */
|
|
||||||
async getToken(): Promise<string> {
|
|
||||||
if (this.isTokenValid()) return this.currentToken!;
|
|
||||||
return this.acquireClientToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
isTokenValid(): boolean {
|
|
||||||
return !!this.currentToken && !!this.tokenExpiry && new Date() < this.tokenExpiry;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearToken(): void {
|
|
||||||
this.currentToken = null;
|
|
||||||
this.tokenExpiry = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getStoredRefreshToken(accessToken: string): Promise<string | null> {
|
|
||||||
return this.userRefreshTokens.get(accessToken) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTokenInfo() {
|
|
||||||
if (!this.currentToken) return null;
|
|
||||||
return {
|
|
||||||
isValid: this.isTokenValid(),
|
|
||||||
expiresAt: this.tokenExpiry,
|
|
||||||
expiresIn: this.tokenExpiry
|
|
||||||
? Math.floor((this.tokenExpiry.getTime() - new Date().getTime()) / 1000)
|
|
||||||
: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,3 +1,4 @@
|
|||||||
export const RESOURCES = {
|
export const RESOURCES = {
|
||||||
USER: 'user',// user resource for /users/* endpoints
|
USER: 'user', // user resource for /users/* endpoints
|
||||||
|
MERCHANT: 'merchants' // merchant resource for /merchants/* endpoints
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,32 +0,0 @@
|
|||||||
import { Controller, Get } from '@nestjs/common';
|
|
||||||
import { Public } from 'nest-keycloak-connect';
|
|
||||||
import { StartupService } from '../auth/services/startup.service';
|
|
||||||
|
|
||||||
@Controller('health')
|
|
||||||
export class HealthController {
|
|
||||||
constructor(private readonly startupService: StartupService) {}
|
|
||||||
|
|
||||||
@Public()
|
|
||||||
@Get()
|
|
||||||
getHealth() {
|
|
||||||
return this.startupService.getStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Public()
|
|
||||||
@Get('readiness')
|
|
||||||
getReadiness() {
|
|
||||||
return {
|
|
||||||
status: this.startupService.isHealthy() ? 'ready' : 'not-ready',
|
|
||||||
timestamp: new Date(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Public()
|
|
||||||
@Get('liveness')
|
|
||||||
getLiveness() {
|
|
||||||
return {
|
|
||||||
status: 'live',
|
|
||||||
timestamp: new Date(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { TerminusModule } from '@nestjs/terminus';
|
|
||||||
import { HealthController } from './health.controller';
|
|
||||||
import { HttpModule } from '@nestjs/axios';
|
|
||||||
import { StartupService } from '../auth/services/startup.service';
|
|
||||||
import { TokenService } from '../auth/services/token.service';
|
|
||||||
import { KeycloakApiService } from '../auth/services/keycloak-api.service';
|
|
||||||
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
TerminusModule,
|
|
||||||
HttpModule,
|
|
||||||
],
|
|
||||||
providers: [StartupService, TokenService, KeycloakApiService],
|
|
||||||
controllers: [HealthController],
|
|
||||||
exports: [StartupService, TokenService, KeycloakApiService],
|
|
||||||
})
|
|
||||||
export class HealthModule {}
|
|
||||||
396
src/users/controllers/merchants.controller.ts
Normal file
396
src/users/controllers/merchants.controller.ts
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
Query,
|
||||||
|
Logger,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { AuthenticatedUser, Resource, Scopes } from "nest-keycloak-connect";
|
||||||
|
import { UsersService } from "../services/users.service";
|
||||||
|
import { MerchantTeamService } from "../services/merchant-team.service";
|
||||||
|
import * as user from "../models/user";
|
||||||
|
import { RESOURCES } from '../../constants/resouces';
|
||||||
|
import { SCOPES } from '../../constants/scopes';
|
||||||
|
|
||||||
|
@Controller('merchants')
|
||||||
|
@Resource(RESOURCES.MERCHANT)
|
||||||
|
export class MerchantsController {
|
||||||
|
private readonly logger = new Logger(MerchantsController.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly usersService: UsersService,
|
||||||
|
private readonly merchantTeamService: MerchantTeamService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// === CREATE USER ===
|
||||||
|
@Post()
|
||||||
|
@Scopes(SCOPES.WRITE)
|
||||||
|
async createUser(@Body() createUserDto: user.CreateUserDto): Promise<user.UserResponse> {
|
||||||
|
this.logger.log(`Creating new user: ${createUserDto.username}`);
|
||||||
|
try {
|
||||||
|
const createdUser = await this.usersService.createUser(createUserDto);
|
||||||
|
return createdUser;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to create user: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to create user", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === GET ALL USERS ===
|
||||||
|
@Get()
|
||||||
|
@Scopes(SCOPES.READ)
|
||||||
|
async findAllUsers(@Query() query: user.UserQueryDto): Promise<user.PaginatedUserResponse> {
|
||||||
|
this.logger.log('Fetching users list');
|
||||||
|
try {
|
||||||
|
const result = await this.usersService.findAllUsers(query);
|
||||||
|
return result;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to fetch users: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to fetch users", HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === GET USER BY ID ===
|
||||||
|
@Get(':id')
|
||||||
|
@Scopes(SCOPES.READ)
|
||||||
|
async getUserById(@Param('id') id: string): Promise<user.UserResponse> {
|
||||||
|
this.logger.log(`Fetching user by ID: ${id}`);
|
||||||
|
try {
|
||||||
|
const user = await this.usersService.getUserById(id);
|
||||||
|
return user;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to fetch user ${id}: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "User not found", HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === GET CURRENT USER PROFILE ===
|
||||||
|
@Get('profile/me')
|
||||||
|
@Scopes(SCOPES.READ)
|
||||||
|
async getCurrentUserProfile(@AuthenticatedUser() user: any): Promise<user.UserResponse> {
|
||||||
|
this.logger.log(`User ${user.sub} accessing own profile`);
|
||||||
|
try {
|
||||||
|
const userProfile = await this.usersService.getUserProfile(user);
|
||||||
|
return userProfile;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to fetch user profile: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to fetch user profile", HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === UPDATE USER ===
|
||||||
|
@Put(':id')
|
||||||
|
@Scopes(SCOPES.WRITE)
|
||||||
|
async updateUser(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() updateUserDto: user.UpdateUserDto
|
||||||
|
): Promise<user.UserResponse> {
|
||||||
|
this.logger.log(`Updating user: ${id}`);
|
||||||
|
try {
|
||||||
|
const updatedUser = await this.usersService.updateUser(id, updateUserDto);
|
||||||
|
return updatedUser;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to update user ${id}: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to update user", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === UPDATE CURRENT USER PROFILE ===
|
||||||
|
@Put('profile/me')
|
||||||
|
async updateCurrentUserProfile(
|
||||||
|
@AuthenticatedUser() user: any,
|
||||||
|
@Body() updateUserDto: user.UpdateUserDto
|
||||||
|
): Promise<user.UserResponse> {
|
||||||
|
this.logger.log(`User ${user.sub} updating own profile`);
|
||||||
|
try {
|
||||||
|
// Un utilisateur ne peut mettre à jour que son propre profil
|
||||||
|
const updatedUser = await this.usersService.updateUser(user.sub, updateUserDto);
|
||||||
|
return updatedUser;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to update user profile: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to update user profile", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === DELETE USER ===
|
||||||
|
@Delete(':id')
|
||||||
|
@Scopes(SCOPES.DELETE)
|
||||||
|
async deleteUser(@Param('id') id: string): Promise<{ message: string }> {
|
||||||
|
this.logger.log(`Deleting user: ${id}`);
|
||||||
|
try {
|
||||||
|
await this.usersService.deleteUser(id);
|
||||||
|
return { message: `User ${id} deleted successfully` };
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to delete user ${id}: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to delete user", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === RESET PASSWORD ===
|
||||||
|
@Put(':id/password')
|
||||||
|
@Scopes(SCOPES.WRITE)
|
||||||
|
async resetPassword(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() resetPasswordDto: user.ResetPasswordDto
|
||||||
|
): Promise<{ message: string }> {
|
||||||
|
this.logger.log(`Resetting password for user: ${id}`);
|
||||||
|
try {
|
||||||
|
await this.usersService.resetPassword(resetPasswordDto);
|
||||||
|
return { message: 'Password reset successfully' };
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to reset password for user ${id}: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to reset password", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === ENABLE USER ===
|
||||||
|
@Put(':id/enable')
|
||||||
|
@Scopes(SCOPES.WRITE)
|
||||||
|
async enableUser(@Param('id') id: string): Promise<{ message: string }> {
|
||||||
|
this.logger.log(`Enabling user: ${id}`);
|
||||||
|
try {
|
||||||
|
await this.usersService.enableUser(id);
|
||||||
|
return { message: 'User enabled successfully' };
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to enable user ${id}: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to enable user", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === DISABLE USER ===
|
||||||
|
@Put(':id/disable')
|
||||||
|
@Scopes(SCOPES.WRITE)
|
||||||
|
async disableUser(@Param('id') id: string): Promise<{ message: string }> {
|
||||||
|
this.logger.log(`Disabling user: ${id}`);
|
||||||
|
try {
|
||||||
|
await this.usersService.disableUser(id);
|
||||||
|
return { message: 'User disabled successfully' };
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to disable user ${id}: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to disable user", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === CHECK USER EXISTS ===
|
||||||
|
@Get('check/:username')
|
||||||
|
@Scopes(SCOPES.READ)
|
||||||
|
async userExists(@Param('username') username: string): Promise<{ exists: boolean }> {
|
||||||
|
this.logger.log(`Checking if user exists: ${username}`);
|
||||||
|
try {
|
||||||
|
const exists = await this.usersService.userExists(username);
|
||||||
|
return { exists };
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to check if user exists ${username}: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to check user existence", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === SEARCH USERS BY USERNAME ===
|
||||||
|
@Get('search/username/:username')
|
||||||
|
@Scopes(SCOPES.READ)
|
||||||
|
async findUserByUsername(@Param('username') username: string): Promise<user.UserResponse[]> {
|
||||||
|
this.logger.log(`Searching users by username: ${username}`);
|
||||||
|
try {
|
||||||
|
const users = await this.usersService.findUserByUsername(username);
|
||||||
|
return users;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to search users by username ${username}: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to search users", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === SEARCH USERS BY EMAIL ===
|
||||||
|
@Get('search/email/:email')
|
||||||
|
@Scopes(SCOPES.READ)
|
||||||
|
async findUserByEmail(@Param('email') email: string): Promise<user.UserResponse[]> {
|
||||||
|
this.logger.log(`Searching users by email: ${email}`);
|
||||||
|
try {
|
||||||
|
const users = await this.usersService.findUserByEmail(email);
|
||||||
|
return users;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to search users by email ${email}: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to search users", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === GET USER ROLES ===
|
||||||
|
@Get(':id/roles')
|
||||||
|
@Scopes(SCOPES.READ)
|
||||||
|
async getUserClientRoles(@Param('id') id: string): Promise<{ roles: string[] }> {
|
||||||
|
this.logger.log(`Fetching roles for user: ${id}`);
|
||||||
|
try {
|
||||||
|
const roles = await this.usersService.getUserClientRoles(id);
|
||||||
|
return { roles };
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to fetch roles for user ${id}: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to fetch user roles", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === ASSIGN CLIENT ROLES TO USER ===
|
||||||
|
@Put(':id/roles')
|
||||||
|
@Scopes(SCOPES.WRITE)
|
||||||
|
async assignClientRoles(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() assignRolesDto: { roles: string[] }
|
||||||
|
): Promise<{ message: string }> {
|
||||||
|
this.logger.log(`Assigning roles to user: ${id}`, assignRolesDto.roles);
|
||||||
|
try {
|
||||||
|
await this.usersService.assignClientRoles(id, assignRolesDto.roles);
|
||||||
|
return { message: 'Roles assigned successfully' };
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to assign roles to user ${id}: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to assign roles", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// === ROUTES SPECIFIQUES POUR LES MARCHANDS ===
|
||||||
|
// =============================================
|
||||||
|
|
||||||
|
// === CREER UN UTILISATEUR DANS L'EQUIPE MARCHAND ===
|
||||||
|
@Post('merchant/team')
|
||||||
|
@Scopes(SCOPES.WRITE)
|
||||||
|
async createMerchantTeamUser(
|
||||||
|
@AuthenticatedUser() user: any,
|
||||||
|
@Body() createMerchantUserDto: any
|
||||||
|
): Promise<any> {
|
||||||
|
this.logger.log(`Merchant ${user.sub} creating team user: ${createMerchantUserDto.username}`);
|
||||||
|
try {
|
||||||
|
const createdUser = await this.merchantTeamService.createMerchantUser(
|
||||||
|
createMerchantUserDto,
|
||||||
|
user.sub
|
||||||
|
);
|
||||||
|
return createdUser;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to create merchant team user: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to create merchant team user", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === OBTENIR L'EQUIPE DU MARCHAND ===
|
||||||
|
@Get('merchant/team')
|
||||||
|
@Scopes(SCOPES.READ)
|
||||||
|
async getMerchantTeam(@AuthenticatedUser() user: any): Promise<any[]> {
|
||||||
|
this.logger.log(`Merchant ${user.sub} fetching team`);
|
||||||
|
try {
|
||||||
|
const team = await this.merchantTeamService.getMerchantTeam(user.sub);
|
||||||
|
return team;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to fetch merchant team: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to fetch merchant team", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === METTRE A JOUR UN MEMBRE DE L'EQUIPE ===
|
||||||
|
@Put('merchant/team/:userId')
|
||||||
|
@Scopes(SCOPES.WRITE)
|
||||||
|
async updateMerchantTeamUser(
|
||||||
|
@AuthenticatedUser() user: any,
|
||||||
|
@Param('userId') userId: string,
|
||||||
|
@Body() updateData: any
|
||||||
|
): Promise<any> {
|
||||||
|
this.logger.log(`Merchant ${user.sub} updating team user: ${userId}`);
|
||||||
|
try {
|
||||||
|
const updatedUser = await this.merchantTeamService.updateMerchantUser(
|
||||||
|
userId,
|
||||||
|
updateData,
|
||||||
|
user.sub
|
||||||
|
);
|
||||||
|
return updatedUser;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to update merchant team user ${userId}: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to update merchant team user", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === RETIRER UN MEMBRE DE L'EQUIPE ===
|
||||||
|
@Delete('merchant/team/:userId')
|
||||||
|
@Scopes(SCOPES.DELETE)
|
||||||
|
async removeMerchantTeamUser(
|
||||||
|
@AuthenticatedUser() user: any,
|
||||||
|
@Param('userId') userId: string
|
||||||
|
): Promise<{ message: string }> {
|
||||||
|
this.logger.log(`Merchant ${user.sub} removing team user: ${userId}`);
|
||||||
|
try {
|
||||||
|
await this.merchantTeamService.removeMerchantUser(userId, user.sub);
|
||||||
|
return { message: 'Team member removed successfully' };
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to remove merchant team user ${userId}: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to remove merchant team user", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === AJOUTER UN ROLE A UN MEMBRE ===
|
||||||
|
@Post('merchant/team/:userId/roles/:role')
|
||||||
|
@Scopes(SCOPES.WRITE)
|
||||||
|
async addMerchantRole(
|
||||||
|
@AuthenticatedUser() user: any,
|
||||||
|
@Param('userId') userId: string,
|
||||||
|
@Param('role') role: string
|
||||||
|
): Promise<{ message: string }> {
|
||||||
|
this.logger.log(`Merchant ${user.sub} adding role ${role} to user ${userId}`);
|
||||||
|
try {
|
||||||
|
const result = await this.merchantTeamService.addMerchantRole(userId, role, user.sub);
|
||||||
|
return result;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to add merchant role to user ${userId}: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to add merchant role", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === RETIRER UN ROLE D'UN MEMBRE ===
|
||||||
|
@Delete('merchant/team/:userId/roles/:role')
|
||||||
|
@Scopes(SCOPES.WRITE)
|
||||||
|
async removeMerchantRole(
|
||||||
|
@AuthenticatedUser() user: any,
|
||||||
|
@Param('userId') userId: string,
|
||||||
|
@Param('role') role: string
|
||||||
|
): Promise<{ message: string }> {
|
||||||
|
this.logger.log(`Merchant ${user.sub} removing role ${role} from user ${userId}`);
|
||||||
|
try {
|
||||||
|
const result = await this.merchantTeamService.removeMerchantRole(userId, role, user.sub);
|
||||||
|
return result;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to remove merchant role from user ${userId}: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to remove merchant role", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === OBTENIR LES ROLES MARCHANDS DISPONIBLES ===
|
||||||
|
@Get('merchant/roles/available')
|
||||||
|
@Scopes(SCOPES.READ)
|
||||||
|
async getAvailableMerchantRoles(): Promise<{ roles: string[] }> {
|
||||||
|
this.logger.log('Fetching available merchant roles');
|
||||||
|
try {
|
||||||
|
const roles = this.merchantTeamService.getAvailableMerchantRoles();
|
||||||
|
return { roles };
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to fetch available merchant roles: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to fetch available merchant roles", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === VERIFIER SI UN UTILISATEUR EST DANS L'EQUIPE ===
|
||||||
|
@Get('merchant/team/:userId/check')
|
||||||
|
@Scopes(SCOPES.READ)
|
||||||
|
async checkUserInTeam(
|
||||||
|
@AuthenticatedUser() user: any,
|
||||||
|
@Param('userId') userId: string
|
||||||
|
): Promise<{ isInTeam: boolean }> {
|
||||||
|
this.logger.log(`Checking if user ${userId} is in merchant ${user.sub} team`);
|
||||||
|
try {
|
||||||
|
const isInTeam = await this.merchantTeamService.isUserInMerchantTeam(userId, user.sub);
|
||||||
|
return { isInTeam };
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to check team membership: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to check team membership", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,9 +11,11 @@ import {
|
|||||||
HttpException,
|
HttpException,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
|
Request,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { Roles, AuthenticatedUser, Resource, Scopes } from "nest-keycloak-connect";
|
import { Roles, AuthenticatedUser, Resource, Scopes } from "nest-keycloak-connect";
|
||||||
import { UsersService } from "../services/users.service";
|
import { UsersService } from "../services/users.service";
|
||||||
|
import { MerchantTeamService } from "../services/merchant-team.service";
|
||||||
import * as user from "../models/user";
|
import * as user from "../models/user";
|
||||||
import { RESOURCES } from '../../constants/resouces';
|
import { RESOURCES } from '../../constants/resouces';
|
||||||
import { SCOPES } from '../../constants/scopes';
|
import { SCOPES } from '../../constants/scopes';
|
||||||
@ -23,7 +25,10 @@ import { SCOPES } from '../../constants/scopes';
|
|||||||
export class UsersController {
|
export class UsersController {
|
||||||
private readonly logger = new Logger(UsersController.name);
|
private readonly logger = new Logger(UsersController.name);
|
||||||
|
|
||||||
constructor(private readonly usersService: UsersService) {}
|
constructor(
|
||||||
|
private readonly usersService: UsersService,
|
||||||
|
private readonly merchantTeamService: MerchantTeamService,
|
||||||
|
) {}
|
||||||
|
|
||||||
// === CREATE USER ===
|
// === CREATE USER ===
|
||||||
@Post()
|
@Post()
|
||||||
@ -246,4 +251,148 @@ export class UsersController {
|
|||||||
throw new HttpException(error.message || "Failed to assign roles", HttpStatus.BAD_REQUEST);
|
throw new HttpException(error.message || "Failed to assign roles", HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// === ROUTES SPECIFIQUES POUR LES MARCHANDS ===
|
||||||
|
// =============================================
|
||||||
|
|
||||||
|
// === CREER UN UTILISATEUR DANS L'EQUIPE MARCHAND ===
|
||||||
|
@Post('merchant/team')
|
||||||
|
@Scopes(SCOPES.WRITE)
|
||||||
|
async createMerchantTeamUser(
|
||||||
|
@AuthenticatedUser() user: any,
|
||||||
|
@Body() createMerchantUserDto: any
|
||||||
|
): Promise<any> {
|
||||||
|
this.logger.log(`Merchant ${user.sub} creating team user: ${createMerchantUserDto.username}`);
|
||||||
|
try {
|
||||||
|
const createdUser = await this.merchantTeamService.createMerchantUser(
|
||||||
|
createMerchantUserDto,
|
||||||
|
user.sub
|
||||||
|
);
|
||||||
|
return createdUser;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to create merchant team user: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to create merchant team user", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === OBTENIR L'EQUIPE DU MARCHAND ===
|
||||||
|
@Get('merchant/team')
|
||||||
|
@Scopes(SCOPES.READ)
|
||||||
|
async getMerchantTeam(@AuthenticatedUser() user: any): Promise<any[]> {
|
||||||
|
this.logger.log(`Merchant ${user.sub} fetching team`);
|
||||||
|
try {
|
||||||
|
const team = await this.merchantTeamService.getMerchantTeam(user.sub);
|
||||||
|
return team;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to fetch merchant team: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to fetch merchant team", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === METTRE A JOUR UN MEMBRE DE L'EQUIPE ===
|
||||||
|
@Put('merchant/team/:userId')
|
||||||
|
@Scopes(SCOPES.WRITE)
|
||||||
|
async updateMerchantTeamUser(
|
||||||
|
@AuthenticatedUser() user: any,
|
||||||
|
@Param('userId') userId: string,
|
||||||
|
@Body() updateData: any
|
||||||
|
): Promise<any> {
|
||||||
|
this.logger.log(`Merchant ${user.sub} updating team user: ${userId}`);
|
||||||
|
try {
|
||||||
|
const updatedUser = await this.merchantTeamService.updateMerchantUser(
|
||||||
|
userId,
|
||||||
|
updateData,
|
||||||
|
user.sub
|
||||||
|
);
|
||||||
|
return updatedUser;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to update merchant team user ${userId}: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to update merchant team user", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === RETIRER UN MEMBRE DE L'EQUIPE ===
|
||||||
|
@Delete('merchant/team/:userId')
|
||||||
|
@Scopes(SCOPES.DELETE)
|
||||||
|
async removeMerchantTeamUser(
|
||||||
|
@AuthenticatedUser() user: any,
|
||||||
|
@Param('userId') userId: string
|
||||||
|
): Promise<{ message: string }> {
|
||||||
|
this.logger.log(`Merchant ${user.sub} removing team user: ${userId}`);
|
||||||
|
try {
|
||||||
|
await this.merchantTeamService.removeMerchantUser(userId, user.sub);
|
||||||
|
return { message: 'Team member removed successfully' };
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to remove merchant team user ${userId}: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to remove merchant team user", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === AJOUTER UN ROLE A UN MEMBRE ===
|
||||||
|
@Post('merchant/team/:userId/roles/:role')
|
||||||
|
@Scopes(SCOPES.WRITE)
|
||||||
|
async addMerchantRole(
|
||||||
|
@AuthenticatedUser() user: any,
|
||||||
|
@Param('userId') userId: string,
|
||||||
|
@Param('role') role: string
|
||||||
|
): Promise<{ message: string }> {
|
||||||
|
this.logger.log(`Merchant ${user.sub} adding role ${role} to user ${userId}`);
|
||||||
|
try {
|
||||||
|
const result = await this.merchantTeamService.addMerchantRole(userId, role, user.sub);
|
||||||
|
return result;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to add merchant role to user ${userId}: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to add merchant role", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === RETIRER UN ROLE D'UN MEMBRE ===
|
||||||
|
@Delete('merchant/team/:userId/roles/:role')
|
||||||
|
@Scopes(SCOPES.WRITE)
|
||||||
|
async removeMerchantRole(
|
||||||
|
@AuthenticatedUser() user: any,
|
||||||
|
@Param('userId') userId: string,
|
||||||
|
@Param('role') role: string
|
||||||
|
): Promise<{ message: string }> {
|
||||||
|
this.logger.log(`Merchant ${user.sub} removing role ${role} from user ${userId}`);
|
||||||
|
try {
|
||||||
|
const result = await this.merchantTeamService.removeMerchantRole(userId, role, user.sub);
|
||||||
|
return result;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to remove merchant role from user ${userId}: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to remove merchant role", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === OBTENIR LES ROLES MARCHANDS DISPONIBLES ===
|
||||||
|
@Get('merchant/roles/available')
|
||||||
|
@Scopes(SCOPES.READ)
|
||||||
|
async getAvailableMerchantRoles(): Promise<{ roles: string[] }> {
|
||||||
|
this.logger.log('Fetching available merchant roles');
|
||||||
|
try {
|
||||||
|
const roles = this.merchantTeamService.getAvailableMerchantRoles();
|
||||||
|
return { roles };
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to fetch available merchant roles: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to fetch available merchant roles", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === VERIFIER SI UN UTILISATEUR EST DANS L'EQUIPE ===
|
||||||
|
@Get('merchant/team/:userId/check')
|
||||||
|
@Scopes(SCOPES.READ)
|
||||||
|
async checkUserInTeam(
|
||||||
|
@AuthenticatedUser() user: any,
|
||||||
|
@Param('userId') userId: string
|
||||||
|
): Promise<{ isInTeam: boolean }> {
|
||||||
|
this.logger.log(`Checking if user ${userId} is in merchant ${user.sub} team`);
|
||||||
|
try {
|
||||||
|
const isInTeam = await this.merchantTeamService.isUserInMerchantTeam(userId, user.sub);
|
||||||
|
return { isInTeam };
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to check team membership: ${error.message}`);
|
||||||
|
throw new HttpException(error.message || "Failed to check team membership", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
201
src/users/models/merchant.ts
Normal file
201
src/users/models/merchant.ts
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import { IsString, IsEmail, IsBoolean, IsOptional, IsArray, MinLength } from 'class-validator';
|
||||||
|
|
||||||
|
export class MerchanUser {
|
||||||
|
id?: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
enabled?: boolean = true;
|
||||||
|
emailVerified?: boolean = false;
|
||||||
|
attributes?: Record<string, any>;
|
||||||
|
merchantRoles?: string[]; // Rôles client uniquement (merchant-admin, merchant-manager, merchant-support)
|
||||||
|
createdTimestamp?: number;
|
||||||
|
merchantOwnerId: string;
|
||||||
|
|
||||||
|
constructor(partial?: Partial<MerchanUser>) {
|
||||||
|
if (partial) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MerchantUserCredentials {
|
||||||
|
type: string;
|
||||||
|
value: string;
|
||||||
|
temporary: boolean = false;
|
||||||
|
|
||||||
|
constructor(type: string, value: string, temporary: boolean = false) {
|
||||||
|
this.type = type;
|
||||||
|
this.value = value;
|
||||||
|
this.temporary = temporary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateMerchantUserDto {
|
||||||
|
@IsString()
|
||||||
|
@MinLength(3)
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
lastName: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@MinLength(8)
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
enabled?: boolean = true;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
emailVerified?: boolean = false;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
attributes?: Record<string, any>;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
merchantRoles: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateMerchantUserDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail()
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
lastName: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
enabled?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
emailVerified?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
attributes?: Record<string, any>;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
merchantRoles?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MerchantUserQueryDto {
|
||||||
|
@IsOptional()
|
||||||
|
page?: number = 1;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
limit?: number = 10;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
search?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
enabled?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
emailVerified?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ResetPasswordDto {
|
||||||
|
@IsString()
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@MinLength(8)
|
||||||
|
newPassword: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
temporary?: boolean = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MerchantUserResponse {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
enabled: boolean;
|
||||||
|
emailVerified: boolean;
|
||||||
|
attributes?: Record<string, any>;
|
||||||
|
merchantRoles: string[]; // Rôles client uniquement
|
||||||
|
createdTimestamp: number;
|
||||||
|
merchantOwnerId: string;
|
||||||
|
|
||||||
|
constructor(user: any) {
|
||||||
|
this.id = user.id;
|
||||||
|
this.username = user.username;
|
||||||
|
this.email = user.email;
|
||||||
|
this.firstName = user.firstName;
|
||||||
|
this.lastName = user.lastName;
|
||||||
|
this.enabled = user.enabled;
|
||||||
|
this.emailVerified = user.emailVerified;
|
||||||
|
this.attributes = user.attributes;
|
||||||
|
this.merchantRoles = user.merchantRoles || [];
|
||||||
|
this.createdTimestamp = user.createdTimestamp;
|
||||||
|
this.merchantOwnerId = user.merchantOwnerId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface pour les réponses paginées
|
||||||
|
export class PaginatedMerchantUserResponse {
|
||||||
|
users: MerchantUserResponse[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalPages: number;
|
||||||
|
|
||||||
|
constructor(users: MerchantUserResponse[], total: number, page: number, limit: number) {
|
||||||
|
this.users = users;
|
||||||
|
this.total = total;
|
||||||
|
this.page = page;
|
||||||
|
this.limit = limit;
|
||||||
|
this.totalPages = Math.ceil(total / limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AssignRolesDto {
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
roles: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Types pour les rôles client
|
||||||
|
export type MerchantRole = 'merchant-admin' | 'merchant-manager' | 'merchant-support';
|
||||||
|
|
||||||
|
// Interface 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;
|
||||||
|
scope?: string;
|
||||||
|
}
|
||||||
@ -1,14 +0,0 @@
|
|||||||
export enum ROLES {
|
|
||||||
// User Management Roles
|
|
||||||
CREATE_USER = 'create-user',
|
|
||||||
UPDATE_USER = 'update-user',
|
|
||||||
DELETE_USER = 'delete-user',
|
|
||||||
READ_USERS = 'read-users',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ROLE_DESCRIPTIONS = {
|
|
||||||
[ROLES.CREATE_USER]: 'Can create new users in the system',
|
|
||||||
[ROLES.UPDATE_USER]: 'Can update existing user information',
|
|
||||||
[ROLES.DELETE_USER]: 'Can delete users from the system',
|
|
||||||
[ROLES.READ_USERS]: 'Can view the list of all users',
|
|
||||||
};
|
|
||||||
@ -4,8 +4,8 @@ export class User {
|
|||||||
id?: string;
|
id?: string;
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
firstName: string;
|
firstName?: string;
|
||||||
lastName: string;
|
lastName?: string;
|
||||||
enabled?: boolean = true;
|
enabled?: boolean = true;
|
||||||
emailVerified?: boolean = false;
|
emailVerified?: boolean = false;
|
||||||
attributes?: Record<string, any>;
|
attributes?: Record<string, any>;
|
||||||
@ -40,10 +40,10 @@ export class CreateUserDto {
|
|||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
firstName: string;
|
firstName?: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
lastName: string;
|
lastName?: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(8)
|
@MinLength(8)
|
||||||
@ -135,8 +135,8 @@ export class UserResponse {
|
|||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
firstName?: string;
|
firstName: string;
|
||||||
lastName?: string;
|
lastName: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
emailVerified: boolean;
|
emailVerified: boolean;
|
||||||
attributes?: Record<string, any>;
|
attributes?: Record<string, any>;
|
||||||
@ -181,7 +181,7 @@ export class AssignRolesDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Types pour les rôles client
|
// Types pour les rôles client
|
||||||
export type ClientRole = 'admin' | 'merchant' | 'support';
|
export type ClientRole = 'admin' | 'merchant' | 'support' | 'merchant-admin' | 'merchant-manager' | 'merchant-support';
|
||||||
|
|
||||||
// Interface pour l'authentification
|
// Interface pour l'authentification
|
||||||
export interface LoginDto {
|
export interface LoginDto {
|
||||||
|
|||||||
352
src/users/services/merchant-team.service.ts
Normal file
352
src/users/services/merchant-team.service.ts
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
Logger,
|
||||||
|
NotFoundException,
|
||||||
|
BadRequestException,
|
||||||
|
ForbiddenException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { KeycloakApiService } from '../../auth/services/keycloak-api.service';
|
||||||
|
import { UsersService } from './users.service';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CreateMerchantUserDto,
|
||||||
|
UpdateMerchantUserDto,
|
||||||
|
MerchantUserResponse,
|
||||||
|
PaginatedMerchantUserResponse,
|
||||||
|
ResetPasswordDto,
|
||||||
|
MerchantUserQueryDto,
|
||||||
|
LoginDto,
|
||||||
|
TokenResponse,
|
||||||
|
MerchantRole
|
||||||
|
} from '../models/merchant';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MerchantTeamService {
|
||||||
|
private readonly logger = new Logger(MerchantTeamService.name);
|
||||||
|
|
||||||
|
// Rôles spécifiques aux équipes de marchands
|
||||||
|
private readonly availableMerchantRoles = [
|
||||||
|
'merchant-admin',
|
||||||
|
'merchant-manager',
|
||||||
|
'merchant-support',
|
||||||
|
'merchant-user'
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly keycloakApi: KeycloakApiService,
|
||||||
|
private readonly usersService: UsersService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// === VALIDATION DES ROLES MARCHAND ===
|
||||||
|
private validateMerchantRole(role: string): MerchantRole {
|
||||||
|
const validRoles: MerchantRole[] = ['merchant-admin', 'merchant-manager', 'merchant-support'];
|
||||||
|
|
||||||
|
if (!validRoles.includes(role as MerchantRole)) {
|
||||||
|
throw new BadRequestException(`Invalid client role: ${role}. Valid roles are: ${validRoles.join(', ')}`);
|
||||||
|
}
|
||||||
|
return role as MerchantRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateMerchantRoles(roles: string[]): MerchantRole[] {
|
||||||
|
return roles.map(role => this.validateMerchantRole(role));
|
||||||
|
}
|
||||||
|
|
||||||
|
// === VERIFICATION DES DROITS D'ACCÈS ===
|
||||||
|
private async validateMerchantAccess(targetUserId: string | null, requestingUserId: string): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
error?: string
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const requestingUserRoles = await this.usersService.getUserClientRoles(requestingUserId);
|
||||||
|
|
||||||
|
// Les admins peuvent tout faire
|
||||||
|
if (requestingUserRoles.includes('admin')) {
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que le demandeur est un marchand
|
||||||
|
if (!requestingUserRoles.includes('merchant')) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Access denied: Merchant role required'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pour les opérations sur un utilisateur spécifique
|
||||||
|
if (targetUserId) {
|
||||||
|
const targetOwnerId = await this.usersService.getMerchantOwner(targetUserId);
|
||||||
|
|
||||||
|
// Si l'utilisateur cible n'a pas de merchantOwnerId, c'est un marchand principal
|
||||||
|
if (!targetOwnerId) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Access denied: Cannot modify principal merchant accounts'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que le demandeur est bien le propriétaire
|
||||||
|
if (targetOwnerId !== requestingUserId) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Access denied: Can only manage your own team members'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Failed to validate access: ${error.message}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === CREATION D'UTILISATEUR MARCHAND ===
|
||||||
|
async createMerchantUser(
|
||||||
|
merchantData: CreateMerchantUserDto,
|
||||||
|
createdByUserId: string
|
||||||
|
): Promise<MerchantUserResponse> {
|
||||||
|
try {
|
||||||
|
await this.validateMerchantAccess(null, createdByUserId);
|
||||||
|
|
||||||
|
const validatedMerchantRoles = this.validateMerchantRoles(merchantData.merchantRoles);
|
||||||
|
|
||||||
|
const userData = {
|
||||||
|
username: merchantData.username,
|
||||||
|
email: merchantData.email,
|
||||||
|
firstName: merchantData.firstName,
|
||||||
|
lastName: merchantData.lastName,
|
||||||
|
password: merchantData.password,
|
||||||
|
enabled: merchantData.enabled ?? true,
|
||||||
|
clientRoles: ['merchant', ...validatedMerchantRoles] as any,
|
||||||
|
attributes: {
|
||||||
|
merchantOwnerId: [createdByUserId]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createdUser = await this.usersService.createUser(userData);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: createdUser.id,
|
||||||
|
username: createdUser.username,
|
||||||
|
email: createdUser.email,
|
||||||
|
firstName: createdUser.firstName,
|
||||||
|
lastName: createdUser.lastName,
|
||||||
|
merchantRoles: validatedMerchantRoles,
|
||||||
|
emailVerified: true,
|
||||||
|
enabled: createdUser.enabled,
|
||||||
|
createdTimestamp: createdUser.createdTimestamp,
|
||||||
|
merchantOwnerId: createdByUserId
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to create merchant user: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === OBTENIR L'EQUIPE DU MARCHAND ===
|
||||||
|
async getMerchantTeam(merchantUserId: string): Promise<MerchantUserResponse[]> {
|
||||||
|
try {
|
||||||
|
// Vérifier les droits
|
||||||
|
await this.validateMerchantAccess(null, merchantUserId);
|
||||||
|
|
||||||
|
// Obtenir tous les utilisateurs
|
||||||
|
const allUsers = await this.usersService.findAllUsers({
|
||||||
|
limit: 1000,
|
||||||
|
page: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
const teamMembers = await Promise.all(
|
||||||
|
allUsers.users.map(async (user) => {
|
||||||
|
try {
|
||||||
|
const roles = await this.usersService.getUserClientRoles(user.id);
|
||||||
|
const merchantRoles = roles.filter(role =>
|
||||||
|
role.startsWith('merchant-') && role !== 'merchant'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Récupérer l'owner ID
|
||||||
|
const merchantOwnerId = user.attributes?.merchantOwnerId?.[0];
|
||||||
|
|
||||||
|
// Filtrer : ne retourner que les utilisateurs de CE marchand
|
||||||
|
if (merchantRoles.length > 0 && merchantOwnerId === merchantUserId) {
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
merchantRoles,
|
||||||
|
enabled: user.enabled,
|
||||||
|
createdTimestamp: user.createdTimestamp,
|
||||||
|
merchantOwnerId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(`Failed to process user ${user.id}: ${error.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return teamMembers.filter((member): member is MerchantUserResponse => member !== null);
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to fetch merchant team: ${error.message}`);
|
||||||
|
throw new BadRequestException('Failed to fetch merchant team');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === METTRE A JOUR UN MEMBRE ===
|
||||||
|
async updateMerchantUser(
|
||||||
|
userId: string,
|
||||||
|
updateData: UpdateMerchantUserDto,
|
||||||
|
updatedByUserId: string
|
||||||
|
): Promise<MerchantUserResponse> {
|
||||||
|
try {
|
||||||
|
// Vérifier les droits
|
||||||
|
await this.validateMerchantAccess(userId, updatedByUserId);
|
||||||
|
|
||||||
|
// Préparer les données de mise à jour
|
||||||
|
const updatePayload: any = {};
|
||||||
|
if (updateData.firstName) updatePayload.firstName = updateData.firstName;
|
||||||
|
if (updateData.lastName) updatePayload.lastName = updateData.lastName;
|
||||||
|
if (updateData.email) updatePayload.email = updateData.email;
|
||||||
|
if (updateData.enabled !== undefined) updatePayload.enabled = updateData.enabled;
|
||||||
|
|
||||||
|
// Mettre à jour les rôles si fournis
|
||||||
|
if (updateData.merchantRoles) {
|
||||||
|
const validatedRoles = this.validateMerchantRoles(updateData.merchantRoles);
|
||||||
|
const finalRoles = ['merchant', ...validatedRoles];
|
||||||
|
await this.usersService.assignClientRoles(userId, finalRoles);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour les autres informations
|
||||||
|
if (Object.keys(updatePayload).length > 0) {
|
||||||
|
await this.usersService.updateUser(userId, updatePayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer l'utilisateur mis à jour
|
||||||
|
const updatedUser = await this.usersService.getUserById(userId);
|
||||||
|
const userRoles = await this.usersService.getUserClientRoles(userId);
|
||||||
|
const merchantRoles = userRoles.filter(role =>
|
||||||
|
role.startsWith('merchant-') && role !== 'merchant'
|
||||||
|
);
|
||||||
|
|
||||||
|
const merchantOwnerId = await this.usersService.getMerchantOwner(userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: updatedUser.id,
|
||||||
|
username: updatedUser.username,
|
||||||
|
email: updatedUser.email,
|
||||||
|
firstName: updatedUser.firstName,
|
||||||
|
lastName: updatedUser.lastName,
|
||||||
|
merchantRoles,
|
||||||
|
enabled: updatedUser.enabled,
|
||||||
|
emailVerified: true,
|
||||||
|
createdTimestamp: updatedUser.createdTimestamp,
|
||||||
|
merchantOwnerId: merchantOwnerId ?? '',
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to update merchant user ${userId}: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === SUPPRIMER UN MEMBRE (retirer les rôles marchands) ===
|
||||||
|
async removeMerchantUser(userId: string, removedByUserId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.validateMerchantAccess(userId, removedByUserId);
|
||||||
|
|
||||||
|
// Retirer les rôles marchands mais garder l'utilisateur
|
||||||
|
const userRoles = await this.usersService.getUserClientRoles(userId);
|
||||||
|
const nonMerchantRoles = userRoles.filter(role =>
|
||||||
|
!role.startsWith('merchant-') && role !== 'merchant'
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.usersService.assignClientRoles(userId, nonMerchantRoles);
|
||||||
|
|
||||||
|
// Optionnel: supprimer l'attribut merchantOwnerId
|
||||||
|
await this.keycloakApi.setUserAttributes(userId, {
|
||||||
|
merchantOwnerId: []
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to remove merchant user ${userId}: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === OBTENIR LES ROLES DISPONIBLES ===
|
||||||
|
getAvailableMerchantRoles(): string[] {
|
||||||
|
return [...this.availableMerchantRoles];
|
||||||
|
}
|
||||||
|
|
||||||
|
// === AJOUTER UN ROLE ===
|
||||||
|
async addMerchantRole(
|
||||||
|
userId: string,
|
||||||
|
role: string,
|
||||||
|
addedByUserId: string
|
||||||
|
): Promise<{ message: string }> {
|
||||||
|
try {
|
||||||
|
await this.validateMerchantAccess(userId, addedByUserId);
|
||||||
|
|
||||||
|
const validatedRole = this.validateMerchantRole(role);
|
||||||
|
const currentRoles = await this.usersService.getUserClientRoles(userId);
|
||||||
|
|
||||||
|
if (!currentRoles.includes('merchant')) {
|
||||||
|
currentRoles.push('merchant');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentRoles.includes(validatedRole)) {
|
||||||
|
currentRoles.push(validatedRole);
|
||||||
|
await this.usersService.assignClientRoles(userId, currentRoles as any); // 🔥 CAST ICI
|
||||||
|
}
|
||||||
|
|
||||||
|
return { message: `Role ${validatedRole} added successfully` };
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to add merchant role to user ${userId}: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === RETIRER UN ROLE ===
|
||||||
|
async removeMerchantRole(
|
||||||
|
userId: string,
|
||||||
|
role: string,
|
||||||
|
removedByUserId: string
|
||||||
|
): Promise<{ message: string }> {
|
||||||
|
try {
|
||||||
|
await this.validateMerchantAccess(userId, removedByUserId);
|
||||||
|
|
||||||
|
const currentRoles = await this.usersService.getUserClientRoles(userId);
|
||||||
|
const updatedRoles = currentRoles.filter(r => r !== role);
|
||||||
|
|
||||||
|
const hasOtherMerchantRoles = updatedRoles.some(r =>
|
||||||
|
r.startsWith('merchant-') && r !== 'merchant'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasOtherMerchantRoles) {
|
||||||
|
const finalRoles = updatedRoles.filter(r => r !== 'merchant');
|
||||||
|
await this.usersService.assignClientRoles(userId, finalRoles as any); // 🔥 CAST ICI
|
||||||
|
} else {
|
||||||
|
await this.usersService.assignClientRoles(userId, updatedRoles as any); // 🔥 CAST ICI
|
||||||
|
}
|
||||||
|
|
||||||
|
return { message: `Role ${role} removed successfully` };
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Failed to remove merchant role from user ${userId}: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === VERIFIER SI UN UTILISATEUR EST DANS L'EQUIPE ===
|
||||||
|
async isUserInMerchantTeam(userId: string, merchantUserId: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const merchantOwnerId = await this.usersService.getMerchantOwner(userId);
|
||||||
|
return merchantOwnerId === merchantUserId;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(`Failed to check if user ${userId} is in merchant ${merchantUserId} team: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -30,7 +30,7 @@ export class UsersService {
|
|||||||
|
|
||||||
// === VALIDATION DES ROLES ===
|
// === VALIDATION DES ROLES ===
|
||||||
private validateClientRole(role: string): ClientRole {
|
private validateClientRole(role: string): ClientRole {
|
||||||
const validRoles: ClientRole[] = ['admin', 'merchant', 'support'];
|
const validRoles: ClientRole[] = ['admin', 'merchant', 'support', 'merchant-admin', 'merchant-manager', 'merchant-support'];
|
||||||
if (!validRoles.includes(role as ClientRole)) {
|
if (!validRoles.includes(role as ClientRole)) {
|
||||||
throw new BadRequestException(`Invalid client role: ${role}. Valid roles are: ${validRoles.join(', ')}`);
|
throw new BadRequestException(`Invalid client role: ${role}. Valid roles are: ${validRoles.join(', ')}`);
|
||||||
}
|
}
|
||||||
@ -57,7 +57,11 @@ export class UsersService {
|
|||||||
const roles = await this.keycloakApi.getUserClientRoles(userId);
|
const roles = await this.keycloakApi.getUserClientRoles(userId);
|
||||||
const clientRoles = roles.map(role => role.name);
|
const clientRoles = roles.map(role => role.name);
|
||||||
|
|
||||||
return new UserResponse({ ...user, clientRoles });
|
return new UserResponse({
|
||||||
|
...user,
|
||||||
|
clientRoles,
|
||||||
|
attributes: user.attributes || {}
|
||||||
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.error(`Failed to fetch user ${userId}: ${error.message}`);
|
this.logger.error(`Failed to fetch user ${userId}: ${error.message}`);
|
||||||
if (error instanceof NotFoundException) throw error;
|
if (error instanceof NotFoundException) throw error;
|
||||||
@ -149,7 +153,6 @@ export class UsersService {
|
|||||||
|
|
||||||
if (userData.email) {
|
if (userData.email) {
|
||||||
const existingByEmail = await this.keycloakApi.findUserByEmail(userData.email);
|
const existingByEmail = await this.keycloakApi.findUserByEmail(userData.email);
|
||||||
|
|
||||||
if (existingByEmail.length > 0) {
|
if (existingByEmail.length > 0) {
|
||||||
throw new ConflictException('User with this email already exists');
|
throw new ConflictException('User with this email already exists');
|
||||||
}
|
}
|
||||||
@ -159,26 +162,45 @@ export class UsersService {
|
|||||||
throw new ConflictException('User with this username already exists');
|
throw new ConflictException('User with this username already exists');
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = await this.keycloakApi.createUser({
|
// Préparer les données utilisateur
|
||||||
|
const keycloakUserData: any = {
|
||||||
username: userData.username,
|
username: userData.username,
|
||||||
email: userData.email,
|
email: userData.email,
|
||||||
firstName: userData.firstName,
|
firstName: userData.firstName,
|
||||||
lastName: userData.lastName,
|
lastName: userData.lastName,
|
||||||
password: userData.password,
|
|
||||||
enabled: userData.enabled ?? true,
|
enabled: userData.enabled ?? true,
|
||||||
});
|
credentials: userData.password ? [
|
||||||
|
{
|
||||||
|
type: 'password',
|
||||||
|
value: userData.password,
|
||||||
|
temporary: false
|
||||||
|
}
|
||||||
|
] : []
|
||||||
|
};
|
||||||
|
|
||||||
// Attribution automatique de rôles client si fournis
|
// Ajouter les attributs si présents
|
||||||
|
if (userData.attributes) {
|
||||||
|
keycloakUserData.attributes = userData.attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = await this.keycloakApi.createUser(keycloakUserData);
|
||||||
|
|
||||||
|
// Attribution des rôles client
|
||||||
if (userData.clientRoles?.length) {
|
if (userData.clientRoles?.length) {
|
||||||
const validatedRoles = this.validateClientRoles(userData.clientRoles);
|
const validatedRoles = this.validateClientRoles(userData.clientRoles);
|
||||||
await this.keycloakApi.setClientRoles(userId, validatedRoles);
|
await this.keycloakApi.setClientRoles(userId, validatedRoles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Récupérer l'utilisateur créé
|
||||||
const createdUser = await this.keycloakApi.getUserById(userId);
|
const createdUser = await this.keycloakApi.getUserById(userId);
|
||||||
const roles = await this.keycloakApi.getUserClientRoles(userId);
|
const roles = await this.keycloakApi.getUserClientRoles(userId);
|
||||||
const clientRoles = roles.map(role => role.name);
|
const clientRoles = roles.map(role => role.name);
|
||||||
|
|
||||||
return new UserResponse({ ...createdUser, clientRoles });
|
return new UserResponse({
|
||||||
|
...createdUser,
|
||||||
|
clientRoles,
|
||||||
|
attributes: createdUser.attributes || {}
|
||||||
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.error(`Failed to create user: ${error.message}`);
|
this.logger.error(`Failed to create user: ${error.message}`);
|
||||||
if (error instanceof ConflictException || error instanceof BadRequestException) throw error;
|
if (error instanceof ConflictException || error instanceof BadRequestException) throw error;
|
||||||
@ -186,6 +208,28 @@ export class UsersService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === METHODE POUR OBTENIR LE MERCHANT OWNER ===
|
||||||
|
async getMerchantOwner(userId: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
return await this.keycloakApi.getUserAttribute(userId, 'merchantOwnerId');
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(`Failed to get merchant owner for user ${userId}: ${error.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === METHODE POUR DEFINIR LE MERCHANT OWNER ===
|
||||||
|
async setMerchantOwner(userId: string, merchantOwnerId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.keycloakApi.setUserAttributes(userId, {
|
||||||
|
merchantOwnerId: [merchantOwnerId]
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to set merchant owner for user ${userId}: ${error.message}`);
|
||||||
|
throw new BadRequestException('Failed to set merchant owner');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// === UPDATE USER ===
|
// === UPDATE USER ===
|
||||||
async updateUser(id: string, userData: UpdateUserDto): Promise<UserResponse> {
|
async updateUser(id: string, userData: UpdateUserDto): Promise<UserResponse> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { JwtModule } from '@nestjs/jwt'
|
|||||||
import { HttpModule } from '@nestjs/axios';
|
import { HttpModule } from '@nestjs/axios';
|
||||||
import { TokenService } from '../auth/services/token.service'
|
import { TokenService } from '../auth/services/token.service'
|
||||||
import { UsersService } from './services/users.service'
|
import { UsersService } from './services/users.service'
|
||||||
|
import { MerchantTeamService } from 'src/users/services/merchant-team.service';
|
||||||
|
|
||||||
import { UsersController } from './controllers/users.controller'
|
import { UsersController } from './controllers/users.controller'
|
||||||
import { KeycloakApiService } from '../auth/services/keycloak-api.service';
|
import { KeycloakApiService } from '../auth/services/keycloak-api.service';
|
||||||
|
|
||||||
@ -12,9 +14,9 @@ import { KeycloakApiService } from '../auth/services/keycloak-api.service';
|
|||||||
HttpModule,
|
HttpModule,
|
||||||
JwtModule.register({}),
|
JwtModule.register({}),
|
||||||
],
|
],
|
||||||
providers: [UsersService, KeycloakApiService, TokenService],
|
providers: [UsersService, MerchantTeamService, KeycloakApiService, TokenService],
|
||||||
controllers: [UsersController],
|
controllers: [UsersController],
|
||||||
exports: [UsersService, KeycloakApiService, TokenService, JwtModule],
|
exports: [UsersService, MerchantTeamService, KeycloakApiService, TokenService, JwtModule],
|
||||||
})
|
})
|
||||||
export class UsersModule {}
|
export class UsersModule {}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user