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

This commit is contained in:
diallolatoile 2025-12-15 15:14:21 +00:00
parent b81710fa59
commit 25d99b4edc
3 changed files with 183 additions and 190 deletions

View File

@ -200,19 +200,6 @@ export class TransactionDetails implements OnInit {
if (diffDays < 7) return `Il y a ${diffDays} j`; if (diffDays < 7) return `Il y a ${diffDays} j`;
return this.formatDate(date); return this.formatDate(date);
} }
canRefund(): boolean {
return this.access.canRefund && this.transaction?.status === 'SUCCESS';
}
canRetry(): boolean {
return this.access.canRetry && this.transaction?.status === 'FAILED';
}
canCancel(): boolean {
return this.access.canCancel && this.transaction?.status === 'PENDING';
}
// Méthodes pour le template // Méthodes pour le template
getUserBadgeClass(): string { getUserBadgeClass(): string {
return this.access.isHubUser ? 'bg-primary' : 'bg-success'; return this.access.isHubUser ? 'bg-primary' : 'bg-success';

View File

@ -21,7 +21,8 @@ import {
lucideStore, lucideStore,
lucideCalendar, lucideCalendar,
lucideRepeat, lucideRepeat,
lucideCreditCard lucideCreditCard,
lucideInfo
} from '@ng-icons/lucide'; } from '@ng-icons/lucide';
import { NgbPaginationModule, NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbPaginationModule, NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
@ -125,12 +126,9 @@ export class TransactionsList implements OnInit, OnDestroy {
private initializePermissions() { private initializePermissions() {
this.access = this.accessService.getTransactionAccess(); this.access = this.accessService.getTransactionAccess();
// Ajouter le merchant ID aux filtres si nécessaire // IMPORTANT: Toujours filtrer par merchant pour les merchant users
if (this.access.isMerchantUser && this.access.allowedMerchantIds.length > 0) { if (this.access.isMerchantUser && this.access.allowedMerchantIds.length > 0) {
this.filters.merchantPartnerId = this.access.allowedMerchantIds[0]; this.filters.merchantPartnerId = this.access.allowedMerchantIds[0];
} else if (this.access.isHubUser && this.access.merchantId) {
// Pour les hub users qui veulent voir un merchant spécifique
this.filters.merchantPartnerId = this.access.merchantId;
} }
// Définir le rôle pour l'affichage // Définir le rôle pour l'affichage
@ -147,27 +145,31 @@ export class TransactionsList implements OnInit, OnDestroy {
this.loading = true; this.loading = true;
this.error = ''; this.error = '';
// Mettre à jour les filtres avec la recherche // Préparer les filtres pour l'API
if (this.searchTerm) { const apiFilters = this.prepareFiltersForApi();
this.filters.search = this.searchTerm;
} else {
delete this.filters.search;
}
// Appliquer le tri
this.filters.sortBy = this.sortField;
this.filters.sortOrder = this.sortDirection;
// Appliquer les restrictions de merchant pour les non-admin hub users console.log('Chargement transactions avec filtres:', apiFilters);
if (!this.access.canManageAll && this.access.allowedMerchantIds.length > 0) {
this.filters.merchantPartnerId = this.access.allowedMerchantIds[0];
}
this.transactionsService.getTransactions(this.filters).subscribe({ this.transactionsService.getTransactions(apiFilters).subscribe({
next: (data) => { next: (data) => {
this.paginatedData = data; this.paginatedData = data;
this.transactions = data.data; this.transactions = data.data;
this.loading = false; this.loading = false;
// Log pour debug
if (data.data.length > 0) {
console.log(`Chargement réussi: ${data.data.length} transactions`);
if (this.access.isMerchantUser && this.currentMerchantId) {
// Vérifier que toutes les transactions appartiennent bien au marchand
const wrongMerchantTx = data.data.filter(tx =>
tx.merchantPartnerId !== this.currentMerchantId
);
if (wrongMerchantTx.length > 0) {
console.warn('ATTENTION: certaines transactions ne sont pas du bon marchand:', wrongMerchantTx);
}
}
}
this.cdRef.detectChanges(); this.cdRef.detectChanges();
}, },
error: (error) => { error: (error) => {
@ -179,12 +181,39 @@ export class TransactionsList implements OnInit, OnDestroy {
}); });
} }
private prepareFiltersForApi(): TransactionQuery {
const apiFilters: TransactionQuery = { ...this.filters };
// Ajouter la recherche si présente
if (this.searchTerm) {
apiFilters.search = this.searchTerm;
}
// Appliquer le tri
apiFilters.sortBy = this.sortField;
apiFilters.sortOrder = this.sortDirection;
// Nettoyer les filtres (enlever les undefined)
Object.keys(apiFilters).forEach(key => {
if (apiFilters[key as keyof TransactionQuery] === undefined) {
delete apiFilters[key as keyof TransactionQuery];
}
});
return apiFilters;
}
// Recherche et filtres // Recherche et filtres
onSearch() { onSearch() {
this.filters.page = 1; this.filters.page = 1;
this.loadTransactions(); this.loadTransactions();
} }
clearSearch() {
this.searchTerm = '';
this.onSearch();
}
onClearFilters() { onClearFilters() {
this.searchTerm = ''; this.searchTerm = '';
this.filters = { this.filters = {
@ -197,7 +226,7 @@ export class TransactionsList implements OnInit, OnDestroy {
sortOrder: 'desc' sortOrder: 'desc'
}; };
// Réappliquer les restrictions de merchant // IMPORTANT: Toujours réappliquer le filtrage par marchand pour les merchant users
if (this.access.isMerchantUser && this.access.allowedMerchantIds.length > 0) { if (this.access.isMerchantUser && this.access.allowedMerchantIds.length > 0) {
this.filters.merchantPartnerId = this.access.allowedMerchantIds[0]; this.filters.merchantPartnerId = this.access.allowedMerchantIds[0];
} }
@ -225,7 +254,10 @@ export class TransactionsList implements OnInit, OnDestroy {
// Permissions pour les filtres // Permissions pour les filtres
canUseMerchantFilter(): boolean { canUseMerchantFilter(): boolean {
return this.access.canFilterByMerchant && this.access.allowedMerchantIds.length > 1; // Uniquement pour les hub users avec plusieurs merchants autorisés
return this.access.canFilterByMerchant &&
this.access.isHubUser &&
this.access.allowedMerchantIds.length > 0;
} }
canUseAllFilters(): boolean { canUseAllFilters(): boolean {
@ -240,6 +272,7 @@ export class TransactionsList implements OnInit, OnDestroy {
this.sortField = field; this.sortField = field;
this.sortDirection = 'desc'; this.sortDirection = 'desc';
} }
this.filters.page = 1;
this.loadTransactions(); this.loadTransactions();
} }
@ -256,15 +289,7 @@ export class TransactionsList implements OnInit, OnDestroy {
// Actions // Actions
viewTransactionDetails(transactionId: string) { viewTransactionDetails(transactionId: string) {
// Vérifier les permissions avant d'afficher this.transactionSelected.emit(transactionId);
this.accessService.canAccessTransaction().subscribe(canAccess => {
if (canAccess) {
this.transactionSelected.emit(transactionId);
} else {
this.error = 'Vous n\'avez pas la permission de voir les détails de cette transaction';
this.cdRef.detectChanges();
}
});
} }
// Sélection multiple // Sélection multiple
@ -283,6 +308,7 @@ export class TransactionsList implements OnInit, OnDestroy {
} else { } else {
this.selectedTransactions.clear(); this.selectedTransactions.clear();
} }
this.updateSelectAllState();
} }
updateSelectAllState() { updateSelectAllState() {
@ -290,7 +316,7 @@ export class TransactionsList implements OnInit, OnDestroy {
this.selectedTransactions.size === this.transactions.length; this.selectedTransactions.size === this.transactions.length;
} }
// Utilitaires d'affichage MIS À JOUR // Utilitaires d'affichage
getStatusBadgeClass(status: TransactionStatus): string { getStatusBadgeClass(status: TransactionStatus): string {
switch (status) { switch (status) {
case 'SUCCESS': return 'badge bg-success'; case 'SUCCESS': return 'badge bg-success';
@ -321,7 +347,7 @@ export class TransactionsList implements OnInit, OnDestroy {
} }
} }
getPeriodicityBadgeClass(periodicity?: string): string { getPeriodicityBadgeClass(periodicity?: string): string {
if (!periodicity) return 'badge bg-secondary'; if (!periodicity) return 'badge bg-secondary';
switch (periodicity.toLowerCase()) { switch (periodicity.toLowerCase()) {
@ -338,101 +364,6 @@ export class TransactionsList implements OnInit, OnDestroy {
} }
} }
formatCurrency(amount: number, currency: Currency = Currency.XOF): string {
return TransactionUtils.formatAmount(amount, currency);
}
formatDate(date: Date | string | undefined | null): string {
// Si la date est null/undefined, retourner une chaîne vide
if (!date) {
return '-';
}
// Si c'est déjà une Date valide
if (date instanceof Date) {
// Vérifier si la Date est valide
if (isNaN(date.getTime())) {
return 'Date invalide';
}
return new Intl.DateTimeFormat('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(date);
}
// Si c'est une chaîne, essayer de la convertir
if (typeof date === 'string') {
const dateObj = new Date(date);
// Vérifier si la conversion a réussi
if (isNaN(dateObj.getTime())) {
// Essayer d'autres formats
const alternativeDate = this.parseDateString(date);
if (alternativeDate && !isNaN(alternativeDate.getTime())) {
return new Intl.DateTimeFormat('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(alternativeDate);
}
return 'Date invalide';
}
return new Intl.DateTimeFormat('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(dateObj);
}
// Pour tout autre type, retourner '-'
return '-';
}
private parseDateString(dateString: string): Date | null {
try {
// Essayer différents formats de date
const formats = [
dateString, // Format ISO original
dateString.replace(' ', 'T'), // Remplacer espace par T
dateString.split('.')[0], // Enlever les millisecondes
];
for (const format of formats) {
const date = new Date(format);
if (!isNaN(date.getTime())) {
return date;
}
}
return null;
} catch {
return null;
}
}
getAmountColor(amount: number): string {
if (amount >= 10000) return 'text-danger fw-bold';
if (amount >= 5000) return 'text-warning fw-semibold';
return 'text-success';
}
// utilitaires pour les abonnements
getStatusDisplayName(status: TransactionStatus): string {
return TransactionUtils.getStatusDisplayName(status);
}
getTypeDisplayName(type: TransactionType): string {
return TransactionUtils.getTypeDisplayName(type);
}
getPeriodicityDisplayName(periodicity?: string): string { getPeriodicityDisplayName(periodicity?: string): string {
if (!periodicity) return ''; if (!periodicity) return '';
@ -445,6 +376,43 @@ export class TransactionsList implements OnInit, OnDestroy {
return periodicityNames[periodicity.toLowerCase()] || periodicity; return periodicityNames[periodicity.toLowerCase()] || periodicity;
} }
formatCurrency(amount: number, currency: Currency = Currency.XOF): string {
return TransactionUtils.formatAmount(amount, currency);
}
formatDate(date: Date | string | undefined | null): string {
if (!date) return '-';
try {
const dateObj = date instanceof Date ? date : new Date(date);
if (isNaN(dateObj.getTime())) return 'Date invalide';
return new Intl.DateTimeFormat('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(dateObj);
} catch {
return '-';
}
}
getAmountColor(amount: number): string {
if (amount >= 10000) return 'text-danger fw-bold';
if (amount >= 5000) return 'text-warning fw-semibold';
return 'text-success';
}
getStatusDisplayName(status: TransactionStatus): string {
return TransactionUtils.getStatusDisplayName(status);
}
getTypeDisplayName(type: TransactionType): string {
return TransactionUtils.getTypeDisplayName(type);
}
// Méthodes pour sécuriser l'accès aux stats // Méthodes pour sécuriser l'accès aux stats
getTotal(): number { getTotal(): number {
@ -482,8 +450,28 @@ export class TransactionsList implements OnInit, OnDestroy {
getScopeText(): string { getScopeText(): string {
if (this.access.isMerchantUser && this.currentMerchantId) { if (this.access.isMerchantUser && this.currentMerchantId) {
return `Merchant ${this.currentMerchantId}`; return `Marchand ${this.currentMerchantId}`;
} else if (this.access.isHubUser && this.filters.merchantPartnerId) {
return `Marchand ${this.filters.merchantPartnerId}`;
} }
return this.access.canManageAll ? 'Tous les merchants' : 'Merchants autorisés'; return 'Tous les marchands';
}
// Méthode pour recharger les données
refreshData() {
this.loadTransactions();
}
// Debug
showDebugInfo() {
console.log('=== DEBUG INFO ===');
console.log('Permissions:', {
isMerchantUser: this.access.isMerchantUser,
isHubUser: this.access.isHubUser,
merchantId: this.currentMerchantId,
allowedMerchantIds: this.access.allowedMerchantIds,
filters: this.filters
});
console.log('Transactions chargées:', this.transactions.length);
} }
} }

View File

@ -1,4 +1,3 @@
// [file name]: transactions/services/transaction-access.service.ts
import { Injectable, Injector, inject } from '@angular/core'; import { Injectable, Injector, inject } from '@angular/core';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { RoleManagementService, UserRole } from '@core/services/hub-users-roles-management.service'; import { RoleManagementService, UserRole } from '@core/services/hub-users-roles-management.service';
@ -10,12 +9,6 @@ export interface TransactionAccess {
canViewAllTransactions: boolean; // Toutes vs seulement les siennes canViewAllTransactions: boolean; // Toutes vs seulement les siennes
canViewDetails: boolean; canViewDetails: boolean;
// Permissions d'actions
canRefund: boolean;
canRetry: boolean;
canCancel: boolean;
canExport: boolean;
// Permissions administratives // Permissions administratives
canManageAll: boolean; // Toutes les transactions canManageAll: boolean; // Toutes les transactions
canFilterByMerchant: boolean; canFilterByMerchant: boolean;
@ -41,7 +34,6 @@ export class TransactionAccessService {
private roleService: RoleManagementService private roleService: RoleManagementService
) {} ) {}
getTransactionAccess(): TransactionAccess { getTransactionAccess(): TransactionAccess {
if (this.accessCache) { if (this.accessCache) {
return this.accessCache; return this.accessCache;
@ -57,19 +49,13 @@ export class TransactionAccessService {
canViewAllTransactions: this.canViewAllTransactions(userRole, isHubUser), canViewAllTransactions: this.canViewAllTransactions(userRole, isHubUser),
canViewDetails: this.canViewDetails(userRole, isHubUser), canViewDetails: this.canViewDetails(userRole, isHubUser),
// Actions selon le rôle
canRefund: this.canPerformRefund(userRole, isHubUser),
canRetry: this.canPerformRetry(userRole, isHubUser),
canCancel: this.canPerformCancel(userRole, isHubUser),
canExport: this.canExport(userRole, isHubUser),
// Permissions administratives // Permissions administratives
canManageAll: this.canManageAll(userRole, isHubUser), canManageAll: this.canManageAll(userRole, isHubUser),
canFilterByMerchant: this.canFilterByMerchant(userRole, isHubUser), canFilterByMerchant: this.canFilterByMerchant(userRole, isHubUser),
canViewSensitiveData: this.canViewSensitiveData(userRole, isHubUser), canViewSensitiveData: this.canViewSensitiveData(userRole, isHubUser),
// Scope // Scope
allowedMerchantIds: this.getAllowedMerchantIds(isHubUser, merchantId), allowedMerchantIds: this.getAllowedMerchantIds(isHubUser, merchantId, userRole),
isHubUser, isHubUser,
isMerchantUser: !isHubUser, isMerchantUser: !isHubUser,
@ -86,13 +72,26 @@ export class TransactionAccessService {
// === MÉTHODES DE DÉTERMINATION DES PERMISSIONS === // === MÉTHODES DE DÉTERMINATION DES PERMISSIONS ===
private canViewTransactions(userRole: UserRole, isHubUser: boolean): boolean { private canViewTransactions(userRole: UserRole, isHubUser: boolean): boolean {
// Tous les rôles peuvent voir les transactions // Tous les rôles peuvent voir les transactions (marchands et hub)
return true; return true;
} }
private canViewAllTransactions(userRole: UserRole, isHubUser: boolean): boolean { private canViewAllTransactions(userRole: UserRole, isHubUser: boolean): boolean {
// Hub users et DCB_PARTNER_ADMIN peuvent voir toutes les transactions // Hub users et DCB_PARTNER_ADMIN peuvent voir toutes les transactions
return isHubUser || userRole === UserRole.DCB_PARTNER_ADMIN; if (isHubUser) {
return true; // Les utilisateurs hub voient toutes les transactions
}
// Pour les utilisateurs marchands :
switch (userRole) {
case UserRole.DCB_PARTNER_ADMIN:
case UserRole.DCB_PARTNER_MANAGER:
case UserRole.DCB_PARTNER_SUPPORT:
// Les admins/support du marchand voient toutes les transactions de leur marchand
return true;
default:
return false;
}
} }
private canViewDetails(userRole: UserRole, isHubUser: boolean): boolean { private canViewDetails(userRole: UserRole, isHubUser: boolean): boolean {
@ -100,32 +99,6 @@ export class TransactionAccessService {
return true; return true;
} }
private canPerformRefund(userRole: UserRole, isHubUser: boolean): boolean {
// DCB_ADMIN: peut rembourser toutes les transactions
// DCB_SUPPORT: peut rembourser les transactions de son périmètre
// DCB_PARTNER_ADMIN: peut rembourser les transactions de son merchant
return isHubUser
? userRole === UserRole.DCB_ADMIN || userRole === UserRole.DCB_SUPPORT
: userRole === UserRole.DCB_PARTNER_ADMIN;
}
private canPerformRetry(userRole: UserRole, isHubUser: boolean): boolean {
// Mêmes permissions que le remboursement
return this.canPerformRefund(userRole, isHubUser);
}
private canPerformCancel(userRole: UserRole, isHubUser: boolean): boolean {
// Plus restrictif - seulement les admins peuvent annuler
return isHubUser
? userRole === UserRole.DCB_ADMIN
: userRole === UserRole.DCB_PARTNER_ADMIN;
}
private canExport(userRole: UserRole, isHubUser: boolean): boolean {
// Tous peuvent exporter leurs propres données
return true;
}
private canManageAll(userRole: UserRole, isHubUser: boolean): boolean { private canManageAll(userRole: UserRole, isHubUser: boolean): boolean {
// Seulement DCB_ADMIN hub peut tout gérer // Seulement DCB_ADMIN hub peut tout gérer
return isHubUser && userRole === UserRole.DCB_ADMIN; return isHubUser && userRole === UserRole.DCB_ADMIN;
@ -137,8 +110,13 @@ export class TransactionAccessService {
} }
private canViewSensitiveData(userRole: UserRole, isHubUser: boolean): boolean { private canViewSensitiveData(userRole: UserRole, isHubUser: boolean): boolean {
// Hub users et DCB_PARTNER_ADMIN peuvent voir les données sensibles if (isHubUser) {
return isHubUser || userRole === UserRole.DCB_PARTNER_ADMIN; // Tous les utilisateurs hub peuvent voir les données sensibles
return true;
} else {
// Pour les marchands, seulement les rôles avec autorisation
return userRole === UserRole.DCB_PARTNER_ADMIN || userRole === UserRole.DCB_PARTNER_SUPPORT;
}
} }
// === GESTION DU SCOPE === // === GESTION DU SCOPE ===
@ -156,16 +134,33 @@ export class TransactionAccessService {
return isNaN(merchantId) ? undefined : merchantId; return isNaN(merchantId) ? undefined : merchantId;
} }
private getAllowedMerchantIds(isHubUser: boolean, merchantId?: number): number[] { private getAllowedMerchantIds(isHubUser: boolean, merchantId?: number, userRole?: UserRole): number[] {
if (isHubUser) { if (isHubUser) {
// Hub users peuvent voir tous les merchants // Hub users peuvent voir tous les merchants
return []; // Tableau vide = tous les merchants return []; // Tableau vide = tous les merchants
} else { } else {
// Merchant users: seulement leur merchant // Merchant users: seulement leur merchant
return merchantId ? [merchantId] : []; // Mais seulement si l'utilisateur a accès aux transactions
if (merchantId && this.hasMerchantAccess(userRole)) {
return [merchantId];
}
return [];
} }
} }
private hasMerchantAccess(userRole?: UserRole): boolean {
if (!userRole) return false;
// Rôles marchands qui ont accès aux transactions
const merchantRolesWithAccess = [
UserRole.DCB_PARTNER_ADMIN,
UserRole.DCB_PARTNER_MANAGER,
UserRole.DCB_PARTNER_SUPPORT
];
return merchantRolesWithAccess.includes(userRole);
}
// === MÉTHODES PUBLIQUES === // === MÉTHODES PUBLIQUES ===
// Vérifie si l'utilisateur peut accéder à une transaction spécifique // Vérifie si l'utilisateur peut accéder à une transaction spécifique
@ -184,6 +179,7 @@ export class TransactionAccessService {
// Merchant users: seulement leur merchant // Merchant users: seulement leur merchant
if (access.isMerchantUser) { if (access.isMerchantUser) {
// Vérifier si l'utilisateur marchand a accès au merchant de la transaction
return of(access.allowedMerchantIds.includes(transactionMerchantId)); return of(access.allowedMerchantIds.includes(transactionMerchantId));
} }
@ -195,6 +191,22 @@ export class TransactionAccessService {
return of(access.allowedMerchantIds.includes(transactionMerchantId)); return of(access.allowedMerchantIds.includes(transactionMerchantId));
} }
// Obtenir le scope des transactions pour les requêtes API
getTransactionScope(): { merchantIds?: number[] } {
const access = this.getTransactionAccess();
if (access.isHubUser) {
// Hub users peuvent voir tous les merchants
return {};
} else {
// Merchant users: seulement leur(s) merchant(s)
if (access.allowedMerchantIds.length > 0) {
return { merchantIds: access.allowedMerchantIds };
}
return {};
}
}
// Nettoyer le cache // Nettoyer le cache
clearCache(): void { clearCache(): void {
this.accessCache = null; this.accessCache = null;
@ -204,4 +216,10 @@ export class TransactionAccessService {
refreshAccess(): void { refreshAccess(): void {
this.clearCache(); this.clearCache();
} }
// Méthode utilitaire pour afficher les permissions (debug)
logAccessInfo(): void {
const access = this.getTransactionAccess();
console.log('Transaction Access Info:', access);
}
} }