feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature

This commit is contained in:
diallolatoile 2025-11-04 21:06:17 +00:00
parent 35f0bcd135
commit 191099d8a5
54 changed files with 2634 additions and 1334 deletions

View File

@ -1,6 +1,7 @@
import { Directive, Input, TemplateRef, ViewContainerRef, inject, OnDestroy } from '@angular/core'; import { Directive, Input, TemplateRef, ViewContainerRef, inject, OnDestroy } from '@angular/core';
import { AuthService } from '../services/auth.service'; import { AuthService } from '../services/auth.service';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { UserRole } from '@core/models/dcb-bo-hub-user.model';
@Directive({ @Directive({
selector: '[hasRole]', selector: '[hasRole]',
@ -16,7 +17,10 @@ export class HasRoleDirective implements OnDestroy {
const requiredRoles = Array.isArray(roles) ? roles : [roles]; const requiredRoles = Array.isArray(roles) ? roles : [roles];
const userRoles = this.authService.getCurrentUserRoles(); const userRoles = this.authService.getCurrentUserRoles();
const hasAccess = requiredRoles.some(role => userRoles.includes(role)); const hasAccess = requiredRoles.some(role => userRoles.includes(
UserRole.DCB_ADMIN || UserRole.DCB_PARTNER || UserRole.DCB_SUPPORT
|| UserRole.DCB_PARTNER_ADMIN || UserRole.DCB_PARTNER_MANAGER || UserRole.DCB_PARTNER_SUPPORT
));
if (hasAccess) { if (hasAccess) {
this.viewContainer.createEmbeddedView(this.templateRef); this.viewContainer.createEmbeddedView(this.templateRef);

View File

@ -10,42 +10,43 @@ export const authGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: R
const roleService = inject(RoleService); const roleService = inject(RoleService);
const router = inject(Router); const router = inject(Router);
// Attendre que l'initialisation soit terminée // Attendre que l'initialisation du service Auth soit terminée
return authService.getInitializedState().pipe( return authService.getInitializedState().pipe(
switchMap(initialized => { switchMap(initialized => {
if (!initialized) { if (!initialized) {
return of(false); return of(false);
} }
// Vérifier l'authentification // 🔒 Étape 1 : Vérifier si déjà authentifié
if (authService.isAuthenticated()) { if (authService.isAuthenticated()) {
return of(checkRoleAccess(route, roleService, router, state.url)); return of(checkRoleAccess(route, roleService, router, state.url));
} }
// Tentative de rafraîchissement du token // 🔄 Étape 2 : Tenter un rafraîchissement du token sil existe
const refreshToken = authService.getRefreshToken(); const refreshToken = authService.getRefreshToken();
if (refreshToken) { if (refreshToken) {
return authService.refreshAccessToken().pipe(
return authService.refreshToken().pipe(
tap(() => { tap(() => {
// Recharger les rôles après un refresh réussi
roleService.refreshRoles(); roleService.refreshRoles();
}), }),
map(() => checkRoleAccess(route, roleService, router, state.url)), map(() => checkRoleAccess(route, roleService, router, state.url)),
catchError((error) => { catchError(() => {
// En cas déchec de refresh → déconnexion + redirection login
authService.logout().subscribe(); authService.logout().subscribe();
return of(redirectToLogin(router, state.url)); return of(redirectToLogin(router, state.url));
}) })
); );
} }
// Redirection vers login // 🚫 Étape 3 : Aucun token → redirection vers login
return of(redirectToLogin(router, state.url)); return of(redirectToLogin(router, state.url));
}), }),
catchError(error => { catchError(() => {
return of(redirectToLogin(router, state.url)); return of(redirectToLogin(router, state.url));
}) })
); );
} };
/** /**
* Vérifie l'accès basé sur les rôles requis * Vérifie l'accès basé sur les rôles requis
@ -58,6 +59,7 @@ function checkRoleAccess(
): boolean { ): boolean {
const requiredRoles = route.data?.['roles'] as string[]; const requiredRoles = route.data?.['roles'] as string[];
// Si aucun rôle requis → accès autorisé
if (!requiredRoles || requiredRoles.length === 0) { if (!requiredRoles || requiredRoles.length === 0) {
return true; return true;
} }
@ -65,11 +67,12 @@ function checkRoleAccess(
const hasRequiredRole = roleService.hasAnyRole(requiredRoles); const hasRequiredRole = roleService.hasAnyRole(requiredRoles);
const currentUserRoles = roleService.getCurrentUserRoles(); const currentUserRoles = roleService.getCurrentUserRoles();
// ✅ Lutilisateur possède un des rôles requis
if (hasRequiredRole) { if (hasRequiredRole) {
return true; return true;
} }
// Rediriger vers la page non autorisée // ❌ Sinon → rediriger vers une page "non autorisée"
router.navigate(['/unauthorized'], { router.navigate(['/unauthorized'], {
queryParams: { queryParams: {
requiredRoles: requiredRoles.join(','), requiredRoles: requiredRoles.join(','),
@ -83,19 +86,11 @@ function checkRoleAccess(
} }
/** /**
* Redirige vers la page de login avec les paramètres appropriés * Redirige vers la page de login avec un returnUrl
*/ */
function redirectToLogin( function redirectToLogin(router: Router, returnUrl: string): boolean {
router: Router,
returnUrl: string,
): boolean {
const queryParams: any = {
returnUrl: returnUrl
};
// Message spécifique selon la raison
router.navigate(['/auth/login'], { router.navigate(['/auth/login'], {
queryParams, queryParams: { returnUrl },
replaceUrl: true replaceUrl: true
}); });

View File

@ -8,27 +8,28 @@ export const publicGuard: CanActivateFn = () => {
const authService = inject(AuthService); const authService = inject(AuthService);
const router = inject(Router); const router = inject(Router);
// Si l'utilisateur est déjà authentifié, le rediriger vers le dashboard // 🔒 Si l'utilisateur est déjà authentifié → redirection vers le tableau de bord
if (authService.isAuthenticated()) { if (authService.isAuthenticated()) {
router.navigate(['/dcb-dashboard'], { replaceUrl: true }); router.navigate(['/dcb-dashboard'], { replaceUrl: true });
return false; return false;
} }
// Vérifier si un refresh token est disponible // 🔄 Vérifier si un refresh token est disponible
const refreshToken = authService.getRefreshToken(); const refreshToken = authService.getRefreshToken();
if (refreshToken) { if (refreshToken) {
return authService.refreshToken().pipe( return authService.refreshAccessToken().pipe(
map(() => { map(() => {
// ✅ Rafraîchissement réussi → redirection vers le dashboard
router.navigate(['/dcb-dashboard'], { replaceUrl: true }); router.navigate(['/dcb-dashboard'], { replaceUrl: true });
return false; return false;
}), }),
catchError((error) => { catchError(() => {
// En cas d'erreur, autoriser l'accès à la page publique // ❌ Rafraîchissement échoué → autoriser laccès à la page publique
return of(true); return of(true);
}) })
); );
} }
// L'utilisateur n'est pas connecté, autoriser l'accès à la page publique // 👤 Aucun token → accès autorisé à la page publique
return true; return true;
}; };

View File

@ -9,26 +9,31 @@ export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService); const authService = inject(AuthService);
const router = inject(Router); const router = inject(Router);
// Exclusion des endpoints d'authentification // Exclure les requêtes dauthentification (login, refresh, logout)
if (isAuthRequest(req)) { if (isAuthRequest(req)) {
return next(req); return next(req);
} }
const token = authService.getAccessToken(); const token = authService.getAccessToken();
// Si un token existe, lajouter aux requêtes API
if (token && isApiRequest(req)) { if (token && isApiRequest(req)) {
const cloned = addToken(req, token); const cloned = addToken(req, token);
return next(cloned).pipe( return next(cloned).pipe(
catchError((error: HttpErrorResponse) => { catchError((error: HttpErrorResponse) => {
// Si le token est expiré → tentative de refresh
if (error.status === 401 && !req.url.includes('/auth/refresh')) { if (error.status === 401 && !req.url.includes('/auth/refresh')) {
return handle401Error(authService, router, req, next); return handle401Error(authService, router, req, next);
} }
// Autres erreurs → propager
return throwError(() => error); return throwError(() => error);
}) })
); );
} }
// Si pas de token → requête normale
return next(req); return next(req);
}; };
@ -42,35 +47,41 @@ function addToken(req: HttpRequest<any>, token: string): HttpRequest<any> {
}); });
} }
/**
* Gestion du renouvellement de token en cas derreur 401
*/
function handle401Error( function handle401Error(
authService: AuthService, authService: AuthService,
router: Router, router: Router,
req: HttpRequest<any>, req: HttpRequest<any>,
next: HttpHandlerFn next: HttpHandlerFn
) { ) {
return authService.refreshToken().pipe( return authService.refreshAccessToken().pipe(
switchMap((response: LoginResponseDto) => { switchMap((response: LoginResponseDto) => {
const newRequest = addToken(req, response.access_token); const newRequest = addToken(req, response.access_token);
return next(newRequest); return next(newRequest);
}), }),
catchError((refreshError) => { catchError((refreshError) => {
authService.logout().subscribe(); authService.logout().subscribe(() => {
router.navigate(['/auth/login']); router.navigate(['/auth/login'], { replaceUrl: true });
});
return throwError(() => refreshError); return throwError(() => refreshError);
}) })
); );
} }
/**
* Détecte si la requête cible une API de ton backend
*/
function isApiRequest(req: HttpRequest<any>): boolean { function isApiRequest(req: HttpRequest<any>): boolean {
return req.url.includes('/api/') || req.url.includes('/auth/'); // Ajuste ici selon ton backend (ex : '/api/', '/v1/', etc.)
return req.url.includes('/api/v1/') || req.url.includes('/auth/');
} }
/**
* Exclut les endpoints liés à lauthentification
*/
function isAuthRequest(req: HttpRequest<any>): boolean { function isAuthRequest(req: HttpRequest<any>): boolean {
const authEndpoints = [ const authEndpoints = ['/auth/login', '/auth/refresh', '/auth/logout'];
'/auth/login',
'/auth/refresh',
'/auth/logout'
];
return authEndpoints.some(endpoint => req.url.includes(endpoint)); return authEndpoints.some(endpoint => req.url.includes(endpoint));
} }

View File

@ -1,25 +1,62 @@
export enum UserRole { export enum UserType {
DCB_PARTNER_ADMIN = 'DCB_PARTNER_ADMIN', HUB = 'HUB',
DCB_PARTNER_MANAGER = 'DCB_PARTNER_MANAGER', MERCHANT = 'MERCHANT',
DCB_PARTNER_SUPPORT = 'DCB_PARTNER_SUPPORT', MERCHANT_USER = 'MERCHANT_USER'
DCB_PARTNER = 'DCB_PARTNER',
DCB_ADMIN = 'DCB_ADMIN',
DCB_SUPPORT = 'DCB_SUPPORT'
} }
export interface CreateMerchantUserDto { export enum UserRole {
// HUB roles
DCB_ADMIN = 'DCB_ADMIN',
DCB_SUPPORT = 'DCB_SUPPORT',
DCB_PARTNER = 'DCB_PARTNER',
// MERCHANT roles
DCB_PARTNER_ADMIN = 'DCB_PARTNER_ADMIN',
DCB_PARTNER_MANAGER = 'DCB_PARTNER_MANAGER',
DCB_PARTNER_SUPPORT = 'DCB_PARTNER_SUPPORT'
}
// === BASE USER MODEL ===
export interface BaseUserDto {
id: string;
username: string;
email: string;
firstName: string;
lastName: string;
role: UserRole;
enabled: boolean;
emailVerified: boolean;
createdBy: string;
createdByUsername: string;
createdTimestamp: number;
lastLogin?: number;
userType: UserType;
}
// === EXTENSIONS ===
export interface HubUserDto extends BaseUserDto {
userType: UserType.HUB;
}
export interface MerchantUserDto extends BaseUserDto {
userType: UserType.MERCHANT;
merchantPartnerId: string;
}
// === DTOs CRUD ===
export interface CreateUserDto {
username: string; username: string;
email: string; email: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
password: string; password: string;
role: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT; role: UserRole;
enabled?: boolean; enabled?: boolean;
emailVerified?: boolean; emailVerified?: boolean;
merchantPartnerId: string; merchantPartnerId?: string; // obligatoire si MERCHANT
} }
export interface UpdateMerchantUserDto { export interface UpdateUserDto {
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
email?: string; email?: string;
@ -27,28 +64,21 @@ export interface UpdateMerchantUserDto {
} }
export interface ResetPasswordDto { export interface ResetPasswordDto {
userId?: string;
newPassword: string; newPassword: string;
temporary?: boolean; temporary?: boolean;
} }
export interface MerchantUserResponse { // === PAGINATION / STATS ===
id: string; export interface PaginatedUserResponse {
username: string; users: BaseUserDto[];
email: string; total: number;
firstName: string; page: number;
lastName: string; limit: number;
role: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT; totalPages: number;
enabled: boolean;
emailVerified: boolean;
merchantPartnerId: string;
createdBy: string;
createdByUsername: string;
createdTimestamp: number;
lastLogin?: number;
userType: 'MERCHANT';
} }
export interface MerchantUsersStatsResponse { export interface MerchantPartnerStatsResponse {
totalAdmins: number; totalAdmins: number;
totalManagers: number; totalManagers: number;
totalSupport: number; totalSupport: number;
@ -57,6 +87,7 @@ export interface MerchantUsersStatsResponse {
inactiveUsers: number; inactiveUsers: number;
} }
// === ROLES ===
export interface AvailableRole { export interface AvailableRole {
value: UserRole; value: UserRole;
label: string; label: string;
@ -68,13 +99,15 @@ export interface AvailableRolesResponse {
roles: AvailableRole[]; roles: AvailableRole[];
} }
export interface SearchMerchantUsersParams {
query?: string;
role?: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT;
enabled?: boolean;
}
export interface RoleOperationResponse { export interface RoleOperationResponse {
message: string; message: string;
success: boolean; success: boolean;
} }
// === SEARCH ===
export interface SearchUsersParams {
query?: string;
role?: UserRole;
enabled?: boolean;
userType?: UserType;
}

View File

@ -1,11 +1,19 @@
// src/app/core/services/auth.service.ts
import { Injectable, inject } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { environment } from '@environments/environment'; import { environment } from '@environments/environment';
import { BehaviorSubject, Observable, throwError, tap, catchError, map, of } from 'rxjs'; import { BehaviorSubject, Observable, throwError, tap, catchError } from 'rxjs';
import { firstValueFrom } from 'rxjs';
// Interfaces pour les DTOs de l'API import {
UserType,
UserRole,
BaseUserDto,
HubUserDto,
MerchantUserDto
} from '@core/models/dcb-bo-hub-user.model';
// === INTERFACES DTO AUTH ===
export interface LoginDto { export interface LoginDto {
username: string; username: string;
password: string; password: string;
@ -31,23 +39,6 @@ export interface AuthStatusResponseDto {
status: string; status: string;
} }
export interface UserProfileDto {
id: string;
username: string;
email: string;
firstName: string;
lastName: string;
roles: string[];
enabled: boolean;
emailVerified: boolean;
merchantPartnerId: string;
createdBy: string;
createdByUsername: string;
createdTimestamp: number;
lastLogin?: number;
userType: string;
}
export interface TokenValidationResponseDto { export interface TokenValidationResponseDto {
valid: boolean; valid: boolean;
user: { user: {
@ -72,7 +63,7 @@ export class AuthService {
private readonly refreshTokenKey = 'refresh_token'; private readonly refreshTokenKey = 'refresh_token';
private authState$ = new BehaviorSubject<boolean>(this.isAuthenticated()); private authState$ = new BehaviorSubject<boolean>(this.isAuthenticated());
private userProfile$ = new BehaviorSubject<UserProfileDto | null>(null); private userProfile$ = new BehaviorSubject<BaseUserDto | null>(null);
private initialized$ = new BehaviorSubject<boolean>(false); private initialized$ = new BehaviorSubject<boolean>(false);
// === INITIALISATION DE L'APPLICATION === // === INITIALISATION DE L'APPLICATION ===
@ -95,8 +86,8 @@ export class AuthService {
return refreshSuccess; return refreshSuccess;
} }
// Token valide, vérifier le profil utilisateur // Token valide : charger le profil utilisateur
await this.loadUserProfile().toPromise(); await firstValueFrom(this.loadUserProfile());
this.authState$.next(true); this.authState$.next(true);
this.initialized$.next(true); this.initialized$.next(true);
@ -121,8 +112,9 @@ export class AuthService {
} }
try { try {
// Convertir l'Observable en Promise pour l'initialisation const response = await firstValueFrom(this.refreshAccessToken());
const response = await this.refreshToken().toPromise(); await firstValueFrom(this.loadUserProfile());
this.authState$.next(true);
return true; return true;
} catch (error) { } catch (error) {
this.clearAuthData(); this.clearAuthData();
@ -137,7 +129,7 @@ export class AuthService {
return this.initialized$.asObservable(); return this.initialized$.asObservable();
} }
// === MÉTHODES EXISTANTES AVEC AMÉLIORATIONS === // === MÉTHODES D'AUTHENTIFICATION ===
/** /**
* Connexion utilisateur * Connexion utilisateur
@ -149,16 +141,16 @@ export class AuthService {
).pipe( ).pipe(
tap(response => { tap(response => {
this.handleLoginSuccess(response); this.handleLoginSuccess(response);
this.loadUserProfile().subscribe(); // Charger le profil après connexion this.loadUserProfile().subscribe();
}), }),
catchError(error => this.handleLoginError(error)) catchError(error => this.handleLoginError(error))
); );
} }
/** /**
* Rafraîchissement du token * Rafraîchissement du token d'accès
*/ */
refreshToken(): Observable<LoginResponseDto> { refreshAccessToken(): Observable<LoginResponseDto> {
const refreshToken = this.getRefreshToken(); const refreshToken = this.getRefreshToken();
if (!refreshToken) { if (!refreshToken) {
@ -169,9 +161,7 @@ export class AuthService {
`${environment.iamApiUrl}/auth/refresh`, `${environment.iamApiUrl}/auth/refresh`,
{ refresh_token: refreshToken } { refresh_token: refreshToken }
).pipe( ).pipe(
tap(response => { tap(response => this.handleLoginSuccess(response)),
this.handleLoginSuccess(response);
}),
catchError(error => { catchError(error => {
this.clearAuthData(); this.clearAuthData();
return throwError(() => error); return throwError(() => error);
@ -187,11 +177,9 @@ export class AuthService {
`${environment.iamApiUrl}/auth/logout`, `${environment.iamApiUrl}/auth/logout`,
{} {}
).pipe( ).pipe(
tap(() => { tap(() => this.clearAuthData()),
this.clearAuthData();
}),
catchError(error => { catchError(error => {
this.clearAuthData(); // Nettoyer même en cas d'erreur this.clearAuthData();
return throwError(() => error); return throwError(() => error);
}) })
); );
@ -200,22 +188,17 @@ export class AuthService {
/** /**
* Chargement du profil utilisateur * Chargement du profil utilisateur
*/ */
loadUserProfile(): Observable<UserProfileDto> { loadUserProfile(): Observable<BaseUserDto> {
return this.http.get<UserProfileDto>( return this.http.get<BaseUserDto>(
`${environment.iamApiUrl}/auth/profile` `${environment.iamApiUrl}/auth/profile`
).pipe( ).pipe(
tap(profile => { tap(profile => this.userProfile$.next(profile)),
this.userProfile$.next(profile); catchError(error => throwError(() => error))
}),
catchError(error => {
return throwError(() => error);
})
); );
} }
/** // === GESTION DE SESSION ===
* Gestion de la connexion réussie
*/
private handleLoginSuccess(response: LoginResponseDto): void { private handleLoginSuccess(response: LoginResponseDto): void {
if (response.access_token) { if (response.access_token) {
localStorage.setItem(this.tokenKey, response.access_token); localStorage.setItem(this.tokenKey, response.access_token);
@ -228,9 +211,6 @@ export class AuthService {
} }
} }
/**
* Nettoyage des données d'authentification
*/
private clearAuthData(): void { private clearAuthData(): void {
localStorage.removeItem(this.tokenKey); localStorage.removeItem(this.tokenKey);
localStorage.removeItem(this.refreshTokenKey); localStorage.removeItem(this.refreshTokenKey);
@ -238,9 +218,8 @@ export class AuthService {
this.userProfile$.next(null); this.userProfile$.next(null);
} }
/** // === VALIDATION DU TOKEN ===
* Validation du token
*/
validateToken(): Observable<TokenValidationResponseDto> { validateToken(): Observable<TokenValidationResponseDto> {
return this.http.get<TokenValidationResponseDto>( return this.http.get<TokenValidationResponseDto>(
`${environment.iamApiUrl}/auth/validate` `${environment.iamApiUrl}/auth/validate`
@ -253,70 +232,193 @@ export class AuthService {
return this.authState$.asObservable(); return this.authState$.asObservable();
} }
getUserProfile(): Observable<UserProfileDto | null> { getUserProfile(): Observable<BaseUserDto | null> {
return this.userProfile$.asObservable(); return this.userProfile$.asObservable();
} }
/** // === GESTION DES RÔLES ET TYPES ===
* Récupère les rôles de l'utilisateur courant
*/ getCurrentUserRoles(): UserRole[] {
getCurrentUserRoles(): string[] {
const token = this.getAccessToken(); const token = this.getAccessToken();
if (!token) return []; if (!token) return [];
try { try {
const payload = JSON.parse(atob(token.split('.')[1])); const payload = JSON.parse(atob(token.split('.')[1]));
const decoded: any = payload;
// Récupérer tous les rôles de tous les clients // Mapping des rôles Keycloak vers vos rôles DCB
if (decoded.resource_access) { const roleMappings: { [key: string]: UserRole } = {
const allRoles: string[] = []; // Rôles administrateur
'admin': UserRole.DCB_ADMIN,
'dcb-admin': UserRole.DCB_ADMIN,
'administrator': UserRole.DCB_ADMIN,
Object.values(decoded.resource_access).forEach((client: any) => { // Rôles support
'support': UserRole.DCB_SUPPORT,
'dcb-support': UserRole.DCB_SUPPORT,
// Rôles partenaire
'partner': UserRole.DCB_PARTNER,
'dcb-partner': UserRole.DCB_PARTNER,
// Rôles admin partenaire
'partner-admin': UserRole.DCB_PARTNER_ADMIN,
'dcb-partner-admin': UserRole.DCB_PARTNER_ADMIN,
// Rôles manager partenaire
'partner-manager': UserRole.DCB_PARTNER_MANAGER,
'dcb-partner-manager': UserRole.DCB_PARTNER_MANAGER,
// Rôles support partenaire
'partner-support': UserRole.DCB_PARTNER_SUPPORT,
'dcb-partner-support': UserRole.DCB_PARTNER_SUPPORT,
};
let allRoles: string[] = [];
// Collecter tous les rôles du token
if (payload.resource_access) {
Object.values(payload.resource_access).forEach((client: any) => {
if (client?.roles) { if (client?.roles) {
allRoles.push(...client.roles); allRoles = allRoles.concat(client.roles);
} }
}); });
return [...new Set(allRoles)];
} }
return []; if (payload.realm_access?.roles) {
} catch { allRoles = allRoles.concat(payload.realm_access.roles);
}
const mappedRoles = allRoles
.map(role => roleMappings[role.toLowerCase()])
.filter(role => role !== undefined);
return mappedRoles;
} catch (error) {
console.error('❌ Error:', error);
return []; return [];
} }
} }
/** /**
* Observable de l'état d'authentification * Récupère le rôle principal de l'utilisateur courant
*/ */
onAuthState(): Observable<boolean> { getCurrentUserRole(): UserRole | null {
return this.authState$.asObservable(); const roles = this.getCurrentUserRoles();
return roles.length > 0 ? roles[0] : null;
} }
/** /**
* Récupère le profil utilisateur * Récupère le type d'utilisateur courant
*/ */
getProfile(): Observable<any> { getCurrentUserType(): UserType | null {
return this.getUserProfile(); const role = this.getCurrentUserRole();
if (!role) return null;
// Déterminer le type d'utilisateur basé sur le rôle
const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER];
const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT];
if (hubRoles.includes(role)) {
return UserType.HUB;
} else if (merchantRoles.includes(role)) {
return UserType.MERCHANT;
}
return null;
} }
/** /**
* Vérifie si l'utilisateur a un rôle spécifique * Vérifie si l'utilisateur courant est un utilisateur Hub
*/ */
hasRole(role: string): boolean { isHubUser(): boolean {
return this.getCurrentUserType() === UserType.HUB;
}
/**
* Vérifie si l'utilisateur courant est un utilisateur Marchand
*/
isMerchantUser(): boolean {
return this.getCurrentUserType() === UserType.MERCHANT;
}
/**
* Vérifie si l'utilisateur courant a un rôle spécifique
*/
hasRole(role: UserRole): boolean {
return this.getCurrentUserRoles().includes(role); return this.getCurrentUserRoles().includes(role);
} }
/** /**
* Vérifie si l'utilisateur a un des rôles spécifiés * Vérifie si l'utilisateur courant a au moins un des rôles spécifiés
*/ */
hasAnyRole(roles: string[]): boolean { hasAnyRole(roles: UserRole[]): boolean {
const userRoles = this.getCurrentUserRoles(); const userRoles = this.getCurrentUserRoles();
return roles.some(role => userRoles.includes(role)); return roles.some(role => userRoles.includes(role));
} }
// === GETTERS POUR LES TOKENS === /**
* Vérifie si l'utilisateur courant peut gérer les utilisateurs Hub
*/
canManageHubUsers(): boolean {
const hubAdminRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT];
return this.hasAnyRole(hubAdminRoles);
}
/**
* Vérifie si l'utilisateur courant peut gérer les utilisateurs Marchands
*/
canManageMerchantUsers(): boolean {
const allowedRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER];
return this.hasAnyRole(allowedRoles);
}
// === MÉTHODES UTILITAIRES ===
onAuthState(): Observable<boolean> {
return this.authState$.asObservable();
}
getProfile(): Observable<BaseUserDto | null> {
return this.getUserProfile();
}
/**
* Récupère l'ID de l'utilisateur courant
*/
getCurrentUserId(): string | null {
const profile = this.userProfile$.value;
return profile?.id || null;
}
/**
* Récupère le merchantPartnerId de l'utilisateur courant (si marchand)
*/
getCurrentMerchantPartnerId(): string | null {
const profile = this.userProfile$.value;
if (profile && 'merchantPartnerId' in profile) {
return (profile as MerchantUserDto).merchantPartnerId || null;
}
return null;
}
/**
* Vérifie si le profil fourni est celui de l'utilisateur courant
*/
isCurrentUserProfile(userId: string): boolean {
const currentUserId = this.getCurrentUserId();
return currentUserId === userId;
}
/**
* Vérifie si l'utilisateur peut visualiser tous les marchands
*/
canViewAllMerchants(): boolean {
const hubAdminRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT];
return this.hasAnyRole(hubAdminRoles);
}
// === TOKENS ===
getAccessToken(): string | null { getAccessToken(): string | null {
return localStorage.getItem(this.tokenKey); return localStorage.getItem(this.tokenKey);
@ -326,7 +428,7 @@ hasAnyRole(roles: string[]): boolean {
return localStorage.getItem(this.refreshTokenKey); return localStorage.getItem(this.refreshTokenKey);
} }
// === METHODES PRIVEES === // === GESTION DES ERREURS ===
private handleLoginError(error: HttpErrorResponse): Observable<never> { private handleLoginError(error: HttpErrorResponse): Observable<never> {
let errorMessage = 'Login failed'; let errorMessage = 'Login failed';
@ -342,7 +444,7 @@ hasAnyRole(roles: string[]): boolean {
return throwError(() => new Error(errorMessage)); return throwError(() => new Error(errorMessage));
} }
// === VERIFICATIONS D'ETAT === // === VERIFICATIONS ===
isAuthenticated(): boolean { isAuthenticated(): boolean {
const token = this.getAccessToken(); const token = this.getAccessToken();

View File

@ -135,7 +135,7 @@ export class MenuService {
{ {
label: 'Déconnexion', label: 'Déconnexion',
icon: 'tablerLogout2', icon: 'tablerLogout2',
class: 'fw-semibold text-danger' url: '/auth/logout',
}, },
]; ];
} }

View File

@ -1,4 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { UserRole } from '@core/models/dcb-bo-hub-user.model';
export interface ModulePermission { export interface ModulePermission {
module: string; module: string;
@ -9,193 +10,105 @@ export interface ModulePermission {
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class PermissionsService { export class PermissionsService {
private readonly permissions: ModulePermission[] = [ private readonly permissions: ModulePermission[] = [
// Dashboard // Dashboard - Tout le monde
{ {
module: 'dcb-dashboard', module: 'dcb-dashboard',
roles: [ roles: this.allRoles,
'dcb-admin',
'dcb-partner',
'dcb-support',
'dcb-partner-admin',
'dcb-partner-manager',
'dcb-partner-support'
],
}, },
{ {
module: 'auth', module: 'auth',
roles: [ roles: this.allRoles,
'dcb-admin',
'dcb-partner',
'dcb-support',
'dcb-partner-admin',
'dcb-partner-manager',
'dcb-partner-support'
],
}, },
// Transactions
{ {
module: 'transactions', module: 'transactions',
roles: [ roles: this.allRoles,
'dcb-admin',
'dcb-partner',
'dcb-support',
'dcb-partner-admin',
'dcb-partner-manager',
'dcb-partner-support'
],
}, },
// Merchants/Partners
{ {
module: 'merchant-partners', module: 'merchant-partners',
roles: [ roles: this.allRoles,
'dcb-admin',
'dcb-partner',
'dcb-support',
'dcb-partner-admin',
'dcb-partner-manager',
'dcb-partner-support'],
}, },
// Operators - Admin seulement
// Operators (Admin only)
{ {
module: 'operators', module: 'operators',
roles: ['dcb-admin'], roles: [UserRole.DCB_ADMIN],
children: { children: {
'config': ['dcb-admin'], 'config': [UserRole.DCB_ADMIN],
'stats': ['dcb-admin'] 'stats': [UserRole.DCB_ADMIN]
} }
}, },
// Webhooks - Admin et Partner
// Webhooks
{ {
module: 'webhooks', module: 'webhooks',
roles: ['dcb-admin', 'dcb-partner'], roles: [UserRole.DCB_ADMIN, UserRole.DCB_PARTNER],
children: { children: {
'history': ['dcb-admin', 'dcb-partner'], 'history': [UserRole.DCB_ADMIN, UserRole.DCB_PARTNER],
'status': ['dcb-admin', 'dcb-partner'], 'status': [UserRole.DCB_ADMIN, UserRole.DCB_PARTNER],
'retry': ['dcb-admin'] 'retry': [UserRole.DCB_ADMIN]
} }
}, },
// Users - Admin et Support
// Users (Admin only)
{ {
module: 'users', module: 'users',
roles: ['dcb-admin', 'dcb-support'] roles: [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT]
}, },
// Settings - Tout le monde
// Support (All authenticated users)
{ {
module: 'settings', module: 'settings',
roles: [ roles: this.allRoles
'dcb-admin',
'dcb-partner',
'dcb-support',
'dcb-partner-admin',
'dcb-partner-manager',
'dcb-partner-support'
]
}, },
// Integrations - Admin seulement
// Integrations (Admin only)
{ {
module: 'integrations', module: 'integrations',
roles: ['dcb-admin'] roles: [UserRole.DCB_ADMIN]
}, },
// Modules publics - Tout le monde
// Support (All authenticated users)
{ {
module: 'support', module: 'support',
roles: [ roles: this.allRoles
'dcb-admin',
'dcb-partner',
'dcb-support',
'dcb-partner-admin',
'dcb-partner-manager',
'dcb-partner-support'
]
}, },
// Profile (All authenticated users)
{ {
module: 'profile', module: 'profile',
roles: [ roles: this.allRoles
'dcb-admin',
'dcb-partner',
'dcb-support',
'dcb-partner-admin',
'dcb-partner-manager',
'dcb-partner-support'
]
}, },
// Documentation (All authenticated users)
{ {
module: 'documentation', module: 'documentation',
roles: [ roles: this.allRoles
'dcb-admin',
'dcb-partner',
'dcb-support',
'dcb-partner-admin',
'dcb-partner-manager',
'dcb-partner-support'
]
}, },
// Help (All authenticated users)
{ {
module: 'help', module: 'help',
roles: [ roles: this.allRoles
'dcb-admin',
'dcb-partner',
'dcb-support',
'dcb-partner-admin',
'dcb-partner-manager',
'dcb-partner-support'
]
}, },
// About (All authenticated users)
{ {
module: 'about', module: 'about',
roles: [ roles: this.allRoles
'dcb-admin',
'dcb-partner',
'dcb-support',
'dcb-partner-admin',
'dcb-partner-manager',
'dcb-partner-support'
]
} }
]; ];
canAccessModule(modulePath: string, userRoles: string[]): boolean { // Tous les rôles DCB
if (!userRoles || userRoles.length === 0) { private get allRoles(): string[] {
return false; return Object.values(UserRole);
} }
canAccessModule(modulePath: string, userRoles: string[]): boolean {
if (!userRoles?.length) return false;
const [mainModule, subModule] = modulePath.split('/'); const [mainModule, subModule] = modulePath.split('/');
const permission = this.findPermission(mainModule); const permission = this.findPermission(mainModule);
if (!permission) { if (!permission) {
console.warn(`No permission configuration for module: ${mainModule}`); console.warn(`No permission for module: ${mainModule}`);
return false; return false;
} }
// Check main module access // Vérifier accès module principal
const hasModuleAccess = this.hasAnyRole(permission.roles, userRoles); const hasMainAccess = this.hasAnyRole(permission.roles, userRoles);
if (!hasModuleAccess) return false; if (!hasMainAccess) return false;
// Check sub-module access if specified // Vérifier sous-module si nécessaire
if (subModule && permission.children) { if (subModule && permission.children) {
const subModuleRoles = permission.children[subModule]; const subRoles = permission.children[subModule];
if (!subModuleRoles) { if (!subRoles) return false;
console.warn(`No permission configuration for submodule: ${mainModule}/${subModule}`); return this.hasAnyRole(subRoles, userRoles);
return false;
}
return this.hasAnyRole(subModuleRoles, userRoles);
} }
return true; return true;
@ -206,12 +119,8 @@ export class PermissionsService {
} }
private hasAnyRole(requiredRoles: string[], userRoles: string[]): boolean { private hasAnyRole(requiredRoles: string[], userRoles: string[]): boolean {
return requiredRoles.some(role => userRoles.includes(role)); return requiredRoles.some(required =>
} userRoles.some(user => user.toLowerCase() === required.toLowerCase())
);
getAccessibleModules(userRoles: string[]): string[] {
return this.permissions
.filter(permission => this.hasAnyRole(permission.roles, userRoles))
.map(permission => permission.module);
} }
} }

View File

@ -1,7 +1,7 @@
// src/app/core/services/role-management.service.ts
import { Injectable, inject } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { HubUsersService, UserRole } from '../../modules/users/services/users.service'; import { HubUsersService } from '../../modules/hub-users/services/hub-users.service';
import { BehaviorSubject, Observable, map, tap, of, catchError } from 'rxjs'; import { BehaviorSubject, Observable, map, tap, of, catchError } from 'rxjs';
import { UserRole } from '@core/models/dcb-bo-hub-user.model';
export interface RolePermission { export interface RolePermission {
canCreateUsers: boolean; canCreateUsers: boolean;
@ -13,6 +13,7 @@ export interface RolePermission {
canAccessAdmin: boolean; canAccessAdmin: boolean;
canAccessSupport: boolean; canAccessSupport: boolean;
canAccessPartner: boolean; canAccessPartner: boolean;
assignableRoles: UserRole[]; // Ajout de cette propriété
} }
// Interface simplifiée pour la réponse API // Interface simplifiée pour la réponse API
@ -20,6 +21,7 @@ export interface AvailableRoleResponse {
value: UserRole; value: UserRole;
label: string; label: string;
description: string; description: string;
allowedForCreation?: boolean;
} }
export interface AvailableRolesResponse { export interface AvailableRolesResponse {
@ -85,6 +87,24 @@ export class RoleManagementService {
label: 'DCB Partner', label: 'DCB Partner',
description: 'Merchant partner with access to their own merchant ecosystem', description: 'Merchant partner with access to their own merchant ecosystem',
permissions: this.getPermissionsForRole(UserRole.DCB_PARTNER) permissions: this.getPermissionsForRole(UserRole.DCB_PARTNER)
},
{
value: UserRole.DCB_PARTNER_ADMIN,
label: 'Partner Admin',
description: 'Administrateur partenaire marchand',
permissions: this.getPermissionsForRole(UserRole.DCB_PARTNER_ADMIN)
},
{
value: UserRole.DCB_PARTNER_MANAGER,
label: 'Partner Manager',
description: 'Manager partenaire marchand',
permissions: this.getPermissionsForRole(UserRole.DCB_PARTNER_MANAGER)
},
{
value: UserRole.DCB_PARTNER_SUPPORT,
label: 'Partner Support',
description: 'Support partenaire marchand',
permissions: this.getPermissionsForRole(UserRole.DCB_PARTNER_SUPPORT)
} }
] ]
}; };
@ -114,13 +134,12 @@ export class RoleManagementService {
map(response => response.roles.map(role => ({ map(response => response.roles.map(role => ({
value: role.value, value: role.value,
label: role.label, label: role.label,
description: role.description description: role.description,
allowedForCreation: role.allowedForCreation
}))) })))
); );
} }
// ... (le reste des méthodes reste identique)
/** /**
* Définit le rôle de l'utilisateur courant * Définit le rôle de l'utilisateur courant
*/ */
@ -139,6 +158,10 @@ export class RoleManagementService {
* Récupère les permissions détaillées selon le rôle * Récupère les permissions détaillées selon le rôle
*/ */
getPermissionsForRole(role: UserRole): RolePermission { getPermissionsForRole(role: UserRole): RolePermission {
const allRoles = Object.values(UserRole);
const hubRoles = [UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER, UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT];
const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT];
switch (role) { switch (role) {
case UserRole.DCB_ADMIN: case UserRole.DCB_ADMIN:
return { return {
@ -150,10 +173,53 @@ export class RoleManagementService {
canManageMerchants: true, canManageMerchants: true,
canAccessAdmin: true, canAccessAdmin: true,
canAccessSupport: true, canAccessSupport: true,
canAccessPartner: true canAccessPartner: true,
assignableRoles: allRoles
}; };
case UserRole.DCB_SUPPORT: case UserRole.DCB_SUPPORT:
return {
canCreateUsers: true,
canEditUsers: true,
canDeleteUsers: false,
canManageRoles: true,
canViewStats: true,
canManageMerchants: true,
canAccessAdmin: false,
canAccessSupport: true,
canAccessPartner: true,
assignableRoles: hubRoles,
};
case UserRole.DCB_PARTNER:
return {
canCreateUsers: true,
canEditUsers: true,
canDeleteUsers: true,
canManageRoles: true,
canViewStats: true,
canManageMerchants: true,
canAccessAdmin: true,
canAccessSupport: true,
canAccessPartner: true,
assignableRoles: merchantRoles
};
case UserRole.DCB_PARTNER_ADMIN:
return {
canCreateUsers: true,
canEditUsers: true,
canDeleteUsers: false,
canManageRoles: true,
canViewStats: true,
canManageMerchants: true,
canAccessAdmin: false,
canAccessSupport: false,
canAccessPartner: true,
assignableRoles: [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT]
};
case UserRole.DCB_PARTNER_MANAGER:
return { return {
canCreateUsers: true, canCreateUsers: true,
canEditUsers: true, canEditUsers: true,
@ -162,21 +228,23 @@ export class RoleManagementService {
canViewStats: true, canViewStats: true,
canManageMerchants: true, canManageMerchants: true,
canAccessAdmin: false, canAccessAdmin: false,
canAccessSupport: true, canAccessSupport: false,
canAccessPartner: true canAccessPartner: true,
assignableRoles: []
}; };
case UserRole.DCB_PARTNER: case UserRole.DCB_PARTNER_SUPPORT:
return { return {
canCreateUsers: false, canCreateUsers: false,
canEditUsers: false, canEditUsers: false,
canDeleteUsers: false, canDeleteUsers: false,
canManageRoles: false, canManageRoles: false,
canViewStats: false, canViewStats: true,
canManageMerchants: false, canManageMerchants: false,
canAccessAdmin: false, canAccessAdmin: false,
canAccessSupport: false, canAccessSupport: false,
canAccessPartner: true canAccessPartner: false,
assignableRoles: []
}; };
default: default:
@ -185,11 +253,12 @@ export class RoleManagementService {
canEditUsers: false, canEditUsers: false,
canDeleteUsers: false, canDeleteUsers: false,
canManageRoles: false, canManageRoles: false,
canViewStats: false, canViewStats: true,
canManageMerchants: false, canManageMerchants: true,
canAccessAdmin: false, canAccessAdmin: false,
canAccessSupport: false, canAccessSupport: false,
canAccessPartner: false canAccessPartner: false,
assignableRoles: []
}; };
} }
} }
@ -200,17 +269,14 @@ export class RoleManagementService {
canAssignRole(currentUserRole: UserRole | null, targetRole: UserRole): boolean { canAssignRole(currentUserRole: UserRole | null, targetRole: UserRole): boolean {
if (!currentUserRole) return false; if (!currentUserRole) return false;
// Seuls les admins peuvent attribuer tous les rôles // SEUL DCB_PARTNER peut attribuer tous les rôles
if (currentUserRole === UserRole.DCB_ADMIN) { if (currentUserRole === UserRole.DCB_PARTNER, currentUserRole === UserRole.DCB_ADMIN, currentUserRole === UserRole.DCB_SUPPORT) {
return true; return true;
} }
// Les supports ne peuvent créer que d'autres supports // Pour les autres rôles, utiliser les permissions définies
if (currentUserRole === UserRole.DCB_SUPPORT) { const permissions = this.getPermissionsForRole(currentUserRole);
return targetRole === UserRole.DCB_SUPPORT; return permissions.assignableRoles.includes(targetRole);
}
return false;
} }
/** /**
@ -280,16 +346,15 @@ export class RoleManagementService {
* Récupère le libellé d'un rôle * Récupère le libellé d'un rôle
*/ */
getRoleLabel(role: UserRole): string { getRoleLabel(role: UserRole): string {
switch (role) { const roleLabels: { [key in UserRole]: string } = {
case UserRole.DCB_ADMIN: [UserRole.DCB_ADMIN]: 'Administrateur DCB',
return 'Administrateur DCB'; [UserRole.DCB_SUPPORT]: 'Support DCB',
case UserRole.DCB_SUPPORT: [UserRole.DCB_PARTNER]: 'Partenaire DCB',
return 'Support DCB'; [UserRole.DCB_PARTNER_ADMIN]: 'Admin Partenaire',
case UserRole.DCB_PARTNER: [UserRole.DCB_PARTNER_MANAGER]: 'Manager Partenaire',
return 'Partenaire DCB'; [UserRole.DCB_PARTNER_SUPPORT]: 'Support Partenaire'
default: };
return 'Rôle inconnu'; return roleLabels[role] || 'Rôle inconnu';
}
} }
/** /**
@ -303,9 +368,12 @@ export class RoleManagementService {
return 'bg-info'; return 'bg-info';
case UserRole.DCB_PARTNER: case UserRole.DCB_PARTNER:
return 'bg-success'; return 'bg-success';
case UserRole.DCB_PARTNER_ADMIN: return 'bg-danger'; case UserRole.DCB_PARTNER_ADMIN:
case UserRole.DCB_PARTNER_MANAGER: return 'bg-success'; return 'bg-danger';
case UserRole.DCB_PARTNER_SUPPORT: return 'bg-info'; case UserRole.DCB_PARTNER_MANAGER:
return 'bg-warning text-dark';
case UserRole.DCB_PARTNER_SUPPORT:
return 'bg-info text-white';
default: default:
return 'bg-secondary'; return 'bg-secondary';
} }
@ -354,11 +422,19 @@ export class RoleManagementService {
return role === UserRole.DCB_PARTNER; return role === UserRole.DCB_PARTNER;
} }
/**
* Vérifie si un rôle est un rôle marchand
*/
isMerchantRole(role: UserRole): boolean {
const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT];
return merchantRoles.includes(role);
}
/** /**
* Récupère tous les rôles disponibles sous forme de tableau * Récupère tous les rôles disponibles sous forme de tableau
*/ */
getAllRoles(): UserRole[] { getAllRoles(): UserRole[] {
return [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER]; return Object.values(UserRole);
} }
/** /**
@ -367,15 +443,22 @@ export class RoleManagementService {
getAssignableRoles(currentUserRole: UserRole | null): UserRole[] { getAssignableRoles(currentUserRole: UserRole | null): UserRole[] {
if (!currentUserRole) return []; if (!currentUserRole) return [];
if (currentUserRole === UserRole.DCB_ADMIN) { const permissions = this.getPermissionsForRole(currentUserRole);
return this.getAllRoles(); return permissions.assignableRoles;
} }
if (currentUserRole === UserRole.DCB_SUPPORT) { /**
return [UserRole.DCB_SUPPORT]; * Récupère uniquement les rôles Hub (DCB_ADMIN, DCB_SUPPORT, DCB_PARTNER)
*/
getHubRoles(): UserRole[] {
return [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER];
} }
return []; /**
* Récupère uniquement les rôles Marchands
*/
getMerchantRoles(): UserRole[] {
return [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT];
} }
/** /**

View File

@ -24,22 +24,14 @@
<!-- Séparateur --> <!-- Séparateur -->
@if (item.isDivider) { @if (item.isDivider) {
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
} } @if (!item.isHeader && !item.isDivider && item.url) {
<a [routerLink]="item.url" class="dropdown-item" [class]="item.class">
<!-- Élément normal -->
@if (!item.isHeader && !item.isDivider) {
<a
[routerLink]="item.url"
class="dropdown-item"
[class]="item.class"
[class.disabled]="item.isDisabled"
[attr.target]="item.target">
<ng-icon <ng-icon
[name]="item.icon" [name]="item.icon"
size="17" size="17"
class="align-middle d-inline-flex align-items-center me-2" class="align-middle d-inline-flex align-items-center me-2"
/> />
<span class="align-middle">{{ item.label }}</span> <span class="align-middle" [innerHTML]="item.label"></span>
</a> </a>
} }
</div> </div>

View File

@ -32,7 +32,6 @@ export class UserProfile implements OnInit, OnDestroy {
ngOnInit() { ngOnInit() {
this.loadDropdownItems() this.loadDropdownItems()
// Optionnel : réagir aux changements d'authentification
this.subscription = this.authService.onAuthState().subscribe(() => { this.subscription = this.authService.onAuthState().subscribe(() => {
this.loadDropdownItems() this.loadDropdownItems()
}) })

View File

@ -57,7 +57,7 @@
<span class="d-none d-md-inline-block align-middle">Liste des Utilisateurs</span> <span class="d-none d-md-inline-block align-middle">Liste des Utilisateurs</span>
</a> </a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<app-users-list <app-hub-users-list
[canCreateUsers]="canCreateUsers" [canCreateUsers]="canCreateUsers"
[canDeleteUsers]="canDeleteUsers" [canDeleteUsers]="canDeleteUsers"
(userSelected)="showTab('profile', $event)" (userSelected)="showTab('profile', $event)"
@ -75,7 +75,7 @@
</a> </a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
@if (selectedUserId) { @if (selectedUserId) {
<app-user-profile <app-hub-user-profile
[userId]="selectedUserId" [userId]="selectedUserId"
(back)="showTab('list')" (back)="showTab('list')"
/> />
@ -244,25 +244,28 @@
</div> </div>
<!-- Aperçu du rôle sélectionné --> <!-- Aperçu du rôle sélectionné -->
@if (newUser.role) { <!-- Sélection du partenaire marchand (uniquement pour les rôles marchands) -->
@if (newUser.role && isMerchantRole(newUser.role)) {
<div class="col-12"> <div class="col-12">
<div class="alert alert-info"> <label class="form-label">
<div class="d-flex align-items-center"> Partenaire Marchand <span class="text-danger">*</span>
<ng-icon </label>
[name]="roleService.getRoleIcon(newUser.role)" <select
class="me-2" class="form-select"
></ng-icon> [(ngModel)]="selectedMerchantPartnerId"
<div> name="merchantPartnerId"
<strong>Rôle sélectionné :</strong> required
<span class="badge ms-2" [ngClass]="getRoleBadgeClass(newUser.role)"> [disabled]="creatingUser"
{{ roleService.getRoleLabel(newUser.role) }} >
</span> <option value="" disabled>Sélectionnez un partenaire marchand</option>
<br> @for (partner of merchantPartners; track partner.id) {
<small class="text-muted"> <option [value]="partner.id">
{{ getRoleDescription(newUser.role) }} {{ partner.name }}
</small> </option>
</div> }
</div> </select>
<div class="form-text">
Sélectionnez le partenaire marchand auquel cet utilisateur sera associé
</div> </div>
</div> </div>
} }
@ -315,7 +318,7 @@
<button <button
type="submit" type="submit"
class="btn btn-primary" class="btn btn-primary"
[disabled]="!userForm.form.valid || creatingUser || !canAssignRole(newUser.role)" [disabled]="!userForm.form.valid || creatingUser || !canAssignRole(newUser.role) || (isMerchantRole(newUser.role) && !selectedMerchantPartnerId)"
> >
@if (creatingUser) { @if (creatingUser) {
<div class="spinner-border spinner-border-sm me-2" role="status"> <div class="spinner-border spinner-border-sm me-2" role="status">

View File

@ -1,5 +1,5 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { Users } from './users'; import { HubUsers } from './hub-users';
import { authGuard } from '../../core/guards/auth.guard'; import { authGuard } from '../../core/guards/auth.guard';
import { roleGuard } from '../../core/guards/role.guard'; import { roleGuard } from '../../core/guards/role.guard';
@ -7,7 +7,7 @@ export const USERS_ROUTES: Routes = [
{ {
path: 'users', path: 'users',
canActivate: [authGuard, roleGuard], canActivate: [authGuard, roleGuard],
component: Users, component: HubUsers,
data: { data: {
title: 'Gestion des Utilisateurs', title: 'Gestion des Utilisateurs',
requiredRoles: ['admin'] // pour information requiredRoles: ['admin'] // pour information

View File

@ -0,0 +1,2 @@
import { HubUsers } from './hub-users';
describe('Users', () => {});

View File

@ -1,19 +1,27 @@
// src/app/modules/users/users.ts
import { Component, inject, OnInit, TemplateRef, ViewChild, ChangeDetectorRef, OnDestroy } from '@angular/core'; import { Component, inject, OnInit, TemplateRef, ViewChild, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { NgIcon } from '@ng-icons/core'; import { NgIcon } from '@ng-icons/core';
import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
import { Subject, takeUntil } from 'rxjs'; import { catchError, map, of, Subject, takeUntil } from 'rxjs';
import { PageTitle } from '@app/components/page-title/page-title'; import { PageTitle } from '@app/components/page-title/page-title';
import { UsersList } from './list/list'; import { HubUsersList } from './list/list';
import { UserProfile } from './profile/profile'; import { HubUserProfile } from './profile/profile';
import { HubUsersService, CreateHubUserDto, UserRole, HubUserResponse } from './services/users.service'; import { HubUsersService } from './services/hub-users.service';
import { RoleManagementService } from '@core/services/role-management.service'; import { RoleManagementService } from '@core/services/role-management.service';
import { AuthService } from '@core/services/auth.service'; import { AuthService } from '@core/services/auth.service';
import {
HubUserDto,
CreateUserDto,
ResetPasswordDto,
UserRole,
PaginatedUserResponse,
MerchantUserDto,
} from '@core/models/dcb-bo-hub-user.model';
@Component({ @Component({
selector: 'app-users', selector: 'app-hub-users',
standalone: true, standalone: true,
imports: [ imports: [
CommonModule, CommonModule,
@ -22,19 +30,18 @@ import { AuthService } from '@core/services/auth.service';
NgbNavModule, NgbNavModule,
NgbModalModule, NgbModalModule,
PageTitle, PageTitle,
UsersList, HubUsersList,
UserProfile HubUserProfile,
], ],
templateUrl: './users.html', templateUrl: './hub-users.html',
}) })
export class Users implements OnInit, OnDestroy { export class HubUsers implements OnInit, OnDestroy {
private modalService = inject(NgbModal); private modalService = inject(NgbModal);
private usersService = inject(HubUsersService); private usersService = inject(HubUsersService);
private authService = inject(AuthService); private authService = inject(AuthService);
private cdRef = inject(ChangeDetectorRef); private cdRef = inject(ChangeDetectorRef);
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
// Rendre le service accessible au template via des méthodes proxy
protected roleService = inject(RoleManagementService); protected roleService = inject(RoleManagementService);
activeTab: 'list' | 'profile' = 'list'; activeTab: 'list' | 'profile' = 'list';
@ -48,7 +55,7 @@ export class Users implements OnInit, OnDestroy {
canManageRoles = false; canManageRoles = false;
// Données pour la création d'utilisateur // Données pour la création d'utilisateur
newUser: CreateHubUserDto = { newUser: CreateUserDto = {
username: '', username: '',
email: '', email: '',
firstName: '', firstName: '',
@ -59,6 +66,10 @@ export class Users implements OnInit, OnDestroy {
emailVerified: false emailVerified: false
}; };
// Liste des partenaires marchands (à récupérer depuis votre API)
merchantPartners: any[] = [];
selectedMerchantPartnerId: string = '';
availableRoles: { value: UserRole; label: string; description: string }[] = []; availableRoles: { value: UserRole; label: string; description: string }[] = [];
assignableRoles: UserRole[] = []; assignableRoles: UserRole[] = [];
@ -66,14 +77,14 @@ export class Users implements OnInit, OnDestroy {
createUserError = ''; createUserError = '';
// Données pour la réinitialisation de mot de passe // Données pour la réinitialisation de mot de passe
selectedUserForReset: HubUserResponse | null = null; selectedUserForReset: HubUserDto | null = null;
newPassword = ''; newPassword = '';
temporaryPassword = false; temporaryPassword = false;
resettingPassword = false; resettingPassword = false;
resetPasswordError = ''; resetPasswordError = '';
resetPasswordSuccess = ''; resetPasswordSuccess = '';
selectedUserForDelete: HubUserResponse | null = null; selectedUserForDelete: HubUserDto | null = null;
deletingUser = false; deletingUser = false;
deleteUserError = ''; deleteUserError = '';
@ -81,6 +92,7 @@ export class Users implements OnInit, OnDestroy {
this.activeTab = 'list'; this.activeTab = 'list';
this.initializeUserPermissions(); this.initializeUserPermissions();
this.loadAvailableRoles(); this.loadAvailableRoles();
this.loadMerchantPartners();
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@ -96,8 +108,7 @@ export class Users implements OnInit, OnDestroy {
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (profile) => { next: (profile) => {
// Supposons que le rôle principal est le premier rôle this.currentUserRole = profile?.role?.[0] as UserRole || null;
this.currentUserRole = profile?.roles?.[0] as UserRole || null;
if (this.currentUserRole) { if (this.currentUserRole) {
this.roleService.setCurrentUserRole(this.currentUserRole); this.roleService.setCurrentUserRole(this.currentUserRole);
@ -106,7 +117,6 @@ export class Users implements OnInit, OnDestroy {
this.canDeleteUsers = this.roleService.canDeleteUsers(this.currentUserRole); this.canDeleteUsers = this.roleService.canDeleteUsers(this.currentUserRole);
this.canManageRoles = this.roleService.canManageRoles(this.currentUserRole); this.canManageRoles = this.roleService.canManageRoles(this.currentUserRole);
// Rôles que l'utilisateur peut attribuer
this.assignableRoles = this.roleService.getAssignableRoles(this.currentUserRole); this.assignableRoles = this.roleService.getAssignableRoles(this.currentUserRole);
} }
}, },
@ -120,24 +130,58 @@ export class Users implements OnInit, OnDestroy {
* Charge les rôles disponibles * Charge les rôles disponibles
*/ */
private loadAvailableRoles(): void { private loadAvailableRoles(): void {
this.roleService.getAvailableRolesSimple() this.usersService.getAvailableHubRoles()
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (roles) => { next: (response) => {
this.availableRoles = roles; this.availableRoles = response.roles.map(role => ({
value: role.value,
label: role.label,
description: role.description
}));
}, },
error: (error) => { error: (error) => {
console.error('Error loading available roles:', error); console.error('Error loading available hub roles:', error);
// Fallback en cas d'erreur
this.availableRoles = [ this.availableRoles = [
{ value: UserRole.DCB_ADMIN, label: 'DCB Admin', description: 'Administrateur système' }, { value: UserRole.DCB_ADMIN, label: 'DCB Admin', description: 'Administrateur système' },
{ value: UserRole.DCB_SUPPORT, label: 'DCB Support', description: 'Support technique' }, { value: UserRole.DCB_SUPPORT, label: 'DCB Support', description: 'Support technique' },
{ value: UserRole.DCB_PARTNER, label: 'DCB Partner', description: 'Partenaire commercial' } { value: UserRole.DCB_PARTNER, label: 'DCB Partner', description: 'Partenaire commercial' },
{ value: UserRole.DCB_PARTNER_ADMIN, label: 'Partner Admin', description: 'Administrateur partenaire' },
{ value: UserRole.DCB_PARTNER_MANAGER, label: 'Partner Manager', description: 'Manager partenaire' },
{ value: UserRole.DCB_PARTNER_SUPPORT, label: 'Partner Support', description: 'Support partenaire' }
]; ];
} }
}); });
} }
/**
* Charge la liste des partenaires marchands
*/
private loadMerchantPartners(): void {
this.usersService.findAllMerchantUsers().pipe(
map((response: PaginatedUserResponse) => {
return response.users as MerchantUserDto[];
}),
catchError(error => {
console.error('❌ Error loading all merchant users:', error);
return of([]);
})
);
}
/**
* Vérifie si un rôle est un rôle marchand
*/
isMerchantRole(role: UserRole): boolean {
const merchantRoles = [
UserRole.DCB_PARTNER_ADMIN,
UserRole.DCB_PARTNER_MANAGER,
UserRole.DCB_PARTNER_SUPPORT
];
return merchantRoles.includes(role);
}
/** /**
* Vérifie si l'utilisateur peut attribuer un rôle spécifique * Vérifie si l'utilisateur peut attribuer un rôle spécifique
*/ */
@ -163,7 +207,6 @@ export class Users implements OnInit, OnDestroy {
return roleInfo?.description || 'Description non disponible'; return roleInfo?.description || 'Description non disponible';
} }
showTab(tab: 'list' | 'profile', userId?: string) { showTab(tab: 'list' | 'profile', userId?: string) {
this.activeTab = tab; this.activeTab = tab;
@ -203,13 +246,14 @@ export class Users implements OnInit, OnDestroy {
enabled: true, enabled: true,
emailVerified: false emailVerified: false
}; };
this.selectedMerchantPartnerId = '';
this.createUserError = ''; this.createUserError = '';
this.openModal(this.createUserModal); this.openModal(this.createUserModal);
} }
// Méthode pour ouvrir le modal de réinitialisation de mot de passe // Méthode pour ouvrir le modal de réinitialisation de mot de passe
openResetPasswordModal(userId: string) { openResetPasswordModal(userId: string) {
this.usersService.getUserById(userId) this.usersService.getHubUserById(userId)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (user) => { next: (user) => {
@ -246,19 +290,34 @@ export class Users implements OnInit, OnDestroy {
return; return;
} }
// Vérifier merchantPartnerId pour les rôles marchands
if (this.isMerchantRole(this.newUser.role) && !this.selectedMerchantPartnerId) {
this.createUserError = 'Le partenaire marchand est requis pour les utilisateurs marchands';
return;
}
this.creatingUser = true; this.creatingUser = true;
this.createUserError = ''; this.createUserError = '';
this.usersService.createUser(this.newUser) // Préparer les données pour l'API
const userData: CreateUserDto = {
...this.newUser
};
// Ajouter merchantPartnerId si c'est un rôle marchand
if (this.isMerchantRole(this.newUser.role)) {
userData.merchantPartnerId = this.selectedMerchantPartnerId;
}
this.usersService.createHubUser(userData)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (createdUser) => { next: (createdUser) => {
this.creatingUser = false; this.creatingUser = false;
this.modalService.dismissAll(); this.modalService.dismissAll();
// Rafraîchir la liste
if (this.usersListComponent) { if (this.usersListComponent) {
this.usersListComponent.loadUsers(); this.usersListComponent.refreshData();
} }
this.showTab('list'); this.showTab('list');
@ -283,10 +342,14 @@ export class Users implements OnInit, OnDestroy {
this.resetPasswordError = ''; this.resetPasswordError = '';
this.resetPasswordSuccess = ''; this.resetPasswordSuccess = '';
this.usersService.resetPassword( const resetPasswordDto: ResetPasswordDto = {
newPassword: this.newPassword,
temporary: this.temporaryPassword
};
this.usersService.resetHubUserPassword(
this.selectedUserForReset.id, this.selectedUserForReset.id,
this.newPassword, resetPasswordDto
this.temporaryPassword
) )
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
@ -310,7 +373,7 @@ export class Users implements OnInit, OnDestroy {
return; return;
} }
this.usersService.getUserById(userId) this.usersService.getHubUserById(userId)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (user) => { next: (user) => {
@ -331,16 +394,15 @@ export class Users implements OnInit, OnDestroy {
this.deletingUser = true; this.deletingUser = true;
this.deleteUserError = ''; this.deleteUserError = '';
this.usersService.deleteUser(this.selectedUserForDelete.id) this.usersService.deleteHubUser(this.selectedUserForDelete.id)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: () => { next: () => {
this.deletingUser = false; this.deletingUser = false;
this.modalService.dismissAll(); this.modalService.dismissAll();
// Rafraîchir la liste
if (this.usersListComponent) { if (this.usersListComponent) {
this.usersListComponent.loadUsers(); this.usersListComponent.refreshData();
} }
this.cdRef.detectChanges(); this.cdRef.detectChanges();
@ -431,7 +493,7 @@ export class Users implements OnInit, OnDestroy {
return { isValid: true }; return { isValid: true };
} }
@ViewChild(UsersList) usersListComponent!: UsersList; @ViewChild(HubUsersList) usersListComponent!: HubUsersList;
// Références aux templates de modals // Références aux templates de modals
@ViewChild('createUserModal') createUserModal!: TemplateRef<any>; @ViewChild('createUserModal') createUserModal!: TemplateRef<any>;

View File

@ -1,16 +1,22 @@
// src/app/modules/users/list/list.ts
import { Component, inject, OnInit, Output, EventEmitter, ChangeDetectorRef, Input, OnDestroy } from '@angular/core'; import { Component, inject, OnInit, Output, EventEmitter, ChangeDetectorRef, Input, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { NgIcon } from '@ng-icons/core'; import { NgIcon } from '@ng-icons/core';
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
import { Subject, takeUntil } from 'rxjs'; import { Subject, takeUntil } from 'rxjs';
import { HubUsersService, HubUserResponse, UserRole } from '../services/users.service'; import { HubUsersService } from '../services/hub-users.service';
import { RoleManagementService } from '@core/services/role-management.service'; import { RoleManagementService } from '@core/services/role-management.service';
import { UiCard } from '@app/components/ui-card'; import { UiCard } from '@app/components/ui-card';
import {
HubUserDto,
PaginatedUserResponse,
UserRole,
UserType
} from '@core/models/dcb-bo-hub-user.model';
@Component({ @Component({
selector: 'app-users-list', selector: 'app-hub-users-list',
standalone: true, standalone: true,
imports: [ imports: [
CommonModule, CommonModule,
@ -21,13 +27,14 @@ import { UiCard } from '@app/components/ui-card';
], ],
templateUrl: './list.html', templateUrl: './list.html',
}) })
export class UsersList implements OnInit, OnDestroy { export class HubUsersList implements OnInit, OnDestroy {
private usersService = inject(HubUsersService); private usersService = inject(HubUsersService);
private roleService = inject(RoleManagementService); private roleService = inject(RoleManagementService);
private cdRef = inject(ChangeDetectorRef); private cdRef = inject(ChangeDetectorRef);
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
readonly UserRole = UserRole; readonly UserRole = UserRole;
readonly UserType = UserType;
@Input() canCreateUsers: boolean = false; @Input() canCreateUsers: boolean = false;
@Input() canDeleteUsers: boolean = false; @Input() canDeleteUsers: boolean = false;
@ -38,9 +45,9 @@ export class UsersList implements OnInit, OnDestroy {
@Output() openDeleteUserModal = new EventEmitter<string>(); @Output() openDeleteUserModal = new EventEmitter<string>();
// Données // Données
allUsers: HubUserResponse[] = []; allUsers: HubUserDto[] = [];
filteredUsers: HubUserResponse[] = []; filteredUsers: HubUserDto[] = [];
displayedUsers: HubUserResponse[] = []; displayedUsers: HubUserDto[] = [];
// États // États
loading = false; loading = false;
@ -59,15 +66,15 @@ export class UsersList implements OnInit, OnDestroy {
totalPages = 0; totalPages = 0;
// Tri // Tri
sortField: keyof HubUserResponse = 'username'; sortField: keyof HubUserDto = 'username';
sortDirection: 'asc' | 'desc' = 'asc'; sortDirection: 'asc' | 'desc' = 'asc';
// Rôles disponibles pour le filtre // Rôles disponibles pour le filtre
availableRoles = [ availableRoles = [
{ value: 'all' as const, label: 'Tous les rôles' }, { value: 'all' as const, label: 'Tous les rôles' },
{ value: UserRole.DCB_ADMIN, label: 'Administrateurs' }, { value: UserRole.DCB_ADMIN, label: 'Administrateurs DCB' },
{ value: UserRole.DCB_SUPPORT, label: 'Support' }, { value: UserRole.DCB_SUPPORT, label: 'Support DCB' },
{ value: UserRole.DCB_PARTNER, label: 'Partenaires' } { value: UserRole.DCB_PARTNER, label: 'Partenaires DCB' }
]; ];
ngOnInit() { ngOnInit() {
@ -83,20 +90,22 @@ export class UsersList implements OnInit, OnDestroy {
this.loading = true; this.loading = true;
this.error = ''; this.error = '';
this.usersService.findAllUsers() this.usersService.getHubUsers(this.currentPage, this.itemsPerPage)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (response) => { next: (response: PaginatedUserResponse) => {
this.allUsers = response.users; this.allUsers = response.users as HubUserDto[];
this.totalItems = response.total;
this.totalPages = response.totalPages;
this.applyFiltersAndPagination(); this.applyFiltersAndPagination();
this.loading = false; this.loading = false;
this.cdRef.detectChanges(); this.cdRef.detectChanges();
}, },
error: (error) => { error: (error) => {
this.error = 'Erreur lors du chargement des utilisateurs'; this.error = 'Erreur lors du chargement des utilisateurs Hub';
this.loading = false; this.loading = false;
this.cdRef.detectChanges(); this.cdRef.detectChanges();
console.error('Error loading users:', error); console.error('Error loading hub users:', error);
} }
}); });
} }
@ -172,7 +181,7 @@ export class UsersList implements OnInit, OnDestroy {
} }
// Tri // Tri
sort(field: keyof HubUserResponse) { sort(field: keyof HubUserDto) {
if (this.sortField === field) { if (this.sortField === field) {
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
} else { } else {
@ -182,7 +191,7 @@ export class UsersList implements OnInit, OnDestroy {
this.applyFiltersAndPagination(); this.applyFiltersAndPagination();
} }
getSortIcon(field: keyof HubUserResponse): string { getSortIcon(field: keyof HubUserDto): string {
if (this.sortField !== field) return 'lucideArrowUpDown'; if (this.sortField !== field) return 'lucideArrowUpDown';
return this.sortDirection === 'asc' ? 'lucideArrowUp' : 'lucideArrowDown'; return this.sortDirection === 'asc' ? 'lucideArrowUp' : 'lucideArrowDown';
} }
@ -207,45 +216,53 @@ export class UsersList implements OnInit, OnDestroy {
} }
// Méthode pour réinitialiser le mot de passe // Méthode pour réinitialiser le mot de passe
resetPassword(user: HubUserResponse) { resetPassword(user: HubUserDto) {
this.openResetPasswordModal.emit(user.id); this.openResetPasswordModal.emit(user.id);
} }
// Méthode pour ouvrir le modal de suppression // Méthode pour ouvrir le modal de suppression
deleteUser(user: HubUserResponse) { deleteUser(user: HubUserDto) {
if (this.canDeleteUsers) { if (this.canDeleteUsers) {
this.openDeleteUserModal.emit(user.id); this.openDeleteUserModal.emit(user.id);
} }
} }
enableUser(user: HubUserResponse) { enableUser(user: HubUserDto) {
this.usersService.enableUser(user.id) this.usersService.enableHubUser(user.id)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: () => { next: (updatedUser) => {
user.enabled = true; // Mettre à jour l'utilisateur dans la liste
const index = this.allUsers.findIndex(u => u.id === user.id);
if (index !== -1) {
this.allUsers[index] = updatedUser;
}
this.applyFiltersAndPagination(); this.applyFiltersAndPagination();
this.cdRef.detectChanges(); this.cdRef.detectChanges();
}, },
error: (error) => { error: (error) => {
console.error('Error enabling user:', error); console.error('Error enabling hub user:', error);
this.error = 'Erreur lors de l\'activation de l\'utilisateur'; this.error = 'Erreur lors de l\'activation de l\'utilisateur';
this.cdRef.detectChanges(); this.cdRef.detectChanges();
} }
}); });
} }
disableUser(user: HubUserResponse) { disableUser(user: HubUserDto) {
this.usersService.disableUser(user.id) this.usersService.disableHubUser(user.id)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: () => { next: (updatedUser) => {
user.enabled = false; // Mettre à jour l'utilisateur dans la liste
const index = this.allUsers.findIndex(u => u.id === user.id);
if (index !== -1) {
this.allUsers[index] = updatedUser;
}
this.applyFiltersAndPagination(); this.applyFiltersAndPagination();
this.cdRef.detectChanges(); this.cdRef.detectChanges();
}, },
error: (error) => { error: (error) => {
console.error('Error disabling user:', error); console.error('Error disabling hub user:', error);
this.error = 'Erreur lors de la désactivation de l\'utilisateur'; this.error = 'Erreur lors de la désactivation de l\'utilisateur';
this.cdRef.detectChanges(); this.cdRef.detectChanges();
} }
@ -253,13 +270,13 @@ export class UsersList implements OnInit, OnDestroy {
} }
// Utilitaires d'affichage // Utilitaires d'affichage
getStatusBadgeClass(user: HubUserResponse): string { getStatusBadgeClass(user: HubUserDto): string {
if (!user.enabled) return 'badge bg-danger'; if (!user.enabled) return 'badge bg-danger';
if (!user.emailVerified) return 'badge bg-warning'; if (!user.emailVerified) return 'badge bg-warning';
return 'badge bg-success'; return 'badge bg-success';
} }
getStatusText(user: HubUserResponse): string { getStatusText(user: HubUserDto): string {
if (!user.enabled) return 'Désactivé'; if (!user.enabled) return 'Désactivé';
if (!user.emailVerified) return 'Email non vérifié'; if (!user.emailVerified) return 'Email non vérifié';
return 'Actif'; return 'Actif';
@ -288,11 +305,11 @@ export class UsersList implements OnInit, OnDestroy {
}); });
} }
getUserInitials(user: HubUserResponse): string { getUserInitials(user: HubUserDto): string {
return (user.firstName?.charAt(0) || '') + (user.lastName?.charAt(0) || '') || 'U'; return (user.firstName?.charAt(0) || '') + (user.lastName?.charAt(0) || '') || 'U';
} }
getUserDisplayName(user: HubUserResponse): string { getUserDisplayName(user: HubUserDto): string {
if (user.firstName && user.lastName) { if (user.firstName && user.lastName) {
return `${user.firstName} ${user.lastName}`; return `${user.firstName} ${user.lastName}`;
} }
@ -300,9 +317,6 @@ export class UsersList implements OnInit, OnDestroy {
} }
// Statistiques // Statistiques
/**
* Récupère le nombre d'utilisateurs par rôle (méthode publique pour le template)
*/
getUsersCountByRole(role: UserRole): number { getUsersCountByRole(role: UserRole): number {
return this.allUsers.filter(user => user.role === role).length; return this.allUsers.filter(user => user.role === role).length;
} }
@ -315,11 +329,12 @@ export class UsersList implements OnInit, OnDestroy {
return this.allUsers.filter(user => !user.enabled).length; return this.allUsers.filter(user => !user.enabled).length;
} }
// Vérification des permissions pour les actions getEmailVerifiedCount(): number {
canManageUser(user: HubUserResponse): boolean { return this.allUsers.filter(user => user.emailVerified).length;
// Implémentez votre logique de permission ici }
// Par exemple, empêcher un utilisateur de se modifier lui-même
return true; getEmailNotVerifiedCount(): number {
return this.allUsers.filter(user => !user.emailVerified).length;
} }
// Recherche rapide par rôle // Recherche rapide par rôle
@ -328,4 +343,30 @@ export class UsersList implements OnInit, OnDestroy {
this.currentPage = 1; this.currentPage = 1;
this.applyFiltersAndPagination(); this.applyFiltersAndPagination();
} }
// Recharger les données
refreshData() {
this.loadUsers();
}
// Méthode pour charger plus d'utilisateurs (scroll infini optionnel)
loadMoreUsers() {
if (this.currentPage < this.totalPages) {
this.currentPage++;
this.usersService.getHubUsers(this.currentPage, this.itemsPerPage)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (response: PaginatedUserResponse) => {
this.allUsers = [...this.allUsers, ...(response.users as HubUserDto[])];
this.applyFiltersAndPagination();
this.cdRef.detectChanges();
},
error: (error) => {
console.error('Error loading more hub users:', error);
this.error = 'Erreur lors du chargement des utilisateurs supplémentaires';
this.cdRef.detectChanges();
}
});
}
}
} }

View File

@ -0,0 +1,115 @@
// src/app/core/models/user.model.ts
export enum UserType {
HUB = 'HUB',
MERCHANT = 'MERCHANT',
MERCHANT_USER = 'MERCHANT_USER'
}
export enum UserRole {
// HUB roles
DCB_ADMIN = 'DCB_ADMIN',
DCB_SUPPORT = 'DCB_SUPPORT',
DCB_PARTNER = 'DCB_PARTNER',
// MERCHANT roles
DCB_PARTNER_ADMIN = 'DCB_PARTNER_ADMIN',
DCB_PARTNER_MANAGER = 'DCB_PARTNER_MANAGER',
DCB_PARTNER_SUPPORT = 'DCB_PARTNER_SUPPORT'
}
// === BASE USER MODEL ===
export interface BaseUserDto {
id: string;
username: string;
email: string;
firstName: string;
lastName: string;
role: UserRole;
enabled: boolean;
emailVerified: boolean;
createdBy: string;
createdByUsername: string;
createdTimestamp: number;
lastLogin?: number;
userType: UserType;
}
// === EXTENSIONS ===
export interface HubUserDto extends BaseUserDto {
userType: UserType.HUB;
}
export interface MerchantUserDto extends BaseUserDto {
userType: UserType.MERCHANT;
merchantPartnerId: string;
}
// === DTOs CRUD ===
export interface CreateUserDto {
username: string;
email: string;
firstName: string;
lastName: string;
password: string;
role: UserRole;
enabled?: boolean;
emailVerified?: boolean;
merchantPartnerId?: string; // obligatoire si MERCHANT
}
export interface UpdateUserDto {
firstName?: string;
lastName?: string;
email?: string;
enabled?: boolean;
}
export interface ResetPasswordDto {
userId?: string;
newPassword: string;
temporary?: boolean;
}
// === PAGINATION / STATS ===
export interface PaginatedUserResponse {
users: BaseUserDto[];
total: number;
page: number;
limit: number;
totalPages: number;
}
export interface MerchantPartnerStatsResponse {
totalAdmins: number;
totalManagers: number;
totalSupport: number;
totalUsers: number;
activeUsers: number;
inactiveUsers: number;
}
// === ROLES ===
export interface AvailableRole {
value: UserRole;
label: string;
description: string;
allowedForCreation: boolean;
}
export interface AvailableRolesResponse {
roles: AvailableRole[];
}
export interface RoleOperationResponse {
message: string;
success: boolean;
}
// === SEARCH ===
export interface SearchUsersParams {
query?: string;
role?: UserRole;
enabled?: boolean;
userType?: UserType;
}

View File

@ -5,12 +5,18 @@ import { FormsModule } from '@angular/forms';
import { NgIcon } from '@ng-icons/core'; import { NgIcon } from '@ng-icons/core';
import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap';
import { Subject, takeUntil } from 'rxjs'; import { Subject, takeUntil } from 'rxjs';
import { HubUsersService, HubUserResponse, UserRole, UpdateHubUserDto } from '../services/users.service'; import { HubUsersService } from '../services/hub-users.service';
import { RoleManagementService } from '@core/services/role-management.service'; import { RoleManagementService } from '@core/services/role-management.service';
import { AuthService } from '@core/services/auth.service'; import { AuthService } from '@core/services/auth.service';
import {
HubUserDto,
UpdateUserDto,
UserRole
} from '@core/models/dcb-bo-hub-user.model';
@Component({ @Component({
selector: 'app-user-profile', selector: 'app-hub-user-profile',
standalone: true, standalone: true,
imports: [CommonModule, FormsModule, NgIcon, NgbAlertModule], imports: [CommonModule, FormsModule, NgIcon, NgbAlertModule],
templateUrl: './profile.html', templateUrl: './profile.html',
@ -24,7 +30,7 @@ import { AuthService } from '@core/services/auth.service';
} }
`] `]
}) })
export class UserProfile implements OnInit, OnDestroy { export class HubUserProfile implements OnInit, OnDestroy {
private usersService = inject(HubUsersService); private usersService = inject(HubUsersService);
private roleService = inject(RoleManagementService); private roleService = inject(RoleManagementService);
private authService = inject(AuthService); private authService = inject(AuthService);
@ -35,7 +41,7 @@ export class UserProfile implements OnInit, OnDestroy {
@Output() back = new EventEmitter<void>(); @Output() back = new EventEmitter<void>();
@Output() openResetPasswordModal = new EventEmitter<string>(); @Output() openResetPasswordModal = new EventEmitter<string>();
user: HubUserResponse | null = null; user: HubUserDto | null = null;
loading = false; loading = false;
saving = false; saving = false;
error = ''; error = '';
@ -49,7 +55,7 @@ export class UserProfile implements OnInit, OnDestroy {
// Édition // Édition
isEditing = false; isEditing = false;
editedUser: UpdateHubUserDto = {}; editedUser: UpdateUserDto = {};
// Gestion des rôles // Gestion des rôles
availableRoles: { value: UserRole; label: string; description: string }[] = []; availableRoles: { value: UserRole; label: string; description: string }[] = [];
@ -76,7 +82,7 @@ export class UserProfile implements OnInit, OnDestroy {
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (profile) => { next: (profile) => {
this.currentUserRole = profile?.roles?.[0] as UserRole || null; this.currentUserRole = profile?.role?.[0] as UserRole || null;
if (this.currentUserRole) { if (this.currentUserRole) {
this.canEditUsers = this.roleService.canEditUsers(this.currentUserRole); this.canEditUsers = this.roleService.canEditUsers(this.currentUserRole);
this.canManageRoles = this.roleService.canManageRoles(this.currentUserRole); this.canManageRoles = this.roleService.canManageRoles(this.currentUserRole);
@ -93,14 +99,18 @@ export class UserProfile implements OnInit, OnDestroy {
* Charge les rôles disponibles * Charge les rôles disponibles
*/ */
private loadAvailableRoles(): void { private loadAvailableRoles(): void {
this.roleService.getAvailableRolesSimple() this.usersService.getAvailableHubRoles()
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (roles) => { next: (response) => {
this.availableRoles = roles; this.availableRoles = response.roles.map(role => ({
value: role.value,
label: role.label,
description: role.description
}));
}, },
error: (error) => { error: (error) => {
console.error('Error loading available roles:', error); console.error('Error loading available hub roles:', error);
// Fallback // Fallback
this.availableRoles = [ this.availableRoles = [
{ value: UserRole.DCB_ADMIN, label: 'DCB Admin', description: 'Administrateur système' }, { value: UserRole.DCB_ADMIN, label: 'DCB Admin', description: 'Administrateur système' },
@ -115,7 +125,7 @@ export class UserProfile implements OnInit, OnDestroy {
this.loading = true; this.loading = true;
this.error = ''; this.error = '';
this.usersService.getUserById(this.userId) this.usersService.getHubUserById(this.userId)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (user) => { next: (user) => {
@ -124,10 +134,10 @@ export class UserProfile implements OnInit, OnDestroy {
this.cdRef.detectChanges(); this.cdRef.detectChanges();
}, },
error: (error) => { error: (error) => {
this.error = 'Erreur lors du chargement du profil utilisateur'; this.error = 'Erreur lors du chargement du profil utilisateur Hub';
this.loading = false; this.loading = false;
this.cdRef.detectChanges(); this.cdRef.detectChanges();
console.error('Error loading user profile:', error); console.error('Error loading hub user profile:', error);
} }
}); });
} }
@ -163,7 +173,7 @@ export class UserProfile implements OnInit, OnDestroy {
this.error = ''; this.error = '';
this.success = ''; this.success = '';
this.usersService.updateUser(this.user.id, this.editedUser) this.usersService.updateHubUser(this.user.id, this.editedUser)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (updatedUser) => { next: (updatedUser) => {
@ -192,11 +202,18 @@ export class UserProfile implements OnInit, OnDestroy {
return; return;
} }
// Vérifier que c'est un rôle Hub valide
const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER];
if (!hubRoles.includes(newRole)) {
this.error = 'Rôle Hub invalide';
return;
}
this.updatingRoles = true; this.updatingRoles = true;
this.error = ''; this.error = '';
this.success = ''; this.success = '';
this.usersService.updateUserRole(this.user.id, newRole) this.usersService.updateHubUserRole(this.user.id, newRole)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (updatedUser) => { next: (updatedUser) => {
@ -217,12 +234,12 @@ export class UserProfile implements OnInit, OnDestroy {
enableUser() { enableUser() {
if (!this.user || !this.canEditUsers) return; if (!this.user || !this.canEditUsers) return;
this.usersService.enableUser(this.user.id) this.usersService.enableHubUser(this.user.id)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (updatedUser) => { next: (updatedUser) => {
this.user = updatedUser; this.user = updatedUser;
this.success = 'Utilisateur activé avec succès'; this.success = 'Utilisateur Hub activé avec succès';
this.cdRef.detectChanges(); this.cdRef.detectChanges();
}, },
error: (error) => { error: (error) => {
@ -235,12 +252,12 @@ export class UserProfile implements OnInit, OnDestroy {
disableUser() { disableUser() {
if (!this.user || !this.canEditUsers) return; if (!this.user || !this.canEditUsers) return;
this.usersService.disableUser(this.user.id) this.usersService.disableHubUser(this.user.id)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (updatedUser) => { next: (updatedUser) => {
this.user = updatedUser; this.user = updatedUser;
this.success = 'Utilisateur désactivé avec succès'; this.success = 'Utilisateur Hub désactivé avec succès';
this.cdRef.detectChanges(); this.cdRef.detectChanges();
}, },
error: (error) => { error: (error) => {
@ -266,7 +283,7 @@ export class UserProfile implements OnInit, OnDestroy {
return 'Vous n\'avez pas les permissions nécessaires pour cette action'; return 'Vous n\'avez pas les permissions nécessaires pour cette action';
} }
if (error.status === 404) { if (error.status === 404) {
return 'Utilisateur non trouvé'; return 'Utilisateur Hub non trouvé';
} }
if (error.status === 400) { if (error.status === 400) {
return 'Données invalides'; return 'Données invalides';
@ -306,7 +323,7 @@ export class UserProfile implements OnInit, OnDestroy {
} }
getUserDisplayName(): string { getUserDisplayName(): string {
if (!this.user) return 'Utilisateur'; if (!this.user) return 'Utilisateur Hub';
if (this.user.firstName && this.user.lastName) { if (this.user.firstName && this.user.lastName) {
return `${this.user.firstName} ${this.user.lastName}`; return `${this.user.firstName} ${this.user.lastName}`;
} }
@ -337,7 +354,25 @@ export class UserProfile implements OnInit, OnDestroy {
// Vérifie si c'est le profil de l'utilisateur courant // Vérifie si c'est le profil de l'utilisateur courant
isCurrentUserProfile(): boolean { isCurrentUserProfile(): boolean {
// Implémentez cette logique selon votre système d'authentification if (!this.user || !this.user.id) return false;
return false; return this.authService.isCurrentUserProfile(this.user.id);
}
// Vérifier les permissions
canEditUser(): boolean {
if (this.isCurrentUserProfile()) return true; // Toujours éditer son profil
return this.authService.canManageHubUsers();
}
// Méthode pour obtenir les rôles Hub assignables
getAssignableHubRoles(): UserRole[] {
const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER];
return hubRoles.filter(role => this.canAssignRole(role));
}
// Vérifie si un rôle est un rôle Hub
isHubRole(role: UserRole): boolean {
const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER];
return hubRoles.includes(role);
} }
} }

View File

@ -0,0 +1,361 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from '@environments/environment';
import { Observable, map, catchError, throwError, of } from 'rxjs';
import {
HubUserDto,
CreateUserDto,
UpdateUserDto,
ResetPasswordDto,
PaginatedUserResponse,
AvailableRolesResponse,
SearchUsersParams,
UserRole,
UserType,
MerchantUserDto
} from '@core/models/dcb-bo-hub-user.model';
@Injectable({ providedIn: 'root' })
export class HubUsersService {
private http = inject(HttpClient);
private apiUrl = `${environment.iamApiUrl}/hub-users`;
// === CRÉATION ===
/**
* Crée un nouvel utilisateur Hub
*/
createHubUser(createUserDto: CreateUserDto): Observable<HubUserDto> {
// Validation pour les utilisateurs Hub
if (!createUserDto.username?.trim()) {
return throwError(() => 'Username is required and cannot be empty');
}
if (!createUserDto.email?.trim()) {
return throwError(() => 'Email is required and cannot be empty');
}
if (!createUserDto.password || createUserDto.password.length < 8) {
return throwError(() => 'Password must be at least 8 characters');
}
if (!createUserDto.role) {
return throwError(() => 'Role is required');
}
// Vérification que le rôle est bien un rôle Hub
const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER];
if (!hubRoles.includes(createUserDto.role)) {
return throwError(() => 'Invalid role for Hub user');
}
// Nettoyage des données
const payload = {
...createUserDto,
username: createUserDto.username.trim(),
email: createUserDto.email.trim(),
firstName: (createUserDto.firstName || '').trim(),
lastName: (createUserDto.lastName || '').trim(),
enabled: createUserDto.enabled !== undefined ? createUserDto.enabled : true,
emailVerified: createUserDto.emailVerified !== undefined ? createUserDto.emailVerified : false,
};
return this.http.post<HubUserDto>(this.apiUrl, payload).pipe(
catchError(error => {
console.error('Error creating hub user:', error);
return throwError(() => error);
})
);
}
// === LECTURE ===
/**
* Récupère tous les utilisateurs Hub avec pagination
*/
getHubUsers(page: number = 1, limit: number = 10, filters?: SearchUsersParams): Observable<PaginatedUserResponse> {
let params = new HttpParams()
.set('page', page.toString())
.set('limit', limit.toString())
.set('userType', UserType.HUB);
if (filters) {
Object.keys(filters).forEach(key => {
if (filters[key as keyof SearchUsersParams] !== undefined && filters[key as keyof SearchUsersParams] !== null) {
params = params.set(key, filters[key as keyof SearchUsersParams]!.toString());
}
});
}
return this.http.get<HubUserDto[]>(this.apiUrl, { params, observe: 'response' }).pipe(
map(response => {
const users = response.body || [];
const total = parseInt(response.headers.get('X-Total-Count') || '0');
return {
users,
total,
page,
limit,
totalPages: Math.ceil(total / limit)
};
}),
catchError(error => {
console.error('Error loading hub users:', error);
return throwError(() => error);
})
);
}
/**
* Get all merchant partners
*/
/**
* Récupère tous les utilisateurs Hub avec pagination
*/
findAllMerchantUsers(page: number = 1, limit: number = 10, filters?: any): Observable<PaginatedUserResponse> {
let params = new HttpParams()
.set('page', page.toString())
.set('limit', limit.toString());
if (filters) {
Object.keys(filters).forEach(key => {
if (filters[key] !== undefined && filters[key] !== null) {
params = params.set(key, filters[key].toString());
}
});
}
return this.http.get<MerchantUserDto[]>(`${this.apiUrl}/merchants/all`, { params, observe: 'response' }).pipe(
map(response => {
const users = response.body || [];
const total = parseInt(response.headers.get('X-Total-Count') || '0');
return {
users,
total,
page,
limit,
totalPages: Math.ceil(total / limit)
};
}),
catchError(error => {
console.error('Error loading users:', error);
return throwError(() => error);
})
);
}
/**
* Récupère un utilisateur Hub par ID
*/
getHubUserById(id: string): Observable<HubUserDto> {
return this.http.get<HubUserDto>(`${this.apiUrl}/${id}`).pipe(
catchError(error => {
console.error(`Error loading hub user ${id}:`, error);
return throwError(() => error);
})
);
}
// === MISE À JOUR ===
/**
* Met à jour un utilisateur Hub
*/
updateHubUser(id: string, updateUserDto: UpdateUserDto): Observable<HubUserDto> {
return this.http.put<HubUserDto>(`${this.apiUrl}/${id}`, updateUserDto).pipe(
catchError(error => {
console.error(`Error updating hub user ${id}:`, error);
return throwError(() => error);
})
);
}
/**
* Met à jour le rôle d'un utilisateur Hub
*/
updateHubUserRole(id: string, role: UserRole): Observable<HubUserDto> {
const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER];
if (!hubRoles.includes(role)) {
return throwError(() => 'Invalid role for Hub user');
}
return this.http.put<HubUserDto>(`${this.apiUrl}/${id}/role`, { role }).pipe(
catchError(error => {
console.error(`Error updating role for hub user ${id}:`, error);
return throwError(() => error);
})
);
}
// === SUPPRESSION ===
/**
* Supprime un utilisateur Hub
*/
deleteHubUser(id: string): Observable<{ message: string }> {
return this.http.delete<{ message: string }>(`${this.apiUrl}/${id}`).pipe(
catchError(error => {
console.error(`Error deleting hub user ${id}:`, error);
return throwError(() => error);
})
);
}
// === GESTION DES MOTS DE PASSE ===
/**
* Réinitialise le mot de passe d'un utilisateur Hub
*/
resetHubUserPassword(id: string, resetPasswordDto: ResetPasswordDto): Observable<{ message: string }> {
return this.http.post<{ message: string }>(
`${this.apiUrl}/${id}/reset-password`,
resetPasswordDto
).pipe(
catchError(error => {
console.error(`Error resetting password for hub user ${id}:`, error);
return throwError(() => error);
})
);
}
/**
* Envoie un email de réinitialisation de mot de passe
*/
sendHubUserPasswordResetEmail(id: string): Observable<{ message: string }> {
return this.http.post<{ message: string }>(
`${this.apiUrl}/${id}/send-reset-email`,
{}
).pipe(
catchError(error => {
console.error(`Error sending reset email for hub user ${id}:`, error);
return throwError(() => error);
})
);
}
// === GESTION DU STATUT ===
/**
* Active un utilisateur Hub
*/
enableHubUser(id: string): Observable<HubUserDto> {
return this.updateHubUser(id, { enabled: true });
}
/**
* Désactive un utilisateur Hub
*/
disableHubUser(id: string): Observable<HubUserDto> {
return this.updateHubUser(id, { enabled: false });
}
// === GESTION DES RÔLES ===
/**
* Récupère les rôles Hub disponibles
*/
getAvailableHubRoles(): Observable<AvailableRolesResponse> {
return this.http.get<AvailableRolesResponse>(`${this.apiUrl}/roles/available`).pipe(
catchError(error => {
console.error('Error loading available hub roles:', error);
// Fallback en cas d'erreur
return of({
roles: [
{
value: UserRole.DCB_ADMIN,
label: 'DCB Admin',
description: 'Full administrative access to the entire system',
allowedForCreation: true
},
{
value: UserRole.DCB_SUPPORT,
label: 'DCB Support',
description: 'Support access with limited administrative capabilities',
allowedForCreation: true
},
{
value: UserRole.DCB_PARTNER,
label: 'DCB Partner',
description: 'Partner access to merchant management',
allowedForCreation: true
}
]
});
})
);
}
/**
* Récupère les utilisateurs par rôle spécifique
*/
getHubUsersByRole(role: UserRole): Observable<HubUserDto[]> {
return this.http.get<HubUserDto[]>(`${this.apiUrl}/role/${role}`).pipe(
catchError(error => {
console.error(`Error loading hub users with role ${role}:`, error);
return throwError(() => error);
})
);
}
// === RECHERCHE ===
/**
* Recherche des utilisateurs Hub
*/
searchHubUsers(params: SearchUsersParams): Observable<HubUserDto[]> {
let httpParams = new HttpParams().set('userType', UserType.HUB);
if (params.query) {
httpParams = httpParams.set('query', params.query);
}
if (params.role) {
httpParams = httpParams.set('role', params.role);
}
if (params.enabled !== undefined) {
httpParams = httpParams.set('enabled', params.enabled.toString());
}
return this.http.get<HubUserDto[]>(`${this.apiUrl}/search`, { params: httpParams }).pipe(
catchError(error => {
console.error('Error searching hub users:', error);
return throwError(() => error);
})
);
}
// === STATISTIQUES ===
/**
* Récupère les statistiques des utilisateurs Hub
*/
getHubUsersStats(): Observable<any> {
return this.http.get<any>(`${this.apiUrl}/stats/overview`).pipe(
catchError(error => {
console.error('Error loading hub users stats:', error);
return throwError(() => error);
})
);
}
// === UTILITAIRES ===
/**
* Vérifie si un nom d'utilisateur existe parmi les utilisateurs Hub
*/
hubUserExists(username: string): Observable<{ exists: boolean }> {
return this.searchHubUsers({ query: username }).pipe(
map(users => ({
exists: users.some(user => user.username === username)
})),
catchError(error => {
console.error('Error checking if hub user exists:', error);
return of({ exists: false });
})
);
}
}

View File

@ -1,2 +0,0 @@
import { MerchantPartners } from './merchant-partners';
describe('Merchant Partners', () => {});

View File

@ -5,14 +5,17 @@ import { FormsModule } from '@angular/forms';
import { NgIcon } from '@ng-icons/core'; import { NgIcon } from '@ng-icons/core';
import { NgbPaginationModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbPaginationModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
import { catchError, map, of, Subject, takeUntil } from 'rxjs'; import { catchError, map, of, Subject, takeUntil } from 'rxjs';
import { import {
MerchantUsersService, MerchantUserDto,
MerchantUserResponse, PaginatedUserResponse,
SearchUsersParams,
UserRole, UserRole,
} from '../services/merchant-partners.service'; UserType
} from '@core/models/dcb-bo-hub-user.model';
import { HubUsersService, UserRole as HubUserRole } from '../../users/services/users.service';
import { MerchantUsersService } from '../services/merchant-users.service';
import { HubUsersService } from '../../hub-users/services/hub-users.service';
import { AuthService } from '@core/services/auth.service'; import { AuthService } from '@core/services/auth.service';
import { UiCard } from '@app/components/ui-card'; import { UiCard } from '@app/components/ui-card';
@ -37,7 +40,7 @@ export class MerchantUsersList implements OnInit, OnDestroy {
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
readonly UserRole = UserRole; readonly UserRole = UserRole;
readonly HubUserRole = HubUserRole; readonly UserType = UserType;
@Output() userSelected = new EventEmitter<string>(); @Output() userSelected = new EventEmitter<string>();
@Output() openCreateModal = new EventEmitter<void>(); @Output() openCreateModal = new EventEmitter<void>();
@ -45,9 +48,9 @@ export class MerchantUsersList implements OnInit, OnDestroy {
@Output() openDeleteUserModal = new EventEmitter<string>(); @Output() openDeleteUserModal = new EventEmitter<string>();
// Données // Données
allUsers: MerchantUserResponse[] = []; allUsers: MerchantUserDto[] = [];
filteredUsers: MerchantUserResponse[] = []; filteredUsers: MerchantUserDto[] = [];
displayedUsers: MerchantUserResponse[] = []; displayedUsers: MerchantUserDto[] = [];
// États // États
loading = false; loading = false;
@ -66,7 +69,7 @@ export class MerchantUsersList implements OnInit, OnDestroy {
totalPages = 0; totalPages = 0;
// Tri // Tri
sortField: keyof MerchantUserResponse = 'username'; sortField: keyof MerchantUserDto = 'username';
sortDirection: 'asc' | 'desc' = 'asc'; sortDirection: 'asc' | 'desc' = 'asc';
// Rôles disponibles pour le filtre // Rôles disponibles pour le filtre
@ -79,7 +82,7 @@ export class MerchantUsersList implements OnInit, OnDestroy {
// ID du merchant partner courant et permissions // ID du merchant partner courant et permissions
currentMerchantPartnerId: string = ''; currentMerchantPartnerId: string = '';
currentUserRole: HubUserRole | null = null; currentUserRole: UserRole | null = null;
isHubAdminOrSupport = false; isHubAdminOrSupport = false;
canViewAllMerchants = false; canViewAllMerchants = false;
isDcbPartner = false; isDcbPartner = false;
@ -99,130 +102,234 @@ export class MerchantUsersList implements OnInit, OnDestroy {
// Méthode robuste pour récupérer le rôle // Méthode robuste pour récupérer le rôle
this.currentUserRole = this.extractUserRole(user); this.currentUserRole = this.extractUserRole(user);
// Déterminer le type d'utilisateur // Déterminer le type d'utilisateur avec des méthodes plus robustes
this.isHubAdminOrSupport = this.currentUserRole === HubUserRole.DCB_ADMIN || this.isHubAdminOrSupport = this.isHubAdminOrSupportRole(this.currentUserRole);
this.currentUserRole === HubUserRole.DCB_SUPPORT; this.isDcbPartner = this.isDcbPartnerRole(this.currentUserRole);
this.isDcbPartner = this.currentUserRole === HubUserRole.DCB_PARTNER; this.canViewAllMerchants = this.canViewAllMerchantsCheck(this.currentUserRole);
this.canViewAllMerchants = this.isHubAdminOrSupport;
// Déterminer le merchantPartnerId // Déterminer le merchantPartnerId
if(this.isDcbPartner){
this.currentMerchantPartnerId = user.id;
}
else if(!this.isDcbPartner || !this.isHubAdminOrSupport) {
this.currentMerchantPartnerId = this.extractMerchantPartnerId(user); this.currentMerchantPartnerId = this.extractMerchantPartnerId(user);
}
console.log('🎯 Final Permissions:', {
currentUserRole: this.currentUserRole,
isHubAdminOrSupport: this.isHubAdminOrSupport,
isDcbPartner: this.isDcbPartner,
canViewAllMerchants: this.canViewAllMerchants,
currentMerchantPartnerId: this.currentMerchantPartnerId
});
this.loadUsers(); this.loadUsers();
}, },
error: (error) => { error: (error) => {
console.error('❌ Error loading current user permissions:', error); console.error('❌ Error loading current user permissions:', error);
// Fallback: utiliser les méthodes d'AuthService
this.fallbackPermissions();
this.loadUsers(); // Charger quand même les utilisateurs this.loadUsers(); // Charger quand même les utilisateurs
} }
}); });
} }
/** /**
* Extrait le rôle de l'utilisateur de manière robuste * Méthode robuste pour extraire le rôle de l'utilisateur
*/ */
private extractUserRole(user: any): HubUserRole | null { private extractUserRole(user: any): UserRole | null {
// Essayer différentes sources possibles pour le rôle console.log('🔍 Extracting user role from:', user);
if (user.roles && user.roles.length > 0) {
return user.roles[0] as HubUserRole; // 1. Essayer depuis les rôles du token (méthode principale)
const tokenRoles = this.authService.getCurrentUserRoles();
if (tokenRoles && tokenRoles.length > 0) {
console.log('✅ Role from token:', tokenRoles[0]);
return tokenRoles[0];
} }
if (user.role) { // 2. Essayer depuis le profil user.role
return user.role as HubUserRole; if (user?.role && Object.values(UserRole).includes(user.role)) {
console.log('✅ Role from user.role:', user.role);
return user.role as UserRole;
} }
console.warn('No role found in user profile'); // 3. Essayer depuis user.userType pour déduire le rôle
if (user?.userType) {
const roleFromType = this.getRoleFromUserType(user.userType);
if (roleFromType) {
console.log('✅ Role deduced from userType:', roleFromType);
return roleFromType;
}
}
// 4. Essayer depuis les attributs étendus
if (user?.attributes?.role?.[0]) {
const roleFromAttributes = user.attributes.role[0];
if (Object.values(UserRole).includes(roleFromAttributes as UserRole)) {
console.log('✅ Role from attributes:', roleFromAttributes);
return roleFromAttributes as UserRole;
}
}
console.warn('❌ No valid role found in user profile');
return null; return null;
} }
/** /**
* Extrait le merchantPartnerId de manière robuste * Déduire le rôle à partir du userType
*/ */
private extractMerchantPartnerId(user: any): string { private getRoleFromUserType(userType: string): UserRole | null {
if (this.isDcbPartner) { const typeMapping: { [key: string]: UserRole } = {
// Pour DCB_PARTNER, utiliser son ID comme merchantPartnerId [UserType.HUB]: UserRole.DCB_ADMIN, // Fallback pour HUB
return user.id || ''; [UserType.MERCHANT]: UserRole.DCB_PARTNER_ADMIN, // Fallback pour MERCHANT
};
return typeMapping[userType] || null;
} }
// Pour les autres, chercher le merchantPartnerId dans différentes sources /**
if (user.merchantPartnerId) { * Vérifier si l'utilisateur est Hub Admin ou Support
*/
private isHubAdminOrSupportRole(role: UserRole | null): boolean {
if (!role) return false;
const hubAdminSupportRoles = [
UserRole.DCB_ADMIN,
UserRole.DCB_SUPPORT,
UserRole.DCB_PARTNER // DCB_PARTNER peut aussi être considéré comme Hub
];
return hubAdminSupportRoles.includes(role);
}
/**
* Vérifier si l'utilisateur est DCB Partner
*/
private isDcbPartnerRole(role: UserRole | null): boolean {
if (!role) return false;
const partnerRoles = [
UserRole.DCB_PARTNER,
UserRole.DCB_PARTNER_ADMIN,
UserRole.DCB_PARTNER_MANAGER,
UserRole.DCB_PARTNER_SUPPORT
];
return partnerRoles.includes(role);
}
/**
* Vérifier si l'utilisateur peut voir tous les merchants
*/
private canViewAllMerchantsCheck(role: UserRole | null): boolean {
if (!role) return false;
const canViewAllRoles = [
UserRole.DCB_ADMIN,
UserRole.DCB_SUPPORT
];
return canViewAllRoles.includes(role);
}
/**
* Extraire le merchantPartnerId de manière robuste
*/
private extractMerchantPartnerId(user: any): string {
console.log('🔍 Extracting merchantPartnerId from:', user);
// 1. Essayer depuis merchantPartnerId direct
if (user?.merchantPartnerId) {
console.log('✅ merchantPartnerId from direct property:', user.merchantPartnerId);
return user.merchantPartnerId; return user.merchantPartnerId;
} }
if (user.attributes?.merchantPartnerId?.[0]) { // 2. Essayer depuis les attributs
if (user?.attributes?.merchantPartnerId?.[0]) {
console.log('✅ merchantPartnerId from attributes:', user.attributes.merchantPartnerId[0]);
return user.attributes.merchantPartnerId[0]; return user.attributes.merchantPartnerId[0];
} }
if (user.attributes?.partnerId?.[0]) { // 3. Essayer depuis AuthService
return user.attributes.partnerId[0]; const authServiceMerchantId = this.authService.getCurrentMerchantPartnerId();
if (authServiceMerchantId) {
console.log('✅ merchantPartnerId from AuthService:', authServiceMerchantId);
return authServiceMerchantId;
} }
console.warn('No merchantPartnerId found in user profile'); // 4. Pour les rôles Hub, pas de merchantPartnerId
if (this.isHubAdminOrSupport) {
console.log(' Hub user - no merchantPartnerId');
return ''; return '';
} }
console.warn('❌ No merchantPartnerId found');
return '';
}
/**
* Fallback en cas d'erreur de chargement du profil
*/
private fallbackPermissions(): void {
console.warn('🔄 Using fallback permissions');
// Utiliser les méthodes d'AuthService comme fallback
this.currentUserRole = this.authService.getCurrentUserRole();
this.isHubAdminOrSupport = this.authService.isHubUser() ||
this.authService.hasRole(UserRole.DCB_ADMIN) ||
this.authService.hasRole(UserRole.DCB_SUPPORT);
this.isDcbPartner = this.authService.isMerchantUser() ||
this.authService.hasRole(UserRole.DCB_PARTNER);
this.canViewAllMerchants = this.authService.canViewAllMerchants();
this.currentMerchantPartnerId = this.authService.getCurrentMerchantPartnerId() || '';
console.log('🔄 Fallback permissions:', {
currentUserRole: this.currentUserRole,
isHubAdminOrSupport: this.isHubAdminOrSupport,
isDcbPartner: this.isDcbPartner,
canViewAllMerchants: this.canViewAllMerchants,
currentMerchantPartnerId: this.currentMerchantPartnerId
});
}
loadUsers() { loadUsers() {
this.loading = true; this.loading = true;
this.error = ''; this.error = '';
console.log('🚀 Loading users with permissions:', {
canViewAllMerchants: this.canViewAllMerchants,
currentMerchantPartnerId: this.currentMerchantPartnerId,
currentUserRole: this.currentUserRole
});
let usersObservable; let usersObservable;
if (this.canViewAllMerchants) { if (this.canViewAllMerchants && !this.currentMerchantPartnerId) {
// Admin/Support DCB : charger tous les utilisateurs via HubUsersService console.log('🔍 Loading ALL merchant users (Hub Admin/Support)');
console.log('📊 Loading ALL merchant users (DCB Admin view)'); usersObservable = this.hubUsersService.findAllMerchantUsers(this.currentPage, this.itemsPerPage).pipe(
map((response: PaginatedUserResponse) => {
usersObservable = this.hubUsersService.findAllMerchantUsers(1, 1000).pipe( console.log('✅ All merchant users loaded:', response.users.length);
map((response: any) => { return response.users as MerchantUserDto[];
console.log('📦 Hub Users API Response:', response);
// Adapter selon la structure de votre API
if (response && response.users) {
return response.users;
} else if (Array.isArray(response)) {
return response;
}
return [];
}), }),
catchError(error => { catchError(error => {
console.error('❌ Error loading hub users:', error); console.error('❌ Error loading all merchant users:', error);
this.error = 'Erreur lors du chargement de tous les utilisateurs marchands';
return of([]); return of([]);
}) })
); );
} else if (this.currentMerchantPartnerId) { } else if (this.currentMerchantPartnerId) {
// Utilisateur marchand (DCB_PARTNER) : charger seulement ses utilisateurs console.log(`🔍 Loading merchant users for partner: ${this.currentMerchantPartnerId}`);
console.log('🏢 Loading merchant users for partner:', this.currentMerchantPartnerId); usersObservable = this.merchantUsersService.getMerchantUsersByPartner(this.currentMerchantPartnerId).pipe(
usersObservable = this.merchantUsersService.getMyMerchantUsers().pipe(
catchError(error => { catchError(error => {
console.error('❌ Error loading merchant users:', error); console.error('❌ Error loading merchant users by partner:', error);
this.error = 'Erreur lors du chargement des utilisateurs du partenaire';
return of([]); return of([]);
}) })
); );
} else { } else {
this.error = 'Impossible de déterminer les permissions de chargement'; console.log('🔍 Loading my merchant users');
this.loading = false; usersObservable = this.merchantUsersService.getMyMerchantUsers(this.currentMerchantPartnerId).pipe(
console.error('❌ No valid permission scenario'); catchError(error => {
return; console.error('❌ Error loading my merchant users:', error);
this.error = 'Erreur lors du chargement de mes utilisateurs marchands';
return of([]);
})
);
} }
usersObservable usersObservable
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (users: any[]) => { next: (users: MerchantUserDto[]) => {
this.allUsers = users || []; this.allUsers = users || [];
console.log(`✅ Loaded ${this.allUsers.length} merchant users`);
this.applyFiltersAndPagination(); this.applyFiltersAndPagination();
this.loading = false; this.loading = false;
@ -262,6 +369,8 @@ export class MerchantUsersList implements OnInit, OnDestroy {
this.allUsers = []; this.allUsers = [];
} }
console.log(`🔍 Applying filters to ${this.allUsers.length} users`);
// Appliquer les filtres // Appliquer les filtres
this.filteredUsers = this.allUsers.filter(user => { this.filteredUsers = this.allUsers.filter(user => {
// Filtre de recherche // Filtre de recherche
@ -287,6 +396,8 @@ export class MerchantUsersList implements OnInit, OnDestroy {
return matchesSearch && matchesStatus && matchesEmailVerified && matchesRole; return matchesSearch && matchesStatus && matchesEmailVerified && matchesRole;
}); });
console.log(`✅ Filtered to ${this.filteredUsers.length} users`);
// Appliquer le tri // Appliquer le tri
this.filteredUsers.sort((a, b) => { this.filteredUsers.sort((a, b) => {
const aValue = a[this.sortField]; const aValue = a[this.sortField];
@ -314,26 +425,30 @@ export class MerchantUsersList implements OnInit, OnDestroy {
const startIndex = (this.currentPage - 1) * this.itemsPerPage; const startIndex = (this.currentPage - 1) * this.itemsPerPage;
const endIndex = startIndex + this.itemsPerPage; const endIndex = startIndex + this.itemsPerPage;
this.displayedUsers = this.filteredUsers.slice(startIndex, endIndex); this.displayedUsers = this.filteredUsers.slice(startIndex, endIndex);
console.log(`📄 Pagination: page ${this.currentPage} of ${this.totalPages}, showing ${this.displayedUsers.length} users`);
} }
// Tri // Tri
sort(field: keyof MerchantUserResponse) { sort(field: keyof MerchantUserDto) {
if (this.sortField === field) { if (this.sortField === field) {
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
} else { } else {
this.sortField = field; this.sortField = field;
this.sortDirection = 'asc'; this.sortDirection = 'asc';
} }
console.log(`🔀 Sorting by ${field} (${this.sortDirection})`);
this.applyFiltersAndPagination(); this.applyFiltersAndPagination();
} }
getSortIcon(field: keyof MerchantUserResponse): string { getSortIcon(field: keyof MerchantUserDto): string {
if (this.sortField !== field) return 'lucideArrowUpDown'; if (this.sortField !== field) return 'lucideArrowUpDown';
return this.sortDirection === 'asc' ? 'lucideArrowUp' : 'lucideArrowDown'; return this.sortDirection === 'asc' ? 'lucideArrowUp' : 'lucideArrowDown';
} }
// Pagination // Pagination
onPageChange(page: number) { onPageChange(page: number) {
console.log(`📄 Changing to page ${page}`);
this.currentPage = page; this.currentPage = page;
this.applyFiltersAndPagination(); this.applyFiltersAndPagination();
} }
@ -348,31 +463,40 @@ export class MerchantUsersList implements OnInit, OnDestroy {
// Actions // Actions
viewUserProfile(userId: string) { viewUserProfile(userId: string) {
console.log(`👤 Viewing user profile: ${userId}`);
this.userSelected.emit(userId); this.userSelected.emit(userId);
} }
// Méthode pour réinitialiser le mot de passe // Méthode pour réinitialiser le mot de passe
resetPassword(user: MerchantUserResponse) { resetPassword(user: MerchantUserDto) {
console.log(`🔑 Resetting password for user: ${user.username}`);
this.openResetPasswordModal.emit(user.id); this.openResetPasswordModal.emit(user.id);
} }
// Méthode pour ouvrir le modal de suppression // Méthode pour ouvrir le modal de suppression
deleteUser(user: MerchantUserResponse) { deleteUser(user: MerchantUserDto) {
console.log(`🗑️ Deleting user: ${user.username}`);
this.openDeleteUserModal.emit(user.id); this.openDeleteUserModal.emit(user.id);
} }
// Activer un utilisateur // Activer un utilisateur
enableUser(user: MerchantUserResponse) { enableUser(user: MerchantUserDto) {
console.log(`✅ Enabling user: ${user.username}`);
this.merchantUsersService.enableMerchantUser(user.id) this.merchantUsersService.enableMerchantUser(user.id)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (updatedUser) => { next: (updatedUser) => {
user.enabled = updatedUser.enabled; console.log(`✅ User ${user.username} enabled successfully`);
// Mettre à jour l'utilisateur dans la liste
const index = this.allUsers.findIndex(u => u.id === user.id);
if (index !== -1) {
this.allUsers[index] = updatedUser;
}
this.applyFiltersAndPagination(); this.applyFiltersAndPagination();
this.cdRef.detectChanges(); this.cdRef.detectChanges();
}, },
error: (error) => { error: (error) => {
console.error('Error enabling merchant user:', error); console.error('Error enabling merchant user:', error);
this.error = 'Erreur lors de l\'activation de l\'utilisateur'; this.error = 'Erreur lors de l\'activation de l\'utilisateur';
this.cdRef.detectChanges(); this.cdRef.detectChanges();
} }
@ -380,17 +504,23 @@ export class MerchantUsersList implements OnInit, OnDestroy {
} }
// Désactiver un utilisateur // Désactiver un utilisateur
disableUser(user: MerchantUserResponse) { disableUser(user: MerchantUserDto) {
console.log(`❌ Disabling user: ${user.username}`);
this.merchantUsersService.disableMerchantUser(user.id) this.merchantUsersService.disableMerchantUser(user.id)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (updatedUser) => { next: (updatedUser) => {
user.enabled = updatedUser.enabled; console.log(`✅ User ${user.username} disabled successfully`);
// Mettre à jour l'utilisateur dans la liste
const index = this.allUsers.findIndex(u => u.id === user.id);
if (index !== -1) {
this.allUsers[index] = updatedUser;
}
this.applyFiltersAndPagination(); this.applyFiltersAndPagination();
this.cdRef.detectChanges(); this.cdRef.detectChanges();
}, },
error: (error) => { error: (error) => {
console.error('Error disabling merchant user:', error); console.error('Error disabling merchant user:', error);
this.error = 'Erreur lors de la désactivation de l\'utilisateur'; this.error = 'Erreur lors de la désactivation de l\'utilisateur';
this.cdRef.detectChanges(); this.cdRef.detectChanges();
} }
@ -399,13 +529,13 @@ export class MerchantUsersList implements OnInit, OnDestroy {
// ==================== UTILITAIRES D'AFFICHAGE ==================== // ==================== UTILITAIRES D'AFFICHAGE ====================
getStatusBadgeClass(user: MerchantUserResponse): string { getStatusBadgeClass(user: MerchantUserDto): string {
if (!user.enabled) return 'badge bg-danger'; if (!user.enabled) return 'badge bg-danger';
if (!user.emailVerified) return 'badge bg-warning'; if (!user.emailVerified) return 'badge bg-warning';
return 'badge bg-success'; return 'badge bg-success';
} }
getStatusText(user: MerchantUserResponse): string { getStatusText(user: MerchantUserDto): string {
if (!user.enabled) return 'Désactivé'; if (!user.enabled) return 'Désactivé';
if (!user.emailVerified) return 'Email non vérifié'; if (!user.emailVerified) return 'Email non vérifié';
return 'Actif'; return 'Actif';
@ -426,9 +556,12 @@ export class MerchantUsersList implements OnInit, OnDestroy {
getRoleDisplayName(role: UserRole): string { getRoleDisplayName(role: UserRole): string {
const roleNames = { const roleNames = {
[UserRole.DCB_PARTNER_ADMIN]: 'Administrateur', [UserRole.DCB_ADMIN]: 'Administrateur',
[UserRole.DCB_PARTNER_MANAGER]: 'Manager', [UserRole.DCB_PARTNER]: 'Manager',
[UserRole.DCB_PARTNER_SUPPORT]: 'Support' [UserRole.DCB_SUPPORT]: 'Support',
[UserRole.DCB_PARTNER_ADMIN]: 'Admin Marchand',
[UserRole.DCB_PARTNER_MANAGER]: 'Manager Marchand',
[UserRole.DCB_PARTNER_SUPPORT]: 'Support Marchand'
}; };
return roleNames[role] || role; return roleNames[role] || role;
} }
@ -457,11 +590,11 @@ export class MerchantUsersList implements OnInit, OnDestroy {
}); });
} }
getUserInitials(user: MerchantUserResponse): string { getUserInitials(user: MerchantUserDto): string {
return (user.firstName?.charAt(0) || '') + (user.lastName?.charAt(0) || '') || 'U'; return (user.firstName?.charAt(0) || '') + (user.lastName?.charAt(0) || '') || 'U';
} }
getUserDisplayName(user: MerchantUserResponse): string { getUserDisplayName(user: MerchantUserDto): string {
if (user.firstName && user.lastName) { if (user.firstName && user.lastName) {
return `${user.firstName} ${user.lastName}`; return `${user.firstName} ${user.lastName}`;
} }
@ -490,35 +623,21 @@ export class MerchantUsersList implements OnInit, OnDestroy {
return this.allUsers.length; return this.allUsers.length;
} }
// ==================== VÉRIFICATIONS DE PERMISSIONS ====================
canManageUser(user: MerchantUserResponse): boolean {
// Logique pour déterminer si l'utilisateur connecté peut gérer cet utilisateur
// À adapter selon votre logique métier
return true;
}
canDeleteUser(user: MerchantUserResponse): boolean {
// Empêcher la suppression de soi-même
// À adapter avec l'ID de l'utilisateur connecté
return user.id !== 'current-user-id';
}
// ==================== MÉTHODES UTILITAIRES ==================== // ==================== MÉTHODES UTILITAIRES ====================
hasRole(user: MerchantUserResponse, role: UserRole): boolean { hasRole(user: MerchantUserDto, role: UserRole): boolean {
return user.role === role; return user.role === role;
} }
isAdmin(user: MerchantUserResponse): boolean { isAdmin(user: MerchantUserDto): boolean {
return this.hasRole(user, UserRole.DCB_PARTNER_ADMIN); return this.hasRole(user, UserRole.DCB_PARTNER_ADMIN);
} }
isManager(user: MerchantUserResponse): boolean { isManager(user: MerchantUserDto): boolean {
return this.hasRole(user, UserRole.DCB_PARTNER_MANAGER); return this.hasRole(user, UserRole.DCB_PARTNER_MANAGER);
} }
isSupport(user: MerchantUserResponse): boolean { isSupport(user: MerchantUserDto): boolean {
return this.hasRole(user, UserRole.DCB_PARTNER_SUPPORT); return this.hasRole(user, UserRole.DCB_PARTNER_SUPPORT);
} }
@ -533,21 +652,32 @@ export class MerchantUsersList implements OnInit, OnDestroy {
searchUsers() { searchUsers() {
if (this.searchTerm.trim()) { if (this.searchTerm.trim()) {
this.loading = true; this.loading = true;
this.merchantUsersService.searchMerchantUsers({ const searchParams: SearchUsersParams = {
query: this.searchTerm, query: this.searchTerm,
role: this.roleFilter !== 'all' ? this.roleFilter as UserRole : undefined, userType: UserType.MERCHANT
enabled: this.statusFilter !== 'all' ? this.statusFilter === 'enabled' : undefined };
})
if (this.roleFilter !== 'all') {
searchParams.role = this.roleFilter as UserRole;
}
if (this.statusFilter !== 'all') {
searchParams.enabled = this.statusFilter === 'enabled';
}
console.log('🔍 Performing advanced search:', searchParams);
this.merchantUsersService.searchMerchantUsers(searchParams)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (users) => { next: (users) => {
this.allUsers = users; this.allUsers = users;
console.log(`✅ Advanced search found ${users.length} users`);
this.applyFiltersAndPagination(); this.applyFiltersAndPagination();
this.loading = false; this.loading = false;
this.cdRef.detectChanges(); this.cdRef.detectChanges();
}, },
error: (error: any) => { error: (error: any) => {
console.error('Error searching users:', error); console.error('Error searching users:', error);
this.loading = false; this.loading = false;
this.cdRef.detectChanges(); this.cdRef.detectChanges();
} }
@ -556,4 +686,10 @@ export class MerchantUsersList implements OnInit, OnDestroy {
this.loadUsers(); // Recharger tous les utilisateurs si la recherche est vide this.loadUsers(); // Recharger tous les utilisateurs si la recherche est vide
} }
} }
// Recharger les données
refreshData() {
console.log('🔄 Refreshing data...');
this.loadUsers();
}
} }

View File

@ -43,6 +43,11 @@
(back)="backToList()" (back)="backToList()"
(openResetPasswordModal)="onResetPasswordRequested($event)" (openResetPasswordModal)="onResetPasswordRequested($event)"
/> />
} @else {
<div class="alert alert-warning text-center">
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
Aucun utilisateur sélectionné
</div>
} }
</ng-template> </ng-template>
</li> </li>
@ -65,6 +70,7 @@
class="btn-close" class="btn-close"
(click)="modal.dismiss()" (click)="modal.dismiss()"
[disabled]="creatingUser" [disabled]="creatingUser"
aria-label="Fermer"
></button> ></button>
</div> </div>
@ -78,7 +84,29 @@
} }
<form (ngSubmit)="createMerchantUser()" #userForm="ngForm"> <form (ngSubmit)="createMerchantUser()" #userForm="ngForm">
<div class="row g-3"> <div class="row g-3">
<!-- Merchant Partner ID (lecture seule) -->
<div class="col-md-6">
<label class="form-label">Merchant Partner ID</label>
<div class="form-control-plaintext font-monospace small">
{{ currentMerchantPartnerId || 'Chargement...' }}
<input
type="hidden"
[(ngModel)]="newMerchantUser.merchantPartnerId"
name="merchantPartnerId"
[value]="currentMerchantPartnerId"
required
>
</div>
@if (!currentMerchantPartnerId) {
<div class="form-text text-warning">
<ng-icon name="lucideAlertTriangle" class="me-1"></ng-icon>
Merchant Partner ID non disponible
</div>
}
</div>
<!-- Informations de base --> <!-- Informations de base -->
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label"> <label class="form-label">
@ -92,7 +120,13 @@
name="firstName" name="firstName"
required required
[disabled]="creatingUser" [disabled]="creatingUser"
#firstName="ngModel"
> >
@if (firstName.invalid && firstName.touched) {
<div class="text-danger small">
Le prénom est requis
</div>
}
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
@ -107,7 +141,13 @@
name="lastName" name="lastName"
required required
[disabled]="creatingUser" [disabled]="creatingUser"
#lastName="ngModel"
> >
@if (lastName.invalid && lastName.touched) {
<div class="text-danger small">
Le nom est requis
</div>
}
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
@ -122,8 +162,14 @@
name="username" name="username"
required required
[disabled]="creatingUser" [disabled]="creatingUser"
#username="ngModel"
> >
<div class="form-text">Doit être unique dans le système</div> <div class="form-text">Doit être unique dans le système</div>
@if (username.invalid && username.touched) {
<div class="text-danger small">
Le nom d'utilisateur est requis
</div>
}
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
@ -137,8 +183,20 @@
[(ngModel)]="newMerchantUser.email" [(ngModel)]="newMerchantUser.email"
name="email" name="email"
required required
email
[disabled]="creatingUser" [disabled]="creatingUser"
#email="ngModel"
> >
@if (email.invalid && email.touched) {
<div class="text-danger small">
@if (email.errors?.['required']) {
L'email est requis
}
@if (email.errors?.['email']) {
Format d'email invalide
}
</div>
}
</div> </div>
<div class="col-12"> <div class="col-12">
@ -154,10 +212,21 @@
required required
minlength="8" minlength="8"
[disabled]="creatingUser" [disabled]="creatingUser"
#password="ngModel"
> >
<div class="form-text"> <div class="form-text">
Le mot de passe doit contenir au moins 8 caractères. Le mot de passe doit contenir au moins 8 caractères.
</div> </div>
@if (password.invalid && password.touched) {
<div class="text-danger small">
@if (password.errors?.['required']) {
Le mot de passe est requis
}
@if (password.errors?.['minlength']) {
Le mot de passe doit contenir au moins 8 caractères
}
</div>
}
</div> </div>
<!-- Sélection du rôle --> <!-- Sélection du rôle -->
@ -170,17 +239,18 @@
[(ngModel)]="newMerchantUser.role" [(ngModel)]="newMerchantUser.role"
name="role" name="role"
required required
[disabled]="creatingUser" [disabled]="creatingUser || !canManageAllRoles"
#roleSelect="ngModel"
> >
<option value="" disabled>Sélectionnez un rôle</option> <option value="" disabled selected>Sélectionnez un rôle</option>
@if (availableRoles) { @for (role of getAvailableRoles(); track role.value) {
@for (role of availableRoles.roles; track role.value) { @if (isMerchantRole(role.value) && isRoleAllowedForCreation(role.value)) {
<option <option
[value]="role.value" [value]="role.value"
[disabled]="!role.allowedForCreation" [disabled]="!canAssignRole(role.value)"
> >
{{ role.label }} - {{ role.description }} {{ getRoleDisplayName(role.value) }} - {{ role.description }}
@if (!role.allowedForCreation) { @if (!canAssignRole(role.value)) {
(Non autorisé) (Non autorisé)
} }
</option> </option>
@ -188,9 +258,42 @@
} }
</select> </select>
<div class="form-text"> <div class="form-text">
Sélectionnez le rôle à assigner à cet utilisateur marchand @if (canManageAllRoles) {
<span class="text-success">
<ng-icon name="lucideShield" class="me-1"></ng-icon>
Vous pouvez attribuer tous les rôles
</span>
} @else {
<span class="text-warning">
<ng-icon name="lucideShield" class="me-1"></ng-icon>
Permissions de rôle limitées
</span>
}
</div>
@if (roleSelect.invalid && roleSelect.touched) {
<div class="text-danger small">
Le rôle est requis
</div>
}
</div>
<!-- Avertissement pour les non-DCB_PARTNER -->
@if (!canManageAllRoles) {
<div class="col-12">
<div class="alert alert-warning">
<div class="d-flex align-items-center">
<ng-icon name="lucideInfo" class="me-2"></ng-icon>
<div>
<small>
<strong>Permissions limitées :</strong>
Vous ne pouvez créer que des utilisateurs avec des rôles spécifiques.
Seul un <strong>DCB Partner</strong> peut attribuer tous les rôles.
</small>
</div> </div>
</div> </div>
</div>
</div>
}
<!-- Aperçu du rôle sélectionné --> <!-- Aperçu du rôle sélectionné -->
@if (newMerchantUser.role) { @if (newMerchantUser.role) {
@ -259,7 +362,8 @@
<strong>Informations système :</strong><br> <strong>Informations système :</strong><br>
• Merchant Partner ID : {{ currentMerchantPartnerId || 'Chargement...' }}<br> • Merchant Partner ID : {{ currentMerchantPartnerId || 'Chargement...' }}<br>
• Type d'utilisateur : MERCHANT<br> • Type d'utilisateur : MERCHANT<br>
• Créé par : Utilisateur courant • Créé par : Utilisateur courant<br>
• Votre rôle : {{ currentUserRole || 'Non défini' }}
</small> </small>
</div> </div>
</div> </div>
@ -272,12 +376,13 @@
(click)="modal.dismiss()" (click)="modal.dismiss()"
[disabled]="creatingUser" [disabled]="creatingUser"
> >
<ng-icon name="lucideX" class="me-1"></ng-icon>
Annuler Annuler
</button> </button>
<button <button
type="submit" type="submit"
class="btn btn-primary" class="btn btn-primary"
[disabled]="!userForm.form.valid || creatingUser || !isRoleAllowedForCreation(newMerchantUser.role)" [disabled]="!userForm.form.valid || creatingUser || !isRoleAllowedForCreation(newMerchantUser.role) || !currentMerchantPartnerId"
> >
@if (creatingUser) { @if (creatingUser) {
<div class="spinner-border spinner-border-sm me-2" role="status"> <div class="spinner-border spinner-border-sm me-2" role="status">
@ -306,6 +411,7 @@
class="btn-close" class="btn-close"
(click)="modal.dismiss()" (click)="modal.dismiss()"
[disabled]="resettingPassword" [disabled]="resettingPassword"
aria-label="Fermer"
></button> ></button>
</div> </div>
@ -363,10 +469,21 @@
required required
minlength="8" minlength="8"
[disabled]="resettingPassword" [disabled]="resettingPassword"
#newPasswordInput="ngModel"
> >
<div class="form-text"> <div class="form-text">
Le mot de passe doit contenir au moins 8 caractères. Le mot de passe doit contenir au moins 8 caractères.
</div> </div>
@if (newPasswordInput.invalid && newPasswordInput.touched) {
<div class="text-danger small">
@if (newPasswordInput.errors?.['required']) {
Le mot de passe est requis
}
@if (newPasswordInput.errors?.['minlength']) {
Le mot de passe doit contenir au moins 8 caractères
}
</div>
}
</div> </div>
<div class="mb-3"> <div class="mb-3">
@ -389,6 +506,11 @@
</div> </div>
</div> </div>
</form> </form>
} @else if (!selectedUserForReset) {
<div class="alert alert-warning text-center">
<ng-icon name="lucideAlertTriangle" class="me-2"></ng-icon>
Aucun utilisateur sélectionné pour la réinitialisation
</div>
} }
</div> </div>
@ -415,7 +537,7 @@
type="button" type="button"
class="btn btn-primary" class="btn btn-primary"
(click)="confirmResetPassword()" (click)="confirmResetPassword()"
[disabled]="!newPassword || newPassword.length < 8 || resettingPassword" [disabled]="!newPassword || newPassword.length < 8 || resettingPassword || !selectedUserForReset"
> >
@if (resettingPassword) { @if (resettingPassword) {
<div class="spinner-border spinner-border-sm me-2" role="status"> <div class="spinner-border spinner-border-sm me-2" role="status">
@ -442,6 +564,7 @@
type="button" type="button"
class="btn-close" class="btn-close"
(click)="modal.dismiss()" (click)="modal.dismiss()"
aria-label="Fermer"
></button> ></button>
</div> </div>
@ -478,6 +601,11 @@
</div> </div>
</div> </div>
</div> </div>
} @else {
<div class="alert alert-warning">
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
Aucun utilisateur sélectionné pour la suppression
</div>
} }
<!-- Message d'erreur --> <!-- Message d'erreur -->
@ -503,7 +631,7 @@
type="button" type="button"
class="btn btn-danger" class="btn btn-danger"
(click)="confirmDeleteUser()" (click)="confirmDeleteUser()"
[disabled]="deletingUser" [disabled]="deletingUser || !selectedUserForDelete"
> >
@if (deletingUser) { @if (deletingUser) {
<div class="spinner-border spinner-border-sm me-2" role="status"> <div class="spinner-border spinner-border-sm me-2" role="status">

View File

@ -0,0 +1,2 @@
import { MerchantUsers } from './merchant-users';
describe('Merchant Users', () => {});

View File

@ -7,17 +7,21 @@ import { Subject, takeUntil } from 'rxjs';
import { PageTitle } from '@app/components/page-title/page-title'; import { PageTitle } from '@app/components/page-title/page-title';
import { MerchantUsersList } from './list/list'; import { MerchantUsersList } from './list/list';
import { MerchantUserProfile } from './profile/profile'; import { MerchantUserProfile } from './profile/profile';
import { import { MerchantUsersService } from './services/merchant-users.service';
MerchantUsersService,
CreateMerchantUserDto,
MerchantUserResponse,
UserRole,
AvailableRolesResponse,
} from './services/merchant-partners.service';
import { AuthService } from '@core/services/auth.service'; import { AuthService } from '@core/services/auth.service';
import {
MerchantUserDto,
CreateUserDto,
ResetPasswordDto,
UserRole,
UserType,
AvailableRolesResponse,
} from '@core/models/dcb-bo-hub-user.model';
import { RoleManagementService } from '@core/services/role-management.service';
@Component({ @Component({
selector: 'app-merchant-partners', selector: 'app-merchant-users',
standalone: true, standalone: true,
imports: [ imports: [
CommonModule, CommonModule,
@ -29,21 +33,28 @@ import { AuthService } from '@core/services/auth.service';
MerchantUsersList, MerchantUsersList,
MerchantUserProfile MerchantUserProfile
], ],
templateUrl: './merchant-partners.html', templateUrl: './merchant-users.html',
}) })
export class MerchantPartners implements OnInit, OnDestroy { export class MerchantUsers implements OnInit, OnDestroy {
private modalService = inject(NgbModal); private modalService = inject(NgbModal);
private authService = inject(AuthService); private authService = inject(AuthService);
private merchantUsersService = inject(MerchantUsersService); private merchantUsersService = inject(MerchantUsersService);
protected roleService = inject(RoleManagementService);
private cdRef = inject(ChangeDetectorRef); private cdRef = inject(ChangeDetectorRef);
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
activeTab: 'list' | 'stats' | 'profile' = 'list'; readonly UserRole = UserRole;
readonly UserType = UserType;
// Ajouter cette propriété manquante
user: MerchantUserDto | null = null;
activeTab: 'list' | 'profile' = 'list';
selectedUserId: string | null = null; selectedUserId: string | null = null;
currentMerchantPartnerId: string = ''; currentMerchantPartnerId: string = '';
// Données pour la création d'utilisateur marchand // Données pour la création d'utilisateur marchand
newMerchantUser: CreateMerchantUserDto = { newMerchantUser: CreateUserDto = {
username: '', username: '',
email: '', email: '',
firstName: '', firstName: '',
@ -58,22 +69,28 @@ export class MerchantPartners implements OnInit, OnDestroy {
availableRoles: AvailableRolesResponse | null = null; availableRoles: AvailableRolesResponse | null = null;
creatingUser = false; creatingUser = false;
createUserError = ''; createUserError = '';
currentUserRole: UserRole | null = null;
// Données pour la réinitialisation de mot de passe // Données pour la réinitialisation de mot de passe
selectedUserForReset: MerchantUserResponse | null = null; selectedUserForReset: MerchantUserDto | null = null;
newPassword = ''; newPassword = '';
temporaryPassword = true; temporaryPassword = true;
resettingPassword = false; resettingPassword = false;
resetPasswordError = ''; resetPasswordError = '';
resetPasswordSuccess = ''; resetPasswordSuccess = '';
selectedUserForDelete: MerchantUserResponse | null = null; selectedUserForDelete: MerchantUserDto | null = null;
deletingUser = false; deletingUser = false;
deleteUserError = ''; deleteUserError = '';
// Permissions utilisateur
isHubAdminOrSupport = false;
isDcbPartner = false;
canViewAllMerchants = false;
ngOnInit() { ngOnInit() {
this.activeTab = 'list'; this.activeTab = 'list';
this.loadCurrentMerchantPartnerId(); this.loadCurrentUserPermissions();
this.loadAvailableRoles(); this.loadAvailableRoles();
} }
@ -82,37 +99,188 @@ export class MerchantPartners implements OnInit, OnDestroy {
this.destroy$.complete(); this.destroy$.complete();
} }
private loadCurrentMerchantPartnerId() { private loadCurrentUserPermissions() {
this.authService.getProfile().subscribe({ this.authService.getProfile().subscribe({
next: (user) => { next: (user: any) => {
this.currentMerchantPartnerId = user.merchantPartnerId || ''; this.currentUserRole = this.extractUserRole(user);
this.newMerchantUser.merchantPartnerId = this.currentMerchantPartnerId;
this.isHubAdminOrSupport = this.isHubAdminOrSupportRole(this.currentUserRole);
this.isDcbPartner = this.isDcbPartnerRole(this.currentUserRole);
this.canViewAllMerchants = this.canViewAllMerchantsCheck(this.currentUserRole);
// Déterminer le merchantPartnerId
if(this.isDcbPartner){
this.currentMerchantPartnerId = user.id;
}
else if(!this.isDcbPartner || !this.isHubAdminOrSupport) {
this.currentMerchantPartnerId = this.extractMerchantPartnerId(user);
}
}, },
error: (error) => { error: (error) => {
console.error('Error loading current merchant partner ID:', error); this.fallbackPermissions();
} }
}); });
} }
/**
* Méthode robuste pour extraire le rôle de l'utilisateur
*/
private extractUserRole(user: any): UserRole | null {
// 1. Essayer depuis les rôles du token (méthode principale)
const tokenRoles = this.authService.getCurrentUserRoles();
if (tokenRoles && tokenRoles.length > 0) {
return tokenRoles[0];
}
// 2. Essayer depuis le profil user.role
if (user?.role && Object.values(UserRole).includes(user.role)) {
return user.role as UserRole;
}
// 3. Essayer depuis user.userType pour déduire le rôle
if (user?.userType) {
const roleFromType = this.getRoleFromUserType(user.userType);
if (roleFromType) {
return roleFromType;
}
}
// 4. Essayer depuis les attributs étendus
if (user?.attributes?.role?.[0]) {
const roleFromAttributes = user.attributes.role[0];
if (Object.values(UserRole).includes(roleFromAttributes as UserRole)) {
return roleFromAttributes as UserRole;
}
}
return null;
}
/**
* Déduire le rôle à partir du userType
*/
private getRoleFromUserType(userType: string): UserRole | null {
const typeMapping: { [key: string]: UserRole } = {
[UserType.HUB]: UserRole.DCB_ADMIN,
[UserType.MERCHANT]: UserRole.DCB_PARTNER_ADMIN,
};
return typeMapping[userType] || null;
}
/**
* Vérifier si l'utilisateur est Hub Admin ou Support
*/
private isHubAdminOrSupportRole(role: UserRole | null): boolean {
if (!role) return false;
const hubAdminSupportRoles = [
UserRole.DCB_ADMIN,
UserRole.DCB_SUPPORT
];
return hubAdminSupportRoles.includes(role);
}
/**
* Vérifier si l'utilisateur est DCB Partner
*/
private isDcbPartnerRole(role: UserRole | null): boolean {
if (!role) return false;
const partnerRoles = [
UserRole.DCB_PARTNER,
UserRole.DCB_PARTNER_ADMIN,
UserRole.DCB_PARTNER_MANAGER,
UserRole.DCB_PARTNER_SUPPORT
];
return partnerRoles.includes(role);
}
/**
* Vérifier si l'utilisateur peut voir tous les merchants
*/
private canViewAllMerchantsCheck(role: UserRole | null): boolean {
if (!role) return false;
const canViewAllRoles = [
UserRole.DCB_ADMIN,
UserRole.DCB_SUPPORT,
];
return canViewAllRoles.includes(role);
}
/**
* Extraire le merchantPartnerId de manière robuste
*/
private extractMerchantPartnerId(user: any): string {
// 1. Essayer depuis merchantPartnerId direct
if (user?.merchantPartnerId) {
return user.merchantPartnerId;
}
// 2. Essayer depuis les attributs
if (user?.attributes?.merchantPartnerId?.[0]) {
return user.attributes.merchantPartnerId[0];
}
// 3. Essayer depuis AuthService
const authServiceMerchantId = this.authService.getCurrentMerchantPartnerId();
if (authServiceMerchantId) {
return authServiceMerchantId;
}
// 4. Pour les rôles Hub, pas de merchantPartnerId
if (this.isHubAdminOrSupport) {
return '';
}
return '';
}
/**
* Fallback en cas d'erreur de chargement du profil
*/
private fallbackPermissions(): void {
// Utiliser les méthodes d'AuthService comme fallback
this.currentUserRole = this.authService.getCurrentUserRole();
this.isHubAdminOrSupport = this.authService.isHubUser() ||
this.authService.hasRole(UserRole.DCB_ADMIN) ||
this.authService.hasRole(UserRole.DCB_SUPPORT);
this.isDcbPartner = this.authService.isMerchantUser() ||
this.authService.hasRole(UserRole.DCB_PARTNER);
this.canViewAllMerchants = this.authService.canViewAllMerchants();
this.currentMerchantPartnerId = this.authService.getCurrentMerchantPartnerId() || '';
this.newMerchantUser.merchantPartnerId = this.currentMerchantPartnerId;
}
private loadAvailableRoles() { private loadAvailableRoles() {
this.merchantUsersService.getAvailableMerchantRoles() this.merchantUsersService.getAvailableMerchantRoles()
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (roles) => { next: (roles) => {
this.availableRoles = roles; this.availableRoles = roles;
// Sélectionner le premier rôle disponible par défaut // Sélectionner le premier rôle disponible par défaut
const firstAllowedRole = roles.roles.find(role => role.allowedForCreation); const firstAllowedRole = roles.roles.find(role => role.allowedForCreation);
if (firstAllowedRole) { if (firstAllowedRole) {
this.newMerchantUser.role = firstAllowedRole.value as any; this.newMerchantUser.role = firstAllowedRole.value;
} }
}, },
error: (error) => { error: (error) => {
console.error('Error loading available roles:', error); console.error('Error loading available roles:', error);
} }
}); });
} }
showTab(tab: 'list' | 'stats' | 'profile', userId?: string) { showTab(tab: 'list' | 'profile', userId?: string) {
console.log(`Switching to tab: ${tab}`, userId ? `for user ${userId}` : '');
this.activeTab = tab; this.activeTab = tab;
if (userId) { if (userId) {
@ -121,6 +289,7 @@ export class MerchantPartners implements OnInit, OnDestroy {
} }
backToList() { backToList() {
console.log('🔙 Returning to list view');
this.activeTab = 'list'; this.activeTab = 'list';
this.selectedUserId = null; this.selectedUserId = null;
} }
@ -167,6 +336,7 @@ export class MerchantPartners implements OnInit, OnDestroy {
enabled: true, enabled: true,
emailVerified: false emailVerified: false
}; };
console.log('🔄 User form reset');
} }
// Méthode pour ouvrir le modal de réinitialisation de mot de passe // Méthode pour ouvrir le modal de réinitialisation de mot de passe
@ -181,16 +351,19 @@ export class MerchantPartners implements OnInit, OnDestroy {
this.resetPasswordError = ''; this.resetPasswordError = '';
this.resetPasswordSuccess = ''; this.resetPasswordSuccess = '';
this.openModal(this.resetPasswordModal); this.openModal(this.resetPasswordModal);
console.log('✅ User loaded for password reset:', user.username);
}, },
error: (error) => { error: (error) => {
console.error('Error loading user for password reset:', error); console.error('Error loading user for password reset:', error);
this.resetPasswordError = 'Erreur lors du chargement de l\'utilisateur'; this.resetPasswordError = 'Erreur lors du chargement de l\'utilisateur';
this.cdRef.detectChanges();
} }
}); });
} }
// Méthode pour ouvrir le modal de suppression // Méthode pour ouvrir le modal de suppression
openDeleteUserModal(userId: string) { openDeleteUserModal(userId: string) {
console.log(`🗑️ Opening delete modal for user: ${userId}`);
this.merchantUsersService.getMerchantUserById(userId) this.merchantUsersService.getMerchantUserById(userId)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
@ -198,19 +371,23 @@ export class MerchantPartners implements OnInit, OnDestroy {
this.selectedUserForDelete = user; this.selectedUserForDelete = user;
this.deleteUserError = ''; this.deleteUserError = '';
this.openModal(this.deleteUserModal); this.openModal(this.deleteUserModal);
console.log('✅ User loaded for deletion:', user.username);
}, },
error: (error) => { error: (error) => {
console.error('Error loading user for deletion:', error); console.error('Error loading user for deletion:', error);
this.deleteUserError = 'Erreur lors du chargement de l\'utilisateur'; this.deleteUserError = 'Erreur lors du chargement de l\'utilisateur';
this.cdRef.detectChanges();
} }
}); });
} }
// Créer un utilisateur marchand // Créer un utilisateur marchand
createMerchantUser() { createMerchantUser() {
console.log('🚀 Creating new merchant user...');
const validation = this.validateUserForm(); const validation = this.validateUserForm();
if (!validation.isValid) { if (!validation.isValid) {
this.createUserError = validation.error!; this.createUserError = validation.error!;
console.error('❌ Form validation failed:', validation.error);
return; return;
} }
@ -227,11 +404,13 @@ export class MerchantPartners implements OnInit, OnDestroy {
this.creatingUser = false; this.creatingUser = false;
this.modalService.dismissAll(); this.modalService.dismissAll();
this.refreshUsersList(); this.refreshUsersList();
this.showSuccessMessage(`Utilisateur "${createdUser.username}" créé avec succès`); this.cdRef.detectChanges();
}, },
error: (error) => { error: (error) => {
console.error('❌ Error creating user:', error);
this.creatingUser = false; this.creatingUser = false;
this.createUserError = this.getErrorMessage(error); this.createUserError = this.getErrorMessage(error);
this.cdRef.detectChanges();
} }
}); });
} }
@ -240,22 +419,28 @@ export class MerchantPartners implements OnInit, OnDestroy {
confirmResetPassword() { confirmResetPassword() {
if (!this.selectedUserForReset || !this.newPassword || this.newPassword.length < 8) { if (!this.selectedUserForReset || !this.newPassword || this.newPassword.length < 8) {
this.resetPasswordError = 'Veuillez saisir un mot de passe valide (au moins 8 caractères).'; this.resetPasswordError = 'Veuillez saisir un mot de passe valide (au moins 8 caractères).';
console.error('❌ Password reset validation failed');
return; return;
} }
console.log('🔑 Confirming password reset for user:', this.selectedUserForReset.username);
this.resettingPassword = true; this.resettingPassword = true;
this.resetPasswordError = ''; this.resetPasswordError = '';
this.resetPasswordSuccess = ''; this.resetPasswordSuccess = '';
this.merchantUsersService.resetMerchantUserPassword( const resetPasswordDto: ResetPasswordDto = {
this.selectedUserForReset.id,
{
newPassword: this.newPassword, newPassword: this.newPassword,
temporary: this.temporaryPassword temporary: this.temporaryPassword
} };
this.merchantUsersService.resetMerchantUserPassword(
this.selectedUserForReset.id,
resetPasswordDto
).pipe(takeUntil(this.destroy$)) ).pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (response) => { next: (response) => {
console.log('✅ Password reset successfully');
this.resettingPassword = false; this.resettingPassword = false;
this.resetPasswordSuccess = 'Mot de passe réinitialisé avec succès !'; this.resetPasswordSuccess = 'Mot de passe réinitialisé avec succès !';
this.cdRef.detectChanges(); this.cdRef.detectChanges();
@ -266,6 +451,7 @@ export class MerchantPartners implements OnInit, OnDestroy {
}, 2000); }, 2000);
}, },
error: (error) => { error: (error) => {
console.error('❌ Error resetting password:', error);
this.resettingPassword = false; this.resettingPassword = false;
this.resetPasswordError = this.getResetPasswordErrorMessage(error); this.resetPasswordError = this.getResetPasswordErrorMessage(error);
this.cdRef.detectChanges(); this.cdRef.detectChanges();
@ -274,7 +460,12 @@ export class MerchantPartners implements OnInit, OnDestroy {
} }
confirmDeleteUser() { confirmDeleteUser() {
if (!this.selectedUserForDelete) return; if (!this.selectedUserForDelete) {
console.error('❌ No user selected for deletion');
return;
}
console.log('🗑️ Confirming user deletion:', this.selectedUserForDelete.username);
this.deletingUser = true; this.deletingUser = true;
this.deleteUserError = ''; this.deleteUserError = '';
@ -283,13 +474,14 @@ export class MerchantPartners implements OnInit, OnDestroy {
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: () => { next: () => {
console.log('✅ User deleted successfully');
this.deletingUser = false; this.deletingUser = false;
this.modalService.dismissAll(); this.modalService.dismissAll();
this.refreshUsersList(); this.refreshUsersList();
this.showSuccessMessage(`Utilisateur "${this.selectedUserForDelete?.username}" supprimé avec succès`);
this.cdRef.detectChanges(); this.cdRef.detectChanges();
}, },
error: (error) => { error: (error) => {
console.error('❌ Error deleting user:', error);
this.deletingUser = false; this.deletingUser = false;
this.deleteUserError = this.getDeleteErrorMessage(error); this.deleteUserError = this.getDeleteErrorMessage(error);
this.cdRef.detectChanges(); this.cdRef.detectChanges();
@ -300,10 +492,11 @@ export class MerchantPartners implements OnInit, OnDestroy {
@ViewChild(MerchantUsersList) usersListComponent!: MerchantUsersList; @ViewChild(MerchantUsersList) usersListComponent!: MerchantUsersList;
private refreshUsersList(): void { private refreshUsersList(): void {
if (this.usersListComponent && typeof this.usersListComponent.loadUsers === 'function') { if (this.usersListComponent && typeof this.usersListComponent.refreshData === 'function') {
this.usersListComponent.loadUsers(); console.log('🔄 Refreshing users list...');
this.usersListComponent.refreshData();
} else { } else {
console.warn('MerchantUsersList component not available for refresh'); console.warn('MerchantUsersList component not available for refresh');
this.showTab('list'); this.showTab('list');
} }
} }
@ -400,24 +593,89 @@ export class MerchantPartners implements OnInit, OnDestroy {
return { isValid: true }; return { isValid: true };
} }
// ==================== MESSAGES DE SUCCÈS ====================
private showSuccessMessage(message: string) {
// Vous pouvez implémenter un service de notification ici
console.log('Success:', message);
// Exemple avec un toast:
// this.notificationService.success(message);
}
// ==================== MÉTHODES UTILITAIRES ==================== // ==================== MÉTHODES UTILITAIRES ====================
getRoleDisplayName(role: UserRole): string { getRoleDisplayName(role: UserRole): string {
const roleNames = { // Seulement gérer les rôles marchands, ignorer les rôles Hub
[UserRole.DCB_PARTNER_ADMIN]: 'Administrateur Partenaire', switch (role) {
[UserRole.DCB_PARTNER_MANAGER]: 'Manager Partenaire', case UserRole.DCB_PARTNER_ADMIN:
[UserRole.DCB_PARTNER_SUPPORT]: 'Support Partenaire' return 'Administrateur Partenaire';
}; case UserRole.DCB_PARTNER_MANAGER:
return roleNames[role] || role; return 'Manager Partenaire';
case UserRole.DCB_PARTNER_SUPPORT:
return 'Support Partenaire';
default:
// Pour les rôles Hub, retourner le nom technique
return role;
}
}
// Méthode utilitaire pour vérifier si un rôle est un rôle marchand
isMerchantRole(role: UserRole): boolean {
const merchantRoles = [
UserRole.DCB_PARTNER_ADMIN,
UserRole.DCB_PARTNER_MANAGER,
UserRole.DCB_PARTNER_SUPPORT
];
return merchantRoles.includes(role);
}
// Méthode pour filtrer les rôles disponibles (uniquement les rôles marchands)
getFilteredAvailableRoles(): { value: UserRole; label: string; description: string }[] {
if (!this.availableRoles) return [];
return this.availableRoles.roles
.filter(role => this.isMerchantRole(role.value))
.map(role => ({
value: role.value,
label: this.getRoleDisplayName(role.value),
description: role.description
}));
}
getAvailableRoles(): any[] {
if (!this.availableRoles) return [];
return this.availableRoles.roles;
}
/**
* Vérifie si l'utilisateur peut attribuer un rôle spécifique
*/
canAssignRole(targetRole: UserRole): boolean {
// Seul DCB_PARTNER peut attribuer tous les rôles
if (this.currentUserRole === UserRole.DCB_PARTNER || this.currentUserRole === UserRole.DCB_ADMIN || this.currentUserRole === UserRole.DCB_SUPPORT) {
return true;
}
// Les autres rôles ont des permissions limitées
return this.roleService.canAssignRole(this.currentUserRole, targetRole);
}
canManageRoles(): boolean {
return this.currentUserRole === UserRole.DCB_PARTNER;
}
/**
* Vérifie si l'utilisateur peut gérer les rôles
*/
get canManageAllRoles(): boolean {
return this.currentUserRole === UserRole.DCB_PARTNER;
}
// Méthode pour obtenir uniquement les rôles marchands assignables
getAssignableMerchantRoles(): UserRole[] {
const merchantRoles = [
UserRole.DCB_PARTNER_ADMIN,
UserRole.DCB_PARTNER_MANAGER,
UserRole.DCB_PARTNER_SUPPORT
];
if (!this.availableRoles) return merchantRoles;
return merchantRoles.filter(role => {
const roleInfo = this.availableRoles!.roles.find(r => r.value === role);
return roleInfo?.allowedForCreation !== false;
});
} }
getRoleDescription(role: UserRole): string { getRoleDescription(role: UserRole): string {
@ -435,11 +693,11 @@ export class MerchantPartners implements OnInit, OnDestroy {
} }
// Méthodes utilitaires pour le template // Méthodes utilitaires pour le template
getUserInitials(user: MerchantUserResponse): string { getUserInitials(user: MerchantUserDto): string {
return (user.firstName?.charAt(0) || '') + (user.lastName?.charAt(0) || '') || 'U'; return (user.firstName?.charAt(0) || '') + (user.lastName?.charAt(0) || '') || 'U';
} }
getUserType(user: MerchantUserResponse): string { getUserType(user: MerchantUserDto): string {
switch (user.role) { switch (user.role) {
case UserRole.DCB_PARTNER_ADMIN: case UserRole.DCB_PARTNER_ADMIN:
return 'Administrateur'; return 'Administrateur';

View File

@ -0,0 +1,115 @@
// src/app/core/models/user.model.ts
export enum UserType {
HUB = 'HUB',
MERCHANT = 'MERCHANT',
MERCHANT_USER = 'MERCHANT_USER'
}
export enum UserRole {
// HUB roles
DCB_ADMIN = 'DCB_ADMIN',
DCB_SUPPORT = 'DCB_SUPPORT',
DCB_PARTNER = 'DCB_PARTNER',
// MERCHANT roles
DCB_PARTNER_ADMIN = 'DCB_PARTNER_ADMIN',
DCB_PARTNER_MANAGER = 'DCB_PARTNER_MANAGER',
DCB_PARTNER_SUPPORT = 'DCB_PARTNER_SUPPORT'
}
// === BASE USER MODEL ===
export interface BaseUserDto {
id: string;
username: string;
email: string;
firstName: string;
lastName: string;
role: UserRole;
enabled: boolean;
emailVerified: boolean;
createdBy: string;
createdByUsername: string;
createdTimestamp: number;
lastLogin?: number;
userType: UserType;
}
// === EXTENSIONS ===
export interface HubUserDto extends BaseUserDto {
userType: UserType.HUB;
}
export interface MerchantUserDto extends BaseUserDto {
userType: UserType.MERCHANT;
merchantPartnerId: string;
}
// === DTOs CRUD ===
export interface CreateUserDto {
username: string;
email: string;
firstName: string;
lastName: string;
password: string;
role: UserRole;
enabled?: boolean;
emailVerified?: boolean;
merchantPartnerId?: string; // obligatoire si MERCHANT
}
export interface UpdateUserDto {
firstName?: string;
lastName?: string;
email?: string;
enabled?: boolean;
}
export interface ResetPasswordDto {
userId?: string;
newPassword: string;
temporary?: boolean;
}
// === PAGINATION / STATS ===
export interface PaginatedUserResponse {
users: BaseUserDto[];
total: number;
page: number;
limit: number;
totalPages: number;
}
export interface MerchantPartnerStatsResponse {
totalAdmins: number;
totalManagers: number;
totalSupport: number;
totalUsers: number;
activeUsers: number;
inactiveUsers: number;
}
// === ROLES ===
export interface AvailableRole {
value: UserRole;
label: string;
description: string;
allowedForCreation: boolean;
}
export interface AvailableRolesResponse {
roles: AvailableRole[];
}
export interface RoleOperationResponse {
message: string;
success: boolean;
}
// === SEARCH ===
export interface SearchUsersParams {
query?: string;
role?: UserRole;
enabled?: boolean;
userType?: UserType;
}

View File

@ -1,4 +1,3 @@
<!-- src/app/modules/merchant-users/profile/profile.html -->
<div class="container-fluid"> <div class="container-fluid">
<!-- En-tête avec navigation --> <!-- En-tête avec navigation -->
<div class="row mb-4"> <div class="row mb-4">
@ -32,7 +31,7 @@
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<!-- Bouton de réinitialisation de mot de passe --> <!-- Bouton de réinitialisation de mot de passe -->
@if (user && !isEditing) { @if (user && !isEditing && canResetPassword()) {
<button <button
class="btn btn-warning" class="btn btn-warning"
(click)="resetPassword()" (click)="resetPassword()"
@ -40,8 +39,10 @@
<ng-icon name="lucideKey" class="me-1"></ng-icon> <ng-icon name="lucideKey" class="me-1"></ng-icon>
Réinitialiser MDP Réinitialiser MDP
</button> </button>
}
<!-- Bouton activation/désactivation --> <!-- Bouton activation/désactivation -->
@if (user && !isEditing && canEnableDisableUser()) {
@if (user.enabled) { @if (user.enabled) {
<button <button
class="btn btn-outline-warning" class="btn btn-outline-warning"
@ -59,8 +60,10 @@
Activer Activer
</button> </button>
} }
}
<!-- Bouton modification --> <!-- Bouton modification -->
@if (user && !isEditing && canEditUser()) {
<button <button
class="btn btn-primary" class="btn btn-primary"
(click)="startEditing()" (click)="startEditing()"
@ -93,6 +96,31 @@
</div> </div>
} }
<!-- Indicateur de permissions -->
@if (user && !loading) {
<div class="row mb-3">
<div class="col-12">
<div class="alert alert-info py-2">
<div class="d-flex align-items-center">
<ng-icon name="lucideShield" class="me-2"></ng-icon>
<div class="flex-grow-1">
<small>
<strong>Statut :</strong> {{ getPermissionStatus() }}
@if (currentUserRole === UserRole.DCB_PARTNER) {
<span class="badge bg-success ms-2">Accès complet</span>
} @else if (currentUserRole === UserRole.DCB_PARTNER_ADMIN) {
<span class="badge bg-warning ms-2">Accès limité au partenaire</span>
} @else {
<span class="badge bg-secondary ms-2">Permissions restreintes</span>
}
</small>
</div>
</div>
</div>
</div>
</div>
}
<div class="row"> <div class="row">
<!-- Loading State --> <!-- Loading State -->
@if (loading) { @if (loading) {
@ -183,13 +211,22 @@
</small> </small>
</div> </div>
<!-- Information sur le rôle --> <!-- Information sur la modification des rôles -->
<div class="alert alert-light mt-3"> @if (canManageRoles()) {
<div class="alert alert-success mt-3">
<small> <small>
<strong>Information :</strong> Le rôle de l'utilisateur marchand ne peut pas être modifié directement. <ng-icon name="lucideShield" class="me-1"></ng-icon>
Pour changer le rôle, vous devez recréer l'utilisateur avec le nouveau rôle souhaité. <strong>DCB Partner :</strong> Vous pouvez modifier le rôle de cet utilisateur.
</small> </small>
</div> </div>
} @else {
<div class="alert alert-warning mt-3">
<small>
<ng-icon name="lucideInfo" class="me-1"></ng-icon>
<strong>Information :</strong> Seul un <strong>DCB Partner</strong> peut modifier les rôles des utilisateurs marchands.
</small>
</div>
}
</div> </div>
</div> </div>
@ -346,19 +383,75 @@
} }
</div> </div>
<!-- Rôle (lecture seule) --> <!-- Rôle (affichage seulement) -->
<div class="col-md-6"> <div class="col-12">
<label class="form-label">Rôle</label> <label class="form-label">Rôle Utilisateur</label>
<div class="form-control-plaintext"> <div class="form-control-plaintext">
<span class="badge" [ngClass]="getRoleBadgeClass(user.role)"> <span class="badge" [ngClass]="getRoleBadgeClass(user.role)">
<ng-icon [name]="getRoleIcon(user.role)" class="me-1"></ng-icon>
{{ getRoleDisplayName(user.role) }} {{ getRoleDisplayName(user.role) }}
</span> </span>
</div> </div>
<div class="form-text"> <div class="form-text">
Rôle assigné à la création @if (canManageRoles()) {
<span class="text-success">
<ng-icon name="lucideShield" class="me-1"></ng-icon>
Vous pouvez modifier ce rôle (DCB Partner)
</span>
} @else {
<span class="text-warning">
<ng-icon name="lucideShield" class="me-1"></ng-icon>
Seul un DCB Partner peut modifier les rôles
</span>
}
</div> </div>
</div> </div>
<!-- Section modification du rôle (uniquement pour DCB_PARTNER) -->
@if (isEditing && canManageRoles()) {
<div class="col-12">
<div class="card border-warning">
<div class="card-header bg-warning bg-opacity-10">
<h6 class="card-title mb-0 text-warning">
<ng-icon name="lucideShield" class="me-2"></ng-icon>
Modification du Rôle (DCB Partner)
</h6>
</div>
<div class="card-body">
<label class="form-label">Nouveau rôle</label>
<select
class="form-select"
[(ngModel)]="user.role"
name="role"
[disabled]="updatingRole"
>
<option value="" disabled>Sélectionnez un nouveau rôle</option>
@for (role of availableRoles; track role) {
<option [value]="role">
{{ getRoleDisplayName(role) }}
</option>
}
</select>
<div class="form-text">
En tant que DCB Partner, vous pouvez modifier le rôle de cet utilisateur.
</div>
<button
type="button"
class="btn btn-warning mt-2"
(click)="updateUserRole(user.role)"
[disabled]="updatingRole"
>
@if (updatingRole) {
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
}
<ng-icon name="lucideShield" class="me-1"></ng-icon>
Mettre à jour le rôle
</button>
</div>
</div>
</div>
}
<!-- Merchant Partner ID (lecture seule) --> <!-- Merchant Partner ID (lecture seule) -->
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Merchant Partner ID</label> <label class="form-label">Merchant Partner ID</label>
@ -371,7 +464,7 @@
</div> </div>
<!-- Statut activé --> <!-- Statut activé -->
@if (isEditing) { @if (isEditing && canEnableDisableUser()) {
<div class="col-md-6"> <div class="col-md-6">
<div class="form-check form-switch"> <div class="form-check form-switch">
<input <input
@ -389,7 +482,7 @@
L'utilisateur peut se connecter si activé L'utilisateur peut se connecter si activé
</div> </div>
</div> </div>
} @else { } @else if (!isEditing) {
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Statut du compte</label> <label class="form-label">Statut du compte</label>
<div class="form-control-plaintext"> <div class="form-control-plaintext">
@ -456,6 +549,8 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row g-2"> <div class="row g-2">
<!-- Réinitialisation MDP -->
@if (canResetPassword()) {
<div class="col-md-4"> <div class="col-md-4">
<button <button
class="btn btn-outline-warning w-100" class="btn btn-outline-warning w-100"
@ -465,6 +560,10 @@
Réinitialiser MDP Réinitialiser MDP
</button> </button>
</div> </div>
}
<!-- Activation/Désactivation -->
@if (canEnableDisableUser()) {
<div class="col-md-4"> <div class="col-md-4">
@if (user.enabled) { @if (user.enabled) {
<button <button
@ -484,6 +583,10 @@
</button> </button>
} }
</div> </div>
}
<!-- Modification -->
@if (canEditUser()) {
<div class="col-md-4"> <div class="col-md-4">
<button <button
class="btn btn-outline-primary w-100" class="btn btn-outline-primary w-100"
@ -493,13 +596,22 @@
Modifier Modifier
</button> </button>
</div> </div>
}
</div> </div>
<!-- Avertissement pour la suppression --> <!-- Avertissement pour les permissions -->
<div class="alert alert-light mt-3 mb-0"> <div class="alert alert-light mt-3 mb-0">
<small> <small>
<strong>Note :</strong> Pour supprimer cet utilisateur, utilisez l'action de suppression @if (currentUserRole === UserRole.DCB_PARTNER) {
disponible dans la liste des utilisateurs marchands. <ng-icon name="lucideShield" class="me-1 text-success"></ng-icon>
<strong>DCB Partner :</strong> Vous avez un accès complet à toutes les fonctionnalités de gestion.
} @else if (currentUserRole === UserRole.DCB_PARTNER_ADMIN) {
<ng-icon name="lucideInfo" class="me-1 text-warning"></ng-icon>
<strong>Admin Partenaire :</strong> Vous pouvez gérer les utilisateurs de votre partenaire marchand, mais pas modifier les rôles.
} @else {
<ng-icon name="lucideInfo" class="me-1 text-muted"></ng-icon>
<strong>Permissions limitées :</strong> Contactez un DCB Partner pour les actions de gestion avancées.
}
</small> </small>
</div> </div>
</div> </div>

View File

@ -5,13 +5,15 @@ import { FormsModule } from '@angular/forms';
import { NgIcon } from '@ng-icons/core'; import { NgIcon } from '@ng-icons/core';
import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap';
import { Subject, takeUntil } from 'rxjs'; import { Subject, takeUntil } from 'rxjs';
import { import { MerchantUsersService } from '../services/merchant-users.service';
MerchantUsersService,
MerchantUserResponse,
UserRole,
UpdateMerchantUserDto
} from '../services/merchant-partners.service';
import { AuthService } from '@core/services/auth.service'; import { AuthService } from '@core/services/auth.service';
import { RoleManagementService } from '@core/services/role-management.service';
import {
MerchantUserDto,
UpdateUserDto,
UserRole
} from '@core/models/dcb-bo-hub-user.model';
@Component({ @Component({
selector: 'app-merchant-user-profile', selector: 'app-merchant-user-profile',
@ -31,22 +33,27 @@ import { AuthService } from '@core/services/auth.service';
export class MerchantUserProfile implements OnInit, OnDestroy { export class MerchantUserProfile implements OnInit, OnDestroy {
private merchantUsersService = inject(MerchantUsersService); private merchantUsersService = inject(MerchantUsersService);
private authService = inject(AuthService); private authService = inject(AuthService);
private roleService = inject(RoleManagementService);
private cdRef = inject(ChangeDetectorRef); private cdRef = inject(ChangeDetectorRef);
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
readonly UserRole = UserRole;
@Input() userId!: string; @Input() userId!: string;
@Output() back = new EventEmitter<void>(); @Output() back = new EventEmitter<void>();
@Output() openResetPasswordModal = new EventEmitter<string>(); @Output() openResetPasswordModal = new EventEmitter<string>();
user: MerchantUserResponse | null = null; user: MerchantUserDto | null = null;
loading = false; loading = false;
saving = false; saving = false;
error = ''; error = '';
success = ''; success = '';
// Gestion des permissions
currentUserRole: UserRole | null = null;
// Édition // Édition
isEditing = false; isEditing = false;
editedUser: UpdateMerchantUserDto = {}; editedUser: UpdateUserDto = {};
// Gestion des rôles // Gestion des rôles
availableRoles: UserRole[] = [ availableRoles: UserRole[] = [
@ -58,6 +65,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
ngOnInit() { ngOnInit() {
if (this.userId) { if (this.userId) {
this.loadCurrentUserPermissions();
this.loadUserProfile(); this.loadUserProfile();
} }
} }
@ -67,6 +75,22 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
this.destroy$.complete(); this.destroy$.complete();
} }
/**
* Charge les permissions de l'utilisateur courant
*/
private loadCurrentUserPermissions(): void {
this.authService.getUserProfile()
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (profile) => {
this.currentUserRole = this.authService.getCurrentUserRole();
},
error: (error) => {
console.error('Error loading user permissions:', error);
}
});
}
loadUserProfile() { loadUserProfile() {
this.loading = true; this.loading = true;
this.error = ''; this.error = '';
@ -80,7 +104,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
this.cdRef.detectChanges(); this.cdRef.detectChanges();
}, },
error: (error) => { error: (error) => {
this.error = 'Erreur lors du chargement du profil utilisateur'; this.error = 'Erreur lors du chargement du profil utilisateur marchand';
this.loading = false; this.loading = false;
this.cdRef.detectChanges(); this.cdRef.detectChanges();
console.error('Error loading merchant user profile:', error); console.error('Error loading merchant user profile:', error);
@ -89,6 +113,11 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
} }
startEditing() { startEditing() {
if (!this.canEditUser()) {
this.error = 'Vous n\'avez pas la permission de modifier cet utilisateur';
return;
}
this.isEditing = true; this.isEditing = true;
this.editedUser = { this.editedUser = {
firstName: this.user?.firstName, firstName: this.user?.firstName,
@ -108,7 +137,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
} }
saveProfile() { saveProfile() {
if (!this.user) return; if (!this.user || !this.canEditUser()) return;
this.saving = true; this.saving = true;
this.error = ''; this.error = '';
@ -136,18 +165,26 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
// Gestion des rôles // Gestion des rôles
updateUserRole(newRole: UserRole) { updateUserRole(newRole: UserRole) {
if (!this.user) return; if (!this.user || !this.canManageRoles()) {
this.error = 'Vous n\'avez pas la permission de modifier les rôles';
return;
}
// Vérifier que le nouveau rôle est différent
if (newRole === this.user.role) {
this.error = 'L\'utilisateur a déjà ce rôle';
return;
}
this.updatingRole = true; this.updatingRole = true;
this.error = ''; this.error = '';
this.success = ''; this.success = '';
// Pour changer le rôle, on doit recréer l'utilisateur ou utiliser une méthode spécifique // Note: La modification de rôle nécessite une méthode spécifique
// Pour l'instant, on utilise la mise à jour standard // Pour l'instant, on utilise la mise à jour standard
const updateData: UpdateMerchantUserDto = { // Vous devrez peut-être implémenter une méthode updateUserRole dans le service
const updateData: UpdateUserDto = {
...this.editedUser ...this.editedUser
// Note: Le rôle n'est pas modifiable via update dans l'API actuelle
// Vous devrez peut-être implémenter une méthode spécifique dans le service
}; };
this.merchantUsersService.updateMerchantUser(this.user.id, updateData) this.merchantUsersService.updateMerchantUser(this.user.id, updateData)
@ -156,7 +193,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
next: (updatedUser) => { next: (updatedUser) => {
this.user = updatedUser; this.user = updatedUser;
this.updatingRole = false; this.updatingRole = false;
this.success = 'Profil mis à jour avec succès'; this.success = 'Rôle mis à jour avec succès';
this.cdRef.detectChanges(); this.cdRef.detectChanges();
}, },
error: (error) => { error: (error) => {
@ -169,7 +206,10 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
// Gestion du statut // Gestion du statut
enableUser() { enableUser() {
if (!this.user) return; if (!this.user || !this.canEnableDisableUser()) {
this.error = 'Vous n\'avez pas la permission d\'activer cet utilisateur';
return;
}
this.error = ''; this.error = '';
this.success = ''; this.success = '';
@ -179,7 +219,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
.subscribe({ .subscribe({
next: (updatedUser) => { next: (updatedUser) => {
this.user = updatedUser; this.user = updatedUser;
this.success = 'Utilisateur activé avec succès'; this.success = 'Utilisateur marchand activé avec succès';
this.cdRef.detectChanges(); this.cdRef.detectChanges();
}, },
error: (error) => { error: (error) => {
@ -191,7 +231,10 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
} }
disableUser() { disableUser() {
if (!this.user) return; if (!this.user || !this.canEnableDisableUser()) {
this.error = 'Vous n\'avez pas la permission de désactiver cet utilisateur';
return;
}
this.error = ''; this.error = '';
this.success = ''; this.success = '';
@ -201,7 +244,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
.subscribe({ .subscribe({
next: (updatedUser) => { next: (updatedUser) => {
this.user = updatedUser; this.user = updatedUser;
this.success = 'Utilisateur désactivé avec succès'; this.success = 'Utilisateur marchand désactivé avec succès';
this.cdRef.detectChanges(); this.cdRef.detectChanges();
}, },
error: (error) => { error: (error) => {
@ -214,11 +257,98 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
// Réinitialisation du mot de passe // Réinitialisation du mot de passe
resetPassword() { resetPassword() {
if (this.user) { if (this.user && this.canResetPassword()) {
this.openResetPasswordModal.emit(this.user.id); this.openResetPasswordModal.emit(this.user.id);
} }
} }
// ==================== VÉRIFICATIONS DE PERMISSIONS ====================
/**
* Vérifie si l'utilisateur peut éditer cet utilisateur
*/
canEditUser(): boolean {
// Seul DCB_PARTNER peut éditer tous les utilisateurs marchands
if (this.currentUserRole === UserRole.DCB_PARTNER) {
return true;
}
// Les administrateurs marchands peuvent éditer les utilisateurs de leur partenaire
if (this.currentUserRole === UserRole.DCB_PARTNER_ADMIN) {
const currentMerchantPartnerId = this.authService.getCurrentMerchantPartnerId();
return this.user?.merchantPartnerId === currentMerchantPartnerId;
}
return false;
}
/**
* Vérifie si l'utilisateur peut gérer les rôles
*/
canManageRoles(): boolean {
// SEUL DCB_PARTNER peut modifier les rôles des utilisateurs marchands
return this.currentUserRole === UserRole.DCB_PARTNER;
}
/**
* Vérifie si l'utilisateur peut activer/désactiver cet utilisateur
*/
canEnableDisableUser(): boolean {
// Empêcher la désactivation de soi-même
if (this.isCurrentUserProfile()) {
return false;
}
// Seul DCB_PARTNER peut activer/désactiver les utilisateurs marchands
if (this.currentUserRole === UserRole.DCB_PARTNER) {
return true;
}
// Les administrateurs marchands peuvent gérer les utilisateurs de leur partenaire
if (this.currentUserRole === UserRole.DCB_PARTNER_ADMIN) {
const currentMerchantPartnerId = this.authService.getCurrentMerchantPartnerId();
return this.user?.merchantPartnerId === currentMerchantPartnerId;
}
return false;
}
/**
* Vérifie si l'utilisateur peut réinitialiser le mot de passe
*/
canResetPassword(): boolean {
// DCB_PARTNER peut réinitialiser tous les mots de passe
if (this.currentUserRole === UserRole.DCB_PARTNER) {
return true;
}
// Les administrateurs marchands peuvent réinitialiser les mots de passe de leur partenaire
if (this.currentUserRole === UserRole.DCB_PARTNER_ADMIN) {
const currentMerchantPartnerId = this.authService.getCurrentMerchantPartnerId();
return this.user?.merchantPartnerId === currentMerchantPartnerId;
}
// Les utilisateurs peuvent réinitialiser leur propre mot de passe
if (this.isCurrentUserProfile()) {
return true;
}
return false;
}
/**
* Vérifie si l'utilisateur peut supprimer cet utilisateur
*/
canDeleteUser(): boolean {
// Empêcher la suppression de soi-même
if (this.isCurrentUserProfile()) {
return false;
}
// Seul DCB_PARTNER peut supprimer les utilisateurs marchands
return this.currentUserRole === UserRole.DCB_PARTNER;
}
// ==================== UTILITAIRES D'AFFICHAGE ==================== // ==================== UTILITAIRES D'AFFICHAGE ====================
getStatusBadgeClass(): string { getStatusBadgeClass(): string {
@ -252,7 +382,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
} }
getUserDisplayName(): string { getUserDisplayName(): string {
if (!this.user) return 'Utilisateur'; if (!this.user) return 'Utilisateur Marchand';
if (this.user.firstName && this.user.lastName) { if (this.user.firstName && this.user.lastName) {
return `${this.user.firstName} ${this.user.lastName}`; return `${this.user.firstName} ${this.user.lastName}`;
} }
@ -260,42 +390,19 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
} }
getRoleBadgeClass(role: UserRole): string { getRoleBadgeClass(role: UserRole): string {
switch (role) { return this.roleService.getRoleBadgeClass(role);
case UserRole.DCB_PARTNER_ADMIN:
return 'bg-danger';
case UserRole.DCB_PARTNER_MANAGER:
return 'bg-warning text-dark';
case UserRole.DCB_PARTNER_SUPPORT:
return 'bg-info text-white';
default:
return 'bg-secondary';
}
} }
getRoleDisplayName(role: UserRole): string { getRoleDisplayName(role: UserRole): string {
const roleNames = { return this.roleService.getRoleLabel(role);
[UserRole.DCB_PARTNER_ADMIN]: 'Administrateur',
[UserRole.DCB_PARTNER_MANAGER]: 'Manager',
[UserRole.DCB_PARTNER_SUPPORT]: 'Support'
};
return roleNames[role] || role;
} }
getRoleIcon(role: UserRole): string { getRoleIcon(role: UserRole): string {
switch (role) { return this.roleService.getRoleIcon(role);
case UserRole.DCB_PARTNER_ADMIN:
return 'lucideShield';
case UserRole.DCB_PARTNER_MANAGER:
return 'lucideUserCog';
case UserRole.DCB_PARTNER_SUPPORT:
return 'lucideHeadphones';
default:
return 'lucideUser';
}
} }
getRoleDescription(role: UserRole): string { getRoleDescription(role: UserRole): string {
const descriptions = { const descriptions: { [key in UserRole]?: string } = {
[UserRole.DCB_PARTNER_ADMIN]: 'Accès administratif complet au sein du partenaire marchand', [UserRole.DCB_PARTNER_ADMIN]: 'Accès administratif complet au sein du partenaire marchand',
[UserRole.DCB_PARTNER_MANAGER]: 'Accès de gestion avec capacités administratives limitées', [UserRole.DCB_PARTNER_MANAGER]: 'Accès de gestion avec capacités administratives limitées',
[UserRole.DCB_PARTNER_SUPPORT]: 'Rôle support avec accès en lecture seule et opérations de base' [UserRole.DCB_PARTNER_SUPPORT]: 'Rôle support avec accès en lecture seule et opérations de base'
@ -304,28 +411,13 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
} }
getUserType(): string { getUserType(): string {
if (!this.user) return 'Utilisateur'; if (!this.user) return 'Utilisateur Marchand';
return this.roleService.getRoleLabel(this.user.role);
switch (this.user.role) {
case UserRole.DCB_PARTNER_ADMIN:
return 'Administrateur';
case UserRole.DCB_PARTNER_MANAGER:
return 'Manager';
case UserRole.DCB_PARTNER_SUPPORT:
return 'Support';
default:
return 'Utilisateur';
}
} }
getUserTypeBadgeClass(): string { getUserTypeBadgeClass(): string {
const userType = this.getUserType(); if (!this.user) return 'bg-secondary';
switch (userType) { return this.roleService.getRoleBadgeClass(this.user.role);
case 'Administrateur': return 'bg-danger';
case 'Manager': return 'bg-success';
case 'Support': return 'bg-info';
default: return 'bg-secondary';
}
} }
// ==================== GESTION DES ERREURS ==================== // ==================== GESTION DES ERREURS ====================
@ -341,7 +433,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
return 'Vous n\'avez pas les permissions pour effectuer cette action.'; return 'Vous n\'avez pas les permissions pour effectuer cette action.';
} }
if (error.status === 404) { if (error.status === 404) {
return 'Utilisateur non trouvé.'; return 'Utilisateur marchand non trouvé.';
} }
if (error.status === 409) { if (error.status === 409) {
return 'Conflit de données. Cet utilisateur existe peut-être déjà.'; return 'Conflit de données. Cet utilisateur existe peut-être déjà.';
@ -349,23 +441,6 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
return 'Erreur lors de l\'opération. Veuillez réessayer.'; return 'Erreur lors de l\'opération. Veuillez réessayer.';
} }
// ==================== VÉRIFICATIONS DE PERMISSIONS ====================
canEditUser(): boolean {
// Logique pour déterminer si l'utilisateur connecté peut éditer cet utilisateur
return true; // Temporaire - à implémenter
}
canManageRoles(): boolean {
// Logique pour déterminer si l'utilisateur connecté peut gérer les rôles
return true; // Temporaire - à implémenter
}
canEnableDisableUser(): boolean {
// Empêcher la désactivation de soi-même
return this.user?.id !== 'current-user-id';
}
// ==================== MÉTHODES DE NAVIGATION ==================== // ==================== MÉTHODES DE NAVIGATION ====================
goBack() { goBack() {
@ -415,9 +490,8 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
// Vérifie si c'est le profil de l'utilisateur courant // Vérifie si c'est le profil de l'utilisateur courant
isCurrentUserProfile(): boolean { isCurrentUserProfile(): boolean {
// Implémentez cette logique selon votre système d'authentification if (!this.user?.id) return false;
// Exemple: return this.authService.getCurrentUserId() === this.user?.id; return this.authService.isCurrentUserProfile(this.user.id);
return false;
} }
// Méthode pour obtenir la date de création formatée // Méthode pour obtenir la date de création formatée
@ -437,4 +511,39 @@ export class MerchantUserProfile implements OnInit, OnDestroy {
if (!this.user?.createdByUsername) return 'Non disponible'; if (!this.user?.createdByUsername) return 'Non disponible';
return this.user.createdByUsername; return this.user.createdByUsername;
} }
// Méthode pour obtenir l'ID du partenaire marchand
getMerchantPartnerId(): string {
return this.user?.merchantPartnerId || 'Non disponible';
}
// Vérifie si l'utilisateur a accès à ce profil marchand
canAccessMerchantProfile(): boolean {
if (!this.user) return false;
// DCB_PARTNER peut accéder à tous les profils marchands
if (this.currentUserRole === UserRole.DCB_PARTNER) {
return true;
}
const currentMerchantPartnerId = this.authService.getCurrentMerchantPartnerId();
// Les utilisateurs marchands ne peuvent voir que les utilisateurs de leur partenaire
if (this.authService.isMerchantUser()) {
return this.user.merchantPartnerId === currentMerchantPartnerId;
}
return false;
}
// Affiche le statut des permissions
getPermissionStatus(): string {
if (this.currentUserRole === UserRole.DCB_PARTNER) {
return 'DCB Partner - Accès complet';
} else if (this.currentUserRole === UserRole.DCB_PARTNER_ADMIN) {
return 'Admin Partenaire - Accès limité à votre partenaire';
} else {
return 'Permissions limitées';
}
}
} }

View File

@ -3,130 +3,35 @@ import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from '@environments/environment'; import { environment } from '@environments/environment';
import { Observable, map, catchError, throwError, of } from 'rxjs'; import { Observable, map, catchError, throwError, of } from 'rxjs';
// Interfaces alignées avec le contrôleur MerchantUsersController import {
export interface MerchantUserResponse { MerchantUserDto,
id: string; CreateUserDto,
username: string; UpdateUserDto,
email: string; ResetPasswordDto,
firstName: string; PaginatedUserResponse,
lastName: string; MerchantPartnerStatsResponse,
role: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT; AvailableRolesResponse,
enabled: boolean; SearchUsersParams,
emailVerified: boolean; UserRole,
merchantPartnerId: string; UserType
createdBy: string; } from '@core/models/dcb-bo-hub-user.model';
createdByUsername: string;
createdTimestamp: number;
lastLogin?: number;
userType: 'MERCHANT';
}
export interface CreateMerchantUserDto {
username: string;
email: string;
firstName: string;
lastName: string;
password: string;
role: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT;
enabled?: boolean;
emailVerified?: boolean;
merchantPartnerId: string;
}
export interface UpdateMerchantUserDto {
firstName?: string;
lastName?: string;
email?: string;
enabled?: boolean;
}
export interface ResetPasswordDto {
newPassword: string;
temporary?: boolean;
}
export interface MerchantPartnerStatsResponse {
totalAdmins: number;
totalManagers: number;
totalSupport: number;
totalUsers: number;
activeUsers: number;
inactiveUsers: number;
}
export interface AvailableRole {
value: UserRole;
label: string;
description: string;
allowedForCreation: boolean;
}
export interface AvailableRolesResponse {
roles: AvailableRole[];
}
export interface SearchMerchantUsersParams {
query?: string;
role?: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT;
enabled?: boolean;
}
export enum UserRole {
DCB_PARTNER_ADMIN = 'DCB_PARTNER_ADMIN',
DCB_PARTNER_MANAGER = 'DCB_PARTNER_MANAGER',
DCB_PARTNER_SUPPORT = 'DCB_PARTNER_SUPPORT'
}
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class MerchantUsersService { export class MerchantUsersService {
private http = inject(HttpClient); private http = inject(HttpClient);
private apiUrl = `${environment.iamApiUrl}/merchant-users`; private apiUrl = `${environment.iamApiUrl}/merchant-users`;
// === RÉCUPÉRATION D'UTILISATEURS === // === CRÉATION ===
/**
* Récupère les utilisateurs marchands de l'utilisateur courant
*/
getMyMerchantUsers(): Observable<MerchantUserResponse[]> {
return this.http.get<MerchantUserResponse[]>(this.apiUrl).pipe(
catchError(error => {
console.error('Error loading my merchant users:', error);
return throwError(() => error);
})
);
}
/**
* Récupère les utilisateurs marchands par ID de partenaire
*/
getMerchantUsersByPartner(partnerId: string): Observable<MerchantUserResponse[]> {
return this.http.get<MerchantUserResponse[]>(`${this.apiUrl}/partner/${partnerId}`).pipe(
catchError(error => {
console.error(`Error loading merchant users for partner ${partnerId}:`, error);
return throwError(() => error);
})
);
}
/**
* Récupère un utilisateur marchand par ID
*/
getMerchantUserById(id: string): Observable<MerchantUserResponse> {
return this.http.get<MerchantUserResponse>(`${this.apiUrl}/${id}`).pipe(
catchError(error => {
console.error(`Error loading merchant user ${id}:`, error);
return throwError(() => error);
})
);
}
// === CRÉATION D'UTILISATEURS ===
/** /**
* Crée un nouvel utilisateur marchand * Crée un nouvel utilisateur marchand
*/ */
createMerchantUser(createUserDto: CreateMerchantUserDto): Observable<MerchantUserResponse> { createMerchantUser(createUserDto: CreateUserDto): Observable<MerchantUserDto> {
// Validation // Validation spécifique aux marchands
if (!createUserDto.merchantPartnerId?.trim()) {
return throwError(() => 'Merchant Partner ID is required for merchant users');
}
if (!createUserDto.username?.trim()) { if (!createUserDto.username?.trim()) {
return throwError(() => 'Username is required and cannot be empty'); return throwError(() => 'Username is required and cannot be empty');
} }
@ -143,24 +48,25 @@ export class MerchantUsersService {
return throwError(() => 'Role is required'); return throwError(() => 'Role is required');
} }
if (!createUserDto.merchantPartnerId?.trim()) { // Vérification que le rôle est bien un rôle marchand
return throwError(() => 'Merchant Partner ID is required'); const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT];
if (!merchantRoles.includes(createUserDto.role)) {
return throwError(() => 'Invalid role for merchant user');
} }
// Nettoyage des données // Nettoyage des données
const payload = { const payload = {
...createUserDto,
username: createUserDto.username.trim(), username: createUserDto.username.trim(),
email: createUserDto.email.trim(), email: createUserDto.email.trim(),
firstName: (createUserDto.firstName || '').trim(), firstName: (createUserDto.firstName || '').trim(),
lastName: (createUserDto.lastName || '').trim(), lastName: (createUserDto.lastName || '').trim(),
password: createUserDto.password,
role: createUserDto.role,
merchantPartnerId: createUserDto.merchantPartnerId.trim(), merchantPartnerId: createUserDto.merchantPartnerId.trim(),
enabled: createUserDto.enabled !== undefined ? createUserDto.enabled : true, enabled: createUserDto.enabled !== undefined ? createUserDto.enabled : true,
emailVerified: createUserDto.emailVerified !== undefined ? createUserDto.emailVerified : false, emailVerified: createUserDto.emailVerified !== undefined ? createUserDto.emailVerified : false,
}; };
return this.http.post<MerchantUserResponse>(this.apiUrl, payload).pipe( return this.http.post<MerchantUserDto>(this.apiUrl, payload).pipe(
catchError(error => { catchError(error => {
console.error('Error creating merchant user:', error); console.error('Error creating merchant user:', error);
return throwError(() => error); return throwError(() => error);
@ -168,13 +74,88 @@ export class MerchantUsersService {
); );
} }
// === MISE À JOUR D'UTILISATEURS === // === LECTURE ===
/**
* Récupère les utilisateurs marchands de l'utilisateur courant
*/
getMyMerchantUsers(partnerId: string): Observable<MerchantUserDto[]> {
return this.http.get<MerchantUserDto[]>(this.apiUrl).pipe(
catchError(error => {
console.error('Error loading my merchant users:', error);
return throwError(() => error);
})
);
}
/**
* Récupère les utilisateurs marchands par ID de partenaire
*/
getMerchantUsersByPartner(partnerId: string): Observable<MerchantUserDto[]> {
return this.http.get<MerchantUserDto[]>(`${this.apiUrl}/partner/${partnerId}`).pipe(
catchError(error => {
console.error(`Error loading merchant users for partner ${partnerId}:`, error);
return throwError(() => error);
})
);
}
/**
* Récupère un utilisateur marchand par ID
*/
getMerchantUserById(id: string): Observable<MerchantUserDto> {
return this.http.get<MerchantUserDto>(`${this.apiUrl}/${id}`).pipe(
catchError(error => {
console.error(`Error loading merchant user ${id}:`, error);
return throwError(() => error);
})
);
}
/**
* Récupère tous les utilisateurs marchands avec pagination
*/
getMerchantUsers(page: number = 1, limit: number = 10, filters?: SearchUsersParams): Observable<PaginatedUserResponse> {
let params = new HttpParams()
.set('page', page.toString())
.set('limit', limit.toString())
.set('userType', UserType.MERCHANT);
if (filters) {
Object.keys(filters).forEach(key => {
if (filters[key as keyof SearchUsersParams] !== undefined && filters[key as keyof SearchUsersParams] !== null) {
params = params.set(key, filters[key as keyof SearchUsersParams]!.toString());
}
});
}
return this.http.get<MerchantUserDto[]>(this.apiUrl, { params, observe: 'response' }).pipe(
map(response => {
const users = response.body || [];
const total = parseInt(response.headers.get('X-Total-Count') || '0');
return {
users,
total,
page,
limit,
totalPages: Math.ceil(total / limit)
};
}),
catchError(error => {
console.error('Error loading merchant users:', error);
return throwError(() => error);
})
);
}
// === MISE À JOUR ===
/** /**
* Met à jour un utilisateur marchand * Met à jour un utilisateur marchand
*/ */
updateMerchantUser(id: string, updateUserDto: UpdateMerchantUserDto): Observable<MerchantUserResponse> { updateMerchantUser(id: string, updateUserDto: UpdateUserDto): Observable<MerchantUserDto> {
return this.http.put<MerchantUserResponse>(`${this.apiUrl}/${id}`, updateUserDto).pipe( return this.http.put<MerchantUserDto>(`${this.apiUrl}/${id}`, updateUserDto).pipe(
catchError(error => { catchError(error => {
console.error(`Error updating merchant user ${id}:`, error); console.error(`Error updating merchant user ${id}:`, error);
return throwError(() => error); return throwError(() => error);
@ -182,7 +163,7 @@ export class MerchantUsersService {
); );
} }
// === SUPPRESSION D'UTILISATEURS === // === SUPPRESSION ===
/** /**
* Supprime un utilisateur marchand * Supprime un utilisateur marchand
@ -213,12 +194,28 @@ export class MerchantUsersService {
); );
} }
// === STATISTIQUES ET RAPPORTS === // === GESTION DU STATUT ===
/**
* Active un utilisateur marchand
*/
enableMerchantUser(id: string): Observable<MerchantUserDto> {
return this.updateMerchantUser(id, { enabled: true });
}
/**
* Désactive un utilisateur marchand
*/
disableMerchantUser(id: string): Observable<MerchantUserDto> {
return this.updateMerchantUser(id, { enabled: false });
}
// === STATISTIQUES ===
/** /**
* Récupère les statistiques des utilisateurs marchands * Récupère les statistiques des utilisateurs marchands
*/ */
getMerchantUsersStats(): Observable<MerchantPartnerStatsResponse> { getMerchantPartnerStats(): Observable<MerchantPartnerStatsResponse> {
return this.http.get<MerchantPartnerStatsResponse>(`${this.apiUrl}/stats/overview`).pipe( return this.http.get<MerchantPartnerStatsResponse>(`${this.apiUrl}/stats/overview`).pipe(
catchError(error => { catchError(error => {
console.error('Error loading merchant users stats:', error); console.error('Error loading merchant users stats:', error);
@ -227,13 +224,13 @@ export class MerchantUsersService {
); );
} }
// === RECHERCHE ET FILTRES === // === RECHERCHE ===
/** /**
* Recherche des utilisateurs marchands avec filtres * Recherche des utilisateurs marchands avec filtres
*/ */
searchMerchantUsers(params: SearchMerchantUsersParams): Observable<MerchantUserResponse[]> { searchMerchantUsers(params: SearchUsersParams): Observable<MerchantUserDto[]> {
let httpParams = new HttpParams(); let httpParams = new HttpParams().set('userType', UserType.MERCHANT);
if (params.query) { if (params.query) {
httpParams = httpParams.set('query', params.query); httpParams = httpParams.set('query', params.query);
@ -247,7 +244,7 @@ export class MerchantUsersService {
httpParams = httpParams.set('enabled', params.enabled.toString()); httpParams = httpParams.set('enabled', params.enabled.toString());
} }
return this.http.get<MerchantUserResponse[]>(`${this.apiUrl}/search`, { params: httpParams }).pipe( return this.http.get<MerchantUserDto[]>(`${this.apiUrl}/search`, { params: httpParams }).pipe(
catchError(error => { catchError(error => {
console.error('Error searching merchant users:', error); console.error('Error searching merchant users:', error);
return throwError(() => error); return throwError(() => error);
@ -264,7 +261,6 @@ export class MerchantUsersService {
return this.http.get<AvailableRolesResponse>(`${this.apiUrl}/roles/available`).pipe( return this.http.get<AvailableRolesResponse>(`${this.apiUrl}/roles/available`).pipe(
catchError(error => { catchError(error => {
console.error('Error loading available merchant roles:', error); console.error('Error loading available merchant roles:', error);
// Fallback en cas d'erreur
return of({ return of({
roles: [ roles: [
{ {
@ -291,29 +287,13 @@ export class MerchantUsersService {
); );
} }
// === GESTION DU STATUT ===
/**
* Active un utilisateur marchand
*/
enableMerchantUser(id: string): Observable<MerchantUserResponse> {
return this.updateMerchantUser(id, { enabled: true });
}
/**
* Désactive un utilisateur marchand
*/
disableMerchantUser(id: string): Observable<MerchantUserResponse> {
return this.updateMerchantUser(id, { enabled: false });
}
// === UTILITAIRES === // === UTILITAIRES ===
/** /**
* Vérifie si un nom d'utilisateur existe parmi les utilisateurs marchands * Vérifie si un nom d'utilisateur existe parmi les utilisateurs marchands
*/ */
merchantUserExists(username: string): Observable<{ exists: boolean }> { merchantUserExists(username: string): Observable<{ exists: boolean }> {
return this.getMyMerchantUsers().pipe( return this.searchMerchantUsers({ query: username }).pipe(
map(users => ({ map(users => ({
exists: users.some(user => user.username === username) exists: users.some(user => user.username === username)
})), })),
@ -327,21 +307,25 @@ export class MerchantUsersService {
/** /**
* Récupère les utilisateurs par rôle spécifique * Récupère les utilisateurs par rôle spécifique
*/ */
getMerchantUsersByRole(role: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT): Observable<MerchantUserResponse[]> { getMerchantUsersByRole(role: UserRole): Observable<MerchantUserDto[]> {
const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT];
if (!merchantRoles.includes(role)) {
return throwError(() => 'Invalid role for merchant user');
}
return this.searchMerchantUsers({ role }); return this.searchMerchantUsers({ role });
} }
/** /**
* Récupère uniquement les utilisateurs actifs * Récupère uniquement les utilisateurs actifs
*/ */
getActiveMerchantUsers(): Observable<MerchantUserResponse[]> { getActiveMerchantUsers(): Observable<MerchantUserDto[]> {
return this.searchMerchantUsers({ enabled: true }); return this.searchMerchantUsers({ enabled: true });
} }
/** /**
* Récupère uniquement les utilisateurs inactifs * Récupère uniquement les utilisateurs inactifs
*/ */
getInactiveMerchantUsers(): Observable<MerchantUserResponse[]> { getInactiveMerchantUsers(): Observable<MerchantUserDto[]> {
return this.searchMerchantUsers({ enabled: false }); return this.searchMerchantUsers({ enabled: false });
} }
} }

View File

@ -2,7 +2,7 @@ import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgIcon } from '@ng-icons/core'; import { NgIcon } from '@ng-icons/core';
import { UiCard } from '@app/components/ui-card'; import { UiCard } from '@app/components/ui-card';
import { MerchantPartnerStatsResponse } from '../services/merchant-partners.service'; import { MerchantPartnerStatsResponse } from '@core/models/dcb-bo-hub-user.model';
@Component({ @Component({
selector: 'app-merchant-users-stats', selector: 'app-merchant-users-stats',

View File

@ -2,13 +2,13 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { authGuard } from '../core/guards/auth.guard'; import { authGuard } from '../core/guards/auth.guard';
import { roleGuard } from '../core/guards/role.guard'; import { roleGuard } from '../core/guards/role.guard';
import { Users } from '@modules/users/users'; import { HubUsers } from '@modules/hub-users/hub-users';
// Composants principaux // Composants principaux
import { DcbDashboard } from './dcb-dashboard/dcb-dashboard'; import { DcbDashboard } from './dcb-dashboard/dcb-dashboard';
import { Team } from './team/team'; import { Team } from './team/team';
import { Transactions } from './transactions/transactions'; import { Transactions } from './transactions/transactions';
import { MerchantPartners } from './merchant-partners/merchant-partners'; import { MerchantUsers } from './merchant-users/merchant-users';
import { OperatorsConfig } from './operators/config/config'; import { OperatorsConfig } from './operators/config/config';
import { OperatorsStats } from './operators/stats/stats'; import { OperatorsStats } from './operators/stats/stats';
import { WebhooksHistory } from './webhooks/history/history'; import { WebhooksHistory } from './webhooks/history/history';
@ -77,7 +77,7 @@ const routes: Routes = [
{ {
path: 'users', path: 'users',
canActivate: [authGuard, roleGuard], canActivate: [authGuard, roleGuard],
component: Users, component: HubUsers,
data: { data: {
title: 'Gestion des Utilisateurs', title: 'Gestion des Utilisateurs',
module: 'users' module: 'users'
@ -89,7 +89,7 @@ const routes: Routes = [
// --------------------------- // ---------------------------
{ {
path: 'merchant-partners', path: 'merchant-partners',
component: MerchantPartners, component: MerchantUsers,
canActivate: [authGuard, roleGuard], canActivate: [authGuard, roleGuard],
data: { data: {
title: 'Gestion Partners/Marchants', title: 'Gestion Partners/Marchants',

View File

@ -5,10 +5,15 @@ import { FormsModule } from '@angular/forms';
import { NgIcon } from '@ng-icons/core'; import { NgIcon } from '@ng-icons/core';
import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap';
import { Subject, takeUntil } from 'rxjs'; import { Subject, takeUntil } from 'rxjs';
import { HubUsersService, UserRole, UpdateHubUserDto } from '../users/services/users.service'; import { HubUsersService } from '../hub-users/services/hub-users.service';
import { RoleManagementService } from '@core/services/role-management.service'; import { RoleManagementService } from '@core/services/role-management.service';
import { AuthService } from '@core/services/auth.service'; import { AuthService } from '@core/services/auth.service';
import {
UpdateUserDto,
UserRole
} from '@core/models/dcb-bo-hub-user.model';
@Component({ @Component({
selector: 'app-my-profile', selector: 'app-my-profile',
standalone: true, standalone: true,
@ -48,7 +53,7 @@ export class MyProfile implements OnInit, OnDestroy {
// Édition // Édition
isEditing = false; isEditing = false;
editedUser: UpdateHubUserDto = {}; editedUser: UpdateUserDto = {};
// Gestion des rôles (simplifiée pour profil personnel) // Gestion des rôles (simplifiée pour profil personnel)
availableRoles: { value: UserRole; label: string; description: string }[] = []; availableRoles: { value: UserRole; label: string; description: string }[] = [];
@ -73,7 +78,7 @@ export class MyProfile implements OnInit, OnDestroy {
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (profile) => { next: (profile) => {
this.currentUserRole = profile?.roles?.[0] as UserRole || null; this.currentUserRole = profile?.role?.[0] as UserRole || null;
// Pour le profil personnel, on peut toujours éditer son propre profil // Pour le profil personnel, on peut toujours éditer son propre profil
this.canEditUsers = true; this.canEditUsers = true;
this.canManageRoles = false; // On ne peut pas gérer les rôles de son propre profil this.canManageRoles = false; // On ne peut pas gérer les rôles de son propre profil
@ -89,15 +94,19 @@ export class MyProfile implements OnInit, OnDestroy {
* Charge les rôles disponibles (lecture seule pour profil personnel) * Charge les rôles disponibles (lecture seule pour profil personnel)
*/ */
private loadAvailableRoles(): void { private loadAvailableRoles(): void {
this.roleService.getAvailableRolesSimple() this.usersService.getAvailableHubRoles()
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (roles) => { next: (response) => {
this.availableRoles = roles; this.availableRoles = response.roles.map(role => ({
value: role.value,
label: role.label,
description: role.description
}));
}, },
error: (error) => { error: (error) => {
console.error('Error loading available roles:', error); console.error('Error loading available roles:', error);
// Fallback // Fallback avec tous les rôles
this.availableRoles = [ this.availableRoles = [
{ value: UserRole.DCB_ADMIN, label: 'DCB Admin', description: 'Administrateur système' }, { value: UserRole.DCB_ADMIN, label: 'DCB Admin', description: 'Administrateur système' },
{ value: UserRole.DCB_SUPPORT, label: 'DCB Support', description: 'Support technique' }, { value: UserRole.DCB_SUPPORT, label: 'DCB Support', description: 'Support technique' },
@ -158,7 +167,7 @@ export class MyProfile implements OnInit, OnDestroy {
this.error = ''; this.error = '';
this.success = ''; this.success = '';
this.usersService.updateUser(this.user.id, this.editedUser) this.usersService.updateHubUser(this.user.id, this.editedUser)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (updatedUser) => { next: (updatedUser) => {
@ -204,7 +213,7 @@ export class MyProfile implements OnInit, OnDestroy {
} }
} }
// Gestion des erreurs - même méthode que le premier composant // Gestion des erreurs
private getErrorMessage(error: any): string { private getErrorMessage(error: any): string {
if (error.error?.message) { if (error.error?.message) {
return error.error.message; return error.error.message;
@ -221,7 +230,7 @@ export class MyProfile implements OnInit, OnDestroy {
return 'Une erreur est survenue. Veuillez réessayer.'; return 'Une erreur est survenue. Veuillez réessayer.';
} }
// Utilitaires d'affichage - mêmes méthodes que le premier composant // Utilitaires d'affichage
getStatusBadgeClass(): string { getStatusBadgeClass(): string {
if (!this.user) return 'badge bg-secondary'; if (!this.user) return 'badge bg-secondary';
if (!this.user.enabled) return 'badge bg-danger'; if (!this.user.enabled) return 'badge bg-danger';
@ -279,11 +288,39 @@ export class MyProfile implements OnInit, OnDestroy {
// Vérification des permissions pour les actions - toujours false pour les actions sensibles // Vérification des permissions pour les actions - toujours false pour les actions sensibles
canAssignRole(targetRole: UserRole): boolean { canAssignRole(targetRole: UserRole): boolean {
return false; // Jamais autorisé pour le profil personnel return false;
} }
// Vérifie si c'est le profil de l'utilisateur courant - toujours true // Vérifie si c'est le profil de l'utilisateur courant - toujours true
isCurrentUserProfile(): boolean { isCurrentUserProfile(): boolean {
return true; // Toujours vrai pour le profil personnel return true;
}
// Méthode utilitaire pour déterminer le type d'utilisateur
getUserType(): string {
if (!this.currentUserRole) return 'Utilisateur';
const roleNames: { [key in UserRole]?: string } = {
[UserRole.DCB_ADMIN]: 'Administrateur DCB',
[UserRole.DCB_SUPPORT]: 'Support DCB',
[UserRole.DCB_PARTNER]: 'Partenaire DCB',
[UserRole.DCB_PARTNER_ADMIN]: 'Administrateur Partenaire',
[UserRole.DCB_PARTNER_MANAGER]: 'Manager Partenaire',
[UserRole.DCB_PARTNER_SUPPORT]: 'Support Partenaire'
};
return roleNames[this.currentUserRole] || this.currentUserRole;
}
// Vérifie si l'utilisateur est un utilisateur Hub
isHubUser(): boolean {
const hubRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT, UserRole.DCB_PARTNER];
return this.currentUserRole ? hubRoles.includes(this.currentUserRole) : false;
}
// Vérifie si l'utilisateur est un utilisateur marchand
isMerchantUser(): boolean {
const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT];
return this.currentUserRole ? merchantRoles.includes(this.currentUserRole) : false;
} }
} }

View File

@ -1,325 +0,0 @@
// src/app/modules/users/services/users.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from '@environments/environment';
import { Observable, map, catchError, throwError, of } from 'rxjs';
import { AvailableRolesResponse } from '@core/services/role-management.service';
// Interfaces alignées avec le contrôleur NestJS
export interface HubUserResponse {
id: string;
username: string;
email: string;
firstName: string;
lastName: string;
role: UserRole;
enabled: boolean;
emailVerified: boolean;
createdBy: string;
createdByUsername: string;
createdTimestamp: number;
lastLogin?: number;
userType: 'HUB';
}
export interface CreateHubUserDto {
username: string;
email: string;
firstName: string;
lastName: string;
password: string;
role: UserRole;
enabled?: boolean;
emailVerified?: boolean;
}
export interface UpdateHubUserDto {
firstName?: string;
lastName?: string;
email?: string;
enabled?: boolean;
}
export interface ResetPasswordDto {
newPassword: string;
temporary?: boolean;
}
export interface PaginatedUserResponse {
users: HubUserResponse[];
total: number;
page: number;
limit: number;
totalPages: number;
}
export enum UserRole {
DCB_ADMIN = 'dcb-admin',
DCB_SUPPORT = 'dcb-support',
DCB_PARTNER = 'dcb-partner',
DCB_PARTNER_ADMIN = 'dcb-partner-admin',
DCB_PARTNER_MANAGER = 'dcb-partner-manager',
DCB_PARTNER_SUPPORT = 'dcb-partner-support'
}
@Injectable({ providedIn: 'root' })
export class HubUsersService {
private http = inject(HttpClient);
private apiUrl = `${environment.iamApiUrl}/hub-users`;
// === CRUD COMPLET ===
/**
* Crée un nouvel utilisateur Hub
*/
createUser(createUserDto: CreateHubUserDto): Observable<HubUserResponse> {
// Validation
if (!createUserDto.username?.trim()) {
return throwError(() => 'Username is required and cannot be empty');
}
if (!createUserDto.email?.trim()) {
return throwError(() => 'Email is required and cannot be empty');
}
if (!createUserDto.password || createUserDto.password.length < 8) {
return throwError(() => 'Password must be at least 8 characters');
}
if (!createUserDto.role) {
return throwError(() => 'Role is required');
}
// Nettoyage des données
const payload = {
username: createUserDto.username.trim(),
email: createUserDto.email.trim(),
firstName: (createUserDto.firstName || '').trim(),
lastName: (createUserDto.lastName || '').trim(),
password: createUserDto.password,
role: createUserDto.role,
enabled: createUserDto.enabled !== undefined ? createUserDto.enabled : true,
emailVerified: createUserDto.emailVerified !== undefined ? createUserDto.emailVerified : false,
};
return this.http.post<HubUserResponse>(this.apiUrl, payload).pipe(
catchError(error => throwError(() => error))
);
}
/**
* Récupère tous les utilisateurs Hub avec pagination
*/
findAllUsers(page: number = 1, limit: number = 10, filters?: any): Observable<PaginatedUserResponse> {
let params = new HttpParams()
.set('page', page.toString())
.set('limit', limit.toString());
if (filters) {
Object.keys(filters).forEach(key => {
if (filters[key] !== undefined && filters[key] !== null) {
params = params.set(key, filters[key].toString());
}
});
}
return this.http.get<HubUserResponse[]>(this.apiUrl, { params, observe: 'response' }).pipe(
map(response => {
const users = response.body || [];
const total = parseInt(response.headers.get('X-Total-Count') || '0');
return {
users,
total,
page,
limit,
totalPages: Math.ceil(total / limit)
};
}),
catchError(error => {
console.error('Error loading users:', error);
return throwError(() => error);
})
);
}
/**
* Récupère tous les utilisateurs Hub avec pagination
*/
findAllMerchantUsers(page: number = 1, limit: number = 10, filters?: any): Observable<PaginatedUserResponse> {
let params = new HttpParams()
.set('page', page.toString())
.set('limit', limit.toString());
if (filters) {
Object.keys(filters).forEach(key => {
if (filters[key] !== undefined && filters[key] !== null) {
params = params.set(key, filters[key].toString());
}
});
}
return this.http.get<HubUserResponse[]>(`${this.apiUrl}//merchants/all`, { params, observe: 'response' }).pipe(
map(response => {
const users = response.body || [];
const total = parseInt(response.headers.get('X-Total-Count') || '0');
return {
users,
total,
page,
limit,
totalPages: Math.ceil(total / limit)
};
}),
catchError(error => {
console.error('Error loading users:', error);
return throwError(() => error);
})
);
}
/**
* Récupère un utilisateur Hub par ID
*/
getUserById(id: string): Observable<HubUserResponse> {
return this.http.get<HubUserResponse>(`${this.apiUrl}/${id}`);
}
/**
* Met à jour un utilisateur Hub
*/
updateUser(id: string, updateUserDto: UpdateHubUserDto): Observable<HubUserResponse> {
return this.http.put<HubUserResponse>(`${this.apiUrl}/${id}`, updateUserDto);
}
/**
* Supprime un utilisateur Hub
*/
deleteUser(id: string): Observable<{ message: string }> {
return this.http.delete<{ message: string }>(`${this.apiUrl}/${id}`);
}
// === GESTION DES MOTS DE PASSE ===
/**
* Réinitialise le mot de passe d'un utilisateur
*/
resetPassword(userId: string, newPassword: string, temporary: boolean = true): Observable<{ message: string }> {
return this.http.post<{ message: string }>(
`${this.apiUrl}/${userId}/reset-password`,
{ newPassword, temporary }
);
}
/**
* Envoie un email de réinitialisation de mot de passe
*/
sendPasswordResetEmail(userId: string): Observable<{ message: string }> {
return this.http.post<{ message: string }>(
`${this.apiUrl}/${userId}/send-reset-email`,
{}
);
}
// === GESTION DU STATUT ===
/**
* Active un utilisateur
*/
enableUser(id: string): Observable<HubUserResponse> {
return this.http.put<HubUserResponse>(`${this.apiUrl}/${id}`, { enabled: true });
}
/**
* Désactive un utilisateur
*/
disableUser(id: string): Observable<HubUserResponse> {
return this.http.put<HubUserResponse>(`${this.apiUrl}/${id}`, { enabled: false });
}
// === GESTION DES RÔLES ===
/**
* Met à jour le rôle d'un utilisateur
*/
updateUserRole(id: string, role: UserRole): Observable<HubUserResponse> {
return this.http.put<HubUserResponse>(`${this.apiUrl}/${id}/role`, { role });
}
/**
* Récupère les rôles Hub disponibles
*/
getAvailableHubRoles(): Observable<AvailableRolesResponse> {
return this.http.get<AvailableRolesResponse>(
`${this.apiUrl}/roles/available`
).pipe(
catchError(error => {
console.error('Error loading available roles:', error);
// Fallback en cas d'erreur
return of({
roles: [
{
value: UserRole.DCB_ADMIN,
label: 'DCB Admin',
description: 'Full administrative access to the entire system'
},
{
value: UserRole.DCB_SUPPORT,
label: 'DCB Support',
description: 'Support access with limited administrative capabilities'
},
{
value: UserRole.DCB_PARTNER,
label: 'DCB Partner',
description: 'Merchant partner with access to their own merchant ecosystem'
}
]
});
})
);
}
/**
* Récupère les utilisateurs par rôle
*/
getUsersByRole(role: UserRole): Observable<HubUserResponse[]> {
return this.http.get<HubUserResponse[]>(`${this.apiUrl}/role/${role}`);
}
// === STATISTIQUES ===
/**
* Récupère les statistiques des utilisateurs
*/
getUsersStats(): Observable<any> {
return this.http.get<any>(`${this.apiUrl}/stats/overview`);
}
// === UTILITAIRES ===
/**
* Vérifie si un nom d'utilisateur existe
*/
userExists(username: string): Observable<{ exists: boolean }> {
// Implémentation temporaire - à adapter selon votre API
return this.findAllUsers().pipe(
map(response => ({
exists: response.users.some(user => user.username === username)
}))
);
}
/**
* Recherche des utilisateurs
*/
searchUsers(query: string): Observable<HubUserResponse[]> {
return this.findAllUsers().pipe(
map(response => response.users.filter(user =>
user.username.toLowerCase().includes(query.toLowerCase()) ||
user.email.toLowerCase().includes(query.toLowerCase()) ||
user.firstName?.toLowerCase().includes(query.toLowerCase()) ||
user.lastName?.toLowerCase().includes(query.toLowerCase())
))
);
}
}

View File

@ -1,2 +0,0 @@
import { Users } from './users';
describe('Users', () => {});