From 191099d8a537bdbd4c2ea8959aef6b6383a40327 Mon Sep 17 00:00:00 2001 From: diallolatoile Date: Tue, 4 Nov 2025 21:06:17 +0000 Subject: [PATCH] feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature --- src/app/core/directive/has-role.directive.ts | 6 +- src/app/core/guards/auth.guard.ts | 41 +- src/app/core/guards/public.guard.ts | 15 +- src/app/core/guards/role.guard.ts | 2 +- src/app/core/interceptors/auth.interceptor.ts | 37 +- .../models/dcb-bo-hub-user.model.ts} | 101 +++-- src/app/core/services/auth.service.ts | 316 ++++++++++----- src/app/core/services/menu.service.ts | 2 +- src/app/core/services/permissions.service.ts | 179 ++------- .../core/services/role-management.service.ts | 169 ++++++-- .../user-profile/user-profile.component.ts | 2 +- .../components/user-profile/user-profile.html | 28 +- .../components/user-profile/user-profile.ts | 1 - .../users.html => hub-users/hub-users.html} | 45 ++- .../hub-users.routes.ts} | 4 +- src/app/modules/hub-users/hub-users.spec.ts | 2 + .../users.ts => hub-users/hub-users.ts} | 134 +++++-- .../{users => hub-users}/list/list.html | 0 .../{users => hub-users}/list/list.spec.ts | 0 .../modules/{users => hub-users}/list/list.ts | 127 ++++-- .../hub-users/models/hub-user.model.ts | 115 ++++++ .../models/hub-userl-v1.mode.ts} | 0 .../{users => hub-users}/models/user.model.ts | 0 .../{users => hub-users}/profile/profile.html | 0 .../profile/profile.spec.ts | 0 .../{users => hub-users}/profile/profile.ts | 81 ++-- .../services/api.service.ts | 0 .../hub-users/services/hub-users.service.ts | 361 +++++++++++++++++ .../{users => hub-users}/structure.txt | 0 .../merchant-partners.spec.ts | 2 - .../config/config.html | 0 .../config/config.spec.ts | 0 .../config/config.ts | 0 .../list/list.html | 0 .../list/list.spec.ts | 0 .../list/list.ts | 378 ++++++++++++------ .../merchant-users.html} | 152 ++++++- .../merchant-users/merchant-users.spec.ts | 2 + .../merchant-users.ts} | 364 ++++++++++++++--- .../models/merchant-user.model.ts | 115 ++++++ .../models/partners-config.model.ts | 0 .../profile/profile.html | 218 +++++++--- .../profile/profile.spec.ts | 0 .../profile/profile.ts | 287 ++++++++----- .../services/merchant-users.service.ts} | 284 +++++++------ .../services/partner-config.service.ts | 0 .../stats/stats.html | 0 .../stats/stats.spec.ts | 0 .../stats/stats.ts | 2 +- .../types.ts | 0 src/app/modules/modules.routes.ts | 8 +- src/app/modules/profile/profile.ts | 61 ++- .../modules/users/services/users.service.ts | 325 --------------- src/app/modules/users/users.spec.ts | 2 - 54 files changed, 2634 insertions(+), 1334 deletions(-) rename src/app/{modules/merchant-partners/models/merchant-user.model.ts => core/models/dcb-bo-hub-user.model.ts} (54%) rename src/app/modules/{users/users.html => hub-users/hub-users.html} (93%) rename src/app/modules/{users/users.routes.ts => hub-users/hub-users.routes.ts} (84%) create mode 100644 src/app/modules/hub-users/hub-users.spec.ts rename src/app/modules/{users/users.ts => hub-users/hub-users.ts} (77%) rename src/app/modules/{users => hub-users}/list/list.html (100%) rename src/app/modules/{users => hub-users}/list/list.spec.ts (100%) rename src/app/modules/{users => hub-users}/list/list.ts (71%) create mode 100644 src/app/modules/hub-users/models/hub-user.model.ts rename src/app/modules/{users/models/hub-user.model.ts => hub-users/models/hub-userl-v1.mode.ts} (100%) rename src/app/modules/{users => hub-users}/models/user.model.ts (100%) rename src/app/modules/{users => hub-users}/profile/profile.html (100%) rename src/app/modules/{users => hub-users}/profile/profile.spec.ts (100%) rename src/app/modules/{users => hub-users}/profile/profile.ts (79%) rename src/app/modules/{users => hub-users}/services/api.service.ts (100%) create mode 100644 src/app/modules/hub-users/services/hub-users.service.ts rename src/app/modules/{users => hub-users}/structure.txt (100%) delete mode 100644 src/app/modules/merchant-partners/merchant-partners.spec.ts rename src/app/modules/{merchant-partners => merchant-users}/config/config.html (100%) rename src/app/modules/{merchant-partners => merchant-users}/config/config.spec.ts (100%) rename src/app/modules/{merchant-partners => merchant-users}/config/config.ts (100%) rename src/app/modules/{merchant-partners => merchant-users}/list/list.html (100%) rename src/app/modules/{merchant-partners => merchant-users}/list/list.spec.ts (100%) rename src/app/modules/{merchant-partners => merchant-users}/list/list.ts (54%) rename src/app/modules/{merchant-partners/merchant-partners.html => merchant-users/merchant-users.html} (74%) create mode 100644 src/app/modules/merchant-users/merchant-users.spec.ts rename src/app/modules/{merchant-partners/merchant-partners.ts => merchant-users/merchant-users.ts} (55%) create mode 100644 src/app/modules/merchant-users/models/merchant-user.model.ts rename src/app/modules/{merchant-partners => merchant-users}/models/partners-config.model.ts (100%) rename src/app/modules/{merchant-partners => merchant-users}/profile/profile.html (69%) rename src/app/modules/{merchant-partners => merchant-users}/profile/profile.spec.ts (100%) rename src/app/modules/{merchant-partners => merchant-users}/profile/profile.ts (60%) rename src/app/modules/{merchant-partners/services/merchant-partners.service.ts => merchant-users/services/merchant-users.service.ts} (64%) rename src/app/modules/{merchant-partners => merchant-users}/services/partner-config.service.ts (100%) rename src/app/modules/{merchant-partners => merchant-users}/stats/stats.html (100%) rename src/app/modules/{merchant-partners => merchant-users}/stats/stats.spec.ts (100%) rename src/app/modules/{merchant-partners => merchant-users}/stats/stats.ts (83%) rename src/app/modules/{merchant-partners => merchant-users}/types.ts (100%) delete mode 100644 src/app/modules/users/services/users.service.ts delete mode 100644 src/app/modules/users/users.spec.ts diff --git a/src/app/core/directive/has-role.directive.ts b/src/app/core/directive/has-role.directive.ts index 0beca08..fb4bc8d 100644 --- a/src/app/core/directive/has-role.directive.ts +++ b/src/app/core/directive/has-role.directive.ts @@ -1,6 +1,7 @@ import { Directive, Input, TemplateRef, ViewContainerRef, inject, OnDestroy } from '@angular/core'; import { AuthService } from '../services/auth.service'; import { Subscription } from 'rxjs'; +import { UserRole } from '@core/models/dcb-bo-hub-user.model'; @Directive({ selector: '[hasRole]', @@ -16,7 +17,10 @@ export class HasRoleDirective implements OnDestroy { const requiredRoles = Array.isArray(roles) ? roles : [roles]; 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) { this.viewContainer.createEmbeddedView(this.templateRef); diff --git a/src/app/core/guards/auth.guard.ts b/src/app/core/guards/auth.guard.ts index 95f40da..b4464c1 100644 --- a/src/app/core/guards/auth.guard.ts +++ b/src/app/core/guards/auth.guard.ts @@ -10,42 +10,43 @@ export const authGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: R const roleService = inject(RoleService); 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( switchMap(initialized => { if (!initialized) { return of(false); } - // Vérifier l'authentification + // 🔒 Étape 1 : Vérifier si déjà authentifié if (authService.isAuthenticated()) { return of(checkRoleAccess(route, roleService, router, state.url)); } - // Tentative de rafraîchissement du token + // 🔄 Étape 2 : Tenter un rafraîchissement du token s’il existe const refreshToken = authService.getRefreshToken(); if (refreshToken) { - - return authService.refreshToken().pipe( + return authService.refreshAccessToken().pipe( tap(() => { + // Recharger les rôles après un refresh réussi roleService.refreshRoles(); }), map(() => checkRoleAccess(route, roleService, router, state.url)), - catchError((error) => { + catchError(() => { + // En cas d’échec de refresh → déconnexion + redirection login authService.logout().subscribe(); return of(redirectToLogin(router, state.url)); }) ); } - // Redirection vers login + // 🚫 Étape 3 : Aucun token → redirection vers login return of(redirectToLogin(router, state.url)); }), - catchError(error => { + catchError(() => { return of(redirectToLogin(router, state.url)); }) ); -} +}; /** * Vérifie l'accès basé sur les rôles requis @@ -57,7 +58,8 @@ function checkRoleAccess( currentUrl: string ): boolean { const requiredRoles = route.data?.['roles'] as string[]; - + + // Si aucun rôle requis → accès autorisé if (!requiredRoles || requiredRoles.length === 0) { return true; } @@ -65,11 +67,12 @@ function checkRoleAccess( const hasRequiredRole = roleService.hasAnyRole(requiredRoles); const currentUserRoles = roleService.getCurrentUserRoles(); + // ✅ L’utilisateur possède un des rôles requis if (hasRequiredRole) { return true; } - // Rediriger vers la page non autorisée + // ❌ Sinon → rediriger vers une page "non autorisée" router.navigate(['/unauthorized'], { queryParams: { requiredRoles: requiredRoles.join(','), @@ -83,21 +86,13 @@ 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( - router: Router, - returnUrl: string, -): boolean { - const queryParams: any = { - returnUrl: returnUrl - }; - - // Message spécifique selon la raison +function redirectToLogin(router: Router, returnUrl: string): boolean { router.navigate(['/auth/login'], { - queryParams, + queryParams: { returnUrl }, replaceUrl: true }); return false; -} \ No newline at end of file +} diff --git a/src/app/core/guards/public.guard.ts b/src/app/core/guards/public.guard.ts index edece89..0cb3749 100644 --- a/src/app/core/guards/public.guard.ts +++ b/src/app/core/guards/public.guard.ts @@ -8,27 +8,28 @@ export const publicGuard: CanActivateFn = () => { const authService = inject(AuthService); 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()) { router.navigate(['/dcb-dashboard'], { replaceUrl: true }); return false; } - // Vérifier si un refresh token est disponible + // 🔄 Vérifier si un refresh token est disponible const refreshToken = authService.getRefreshToken(); if (refreshToken) { - return authService.refreshToken().pipe( + return authService.refreshAccessToken().pipe( map(() => { + // ✅ Rafraîchissement réussi → redirection vers le dashboard router.navigate(['/dcb-dashboard'], { replaceUrl: true }); return false; }), - catchError((error) => { - // En cas d'erreur, autoriser l'accès à la page publique + catchError(() => { + // ❌ Rafraîchissement échoué → autoriser l’accès à la page publique 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; -}; \ No newline at end of file +}; diff --git a/src/app/core/guards/role.guard.ts b/src/app/core/guards/role.guard.ts index bbf0a14..9ef29a3 100644 --- a/src/app/core/guards/role.guard.ts +++ b/src/app/core/guards/role.guard.ts @@ -21,7 +21,7 @@ export const roleGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state) = // Récupérer les rôles depuis le token const userRoles = authService.getCurrentUserRoles(); - + if (!userRoles || userRoles.length === 0) { router.navigate(['/unauthorized']); return false; diff --git a/src/app/core/interceptors/auth.interceptor.ts b/src/app/core/interceptors/auth.interceptor.ts index b5665f3..e574f23 100644 --- a/src/app/core/interceptors/auth.interceptor.ts +++ b/src/app/core/interceptors/auth.interceptor.ts @@ -9,26 +9,31 @@ export const authInterceptor: HttpInterceptorFn = (req, next) => { const authService = inject(AuthService); const router = inject(Router); - // Exclusion des endpoints d'authentification + // Exclure les requêtes d’authentification (login, refresh, logout) if (isAuthRequest(req)) { return next(req); } const token = authService.getAccessToken(); + // Si un token existe, l’ajouter aux requêtes API if (token && isApiRequest(req)) { const cloned = addToken(req, token); - + return next(cloned).pipe( catchError((error: HttpErrorResponse) => { + // Si le token est expiré → tentative de refresh if (error.status === 401 && !req.url.includes('/auth/refresh')) { return handle401Error(authService, router, req, next); } + + // Autres erreurs → propager return throwError(() => error); }) ); } + // Si pas de token → requête normale return next(req); }; @@ -42,35 +47,41 @@ function addToken(req: HttpRequest, token: string): HttpRequest { }); } +/** + * Gestion du renouvellement de token en cas d’erreur 401 + */ function handle401Error( authService: AuthService, router: Router, req: HttpRequest, next: HttpHandlerFn ) { - return authService.refreshToken().pipe( + return authService.refreshAccessToken().pipe( switchMap((response: LoginResponseDto) => { const newRequest = addToken(req, response.access_token); return next(newRequest); }), catchError((refreshError) => { - authService.logout().subscribe(); - router.navigate(['/auth/login']); + authService.logout().subscribe(() => { + router.navigate(['/auth/login'], { replaceUrl: true }); + }); return throwError(() => refreshError); }) ); } +/** + * Détecte si la requête cible une API de ton backend + */ function isApiRequest(req: HttpRequest): 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 à l’authentification + */ function isAuthRequest(req: HttpRequest): boolean { - const authEndpoints = [ - '/auth/login', - '/auth/refresh', - '/auth/logout' - ]; - + const authEndpoints = ['/auth/login', '/auth/refresh', '/auth/logout']; return authEndpoints.some(endpoint => req.url.includes(endpoint)); -} \ No newline at end of file +} diff --git a/src/app/modules/merchant-partners/models/merchant-user.model.ts b/src/app/core/models/dcb-bo-hub-user.model.ts similarity index 54% rename from src/app/modules/merchant-partners/models/merchant-user.model.ts rename to src/app/core/models/dcb-bo-hub-user.model.ts index 4ee1400..514749e 100644 --- a/src/app/modules/merchant-partners/models/merchant-user.model.ts +++ b/src/app/core/models/dcb-bo-hub-user.model.ts @@ -1,25 +1,62 @@ -export enum UserRole { - DCB_PARTNER_ADMIN = 'DCB_PARTNER_ADMIN', - DCB_PARTNER_MANAGER = 'DCB_PARTNER_MANAGER', - DCB_PARTNER_SUPPORT = 'DCB_PARTNER_SUPPORT', - DCB_PARTNER = 'DCB_PARTNER', - DCB_ADMIN = 'DCB_ADMIN', - DCB_SUPPORT = 'DCB_SUPPORT' +export enum UserType { + HUB = 'HUB', + MERCHANT = 'MERCHANT', + MERCHANT_USER = 'MERCHANT_USER' } -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; email: string; firstName: string; lastName: string; password: string; - role: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT; + role: UserRole; enabled?: boolean; emailVerified?: boolean; - merchantPartnerId: string; + merchantPartnerId?: string; // obligatoire si MERCHANT } -export interface UpdateMerchantUserDto { +export interface UpdateUserDto { firstName?: string; lastName?: string; email?: string; @@ -27,28 +64,21 @@ export interface UpdateMerchantUserDto { } export interface ResetPasswordDto { + userId?: string; newPassword: string; temporary?: boolean; } -export interface MerchantUserResponse { - id: string; - username: string; - email: string; - firstName: string; - lastName: string; - role: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT; - enabled: boolean; - emailVerified: boolean; - merchantPartnerId: string; - createdBy: string; - createdByUsername: string; - createdTimestamp: number; - lastLogin?: number; - userType: 'MERCHANT'; +// === PAGINATION / STATS === +export interface PaginatedUserResponse { + users: BaseUserDto[]; + total: number; + page: number; + limit: number; + totalPages: number; } -export interface MerchantUsersStatsResponse { +export interface MerchantPartnerStatsResponse { totalAdmins: number; totalManagers: number; totalSupport: number; @@ -57,6 +87,7 @@ export interface MerchantUsersStatsResponse { inactiveUsers: number; } +// === ROLES === export interface AvailableRole { value: UserRole; label: string; @@ -68,13 +99,15 @@ 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 interface RoleOperationResponse { message: string; success: boolean; -} \ No newline at end of file +} + +// === SEARCH === +export interface SearchUsersParams { + query?: string; + role?: UserRole; + enabled?: boolean; + userType?: UserType; +} diff --git a/src/app/core/services/auth.service.ts b/src/app/core/services/auth.service.ts index 8390064..d22b6ea 100644 --- a/src/app/core/services/auth.service.ts +++ b/src/app/core/services/auth.service.ts @@ -1,11 +1,19 @@ -// src/app/core/services/auth.service.ts import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Router } from '@angular/router'; 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 { username: string; password: string; @@ -31,23 +39,6 @@ export interface AuthStatusResponseDto { 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 { valid: boolean; user: { @@ -72,7 +63,7 @@ export class AuthService { private readonly refreshTokenKey = 'refresh_token'; private authState$ = new BehaviorSubject(this.isAuthenticated()); - private userProfile$ = new BehaviorSubject(null); + private userProfile$ = new BehaviorSubject(null); private initialized$ = new BehaviorSubject(false); // === INITIALISATION DE L'APPLICATION === @@ -95,8 +86,8 @@ export class AuthService { return refreshSuccess; } - // Token valide, vérifier le profil utilisateur - await this.loadUserProfile().toPromise(); + // Token valide : charger le profil utilisateur + await firstValueFrom(this.loadUserProfile()); this.authState$.next(true); this.initialized$.next(true); @@ -121,8 +112,9 @@ export class AuthService { } try { - // Convertir l'Observable en Promise pour l'initialisation - const response = await this.refreshToken().toPromise(); + const response = await firstValueFrom(this.refreshAccessToken()); + await firstValueFrom(this.loadUserProfile()); + this.authState$.next(true); return true; } catch (error) { this.clearAuthData(); @@ -137,7 +129,7 @@ export class AuthService { return this.initialized$.asObservable(); } - // === MÉTHODES EXISTANTES AVEC AMÉLIORATIONS === + // === MÉTHODES D'AUTHENTIFICATION === /** * Connexion utilisateur @@ -149,16 +141,16 @@ export class AuthService { ).pipe( tap(response => { this.handleLoginSuccess(response); - this.loadUserProfile().subscribe(); // Charger le profil après connexion + this.loadUserProfile().subscribe(); }), catchError(error => this.handleLoginError(error)) ); } /** - * Rafraîchissement du token + * Rafraîchissement du token d'accès */ - refreshToken(): Observable { + refreshAccessToken(): Observable { const refreshToken = this.getRefreshToken(); if (!refreshToken) { @@ -169,9 +161,7 @@ export class AuthService { `${environment.iamApiUrl}/auth/refresh`, { refresh_token: refreshToken } ).pipe( - tap(response => { - this.handleLoginSuccess(response); - }), + tap(response => this.handleLoginSuccess(response)), catchError(error => { this.clearAuthData(); return throwError(() => error); @@ -187,11 +177,9 @@ export class AuthService { `${environment.iamApiUrl}/auth/logout`, {} ).pipe( - tap(() => { - this.clearAuthData(); - }), + tap(() => this.clearAuthData()), catchError(error => { - this.clearAuthData(); // Nettoyer même en cas d'erreur + this.clearAuthData(); return throwError(() => error); }) ); @@ -200,22 +188,17 @@ export class AuthService { /** * Chargement du profil utilisateur */ - loadUserProfile(): Observable { - return this.http.get( + loadUserProfile(): Observable { + return this.http.get( `${environment.iamApiUrl}/auth/profile` ).pipe( - tap(profile => { - this.userProfile$.next(profile); - }), - catchError(error => { - return throwError(() => error); - }) + tap(profile => this.userProfile$.next(profile)), + catchError(error => throwError(() => error)) ); } - /** - * Gestion de la connexion réussie - */ + // === GESTION DE SESSION === + private handleLoginSuccess(response: LoginResponseDto): void { if (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 { localStorage.removeItem(this.tokenKey); localStorage.removeItem(this.refreshTokenKey); @@ -238,9 +218,8 @@ export class AuthService { this.userProfile$.next(null); } - /** - * Validation du token - */ + // === VALIDATION DU TOKEN === + validateToken(): Observable { return this.http.get( `${environment.iamApiUrl}/auth/validate` @@ -253,70 +232,193 @@ export class AuthService { return this.authState$.asObservable(); } - getUserProfile(): Observable { + getUserProfile(): Observable { return this.userProfile$.asObservable(); } - /** - * Récupère les rôles de l'utilisateur courant - */ -getCurrentUserRoles(): string[] { - const token = this.getAccessToken(); - if (!token) return []; + // === GESTION DES RÔLES ET TYPES === - try { - const payload = JSON.parse(atob(token.split('.')[1])); - const decoded: any = payload; - - // Récupérer tous les rôles de tous les clients - if (decoded.resource_access) { - const allRoles: string[] = []; + getCurrentUserRoles(): UserRole[] { + const token = this.getAccessToken(); + if (!token) return []; + + try { + const payload = JSON.parse(atob(token.split('.')[1])); - Object.values(decoded.resource_access).forEach((client: any) => { - if (client?.roles) { - allRoles.push(...client.roles); - } - }); - - return [...new Set(allRoles)]; + // Mapping des rôles Keycloak vers vos rôles DCB + const roleMappings: { [key: string]: UserRole } = { + // Rôles administrateur + 'admin': UserRole.DCB_ADMIN, + 'dcb-admin': UserRole.DCB_ADMIN, + 'administrator': UserRole.DCB_ADMIN, + + // 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) { + allRoles = allRoles.concat(client.roles); + } + }); + } + + if (payload.realm_access?.roles) { + 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 []; - } catch { - return []; } -} -/** - * Observable de l'état d'authentification - */ -onAuthState(): Observable { - return this.authState$.asObservable(); -} + /** + * Récupère le rôle principal de l'utilisateur courant + */ + getCurrentUserRole(): UserRole | null { + const roles = this.getCurrentUserRoles(); + return roles.length > 0 ? roles[0] : null; + } -/** - * Récupère le profil utilisateur - */ -getProfile(): Observable { - return this.getUserProfile(); -} + /** + * Récupère le type d'utilisateur courant + */ + getCurrentUserType(): UserType | null { + const role = this.getCurrentUserRole(); + if (!role) return null; -/** - * Vérifie si l'utilisateur a un rôle spécifique - */ -hasRole(role: string): boolean { - return this.getCurrentUserRoles().includes(role); -} + // 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]; -/** - * Vérifie si l'utilisateur a un des rôles spécifiés - */ -hasAnyRole(roles: string[]): boolean { - const userRoles = this.getCurrentUserRoles(); - return roles.some(role => userRoles.includes(role)); -} + if (hubRoles.includes(role)) { + return UserType.HUB; + } else if (merchantRoles.includes(role)) { + return UserType.MERCHANT; + } - // === GETTERS POUR LES TOKENS === + return null; + } + + /** + * Vérifie si l'utilisateur courant est un utilisateur Hub + */ + 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); + } + + /** + * Vérifie si l'utilisateur courant a au moins un des rôles spécifiés + */ + hasAnyRole(roles: UserRole[]): boolean { + const userRoles = this.getCurrentUserRoles(); + return roles.some(role => userRoles.includes(role)); + } + + /** + * 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 { + return this.authState$.asObservable(); + } + + getProfile(): Observable { + 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 { return localStorage.getItem(this.tokenKey); @@ -326,7 +428,7 @@ hasAnyRole(roles: string[]): boolean { return localStorage.getItem(this.refreshTokenKey); } - // === METHODES PRIVEES === + // === GESTION DES ERREURS === private handleLoginError(error: HttpErrorResponse): Observable { let errorMessage = 'Login failed'; @@ -342,7 +444,7 @@ hasAnyRole(roles: string[]): boolean { return throwError(() => new Error(errorMessage)); } - // === VERIFICATIONS D'ETAT === + // === VERIFICATIONS === isAuthenticated(): boolean { const token = this.getAccessToken(); diff --git a/src/app/core/services/menu.service.ts b/src/app/core/services/menu.service.ts index c93252e..745285b 100644 --- a/src/app/core/services/menu.service.ts +++ b/src/app/core/services/menu.service.ts @@ -135,7 +135,7 @@ export class MenuService { { label: 'Déconnexion', icon: 'tablerLogout2', - class: 'fw-semibold text-danger' + url: '/auth/logout', }, ]; } diff --git a/src/app/core/services/permissions.service.ts b/src/app/core/services/permissions.service.ts index 53708be..99e5e2b 100644 --- a/src/app/core/services/permissions.service.ts +++ b/src/app/core/services/permissions.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@angular/core'; +import { UserRole } from '@core/models/dcb-bo-hub-user.model'; export interface ModulePermission { module: string; @@ -9,193 +10,105 @@ export interface ModulePermission { @Injectable({ providedIn: 'root' }) export class PermissionsService { private readonly permissions: ModulePermission[] = [ - // Dashboard + // Dashboard - Tout le monde { module: 'dcb-dashboard', - roles: [ - 'dcb-admin', - 'dcb-partner', - 'dcb-support', - 'dcb-partner-admin', - 'dcb-partner-manager', - 'dcb-partner-support' - ], + roles: this.allRoles, }, - { module: 'auth', - roles: [ - 'dcb-admin', - 'dcb-partner', - 'dcb-support', - 'dcb-partner-admin', - 'dcb-partner-manager', - 'dcb-partner-support' - ], + roles: this.allRoles, }, - - // Transactions { module: 'transactions', - roles: [ - 'dcb-admin', - 'dcb-partner', - 'dcb-support', - 'dcb-partner-admin', - 'dcb-partner-manager', - 'dcb-partner-support' - ], + roles: this.allRoles, }, - - // Merchants/Partners { module: 'merchant-partners', - roles: [ - 'dcb-admin', - 'dcb-partner', - 'dcb-support', - 'dcb-partner-admin', - 'dcb-partner-manager', - 'dcb-partner-support'], + roles: this.allRoles, }, - - // Operators (Admin only) + // Operators - Admin seulement { module: 'operators', - roles: ['dcb-admin'], + roles: [UserRole.DCB_ADMIN], children: { - 'config': ['dcb-admin'], - 'stats': ['dcb-admin'] + 'config': [UserRole.DCB_ADMIN], + 'stats': [UserRole.DCB_ADMIN] } }, - - // Webhooks + // Webhooks - Admin et Partner { module: 'webhooks', - roles: ['dcb-admin', 'dcb-partner'], + roles: [UserRole.DCB_ADMIN, UserRole.DCB_PARTNER], children: { - 'history': ['dcb-admin', 'dcb-partner'], - 'status': ['dcb-admin', 'dcb-partner'], - 'retry': ['dcb-admin'] + 'history': [UserRole.DCB_ADMIN, UserRole.DCB_PARTNER], + 'status': [UserRole.DCB_ADMIN, UserRole.DCB_PARTNER], + 'retry': [UserRole.DCB_ADMIN] } }, - - // Users (Admin only) + // Users - Admin et Support { module: 'users', - roles: ['dcb-admin', 'dcb-support'] + roles: [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT] }, - - // Support (All authenticated users) + // Settings - Tout le monde { module: 'settings', - roles: [ - 'dcb-admin', - 'dcb-partner', - 'dcb-support', - 'dcb-partner-admin', - 'dcb-partner-manager', - 'dcb-partner-support' - ] + roles: this.allRoles }, - - // Integrations (Admin only) + // Integrations - Admin seulement { module: 'integrations', - roles: ['dcb-admin'] + roles: [UserRole.DCB_ADMIN] }, - - // Support (All authenticated users) + // Modules publics - Tout le monde { module: 'support', - roles: [ - 'dcb-admin', - 'dcb-partner', - 'dcb-support', - 'dcb-partner-admin', - 'dcb-partner-manager', - 'dcb-partner-support' - ] + roles: this.allRoles }, - - // Profile (All authenticated users) { module: 'profile', - roles: [ - 'dcb-admin', - 'dcb-partner', - 'dcb-support', - 'dcb-partner-admin', - 'dcb-partner-manager', - 'dcb-partner-support' - ] + roles: this.allRoles }, - - // Documentation (All authenticated users) { module: 'documentation', - roles: [ - 'dcb-admin', - 'dcb-partner', - 'dcb-support', - 'dcb-partner-admin', - 'dcb-partner-manager', - 'dcb-partner-support' - ] + roles: this.allRoles }, - - // Help (All authenticated users) { module: 'help', - roles: [ - 'dcb-admin', - 'dcb-partner', - 'dcb-support', - 'dcb-partner-admin', - 'dcb-partner-manager', - 'dcb-partner-support' - ] + roles: this.allRoles }, - - // About (All authenticated users) { module: 'about', - roles: [ - 'dcb-admin', - 'dcb-partner', - 'dcb-support', - 'dcb-partner-admin', - 'dcb-partner-manager', - 'dcb-partner-support' - ] + roles: this.allRoles } ]; + // Tous les rôles DCB + private get allRoles(): string[] { + return Object.values(UserRole); + } + canAccessModule(modulePath: string, userRoles: string[]): boolean { - if (!userRoles || userRoles.length === 0) { - return false; - } + if (!userRoles?.length) return false; const [mainModule, subModule] = modulePath.split('/'); const permission = this.findPermission(mainModule); if (!permission) { - console.warn(`No permission configuration for module: ${mainModule}`); + console.warn(`No permission for module: ${mainModule}`); return false; } - // Check main module access - const hasModuleAccess = this.hasAnyRole(permission.roles, userRoles); - if (!hasModuleAccess) return false; + // Vérifier accès module principal + const hasMainAccess = this.hasAnyRole(permission.roles, userRoles); + if (!hasMainAccess) return false; - // Check sub-module access if specified + // Vérifier sous-module si nécessaire if (subModule && permission.children) { - const subModuleRoles = permission.children[subModule]; - if (!subModuleRoles) { - console.warn(`No permission configuration for submodule: ${mainModule}/${subModule}`); - return false; - } - return this.hasAnyRole(subModuleRoles, userRoles); + const subRoles = permission.children[subModule]; + if (!subRoles) return false; + return this.hasAnyRole(subRoles, userRoles); } return true; @@ -206,12 +119,8 @@ export class PermissionsService { } private hasAnyRole(requiredRoles: string[], userRoles: string[]): boolean { - return requiredRoles.some(role => userRoles.includes(role)); - } - - getAccessibleModules(userRoles: string[]): string[] { - return this.permissions - .filter(permission => this.hasAnyRole(permission.roles, userRoles)) - .map(permission => permission.module); + return requiredRoles.some(required => + userRoles.some(user => user.toLowerCase() === required.toLowerCase()) + ); } } \ No newline at end of file diff --git a/src/app/core/services/role-management.service.ts b/src/app/core/services/role-management.service.ts index 42ee966..45d5032 100644 --- a/src/app/core/services/role-management.service.ts +++ b/src/app/core/services/role-management.service.ts @@ -1,7 +1,7 @@ -// src/app/core/services/role-management.service.ts 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 { UserRole } from '@core/models/dcb-bo-hub-user.model'; export interface RolePermission { canCreateUsers: boolean; @@ -13,6 +13,7 @@ export interface RolePermission { canAccessAdmin: boolean; canAccessSupport: boolean; canAccessPartner: boolean; + assignableRoles: UserRole[]; // Ajout de cette propriété } // Interface simplifiée pour la réponse API @@ -20,6 +21,7 @@ export interface AvailableRoleResponse { value: UserRole; label: string; description: string; + allowedForCreation?: boolean; } export interface AvailableRolesResponse { @@ -85,6 +87,24 @@ export class RoleManagementService { label: 'DCB Partner', description: 'Merchant partner with access to their own merchant ecosystem', 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 => ({ value: role.value, 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 */ @@ -139,6 +158,10 @@ export class RoleManagementService { * Récupère les permissions détaillées selon le rôle */ 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) { case UserRole.DCB_ADMIN: return { @@ -150,10 +173,53 @@ export class RoleManagementService { canManageMerchants: true, canAccessAdmin: true, canAccessSupport: true, - canAccessPartner: true + canAccessPartner: true, + assignableRoles: allRoles }; 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 { canCreateUsers: true, canEditUsers: true, @@ -162,21 +228,23 @@ export class RoleManagementService { canViewStats: true, canManageMerchants: true, canAccessAdmin: false, - canAccessSupport: true, - canAccessPartner: true + canAccessSupport: false, + canAccessPartner: true, + assignableRoles: [] }; - case UserRole.DCB_PARTNER: + case UserRole.DCB_PARTNER_SUPPORT: return { canCreateUsers: false, canEditUsers: false, canDeleteUsers: false, canManageRoles: false, - canViewStats: false, + canViewStats: true, canManageMerchants: false, canAccessAdmin: false, canAccessSupport: false, - canAccessPartner: true + canAccessPartner: false, + assignableRoles: [] }; default: @@ -185,11 +253,12 @@ export class RoleManagementService { canEditUsers: false, canDeleteUsers: false, canManageRoles: false, - canViewStats: false, - canManageMerchants: false, + canViewStats: true, + canManageMerchants: true, canAccessAdmin: false, canAccessSupport: false, - canAccessPartner: false + canAccessPartner: false, + assignableRoles: [] }; } } @@ -200,17 +269,14 @@ export class RoleManagementService { canAssignRole(currentUserRole: UserRole | null, targetRole: UserRole): boolean { if (!currentUserRole) return false; - // Seuls les admins peuvent attribuer tous les rôles - if (currentUserRole === UserRole.DCB_ADMIN) { + // SEUL DCB_PARTNER peut attribuer tous les rôles + if (currentUserRole === UserRole.DCB_PARTNER, currentUserRole === UserRole.DCB_ADMIN, currentUserRole === UserRole.DCB_SUPPORT) { return true; } - // Les supports ne peuvent créer que d'autres supports - if (currentUserRole === UserRole.DCB_SUPPORT) { - return targetRole === UserRole.DCB_SUPPORT; - } - - return false; + // Pour les autres rôles, utiliser les permissions définies + const permissions = this.getPermissionsForRole(currentUserRole); + return permissions.assignableRoles.includes(targetRole); } /** @@ -280,16 +346,15 @@ export class RoleManagementService { * Récupère le libellé d'un rôle */ getRoleLabel(role: UserRole): string { - switch (role) { - case UserRole.DCB_ADMIN: - return 'Administrateur DCB'; - case UserRole.DCB_SUPPORT: - return 'Support DCB'; - case UserRole.DCB_PARTNER: - return 'Partenaire DCB'; - default: - return 'Rôle inconnu'; - } + const roleLabels: { [key in UserRole]: string } = { + [UserRole.DCB_ADMIN]: 'Administrateur DCB', + [UserRole.DCB_SUPPORT]: 'Support DCB', + [UserRole.DCB_PARTNER]: 'Partenaire DCB', + [UserRole.DCB_PARTNER_ADMIN]: 'Admin Partenaire', + [UserRole.DCB_PARTNER_MANAGER]: 'Manager Partenaire', + [UserRole.DCB_PARTNER_SUPPORT]: 'Support Partenaire' + }; + return roleLabels[role] || 'Rôle inconnu'; } /** @@ -303,9 +368,12 @@ export class RoleManagementService { return 'bg-info'; case UserRole.DCB_PARTNER: return 'bg-success'; - case UserRole.DCB_PARTNER_ADMIN: return 'bg-danger'; - case UserRole.DCB_PARTNER_MANAGER: return 'bg-success'; - case UserRole.DCB_PARTNER_SUPPORT: return 'bg-info'; + 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'; } @@ -354,11 +422,19 @@ export class RoleManagementService { 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 */ 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[] { if (!currentUserRole) return []; - if (currentUserRole === UserRole.DCB_ADMIN) { - return this.getAllRoles(); - } + const permissions = this.getPermissionsForRole(currentUserRole); + 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]; } /** diff --git a/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.ts b/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.ts index 906ebfe..cd9db9e 100644 --- a/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.ts +++ b/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.ts @@ -23,7 +23,7 @@ export class UserProfileComponent { loadUser() { this.authService.getProfile().subscribe({ next: profile => { - this.user = profile; + this.user = profile; this.cdr.detectChanges(); }, error: () => { diff --git a/src/app/layouts/components/topbar/components/user-profile/user-profile.html b/src/app/layouts/components/topbar/components/user-profile/user-profile.html index 383530b..b0c25dc 100644 --- a/src/app/layouts/components/topbar/components/user-profile/user-profile.html +++ b/src/app/layouts/components/topbar/components/user-profile/user-profile.html @@ -24,24 +24,16 @@ @if (item.isDivider) { - } - - - @if (!item.isHeader && !item.isDivider) { - - - {{ item.label }} - - } + } @if (!item.isHeader && !item.isDivider && item.url) { + + + + + } } diff --git a/src/app/layouts/components/topbar/components/user-profile/user-profile.ts b/src/app/layouts/components/topbar/components/user-profile/user-profile.ts index 30126ce..30dd1d8 100644 --- a/src/app/layouts/components/topbar/components/user-profile/user-profile.ts +++ b/src/app/layouts/components/topbar/components/user-profile/user-profile.ts @@ -32,7 +32,6 @@ export class UserProfile implements OnInit, OnDestroy { ngOnInit() { this.loadDropdownItems() - // Optionnel : réagir aux changements d'authentification this.subscription = this.authService.onAuthState().subscribe(() => { this.loadDropdownItems() }) diff --git a/src/app/modules/users/users.html b/src/app/modules/hub-users/hub-users.html similarity index 93% rename from src/app/modules/users/users.html rename to src/app/modules/hub-users/hub-users.html index 059b7d6..d365f0b 100644 --- a/src/app/modules/users/users.html +++ b/src/app/modules/hub-users/hub-users.html @@ -57,7 +57,7 @@ Liste des Utilisateurs - @if (selectedUserId) { - @@ -244,25 +244,28 @@ - @if (newUser.role) { + + @if (newUser.role && isMerchantRole(newUser.role)) {
-
-
- -
- Rôle sélectionné : - - {{ roleService.getRoleLabel(newUser.role) }} - -
- - {{ getRoleDescription(newUser.role) }} - -
-
+ + +
+ Sélectionnez le partenaire marchand auquel cet utilisateur sera associé
} @@ -315,7 +318,7 @@
@@ -78,7 +84,29 @@ }
+
+ +
+ +
+ {{ currentMerchantPartnerId || 'Chargement...' }} + +
+ @if (!currentMerchantPartnerId) { +
+ + Merchant Partner ID non disponible +
+ } +
+
@@ -107,7 +141,13 @@ name="lastName" required [disabled]="creatingUser" + #lastName="ngModel" > + @if (lastName.invalid && lastName.touched) { +
+ Le nom est requis +
+ }
@@ -122,8 +162,14 @@ name="username" required [disabled]="creatingUser" + #username="ngModel" >
Doit être unique dans le système
+ @if (username.invalid && username.touched) { +
+ Le nom d'utilisateur est requis +
+ }
@@ -137,8 +183,20 @@ [(ngModel)]="newMerchantUser.email" name="email" required + email [disabled]="creatingUser" + #email="ngModel" > + @if (email.invalid && email.touched) { +
+ @if (email.errors?.['required']) { + L'email est requis + } + @if (email.errors?.['email']) { + Format d'email invalide + } +
+ }
@@ -154,10 +212,21 @@ required minlength="8" [disabled]="creatingUser" + #password="ngModel" >
Le mot de passe doit contenir au moins 8 caractères.
+ @if (password.invalid && password.touched) { +
+ @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 + } +
+ }
@@ -170,17 +239,18 @@ [(ngModel)]="newMerchantUser.role" name="role" required - [disabled]="creatingUser" + [disabled]="creatingUser || !canManageAllRoles" + #roleSelect="ngModel" > - - @if (availableRoles) { - @for (role of availableRoles.roles; track role.value) { + + @for (role of getAvailableRoles(); track role.value) { + @if (isMerchantRole(role.value) && isRoleAllowedForCreation(role.value)) { @@ -188,10 +258,43 @@ }
- Sélectionnez le rôle à assigner à cet utilisateur marchand + @if (canManageAllRoles) { + + + Vous pouvez attribuer tous les rôles + + } @else { + + + Permissions de rôle limitées + + }
+ @if (roleSelect.invalid && roleSelect.touched) { +
+ Le rôle est requis +
+ }
+ + @if (!canManageAllRoles) { +
+
+
+ +
+ + Permissions limitées : + Vous ne pouvez créer que des utilisateurs avec des rôles spécifiques. + Seul un DCB Partner peut attribuer tous les rôles. + +
+
+
+
+ } + @if (newMerchantUser.role) {
@@ -259,7 +362,8 @@ Informations système :
• Merchant Partner ID : {{ currentMerchantPartnerId || 'Chargement...' }}
• Type d'utilisateur : MERCHANT
- • Créé par : Utilisateur courant + • Créé par : Utilisateur courant
+ • Votre rôle : {{ currentUserRole || 'Non défini' }}
@@ -272,12 +376,13 @@ (click)="modal.dismiss()" [disabled]="creatingUser" > + Annuler @@ -363,10 +469,21 @@ required minlength="8" [disabled]="resettingPassword" + #newPasswordInput="ngModel" >
Le mot de passe doit contenir au moins 8 caractères.
+ @if (newPasswordInput.invalid && newPasswordInput.touched) { +
+ @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 + } +
+ }
@@ -389,6 +506,11 @@
+ } @else if (!selectedUserForReset) { +
+ + Aucun utilisateur sélectionné pour la réinitialisation +
} @@ -415,7 +537,7 @@ type="button" class="btn btn-primary" (click)="confirmResetPassword()" - [disabled]="!newPassword || newPassword.length < 8 || resettingPassword" + [disabled]="!newPassword || newPassword.length < 8 || resettingPassword || !selectedUserForReset" > @if (resettingPassword) {
@@ -442,6 +564,7 @@ type="button" class="btn-close" (click)="modal.dismiss()" + aria-label="Fermer" >
@@ -478,6 +601,11 @@ + } @else { +
+ + Aucun utilisateur sélectionné pour la suppression +
} @@ -503,7 +631,7 @@ type="button" class="btn btn-danger" (click)="confirmDeleteUser()" - [disabled]="deletingUser" + [disabled]="deletingUser || !selectedUserForDelete" > @if (deletingUser) {
diff --git a/src/app/modules/merchant-users/merchant-users.spec.ts b/src/app/modules/merchant-users/merchant-users.spec.ts new file mode 100644 index 0000000..dba9a7d --- /dev/null +++ b/src/app/modules/merchant-users/merchant-users.spec.ts @@ -0,0 +1,2 @@ +import { MerchantUsers } from './merchant-users'; +describe('Merchant Users', () => {}); \ No newline at end of file diff --git a/src/app/modules/merchant-partners/merchant-partners.ts b/src/app/modules/merchant-users/merchant-users.ts similarity index 55% rename from src/app/modules/merchant-partners/merchant-partners.ts rename to src/app/modules/merchant-users/merchant-users.ts index 4ec3662..88f7ed5 100644 --- a/src/app/modules/merchant-partners/merchant-partners.ts +++ b/src/app/modules/merchant-users/merchant-users.ts @@ -7,17 +7,21 @@ import { Subject, takeUntil } from 'rxjs'; import { PageTitle } from '@app/components/page-title/page-title'; import { MerchantUsersList } from './list/list'; import { MerchantUserProfile } from './profile/profile'; -import { - MerchantUsersService, - CreateMerchantUserDto, - MerchantUserResponse, - UserRole, - AvailableRolesResponse, -} from './services/merchant-partners.service'; +import { MerchantUsersService } from './services/merchant-users.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({ - selector: 'app-merchant-partners', + selector: 'app-merchant-users', standalone: true, imports: [ CommonModule, @@ -29,21 +33,28 @@ import { AuthService } from '@core/services/auth.service'; MerchantUsersList, 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 authService = inject(AuthService); private merchantUsersService = inject(MerchantUsersService); + protected roleService = inject(RoleManagementService); private cdRef = inject(ChangeDetectorRef); private destroy$ = new Subject(); + + readonly UserRole = UserRole; + readonly UserType = UserType; - activeTab: 'list' | 'stats' | 'profile' = 'list'; + // Ajouter cette propriété manquante + user: MerchantUserDto | null = null; + + activeTab: 'list' | 'profile' = 'list'; selectedUserId: string | null = null; currentMerchantPartnerId: string = ''; // Données pour la création d'utilisateur marchand - newMerchantUser: CreateMerchantUserDto = { + newMerchantUser: CreateUserDto = { username: '', email: '', firstName: '', @@ -58,22 +69,28 @@ export class MerchantPartners implements OnInit, OnDestroy { availableRoles: AvailableRolesResponse | null = null; creatingUser = false; createUserError = ''; - + currentUserRole: UserRole | null = null; + // Données pour la réinitialisation de mot de passe - selectedUserForReset: MerchantUserResponse | null = null; + selectedUserForReset: MerchantUserDto | null = null; newPassword = ''; temporaryPassword = true; resettingPassword = false; resetPasswordError = ''; resetPasswordSuccess = ''; - selectedUserForDelete: MerchantUserResponse | null = null; + selectedUserForDelete: MerchantUserDto | null = null; deletingUser = false; deleteUserError = ''; + // Permissions utilisateur + isHubAdminOrSupport = false; + isDcbPartner = false; + canViewAllMerchants = false; + ngOnInit() { this.activeTab = 'list'; - this.loadCurrentMerchantPartnerId(); + this.loadCurrentUserPermissions(); this.loadAvailableRoles(); } @@ -82,37 +99,188 @@ export class MerchantPartners implements OnInit, OnDestroy { this.destroy$.complete(); } - private loadCurrentMerchantPartnerId() { + private loadCurrentUserPermissions() { this.authService.getProfile().subscribe({ - next: (user) => { - this.currentMerchantPartnerId = user.merchantPartnerId || ''; - this.newMerchantUser.merchantPartnerId = this.currentMerchantPartnerId; + next: (user: any) => { + this.currentUserRole = this.extractUserRole(user); + + 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) => { - 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() { this.merchantUsersService.getAvailableMerchantRoles() .pipe(takeUntil(this.destroy$)) .subscribe({ next: (roles) => { this.availableRoles = roles; + // Sélectionner le premier rôle disponible par défaut const firstAllowedRole = roles.roles.find(role => role.allowedForCreation); if (firstAllowedRole) { - this.newMerchantUser.role = firstAllowedRole.value as any; + this.newMerchantUser.role = firstAllowedRole.value; } }, 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; if (userId) { @@ -121,6 +289,7 @@ export class MerchantPartners implements OnInit, OnDestroy { } backToList() { + console.log('🔙 Returning to list view'); this.activeTab = 'list'; this.selectedUserId = null; } @@ -167,6 +336,7 @@ export class MerchantPartners implements OnInit, OnDestroy { enabled: true, emailVerified: false }; + console.log('🔄 User form reset'); } // 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.resetPasswordSuccess = ''; this.openModal(this.resetPasswordModal); + console.log('✅ User loaded for password reset:', user.username); }, 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.cdRef.detectChanges(); } }); } // Méthode pour ouvrir le modal de suppression openDeleteUserModal(userId: string) { + console.log(`🗑️ Opening delete modal for user: ${userId}`); this.merchantUsersService.getMerchantUserById(userId) .pipe(takeUntil(this.destroy$)) .subscribe({ @@ -198,19 +371,23 @@ export class MerchantPartners implements OnInit, OnDestroy { this.selectedUserForDelete = user; this.deleteUserError = ''; this.openModal(this.deleteUserModal); + console.log('✅ User loaded for deletion:', user.username); }, 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.cdRef.detectChanges(); } }); } // Créer un utilisateur marchand createMerchantUser() { + console.log('🚀 Creating new merchant user...'); const validation = this.validateUserForm(); if (!validation.isValid) { this.createUserError = validation.error!; + console.error('❌ Form validation failed:', validation.error); return; } @@ -227,11 +404,13 @@ export class MerchantPartners implements OnInit, OnDestroy { this.creatingUser = false; this.modalService.dismissAll(); this.refreshUsersList(); - this.showSuccessMessage(`Utilisateur "${createdUser.username}" créé avec succès`); + this.cdRef.detectChanges(); }, error: (error) => { + console.error('❌ Error creating user:', error); this.creatingUser = false; this.createUserError = this.getErrorMessage(error); + this.cdRef.detectChanges(); } }); } @@ -240,22 +419,28 @@ export class MerchantPartners implements OnInit, OnDestroy { confirmResetPassword() { if (!this.selectedUserForReset || !this.newPassword || this.newPassword.length < 8) { this.resetPasswordError = 'Veuillez saisir un mot de passe valide (au moins 8 caractères).'; + console.error('❌ Password reset validation failed'); return; } + console.log('🔑 Confirming password reset for user:', this.selectedUserForReset.username); + this.resettingPassword = true; this.resetPasswordError = ''; this.resetPasswordSuccess = ''; + const resetPasswordDto: ResetPasswordDto = { + newPassword: this.newPassword, + temporary: this.temporaryPassword + }; + this.merchantUsersService.resetMerchantUserPassword( this.selectedUserForReset.id, - { - newPassword: this.newPassword, - temporary: this.temporaryPassword - } + resetPasswordDto ).pipe(takeUntil(this.destroy$)) .subscribe({ next: (response) => { + console.log('✅ Password reset successfully'); this.resettingPassword = false; this.resetPasswordSuccess = 'Mot de passe réinitialisé avec succès !'; this.cdRef.detectChanges(); @@ -266,6 +451,7 @@ export class MerchantPartners implements OnInit, OnDestroy { }, 2000); }, error: (error) => { + console.error('❌ Error resetting password:', error); this.resettingPassword = false; this.resetPasswordError = this.getResetPasswordErrorMessage(error); this.cdRef.detectChanges(); @@ -274,7 +460,12 @@ export class MerchantPartners implements OnInit, OnDestroy { } 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.deleteUserError = ''; @@ -283,13 +474,14 @@ export class MerchantPartners implements OnInit, OnDestroy { .pipe(takeUntil(this.destroy$)) .subscribe({ next: () => { + console.log('✅ User deleted successfully'); this.deletingUser = false; this.modalService.dismissAll(); this.refreshUsersList(); - this.showSuccessMessage(`Utilisateur "${this.selectedUserForDelete?.username}" supprimé avec succès`); this.cdRef.detectChanges(); }, error: (error) => { + console.error('❌ Error deleting user:', error); this.deletingUser = false; this.deleteUserError = this.getDeleteErrorMessage(error); this.cdRef.detectChanges(); @@ -300,10 +492,11 @@ export class MerchantPartners implements OnInit, OnDestroy { @ViewChild(MerchantUsersList) usersListComponent!: MerchantUsersList; private refreshUsersList(): void { - if (this.usersListComponent && typeof this.usersListComponent.loadUsers === 'function') { - this.usersListComponent.loadUsers(); + if (this.usersListComponent && typeof this.usersListComponent.refreshData === 'function') { + console.log('🔄 Refreshing users list...'); + this.usersListComponent.refreshData(); } else { - console.warn('MerchantUsersList component not available for refresh'); + console.warn('❌ MerchantUsersList component not available for refresh'); this.showTab('list'); } } @@ -400,24 +593,89 @@ export class MerchantPartners implements OnInit, OnDestroy { 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 ==================== getRoleDisplayName(role: UserRole): string { - const roleNames = { - [UserRole.DCB_PARTNER_ADMIN]: 'Administrateur Partenaire', - [UserRole.DCB_PARTNER_MANAGER]: 'Manager Partenaire', - [UserRole.DCB_PARTNER_SUPPORT]: 'Support Partenaire' - }; - return roleNames[role] || role; + // Seulement gérer les rôles marchands, ignorer les rôles Hub + switch (role) { + case UserRole.DCB_PARTNER_ADMIN: + return 'Administrateur Partenaire'; + case UserRole.DCB_PARTNER_MANAGER: + 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 { @@ -435,11 +693,11 @@ export class MerchantPartners implements OnInit, OnDestroy { } // Méthodes utilitaires pour le template - getUserInitials(user: MerchantUserResponse): string { + getUserInitials(user: MerchantUserDto): string { return (user.firstName?.charAt(0) || '') + (user.lastName?.charAt(0) || '') || 'U'; } - getUserType(user: MerchantUserResponse): string { + getUserType(user: MerchantUserDto): string { switch (user.role) { case UserRole.DCB_PARTNER_ADMIN: return 'Administrateur'; diff --git a/src/app/modules/merchant-users/models/merchant-user.model.ts b/src/app/modules/merchant-users/models/merchant-user.model.ts new file mode 100644 index 0000000..9e7b94e --- /dev/null +++ b/src/app/modules/merchant-users/models/merchant-user.model.ts @@ -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; +} diff --git a/src/app/modules/merchant-partners/models/partners-config.model.ts b/src/app/modules/merchant-users/models/partners-config.model.ts similarity index 100% rename from src/app/modules/merchant-partners/models/partners-config.model.ts rename to src/app/modules/merchant-users/models/partners-config.model.ts diff --git a/src/app/modules/merchant-partners/profile/profile.html b/src/app/modules/merchant-users/profile/profile.html similarity index 69% rename from src/app/modules/merchant-partners/profile/profile.html rename to src/app/modules/merchant-users/profile/profile.html index 0cfe936..b50d9fd 100644 --- a/src/app/modules/merchant-partners/profile/profile.html +++ b/src/app/modules/merchant-users/profile/profile.html @@ -1,4 +1,3 @@ -
@@ -32,7 +31,7 @@
- @if (user && !isEditing) { + @if (user && !isEditing && canResetPassword()) { - - + } + + + @if (user && !isEditing && canEnableDisableUser()) { @if (user.enabled) {
@@ -346,19 +383,75 @@ }
- -
- + +
+
+ {{ getRoleDisplayName(user.role) }}
- Rôle assigné à la création + @if (canManageRoles()) { + + + Vous pouvez modifier ce rôle (DCB Partner) + + } @else { + + + Seul un DCB Partner peut modifier les rôles + + }
+ + @if (isEditing && canManageRoles()) { +
+
+
+
+ + Modification du Rôle (DCB Partner) +
+
+
+ + +
+ En tant que DCB Partner, vous pouvez modifier le rôle de cet utilisateur. +
+ +
+
+
+ } +
@@ -371,7 +464,7 @@
- @if (isEditing) { + @if (isEditing && canEnableDisableUser()) {
- } @else { + } @else if (!isEditing) {
@@ -456,50 +549,69 @@
-
- -
-
- @if (user.enabled) { + + @if (canResetPassword()) { +
- } @else { +
+ } + + + @if (canEnableDisableUser()) { +
+ @if (user.enabled) { + + } @else { + + } +
+ } + + + @if (canEditUser()) { +
- } -
-
- -
+
+ }
- +
- Note : Pour supprimer cet utilisateur, utilisez l'action de suppression - disponible dans la liste des utilisateurs marchands. + @if (currentUserRole === UserRole.DCB_PARTNER) { + + DCB Partner : Vous avez un accès complet à toutes les fonctionnalités de gestion. + } @else if (currentUserRole === UserRole.DCB_PARTNER_ADMIN) { + + Admin Partenaire : Vous pouvez gérer les utilisateurs de votre partenaire marchand, mais pas modifier les rôles. + } @else { + + Permissions limitées : Contactez un DCB Partner pour les actions de gestion avancées. + }
diff --git a/src/app/modules/merchant-partners/profile/profile.spec.ts b/src/app/modules/merchant-users/profile/profile.spec.ts similarity index 100% rename from src/app/modules/merchant-partners/profile/profile.spec.ts rename to src/app/modules/merchant-users/profile/profile.spec.ts diff --git a/src/app/modules/merchant-partners/profile/profile.ts b/src/app/modules/merchant-users/profile/profile.ts similarity index 60% rename from src/app/modules/merchant-partners/profile/profile.ts rename to src/app/modules/merchant-users/profile/profile.ts index 2278a0d..4d73be3 100644 --- a/src/app/modules/merchant-partners/profile/profile.ts +++ b/src/app/modules/merchant-users/profile/profile.ts @@ -5,13 +5,15 @@ import { FormsModule } from '@angular/forms'; import { NgIcon } from '@ng-icons/core'; import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'; import { Subject, takeUntil } from 'rxjs'; -import { - MerchantUsersService, - MerchantUserResponse, - UserRole, - UpdateMerchantUserDto -} from '../services/merchant-partners.service'; +import { MerchantUsersService } from '../services/merchant-users.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({ selector: 'app-merchant-user-profile', @@ -31,22 +33,27 @@ import { AuthService } from '@core/services/auth.service'; export class MerchantUserProfile implements OnInit, OnDestroy { private merchantUsersService = inject(MerchantUsersService); private authService = inject(AuthService); + private roleService = inject(RoleManagementService); private cdRef = inject(ChangeDetectorRef); private destroy$ = new Subject(); + readonly UserRole = UserRole; @Input() userId!: string; @Output() back = new EventEmitter(); @Output() openResetPasswordModal = new EventEmitter(); - user: MerchantUserResponse | null = null; + user: MerchantUserDto | null = null; loading = false; saving = false; error = ''; success = ''; + // Gestion des permissions + currentUserRole: UserRole | null = null; + // Édition isEditing = false; - editedUser: UpdateMerchantUserDto = {}; + editedUser: UpdateUserDto = {}; // Gestion des rôles availableRoles: UserRole[] = [ @@ -58,6 +65,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy { ngOnInit() { if (this.userId) { + this.loadCurrentUserPermissions(); this.loadUserProfile(); } } @@ -67,6 +75,22 @@ export class MerchantUserProfile implements OnInit, OnDestroy { 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() { this.loading = true; this.error = ''; @@ -80,7 +104,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy { this.cdRef.detectChanges(); }, 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.cdRef.detectChanges(); console.error('Error loading merchant user profile:', error); @@ -89,6 +113,11 @@ export class MerchantUserProfile implements OnInit, OnDestroy { } startEditing() { + if (!this.canEditUser()) { + this.error = 'Vous n\'avez pas la permission de modifier cet utilisateur'; + return; + } + this.isEditing = true; this.editedUser = { firstName: this.user?.firstName, @@ -108,7 +137,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy { } saveProfile() { - if (!this.user) return; + if (!this.user || !this.canEditUser()) return; this.saving = true; this.error = ''; @@ -136,18 +165,26 @@ export class MerchantUserProfile implements OnInit, OnDestroy { // Gestion des rôles 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.error = ''; 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 - const updateData: UpdateMerchantUserDto = { + // Vous devrez peut-être implémenter une méthode updateUserRole dans le service + const updateData: UpdateUserDto = { ...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) @@ -156,7 +193,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy { next: (updatedUser) => { this.user = updatedUser; this.updatingRole = false; - this.success = 'Profil mis à jour avec succès'; + this.success = 'Rôle mis à jour avec succès'; this.cdRef.detectChanges(); }, error: (error) => { @@ -169,7 +206,10 @@ export class MerchantUserProfile implements OnInit, OnDestroy { // Gestion du statut 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.success = ''; @@ -179,7 +219,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy { .subscribe({ next: (updatedUser) => { this.user = updatedUser; - this.success = 'Utilisateur activé avec succès'; + this.success = 'Utilisateur marchand activé avec succès'; this.cdRef.detectChanges(); }, error: (error) => { @@ -191,7 +231,10 @@ export class MerchantUserProfile implements OnInit, OnDestroy { } 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.success = ''; @@ -201,7 +244,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy { .subscribe({ next: (updatedUser) => { this.user = updatedUser; - this.success = 'Utilisateur désactivé avec succès'; + this.success = 'Utilisateur marchand désactivé avec succès'; this.cdRef.detectChanges(); }, error: (error) => { @@ -214,11 +257,98 @@ export class MerchantUserProfile implements OnInit, OnDestroy { // Réinitialisation du mot de passe resetPassword() { - if (this.user) { + if (this.user && this.canResetPassword()) { 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 ==================== getStatusBadgeClass(): string { @@ -252,7 +382,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy { } getUserDisplayName(): string { - if (!this.user) return 'Utilisateur'; + if (!this.user) return 'Utilisateur Marchand'; if (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 { - switch (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'; - } + return this.roleService.getRoleBadgeClass(role); } getRoleDisplayName(role: UserRole): string { - const roleNames = { - [UserRole.DCB_PARTNER_ADMIN]: 'Administrateur', - [UserRole.DCB_PARTNER_MANAGER]: 'Manager', - [UserRole.DCB_PARTNER_SUPPORT]: 'Support' - }; - return roleNames[role] || role; + return this.roleService.getRoleLabel(role); } getRoleIcon(role: UserRole): string { - switch (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'; - } + return this.roleService.getRoleIcon(role); } 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_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' @@ -304,28 +411,13 @@ export class MerchantUserProfile implements OnInit, OnDestroy { } getUserType(): string { - if (!this.user) return 'Utilisateur'; - - 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'; - } + if (!this.user) return 'Utilisateur Marchand'; + return this.roleService.getRoleLabel(this.user.role); } getUserTypeBadgeClass(): string { - const userType = this.getUserType(); - switch (userType) { - case 'Administrateur': return 'bg-danger'; - case 'Manager': return 'bg-success'; - case 'Support': return 'bg-info'; - default: return 'bg-secondary'; - } + if (!this.user) return 'bg-secondary'; + return this.roleService.getRoleBadgeClass(this.user.role); } // ==================== GESTION DES ERREURS ==================== @@ -341,7 +433,7 @@ export class MerchantUserProfile implements OnInit, OnDestroy { return 'Vous n\'avez pas les permissions pour effectuer cette action.'; } if (error.status === 404) { - return 'Utilisateur non trouvé.'; + return 'Utilisateur marchand non trouvé.'; } if (error.status === 409) { 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.'; } - // ==================== 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 ==================== goBack() { @@ -415,9 +490,8 @@ export class MerchantUserProfile implements OnInit, OnDestroy { // Vérifie si c'est le profil de l'utilisateur courant isCurrentUserProfile(): boolean { - // Implémentez cette logique selon votre système d'authentification - // Exemple: return this.authService.getCurrentUserId() === this.user?.id; - return false; + if (!this.user?.id) return false; + return this.authService.isCurrentUserProfile(this.user.id); } // 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'; 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'; + } + } } \ No newline at end of file diff --git a/src/app/modules/merchant-partners/services/merchant-partners.service.ts b/src/app/modules/merchant-users/services/merchant-users.service.ts similarity index 64% rename from src/app/modules/merchant-partners/services/merchant-partners.service.ts rename to src/app/modules/merchant-users/services/merchant-users.service.ts index e9425f8..af45d4f 100644 --- a/src/app/modules/merchant-partners/services/merchant-partners.service.ts +++ b/src/app/modules/merchant-users/services/merchant-users.service.ts @@ -3,130 +3,35 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { environment } from '@environments/environment'; import { Observable, map, catchError, throwError, of } from 'rxjs'; -// Interfaces alignées avec le contrôleur MerchantUsersController -export interface MerchantUserResponse { - id: string; - username: string; - email: string; - firstName: string; - lastName: string; - role: UserRole.DCB_PARTNER_ADMIN | UserRole.DCB_PARTNER_MANAGER | UserRole.DCB_PARTNER_SUPPORT; - enabled: boolean; - emailVerified: boolean; - merchantPartnerId: string; - createdBy: string; - createdByUsername: string; - createdTimestamp: number; - lastLogin?: number; - userType: 'MERCHANT'; -} - -export interface 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' -} +import { + MerchantUserDto, + CreateUserDto, + UpdateUserDto, + ResetPasswordDto, + PaginatedUserResponse, + MerchantPartnerStatsResponse, + AvailableRolesResponse, + SearchUsersParams, + UserRole, + UserType +} from '@core/models/dcb-bo-hub-user.model'; @Injectable({ providedIn: 'root' }) export class MerchantUsersService { private http = inject(HttpClient); private apiUrl = `${environment.iamApiUrl}/merchant-users`; - // === RÉCUPÉRATION D'UTILISATEURS === - - /** - * Récupère les utilisateurs marchands de l'utilisateur courant - */ - getMyMerchantUsers(): Observable { - return this.http.get(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 { - return this.http.get(`${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 { - return this.http.get(`${this.apiUrl}/${id}`).pipe( - catchError(error => { - console.error(`Error loading merchant user ${id}:`, error); - return throwError(() => error); - }) - ); - } - - // === CRÉATION D'UTILISATEURS === + // === CRÉATION === /** * Crée un nouvel utilisateur marchand */ - createMerchantUser(createUserDto: CreateMerchantUserDto): Observable { - // Validation + createMerchantUser(createUserDto: CreateUserDto): Observable { + // Validation spécifique aux marchands + if (!createUserDto.merchantPartnerId?.trim()) { + return throwError(() => 'Merchant Partner ID is required for merchant users'); + } + if (!createUserDto.username?.trim()) { return throwError(() => 'Username is required and cannot be empty'); } @@ -143,24 +48,25 @@ export class MerchantUsersService { return throwError(() => 'Role is required'); } - if (!createUserDto.merchantPartnerId?.trim()) { - return throwError(() => 'Merchant Partner ID is required'); + // Vérification que le rôle est bien un rôle marchand + 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 const payload = { + ...createUserDto, username: createUserDto.username.trim(), email: createUserDto.email.trim(), firstName: (createUserDto.firstName || '').trim(), lastName: (createUserDto.lastName || '').trim(), - password: createUserDto.password, - role: createUserDto.role, merchantPartnerId: createUserDto.merchantPartnerId.trim(), enabled: createUserDto.enabled !== undefined ? createUserDto.enabled : true, emailVerified: createUserDto.emailVerified !== undefined ? createUserDto.emailVerified : false, }; - return this.http.post(this.apiUrl, payload).pipe( + return this.http.post(this.apiUrl, payload).pipe( catchError(error => { console.error('Error creating merchant user:', 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 { + return this.http.get(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 { + return this.http.get(`${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 { + return this.http.get(`${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 { + 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(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 */ - updateMerchantUser(id: string, updateUserDto: UpdateMerchantUserDto): Observable { - return this.http.put(`${this.apiUrl}/${id}`, updateUserDto).pipe( + updateMerchantUser(id: string, updateUserDto: UpdateUserDto): Observable { + return this.http.put(`${this.apiUrl}/${id}`, updateUserDto).pipe( catchError(error => { console.error(`Error updating merchant user ${id}:`, error); return throwError(() => error); @@ -182,7 +163,7 @@ export class MerchantUsersService { ); } - // === SUPPRESSION D'UTILISATEURS === + // === SUPPRESSION === /** * 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 { + return this.updateMerchantUser(id, { enabled: true }); + } + + /** + * Désactive un utilisateur marchand + */ + disableMerchantUser(id: string): Observable { + return this.updateMerchantUser(id, { enabled: false }); + } + + // === STATISTIQUES === /** * Récupère les statistiques des utilisateurs marchands */ - getMerchantUsersStats(): Observable { + getMerchantPartnerStats(): Observable { return this.http.get(`${this.apiUrl}/stats/overview`).pipe( catchError(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 */ - searchMerchantUsers(params: SearchMerchantUsersParams): Observable { - let httpParams = new HttpParams(); + searchMerchantUsers(params: SearchUsersParams): Observable { + let httpParams = new HttpParams().set('userType', UserType.MERCHANT); if (params.query) { httpParams = httpParams.set('query', params.query); @@ -247,7 +244,7 @@ export class MerchantUsersService { httpParams = httpParams.set('enabled', params.enabled.toString()); } - return this.http.get(`${this.apiUrl}/search`, { params: httpParams }).pipe( + return this.http.get(`${this.apiUrl}/search`, { params: httpParams }).pipe( catchError(error => { console.error('Error searching merchant users:', error); return throwError(() => error); @@ -264,7 +261,6 @@ export class MerchantUsersService { return this.http.get(`${this.apiUrl}/roles/available`).pipe( catchError(error => { console.error('Error loading available merchant roles:', error); - // Fallback en cas d'erreur return of({ roles: [ { @@ -291,29 +287,13 @@ export class MerchantUsersService { ); } - // === GESTION DU STATUT === - - /** - * Active un utilisateur marchand - */ - enableMerchantUser(id: string): Observable { - return this.updateMerchantUser(id, { enabled: true }); - } - - /** - * Désactive un utilisateur marchand - */ - disableMerchantUser(id: string): Observable { - return this.updateMerchantUser(id, { enabled: false }); - } - // === UTILITAIRES === /** * Vérifie si un nom d'utilisateur existe parmi les utilisateurs marchands */ merchantUserExists(username: string): Observable<{ exists: boolean }> { - return this.getMyMerchantUsers().pipe( + return this.searchMerchantUsers({ query: username }).pipe( map(users => ({ exists: users.some(user => user.username === username) })), @@ -327,21 +307,25 @@ export class MerchantUsersService { /** * 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 { + getMerchantUsersByRole(role: UserRole): Observable { + 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 }); } /** * Récupère uniquement les utilisateurs actifs */ - getActiveMerchantUsers(): Observable { + getActiveMerchantUsers(): Observable { return this.searchMerchantUsers({ enabled: true }); } /** * Récupère uniquement les utilisateurs inactifs */ - getInactiveMerchantUsers(): Observable { + getInactiveMerchantUsers(): Observable { return this.searchMerchantUsers({ enabled: false }); } } \ No newline at end of file diff --git a/src/app/modules/merchant-partners/services/partner-config.service.ts b/src/app/modules/merchant-users/services/partner-config.service.ts similarity index 100% rename from src/app/modules/merchant-partners/services/partner-config.service.ts rename to src/app/modules/merchant-users/services/partner-config.service.ts diff --git a/src/app/modules/merchant-partners/stats/stats.html b/src/app/modules/merchant-users/stats/stats.html similarity index 100% rename from src/app/modules/merchant-partners/stats/stats.html rename to src/app/modules/merchant-users/stats/stats.html diff --git a/src/app/modules/merchant-partners/stats/stats.spec.ts b/src/app/modules/merchant-users/stats/stats.spec.ts similarity index 100% rename from src/app/modules/merchant-partners/stats/stats.spec.ts rename to src/app/modules/merchant-users/stats/stats.spec.ts diff --git a/src/app/modules/merchant-partners/stats/stats.ts b/src/app/modules/merchant-users/stats/stats.ts similarity index 83% rename from src/app/modules/merchant-partners/stats/stats.ts rename to src/app/modules/merchant-users/stats/stats.ts index ca3b9d2..b0fc903 100644 --- a/src/app/modules/merchant-partners/stats/stats.ts +++ b/src/app/modules/merchant-users/stats/stats.ts @@ -2,7 +2,7 @@ import { Component, Input } from '@angular/core'; import { CommonModule } from '@angular/common'; import { NgIcon } from '@ng-icons/core'; 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({ selector: 'app-merchant-users-stats', diff --git a/src/app/modules/merchant-partners/types.ts b/src/app/modules/merchant-users/types.ts similarity index 100% rename from src/app/modules/merchant-partners/types.ts rename to src/app/modules/merchant-users/types.ts diff --git a/src/app/modules/modules.routes.ts b/src/app/modules/modules.routes.ts index 20c3abb..b1e1bb8 100644 --- a/src/app/modules/modules.routes.ts +++ b/src/app/modules/modules.routes.ts @@ -2,13 +2,13 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { authGuard } from '../core/guards/auth.guard'; import { roleGuard } from '../core/guards/role.guard'; -import { Users } from '@modules/users/users'; +import { HubUsers } from '@modules/hub-users/hub-users'; // Composants principaux import { DcbDashboard } from './dcb-dashboard/dcb-dashboard'; import { Team } from './team/team'; 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 { OperatorsStats } from './operators/stats/stats'; import { WebhooksHistory } from './webhooks/history/history'; @@ -77,7 +77,7 @@ const routes: Routes = [ { path: 'users', canActivate: [authGuard, roleGuard], - component: Users, + component: HubUsers, data: { title: 'Gestion des Utilisateurs', module: 'users' @@ -89,7 +89,7 @@ const routes: Routes = [ // --------------------------- { path: 'merchant-partners', - component: MerchantPartners, + component: MerchantUsers, canActivate: [authGuard, roleGuard], data: { title: 'Gestion Partners/Marchants', diff --git a/src/app/modules/profile/profile.ts b/src/app/modules/profile/profile.ts index a4d321e..f77f340 100644 --- a/src/app/modules/profile/profile.ts +++ b/src/app/modules/profile/profile.ts @@ -5,10 +5,15 @@ import { FormsModule } from '@angular/forms'; import { NgIcon } from '@ng-icons/core'; import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'; 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 { AuthService } from '@core/services/auth.service'; +import { + UpdateUserDto, + UserRole +} from '@core/models/dcb-bo-hub-user.model'; + @Component({ selector: 'app-my-profile', standalone: true, @@ -48,7 +53,7 @@ export class MyProfile implements OnInit, OnDestroy { // Édition isEditing = false; - editedUser: UpdateHubUserDto = {}; + editedUser: UpdateUserDto = {}; // Gestion des rôles (simplifiée pour profil personnel) availableRoles: { value: UserRole; label: string; description: string }[] = []; @@ -73,7 +78,7 @@ export class MyProfile implements OnInit, OnDestroy { .pipe(takeUntil(this.destroy$)) .subscribe({ 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 this.canEditUsers = true; 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) */ private loadAvailableRoles(): void { - this.roleService.getAvailableRolesSimple() + this.usersService.getAvailableHubRoles() .pipe(takeUntil(this.destroy$)) .subscribe({ - next: (roles) => { - this.availableRoles = roles; + next: (response) => { + this.availableRoles = response.roles.map(role => ({ + value: role.value, + label: role.label, + description: role.description + })); }, error: (error) => { console.error('Error loading available roles:', error); - // Fallback + // Fallback avec tous les rôles this.availableRoles = [ { value: UserRole.DCB_ADMIN, label: 'DCB Admin', description: 'Administrateur système' }, { value: UserRole.DCB_SUPPORT, label: 'DCB Support', description: 'Support technique' }, @@ -158,7 +167,7 @@ export class MyProfile implements OnInit, OnDestroy { this.error = ''; this.success = ''; - this.usersService.updateUser(this.user.id, this.editedUser) + this.usersService.updateHubUser(this.user.id, this.editedUser) .pipe(takeUntil(this.destroy$)) .subscribe({ 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 { if (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.'; } - // Utilitaires d'affichage - mêmes méthodes que le premier composant + // Utilitaires d'affichage getStatusBadgeClass(): string { if (!this.user) return 'badge bg-secondary'; 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 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 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; } } \ No newline at end of file diff --git a/src/app/modules/users/services/users.service.ts b/src/app/modules/users/services/users.service.ts deleted file mode 100644 index ff8b58c..0000000 --- a/src/app/modules/users/services/users.service.ts +++ /dev/null @@ -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 { - // 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(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 { - 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(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 { - 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(`${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 { - return this.http.get(`${this.apiUrl}/${id}`); - } - - /** - * Met à jour un utilisateur Hub - */ - updateUser(id: string, updateUserDto: UpdateHubUserDto): Observable { - return this.http.put(`${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 { - return this.http.put(`${this.apiUrl}/${id}`, { enabled: true }); - } - - /** - * Désactive un utilisateur - */ - disableUser(id: string): Observable { - return this.http.put(`${this.apiUrl}/${id}`, { enabled: false }); - } - - // === GESTION DES RÔLES === - - /** - * Met à jour le rôle d'un utilisateur - */ - updateUserRole(id: string, role: UserRole): Observable { - return this.http.put(`${this.apiUrl}/${id}/role`, { role }); - } - - /** - * Récupère les rôles Hub disponibles - */ - getAvailableHubRoles(): Observable { - return this.http.get( - `${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 { - return this.http.get(`${this.apiUrl}/role/${role}`); - } - - // === STATISTIQUES === - - /** - * Récupère les statistiques des utilisateurs - */ - getUsersStats(): Observable { - return this.http.get(`${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 { - 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()) - )) - ); - } -} \ No newline at end of file diff --git a/src/app/modules/users/users.spec.ts b/src/app/modules/users/users.spec.ts deleted file mode 100644 index 24f8c93..0000000 --- a/src/app/modules/users/users.spec.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { Users } from './users'; -describe('Users', () => {}); \ No newline at end of file