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-03 21:07:19 +00:00
parent d586552944
commit 35f0bcd135
25 changed files with 183 additions and 168 deletions

View File

@ -34,7 +34,7 @@ export const routes: Routes = [
// ===== REDIRECTIONS POUR LES ERREURS ===== // ===== REDIRECTIONS POUR LES ERREURS =====
{ path: '404', redirectTo: '/error/404' }, { path: '404', redirectTo: '/error/404' },
{ path: '403', redirectTo: '/error/403' }, { path: '403', redirectTo: '/error/403' },
{ path: '401', redirectTo: '/auth/sign-in' }, { path: '401', redirectTo: '/auth/login' },
{ path: 'unauthorized', redirectTo: '/error/403' }, { path: 'unauthorized', redirectTo: '/error/403' },
// ===== CATCH-ALL ===== // ===== CATCH-ALL =====

View File

@ -25,23 +25,32 @@ export class App implements OnInit {
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
try { try {
// Initialiser l'authentification avec gestion d'erreur
const isAuthenticated = await this.authService.initialize(); const isAuthenticated = await this.authService.initialize();
console.log('Authentication initialized:', isAuthenticated); if (!isAuthenticated && this.router.url === '/') {
this.router.navigate(['/auth/login']);
if (!isAuthenticated) { } else if (isAuthenticated && this.router.url === '/') {
console.log('👤 User not authenticated, may redirect to login'); this.router.navigate(['/dcb-dashboard']);
// Note: Votre AuthService gère déjà la redirection dans logout()
} }
} catch (error) { } catch (error) {
console.error('Error during authentication initialization:', error); console.error('Error during authentication initialization:', error);
this.router.navigate(['/auth/login']);
} }
// Configurer le titre de la page
this.setupTitleListener(); this.setupTitleListener();
} }
private checkPublicRouteRedirection(): void {
const currentUrl = this.router.url;
const publicRoutes = ['/auth/login', '/auth/reset-password', '/auth/forgot-password'];
// Si l'utilisateur est authentifié et sur une route publique, rediriger vers la page d'accueil
if (publicRoutes.includes(currentUrl)) {
this.router.navigate(['/dcb-dashboard']);
}
}
private setupTitleListener(): void { private setupTitleListener(): void {
this.router.events this.router.events
.pipe( .pipe(

View File

@ -10,14 +10,11 @@ export const authGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: R
const roleService = inject(RoleService); const roleService = inject(RoleService);
const router = inject(Router); const router = inject(Router);
console.log('🔐 AuthGuard check for:', state.url);
// Attendre que l'initialisation soit terminée // Attendre que l'initialisation soit terminée
return authService.getInitializedState().pipe( return authService.getInitializedState().pipe(
switchMap(initialized => { switchMap(initialized => {
if (!initialized) { if (!initialized) {
console.log('⏳ AuthService pas encore initialisé, attente...'); return of(false);
return of(false); // Bloquer en attendant l'initialisation
} }
// Vérifier l'authentification // Vérifier l'authentification
@ -28,29 +25,24 @@ export const authGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: R
// Tentative de rafraîchissement du token // Tentative de rafraîchissement du token
const refreshToken = authService.getRefreshToken(); const refreshToken = authService.getRefreshToken();
if (refreshToken) { if (refreshToken) {
console.log('🔄 Token expiré, tentative de rafraîchissement...');
return authService.refreshToken().pipe( return authService.refreshToken().pipe(
tap(() => { tap(() => {
console.log('✅ Token rafraîchi avec succès');
roleService.refreshRoles(); roleService.refreshRoles();
}), }),
map(() => checkRoleAccess(route, roleService, router, state.url)), map(() => checkRoleAccess(route, roleService, router, state.url)),
catchError((error) => { catchError((error) => {
console.error('❌ Échec du rafraîchissement du token:', error);
authService.logout().subscribe(); authService.logout().subscribe();
return of(redirectToLogin(router, state.url, 'session_expired')); return of(redirectToLogin(router, state.url));
}) })
); );
} }
// Redirection vers login // Redirection vers login
console.log('🔒 Redirection vers login depuis:', state.url); return of(redirectToLogin(router, state.url));
return of(redirectToLogin(router, state.url, 'not_authenticated'));
}), }),
catchError(error => { catchError(error => {
console.error('❌ Erreur dans le guard d\'auth:', error); return of(redirectToLogin(router, state.url));
return of(redirectToLogin(router, state.url, 'not_authenticated'));
}) })
); );
} }
@ -67,7 +59,6 @@ function checkRoleAccess(
const requiredRoles = route.data?.['roles'] as string[]; const requiredRoles = route.data?.['roles'] as string[];
if (!requiredRoles || requiredRoles.length === 0) { if (!requiredRoles || requiredRoles.length === 0) {
console.log('✅ Accès autorisé (aucun rôle requis):', currentUrl);
return true; return true;
} }
@ -75,17 +66,9 @@ function checkRoleAccess(
const currentUserRoles = roleService.getCurrentUserRoles(); const currentUserRoles = roleService.getCurrentUserRoles();
if (hasRequiredRole) { if (hasRequiredRole) {
console.log('✅ Accès autorisé avec rôles:', currentUrl);
console.log(' Rôles requis:', requiredRoles);
console.log(' Rôles actuels:', currentUserRoles);
return true; return true;
} }
console.warn('❌ Accès refusé: rôles insuffisants');
console.warn(' URL demandée:', currentUrl);
console.warn(' Rôles requis:', requiredRoles);
console.warn(' Rôles actuels:', currentUserRoles);
// Rediriger vers la page non autorisée // Rediriger vers la page non autorisée
router.navigate(['/unauthorized'], { router.navigate(['/unauthorized'], {
queryParams: { queryParams: {
@ -104,20 +87,14 @@ function checkRoleAccess(
*/ */
function redirectToLogin( function redirectToLogin(
router: Router, router: Router,
returnUrl: string, returnUrl: string,
reason: 'not_authenticated' | 'session_expired'
): boolean { ): boolean {
const queryParams: any = { const queryParams: any = {
returnUrl: returnUrl, returnUrl: returnUrl
reason: reason
}; };
// Message spécifique selon la raison // Message spécifique selon la raison
if (reason === 'session_expired') { router.navigate(['/auth/login'], {
queryParams.message = 'Votre session a expiré. Veuillez vous reconnecter.';
}
router.navigate(['/auth/sign-in'], {
queryParams, queryParams,
replaceUrl: true replaceUrl: true
}); });

View File

@ -10,7 +10,6 @@ export const publicGuard: CanActivateFn = () => {
// Si l'utilisateur est déjà authentifié, le rediriger vers le dashboard // Si l'utilisateur est déjà authentifié, le rediriger vers le dashboard
if (authService.isAuthenticated()) { if (authService.isAuthenticated()) {
console.log('🔄 Utilisateur déjà authentifié, redirection vers le dashboard');
router.navigate(['/dcb-dashboard'], { replaceUrl: true }); router.navigate(['/dcb-dashboard'], { replaceUrl: true });
return false; return false;
} }
@ -18,16 +17,12 @@ export const publicGuard: CanActivateFn = () => {
// Vérifier si un refresh token est disponible // Vérifier si un refresh token est disponible
const refreshToken = authService.getRefreshToken(); const refreshToken = authService.getRefreshToken();
if (refreshToken) { if (refreshToken) {
console.log('🔄 Token de rafraîchissement détecté, tentative de reconnexion...');
return authService.refreshToken().pipe( return authService.refreshToken().pipe(
map(() => { map(() => {
console.log('✅ Reconnexion automatique réussie');
router.navigate(['/dcb-dashboard'], { replaceUrl: true }); router.navigate(['/dcb-dashboard'], { replaceUrl: true });
return false; return false;
}), }),
catchError((error) => { catchError((error) => {
console.error('❌ Échec de la reconnexion automatique:', error);
// En cas d'erreur, autoriser l'accès à la page publique // En cas d'erreur, autoriser l'accès à la page publique
return of(true); return of(true);
}) })
@ -35,6 +30,5 @@ export const publicGuard: CanActivateFn = () => {
} }
// L'utilisateur n'est pas connecté, autoriser l'accès à la page publique // L'utilisateur n'est pas connecté, autoriser l'accès à la page publique
console.log('🌐 Accès public autorisé');
return true; return true;
}; };

View File

@ -10,7 +10,6 @@ export const roleGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state) =
// Vérifier d'abord l'authentification // Vérifier d'abord l'authentification
if (!authService.isAuthenticated()) { if (!authService.isAuthenticated()) {
console.log('RoleGuard: User not authenticated, redirecting to login');
router.navigate(['/auth/login'], { router.navigate(['/auth/login'], {
queryParams: { queryParams: {
returnUrl: state.url, returnUrl: state.url,
@ -24,26 +23,16 @@ export const roleGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state) =
const userRoles = authService.getCurrentUserRoles(); const userRoles = authService.getCurrentUserRoles();
if (!userRoles || userRoles.length === 0) { if (!userRoles || userRoles.length === 0) {
console.warn('RoleGuard: User has no roles'); router.navigate(['/unauthorized']);
router.navigate(['/unauthorized'], {
queryParams: { reason: 'no_roles' }
});
return false; return false;
} }
const modulePath = getModulePath(route); const modulePath = getModulePath(route);
console.log('🔐 RoleGuard check:', {
module: modulePath,
userRoles: userRoles,
url: state.url
});
// Vérifier les permissions // Vérifier les permissions
const hasAccess = permissionsService.canAccessModule(modulePath, userRoles); const hasAccess = permissionsService.canAccessModule(modulePath, userRoles);
if (!hasAccess) { if (!hasAccess) {
console.warn('❌ RoleGuard: Access denied for', modulePath, 'User roles:', userRoles);
router.navigate(['/unauthorized'], { router.navigate(['/unauthorized'], {
queryParams: { queryParams: {
module: modulePath, module: modulePath,
@ -54,7 +43,6 @@ export const roleGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state) =
return false; return false;
} }
console.log('✅ RoleGuard: Access granted for', modulePath);
return true; return true;
}; };
@ -92,8 +80,6 @@ function buildPathFromUrl(route: ActivatedRouteSnapshot): string {
} }
function getRequiredRolesForModule(permissionsService: PermissionsService, modulePath: string): string[] { function getRequiredRolesForModule(permissionsService: PermissionsService, modulePath: string): string[] {
// Cette fonction récupère les rôles requis pour un module donné
// Vous devrez peut-être l'adapter selon votre implémentation PermissionsService
const [mainModule] = modulePath.split('/'); const [mainModule] = modulePath.split('/');
const permission = (permissionsService as any).findPermission?.(mainModule); const permission = (permissionsService as any).findPermission?.(mainModule);
return permission?.roles || []; return permission?.roles || [];

View File

@ -55,7 +55,7 @@ function handle401Error(
}), }),
catchError((refreshError) => { catchError((refreshError) => {
authService.logout().subscribe(); authService.logout().subscribe();
router.navigate(['/auth/sign-in']); router.navigate(['/auth/login']);
return throwError(() => refreshError); return throwError(() => refreshError);
}) })
); );

View File

@ -38,7 +38,14 @@ export interface UserProfileDto {
firstName: string; firstName: string;
lastName: string; lastName: string;
roles: string[]; roles: string[];
enabled: boolean;
emailVerified: boolean; emailVerified: boolean;
merchantPartnerId: string;
createdBy: string;
createdByUsername: string;
createdTimestamp: number;
lastLogin?: number;
userType: string;
} }
export interface TokenValidationResponseDto { export interface TokenValidationResponseDto {
@ -74,36 +81,29 @@ export class AuthService {
* Initialise l'authentification au démarrage de l'application * Initialise l'authentification au démarrage de l'application
*/ */
async initialize(): Promise<boolean> { async initialize(): Promise<boolean> {
console.log('🔄 Initialisation du service d\'authentification...');
try { try {
const token = this.getAccessToken(); const token = this.getAccessToken();
if (!token) { if (!token) {
console.log('🔍 Aucun token trouvé, utilisateur non authentifié');
this.initialized$.next(true); this.initialized$.next(true);
return false; return false;
} }
if (this.isTokenExpired(token)) { if (this.isTokenExpired(token)) {
console.log('⚠️ Token expiré, tentative de rafraîchissement...');
const refreshSuccess = await this.tryRefreshToken(); const refreshSuccess = await this.tryRefreshToken();
this.initialized$.next(true); this.initialized$.next(true);
return refreshSuccess; return refreshSuccess;
} }
// Token valide, vérifier le profil utilisateur // Token valide, vérifier le profil utilisateur
console.log('✅ Token valide détecté, vérification du profil...');
await this.loadUserProfile().toPromise(); await this.loadUserProfile().toPromise();
this.authState$.next(true); this.authState$.next(true);
this.initialized$.next(true); this.initialized$.next(true);
console.log('🎯 Authentification initialisée avec succès');
return true; return true;
} catch (error) { } catch (error) {
console.error('❌ Erreur lors de l\'initialisation de l\'auth:', error);
this.clearAuthData(); this.clearAuthData();
this.initialized$.next(true); this.initialized$.next(true);
return false; return false;
@ -117,17 +117,14 @@ export class AuthService {
const refreshToken = this.getRefreshToken(); const refreshToken = this.getRefreshToken();
if (!refreshToken) { if (!refreshToken) {
console.log('🔍 Aucun refresh token disponible');
return false; return false;
} }
try { try {
// Convertir l'Observable en Promise pour l'initialisation // Convertir l'Observable en Promise pour l'initialisation
const response = await this.refreshToken().toPromise(); const response = await this.refreshToken().toPromise();
console.log('✅ Token rafraîchi avec succès lors de l\'initialisation');
return true; return true;
} catch (error) { } catch (error) {
console.error('❌ Échec du rafraîchissement du token:', error);
this.clearAuthData(); this.clearAuthData();
return false; return false;
} }
@ -174,10 +171,8 @@ export class AuthService {
).pipe( ).pipe(
tap(response => { tap(response => {
this.handleLoginSuccess(response); this.handleLoginSuccess(response);
console.log('🔄 Token rafraîchi avec succès');
}), }),
catchError(error => { catchError(error => {
console.error('❌ Échec du rafraîchissement du token:', error);
this.clearAuthData(); this.clearAuthData();
return throwError(() => error); return throwError(() => error);
}) })
@ -194,7 +189,6 @@ export class AuthService {
).pipe( ).pipe(
tap(() => { tap(() => {
this.clearAuthData(); this.clearAuthData();
console.log('👋 Déconnexion réussie');
}), }),
catchError(error => { catchError(error => {
this.clearAuthData(); // Nettoyer même en cas d'erreur this.clearAuthData(); // Nettoyer même en cas d'erreur
@ -212,10 +206,8 @@ export class AuthService {
).pipe( ).pipe(
tap(profile => { tap(profile => {
this.userProfile$.next(profile); this.userProfile$.next(profile);
console.log('👤 Profil utilisateur chargé:', profile.username);
}), }),
catchError(error => { catchError(error => {
console.error('❌ Erreur lors du chargement du profil:', error);
return throwError(() => error); return throwError(() => error);
}) })
); );
@ -233,7 +225,6 @@ export class AuthService {
} }
this.authState$.next(true); this.authState$.next(true);
console.log('✅ Connexion réussie');
} }
} }
@ -245,7 +236,6 @@ export class AuthService {
localStorage.removeItem(this.refreshTokenKey); localStorage.removeItem(this.refreshTokenKey);
this.authState$.next(false); this.authState$.next(false);
this.userProfile$.next(null); this.userProfile$.next(null);
console.log('🧹 Données d\'authentification nettoyées');
} }
/** /**

View File

@ -124,19 +124,19 @@ export class MenuService {
]; ];
} }
// Mettez à jour votre méthode
private getFullUserDropdown(): UserDropdownItemType[] { private getFullUserDropdown(): UserDropdownItemType[] {
return [ return [
{ label: 'Welcome back!', isHeader: true }, { label: 'Welcome back!', isHeader: true },
{ label: 'Profile', icon: 'tablerUserCircle', url: '/profile' }, { label: 'Profile', icon: 'tablerUserCircle', url: '/profile' },
{ label: 'Account Settings', icon: 'tablerSettings2', url: '/settings' }, { label: 'Account Settings', icon: 'tablerSettings2', url: '/settings' },
{ label: 'Support Center', icon: 'tablerHeadset', url: '/support' }, { label: 'Support Center', icon: 'tablerHeadset', url: '/support' },
{ isDivider: true }, { isDivider: true },
{ {
label: 'Log Out', label: 'Déconnexion',
icon: 'tablerLogout2', icon: 'tablerLogout2',
url: '/auth/logout', class: 'fw-semibold text-danger'
class: 'fw-semibold text-danger' },
}, ];
]; }
}
} }

View File

@ -22,6 +22,18 @@ export class PermissionsService {
], ],
}, },
{
module: 'auth',
roles: [
'dcb-admin',
'dcb-partner',
'dcb-support',
'dcb-partner-admin',
'dcb-partner-manager',
'dcb-partner-support'
],
},
// Transactions // Transactions
{ {
module: 'transactions', module: 'transactions',

View File

@ -303,6 +303,9 @@ export class RoleManagementService {
return 'bg-info'; return 'bg-info';
case UserRole.DCB_PARTNER: case UserRole.DCB_PARTNER:
return 'bg-success'; return 'bg-success';
case UserRole.DCB_PARTNER_ADMIN: return 'bg-danger';
case UserRole.DCB_PARTNER_MANAGER: return 'bg-success';
case UserRole.DCB_PARTNER_SUPPORT: return 'bg-info';
default: default:
return 'bg-secondary'; return 'bg-secondary';
} }
@ -319,6 +322,12 @@ export class RoleManagementService {
return 'lucideHeadphones'; return 'lucideHeadphones';
case UserRole.DCB_PARTNER: case UserRole.DCB_PARTNER:
return 'lucideBuilding'; return 'lucideBuilding';
case UserRole.DCB_PARTNER_ADMIN:
return 'lucideShield';
case UserRole.DCB_PARTNER_MANAGER:
return 'lucideUserCog';
case UserRole.DCB_PARTNER_SUPPORT:
return 'lucideHeadphones';
default: default:
return 'lucideUser'; return 'lucideUser';
} }

View File

@ -33,10 +33,9 @@ export const userDropdownItems: UserDropdownItemType[] = [
isDivider: true, isDivider: true,
}, },
{ {
label: 'Log Out', label: 'Déconnexion',
icon: 'tablerLogout2', icon: 'tablerLogout2',
url: '/auth/logout', url: '/auth/logout',
class: 'fw-semibold',
}, },
] ]
@ -112,16 +111,6 @@ export const menuItems: MenuItemType[] = [
{ label: 'Liste des Utilisateurs', url: '/users' }, { label: 'Liste des Utilisateurs', url: '/users' },
], ],
}, },
{
label: 'Authentification',
icon: 'lucideFingerprint',
isCollapsed: true,
children: [
{ label: 'Login / Logout', url: '/auth/login' },
{ label: 'Réinitialisation Mot de Passe', url: '/auth/reset-password' },
{ label: 'Gestion des Sessions', url: '/auth/sessions' },
],
},
// --------------------------- // ---------------------------
// Paramètres & Intégrations // Paramètres & Intégrations

View File

@ -28,21 +28,7 @@
<!-- Élément normal --> <!-- Élément normal -->
@if (!item.isHeader && !item.isDivider) { @if (!item.isHeader && !item.isDivider) {
@if (item.label === 'Log Out') { <a
<!-- Bouton Logout avec appel de méthode -->
<button
class="dropdown-item fw-semibold text-danger"
(click)="logout()">
<ng-icon
name="tablerLogout2"
size="17"
class="align-middle d-inline-flex align-items-center me-2"
/>
<span class="align-middle">Log Out</span>
</button>
} @else {
<!-- Autres items avec navigation normale -->
<a
[routerLink]="item.url" [routerLink]="item.url"
class="dropdown-item" class="dropdown-item"
[class]="item.class" [class]="item.class"
@ -55,7 +41,6 @@
/> />
<span class="align-middle">{{ item.label }}</span> <span class="align-middle">{{ item.label }}</span>
</a> </a>
}
} }
</div> </div>
} }

View File

@ -45,14 +45,4 @@ export class UserProfile implements OnInit, OnDestroy {
private loadDropdownItems() { private loadDropdownItems() {
this.menuItems = this.menuService.getUserDropdownItems() this.menuItems = this.menuService.getUserDropdownItems()
} }
logout() {
this.authService.logout()
}
handleItemClick(item: UserDropdownItemType) {
if (item.label === 'Log Out') {
this.logout()
}
}
} }

View File

@ -1,13 +1,14 @@
import { Routes } from '@angular/router' import { Routes } from '@angular/router'
import { SignIn } from '@/app/modules/auth/sign-in' import { Login } from '@/app/modules/auth/login'
import { Logout } from '@/app/modules/auth/logout'
import { ResetPassword } from '@/app/modules/auth/reset-password' import { ResetPassword } from '@/app/modules/auth/reset-password'
import { NewPassword } from '@/app/modules/auth/new-password' import { NewPassword } from '@/app/modules/auth/new-password'
import { publicGuard } from '@core/guards/public.guard'; import { publicGuard } from '@core/guards/public.guard';
export const AUTH_ROUTES: Routes = [ export const AUTH_ROUTES: Routes = [
{ {
path: 'sign-in', path: 'login',
component: SignIn, component: Login,
canActivate: [publicGuard], canActivate: [publicGuard],
data: { data: {
title: 'Connexion - DCB', title: 'Connexion - DCB',
@ -35,7 +36,10 @@ export const AUTH_ROUTES: Routes = [
}, },
{ {
path: 'logout', path: 'logout',
redirectTo: 'sign-in', component: Logout,
data: { module: 'auth' } data: {
title: 'Déconnexion - DCB',
module: 'auth'
}
} }
]; ];

View File

@ -1,4 +1,4 @@
// src/app/modules/auth/sign-in/sign-in.ts // src/app/modules/auth/login/sign-in.ts
import { Component, inject, ChangeDetectorRef, OnInit, OnDestroy } from '@angular/core'; import { Component, inject, ChangeDetectorRef, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule, NgForm } from '@angular/forms'; import { FormsModule, NgForm } from '@angular/forms';
@ -11,7 +11,7 @@ import { PasswordStrengthBar } from '@app/components/password-strength-bar';
import { appName, credits, currentYear } from '@/app/constants'; import { appName, credits, currentYear } from '@/app/constants';
@Component({ @Component({
selector: 'app-sign-in', selector: 'app-login',
standalone: true, standalone: true,
imports: [FormsModule, CommonModule, RouterLink, AppLogo, PasswordStrengthBar], imports: [FormsModule, CommonModule, RouterLink, AppLogo, PasswordStrengthBar],
template: ` template: `
@ -149,7 +149,7 @@ import { appName, credits, currentYear } from '@/app/constants';
`, `,
styles: [] styles: []
}) })
export class SignIn implements OnInit, OnDestroy { export class Login implements OnInit, OnDestroy {
protected readonly appName = appName; protected readonly appName = appName;
protected readonly currentYear = currentYear; protected readonly currentYear = currentYear;
protected readonly credits = credits; protected readonly credits = credits;
@ -258,16 +258,18 @@ export class SignIn implements OnInit, OnDestroy {
const userRoles = this.roleService.getCurrentUserRoles(); const userRoles = this.roleService.getCurrentUserRoles();
// Logique de redirection selon les rôles // Logique de redirection selon les rôles
if (userRoles.includes('dcb-admin')) { /*if (userRoles.includes('dcb-admin')) {
this.router.navigate(['/admin/dcb-dashboard']); this.router.navigate(['/dcb-dashboard']);
} else if (userRoles.includes('dcb-partner-admin')) { } else if (userRoles.includes('dcb-partner-admin')) {
this.router.navigate(['/partner/dcb-dashboard']); this.router.navigate(['/dcb-dashboard']);
} else if (userRoles.includes('dcb-support')) { } else if (userRoles.includes('dcb-support')) {
this.router.navigate(['/support/dcb-dashboard']); this.router.navigate(['/support/dcb-dashboard']);
} else { } else {
// Route par défaut // Route par défaut
this.router.navigate(['/dcb-dashboard']); this.router.navigate(['/dcb-dashboard']);
} }*/
// Route par défaut
this.router.navigate(['/dcb-dashboard']);
} }
/** /**

View File

@ -0,0 +1,51 @@
// app/modules/auth/logout/logout.component.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '@core/services/auth.service';
@Component({
template: `
<div class="logout-container">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Déconnexion en cours...</span>
</div>
<p class="mt-2">Déconnexion en cours...</p>
</div>
`,
styles: [`
.logout-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
}
`]
})
export class Logout implements OnInit {
constructor(
private authService: AuthService,
private router: Router
) {}
ngOnInit() {
this.logout();
}
private logout(): void {
this.authService.logout().subscribe({
next: () => {
// Redirection vers la page de login après déconnexion
this.router.navigate(['/auth/login'], {
queryParams: { logout: 'success' }
});
},
error: (error) => {
console.error('Erreur lors de la déconnexion:', error);
// Rediriger même en cas d'erreur
this.router.navigate(['/auth/login']);
}
});
}
}

View File

@ -136,7 +136,7 @@ import { NgOtpInputModule } from 'ng-otp-input'
<p class="text-muted text-center mb-0"> <p class="text-muted text-center mb-0">
Return to Return to
<a <a
routerLink="/auth/sign-in" routerLink="/auth/login"
class="text-decoration-underline link-offset-3 fw-semibold" class="text-decoration-underline link-offset-3 fw-semibold"
>Sign in</a >Sign in</a
> >

View File

@ -63,7 +63,7 @@ import { AppLogo } from '@app/components/app-logo'
<p class="text-muted text-center mt-4 mb-0"> <p class="text-muted text-center mt-4 mb-0">
Return to Return to
<a <a
routerLink="/auth/sign-in" routerLink="/auth/login"
class="text-decoration-underline link-offset-3 fw-semibold" class="text-decoration-underline link-offset-3 fw-semibold"
>Sign in</a >Sign in</a
> >

View File

@ -66,7 +66,7 @@ import { credits, currentYear } from '@/app/constants'
<p class="text-muted text-center mb-0"> <p class="text-muted text-center mb-0">
Return to Return to
<a <a
routerLink="/auth/sign-in" routerLink="/auth/login"
class="text-decoration-underline link-offset-3 fw-semibold" class="text-decoration-underline link-offset-3 fw-semibold"
>Sign in</a >Sign in</a
> >

View File

@ -251,8 +251,4 @@ const routes: Routes = [
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule], exports: [RouterModule],
}) })
export class ModulesRoutes { export class ModulesRoutes {}
constructor() {
console.log('Modules routes loaded:', routes);
}
}

View File

@ -22,7 +22,7 @@
@if (user) { @if (user) {
{{ getUserDisplayName() }} {{ getUserDisplayName() }}
} @else { } @else {
Profil Profile
} }
</li> </li>
</ol> </ol>
@ -178,13 +178,29 @@
<div class="card-body"> <div class="card-body">
<!-- Rôle actuel --> <!-- Rôle actuel -->
<div class="text-center mb-3"> <div class="text-center mb-3">
<span class="badge d-flex align-items-center justify-content-center" [ngClass]="getRoleBadgeClass(user.role)"> <!-- Afficher le premier rôle ou tous les rôles -->
<ng-icon [name]="getRoleIcon(user.role)" class="me-2"></ng-icon> @if (user.roles && user.roles.length > 0) {
{{ getRoleLabel(user.role) }} @for (role of user.roles; track role; let first = $first) {
</span> @if (first) { <!-- Afficher seulement le premier rôle pour l'instant -->
<small class="text-muted d-block mt-1"> <span class="badge d-flex align-items-center justify-content-center" [ngClass]="getRoleBadgeClass(role)">
{{ getRoleDescription(user.role) }} <ng-icon [name]="getRoleIcon(role)" class="me-2"></ng-icon>
</small> {{ getRoleLabel(role) }}
</span>
<small class="text-muted d-block mt-1">
{{ getRoleDescription(role) }}
</small>
}
}
<!-- Indicateur si plusieurs rôles -->
@if (user.roles.length > 1) {
<small class="text-muted">
+ {{ user.roles.length - 1 }} autre(s) rôle(s)
</small>
}
} @else {
<span class="badge bg-secondary">Aucun rôle</span>
}
</div> </div>
<!-- Changement de rôle --> <!-- Changement de rôle -->
@ -193,7 +209,7 @@
<label class="form-label fw-semibold">Changer le rôle</label> <label class="form-label fw-semibold">Changer le rôle</label>
<select <select
class="form-select" class="form-select"
[value]="user.role" [value]="user.roles[0]"
(change)="updateUserRole($any($event.target).value)" (change)="updateUserRole($any($event.target).value)"
[disabled]="updatingRoles" [disabled]="updatingRoles"
> >
@ -201,12 +217,12 @@
@for (role of availableRoles; track role.value) { @for (role of availableRoles; track role.value) {
<option <option
[value]="role.value" [value]="role.value"
[disabled]="!canAssignRole(role.value) || role.value === user.role" [disabled]="!canAssignRole(role.value) || role.value === user.roles[0]"
> >
{{ role.label }} {{ role.label }}
@if (!canAssignRole(role.value)) { @if (!canAssignRole(role.value)) {
(Non autorisé) (Non autorisé)
} @else if (role.value === user.role) { } @else if (role.value === user.roles[0]) {
(Actuel) (Actuel)
} }
</option> </option>

View File

@ -101,7 +101,10 @@ export class MyProfile implements OnInit, OnDestroy {
this.availableRoles = [ this.availableRoles = [
{ value: UserRole.DCB_ADMIN, label: 'DCB Admin', description: 'Administrateur système' }, { value: UserRole.DCB_ADMIN, label: 'DCB Admin', description: 'Administrateur système' },
{ value: UserRole.DCB_SUPPORT, label: 'DCB Support', description: 'Support technique' }, { value: UserRole.DCB_SUPPORT, label: 'DCB Support', description: 'Support technique' },
{ value: UserRole.DCB_PARTNER, label: 'DCB Partner', description: 'Partenaire commercial' } { value: UserRole.DCB_PARTNER, label: 'DCB Partner', description: 'Partenaire commercial' },
{ value: UserRole.DCB_PARTNER_ADMIN, label: 'DCB Partner Admin', description: 'Admin Partenaire commercial' },
{ value: UserRole.DCB_PARTNER_MANAGER, label: 'DCB Partner Manager', description: 'Manager Partenaire commercial' },
{ value: UserRole.DCB_PARTNER_SUPPORT, label: 'DCB Partner Support', description: 'Support Partenaire commercial' }
]; ];
} }
}); });
@ -111,11 +114,11 @@ export class MyProfile implements OnInit, OnDestroy {
this.loading = true; this.loading = true;
this.error = ''; this.error = '';
this.usersService.getUserById(this.user.id) this.authService.getProfile()
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (user) => { next: (profile) => {
this.user = user; this.user = profile;
this.loading = false; this.loading = false;
this.cdRef.detectChanges(); this.cdRef.detectChanges();
}, },

View File

@ -55,7 +55,10 @@ export interface PaginatedUserResponse {
export enum UserRole { export enum UserRole {
DCB_ADMIN = 'dcb-admin', DCB_ADMIN = 'dcb-admin',
DCB_SUPPORT = 'dcb-support', DCB_SUPPORT = 'dcb-support',
DCB_PARTNER = 'dcb-partner' 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' }) @Injectable({ providedIn: 'root' })

View File

@ -54,13 +54,13 @@ export type MenuItemType = {
isCollapsed?: boolean isCollapsed?: boolean
} }
// types/layout.ts - Ajoutez ce type
export type UserDropdownItemType = { export type UserDropdownItemType = {
label?: string; label?: string;
icon?: string; icon?: string;
url?: string; url?: string;
isDivider?: boolean; isDivider?: boolean;
isHeader?: boolean; isHeader?: boolean;
isLogout?: boolean;
class?: string; class?: string;
target?: string; target?: string;
isDisabled?: boolean; isDisabled?: boolean;

View File

@ -8,7 +8,6 @@ bootstrapApplication(App, {
...appConfig.providers, ...appConfig.providers,
] ]
}).then(async appRef => { }).then(async appRef => {
console.log('Application BO Admin completed.');
const authService = appRef.injector.get(AuthService); const authService = appRef.injector.get(AuthService);
await authService.initialize(); await authService.initialize();
}).catch(err => console.error('BO Admin error', err)); }).catch(err => console.error('BO Admin error', err));