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

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

View File

@ -1,6 +1,7 @@
import { Directive, Input, TemplateRef, ViewContainerRef, inject, OnDestroy } from '@angular/core';
import { 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);

View File

@ -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 sil 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();
// ✅ Lutilisateur 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;
}
}

View File

@ -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 laccè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;
};
};

View File

@ -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;

View File

@ -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 dauthentification (login, refresh, logout)
if (isAuthRequest(req)) {
return next(req);
}
const token = authService.getAccessToken();
// Si un token existe, lajouter 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<any>, token: string): HttpRequest<any> {
});
}
/**
* Gestion du renouvellement de token en cas derreur 401
*/
function handle401Error(
authService: AuthService,
router: Router,
req: HttpRequest<any>,
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<any>): boolean {
return req.url.includes('/api/') || req.url.includes('/auth/');
// Ajuste ici selon ton backend (ex : '/api/', '/v1/', etc.)
return req.url.includes('/api/v1/') || req.url.includes('/auth/');
}
/**
* Exclut les endpoints liés à lauthentification
*/
function isAuthRequest(req: HttpRequest<any>): boolean {
const authEndpoints = [
'/auth/login',
'/auth/refresh',
'/auth/logout'
];
const authEndpoints = ['/auth/login', '/auth/refresh', '/auth/logout'];
return authEndpoints.some(endpoint => req.url.includes(endpoint));
}
}

View File

@ -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;
}
}
// === SEARCH ===
export interface SearchUsersParams {
query?: string;
role?: UserRole;
enabled?: boolean;
userType?: UserType;
}

View File

@ -1,11 +1,19 @@
// src/app/core/services/auth.service.ts
import { Injectable, inject } from '@angular/core';
import { 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<boolean>(this.isAuthenticated());
private userProfile$ = new BehaviorSubject<UserProfileDto | null>(null);
private userProfile$ = new BehaviorSubject<BaseUserDto | null>(null);
private initialized$ = new BehaviorSubject<boolean>(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<LoginResponseDto> {
refreshAccessToken(): Observable<LoginResponseDto> {
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<UserProfileDto> {
return this.http.get<UserProfileDto>(
loadUserProfile(): Observable<BaseUserDto> {
return this.http.get<BaseUserDto>(
`${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<TokenValidationResponseDto> {
return this.http.get<TokenValidationResponseDto>(
`${environment.iamApiUrl}/auth/validate`
@ -253,70 +232,193 @@ export class AuthService {
return this.authState$.asObservable();
}
getUserProfile(): Observable<UserProfileDto | null> {
getUserProfile(): Observable<BaseUserDto | null> {
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<boolean> {
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<any> {
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<boolean> {
return this.authState$.asObservable();
}
getProfile(): Observable<BaseUserDto | null> {
return this.getUserProfile();
}
/**
* Récupère l'ID de l'utilisateur courant
*/
getCurrentUserId(): string | null {
const profile = this.userProfile$.value;
return profile?.id || null;
}
/**
* Récupère le merchantPartnerId de l'utilisateur courant (si marchand)
*/
getCurrentMerchantPartnerId(): string | null {
const profile = this.userProfile$.value;
if (profile && 'merchantPartnerId' in profile) {
return (profile as MerchantUserDto).merchantPartnerId || null;
}
return null;
}
/**
* Vérifie si le profil fourni est celui de l'utilisateur courant
*/
isCurrentUserProfile(userId: string): boolean {
const currentUserId = this.getCurrentUserId();
return currentUserId === userId;
}
/**
* Vérifie si l'utilisateur peut visualiser tous les marchands
*/
canViewAllMerchants(): boolean {
const hubAdminRoles = [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT];
return this.hasAnyRole(hubAdminRoles);
}
// === TOKENS ===
getAccessToken(): string | null {
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<never> {
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();

View File

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

View File

@ -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())
);
}
}

View File

@ -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];
}
/**

View File

@ -23,7 +23,7 @@ export class UserProfileComponent {
loadUser() {
this.authService.getProfile().subscribe({
next: profile => {
this.user = profile;
this.user = profile;
this.cdr.detectChanges();
},
error: () => {

View File

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

View File

@ -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()
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,17 +7,21 @@ import { Subject, takeUntil } from 'rxjs';
import { PageTitle } from '@app/components/page-title/page-title';
import { 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<void>();
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';

View File

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

View File

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

View File

@ -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<void>();
readonly UserRole = UserRole;
@Input() userId!: string;
@Output() back = new EventEmitter<void>();
@Output() openResetPasswordModal = new EventEmitter<string>();
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';
}
}
}

View File

@ -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<MerchantUserResponse[]> {
return this.http.get<MerchantUserResponse[]>(this.apiUrl).pipe(
catchError(error => {
console.error('Error loading my merchant users:', error);
return throwError(() => error);
})
);
}
/**
* Récupère les utilisateurs marchands par ID de partenaire
*/
getMerchantUsersByPartner(partnerId: string): Observable<MerchantUserResponse[]> {
return this.http.get<MerchantUserResponse[]>(`${this.apiUrl}/partner/${partnerId}`).pipe(
catchError(error => {
console.error(`Error loading merchant users for partner ${partnerId}:`, error);
return throwError(() => error);
})
);
}
/**
* Récupère un utilisateur marchand par ID
*/
getMerchantUserById(id: string): Observable<MerchantUserResponse> {
return this.http.get<MerchantUserResponse>(`${this.apiUrl}/${id}`).pipe(
catchError(error => {
console.error(`Error loading merchant user ${id}:`, error);
return throwError(() => error);
})
);
}
// === CRÉATION D'UTILISATEURS ===
// === CRÉATION ===
/**
* Crée un nouvel utilisateur marchand
*/
createMerchantUser(createUserDto: CreateMerchantUserDto): Observable<MerchantUserResponse> {
// Validation
createMerchantUser(createUserDto: CreateUserDto): Observable<MerchantUserDto> {
// 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<MerchantUserResponse>(this.apiUrl, payload).pipe(
return this.http.post<MerchantUserDto>(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<MerchantUserDto[]> {
return this.http.get<MerchantUserDto[]>(this.apiUrl).pipe(
catchError(error => {
console.error('Error loading my merchant users:', error);
return throwError(() => error);
})
);
}
/**
* Récupère les utilisateurs marchands par ID de partenaire
*/
getMerchantUsersByPartner(partnerId: string): Observable<MerchantUserDto[]> {
return this.http.get<MerchantUserDto[]>(`${this.apiUrl}/partner/${partnerId}`).pipe(
catchError(error => {
console.error(`Error loading merchant users for partner ${partnerId}:`, error);
return throwError(() => error);
})
);
}
/**
* Récupère un utilisateur marchand par ID
*/
getMerchantUserById(id: string): Observable<MerchantUserDto> {
return this.http.get<MerchantUserDto>(`${this.apiUrl}/${id}`).pipe(
catchError(error => {
console.error(`Error loading merchant user ${id}:`, error);
return throwError(() => error);
})
);
}
/**
* Récupère tous les utilisateurs marchands avec pagination
*/
getMerchantUsers(page: number = 1, limit: number = 10, filters?: SearchUsersParams): Observable<PaginatedUserResponse> {
let params = new HttpParams()
.set('page', page.toString())
.set('limit', limit.toString())
.set('userType', UserType.MERCHANT);
if (filters) {
Object.keys(filters).forEach(key => {
if (filters[key as keyof SearchUsersParams] !== undefined && filters[key as keyof SearchUsersParams] !== null) {
params = params.set(key, filters[key as keyof SearchUsersParams]!.toString());
}
});
}
return this.http.get<MerchantUserDto[]>(this.apiUrl, { params, observe: 'response' }).pipe(
map(response => {
const users = response.body || [];
const total = parseInt(response.headers.get('X-Total-Count') || '0');
return {
users,
total,
page,
limit,
totalPages: Math.ceil(total / limit)
};
}),
catchError(error => {
console.error('Error loading merchant users:', error);
return throwError(() => error);
})
);
}
// === MISE À JOUR ===
/**
* Met à jour un utilisateur marchand
*/
updateMerchantUser(id: string, updateUserDto: UpdateMerchantUserDto): Observable<MerchantUserResponse> {
return this.http.put<MerchantUserResponse>(`${this.apiUrl}/${id}`, updateUserDto).pipe(
updateMerchantUser(id: string, updateUserDto: UpdateUserDto): Observable<MerchantUserDto> {
return this.http.put<MerchantUserDto>(`${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<MerchantUserDto> {
return this.updateMerchantUser(id, { enabled: true });
}
/**
* Désactive un utilisateur marchand
*/
disableMerchantUser(id: string): Observable<MerchantUserDto> {
return this.updateMerchantUser(id, { enabled: false });
}
// === STATISTIQUES ===
/**
* Récupère les statistiques des utilisateurs marchands
*/
getMerchantUsersStats(): Observable<MerchantPartnerStatsResponse> {
getMerchantPartnerStats(): Observable<MerchantPartnerStatsResponse> {
return this.http.get<MerchantPartnerStatsResponse>(`${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<MerchantUserResponse[]> {
let httpParams = new HttpParams();
searchMerchantUsers(params: SearchUsersParams): Observable<MerchantUserDto[]> {
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<MerchantUserResponse[]>(`${this.apiUrl}/search`, { params: httpParams }).pipe(
return this.http.get<MerchantUserDto[]>(`${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<AvailableRolesResponse>(`${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<MerchantUserResponse> {
return this.updateMerchantUser(id, { enabled: true });
}
/**
* Désactive un utilisateur marchand
*/
disableMerchantUser(id: string): Observable<MerchantUserResponse> {
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<MerchantUserResponse[]> {
getMerchantUsersByRole(role: UserRole): Observable<MerchantUserDto[]> {
const merchantRoles = [UserRole.DCB_PARTNER_ADMIN, UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT];
if (!merchantRoles.includes(role)) {
return throwError(() => 'Invalid role for merchant user');
}
return this.searchMerchantUsers({ role });
}
/**
* Récupère uniquement les utilisateurs actifs
*/
getActiveMerchantUsers(): Observable<MerchantUserResponse[]> {
getActiveMerchantUsers(): Observable<MerchantUserDto[]> {
return this.searchMerchantUsers({ enabled: true });
}
/**
* Récupère uniquement les utilisateurs inactifs
*/
getInactiveMerchantUsers(): Observable<MerchantUserResponse[]> {
getInactiveMerchantUsers(): Observable<MerchantUserDto[]> {
return this.searchMerchantUsers({ enabled: false });
}
}

View File

@ -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',

View File

@ -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',

View File

@ -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;
}
}

View File

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

View File

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