feat: WebSocket temps réel + Finance Workflow + corrections

- Task #6: WebSocket /ws/dashboard + Kafka events (5 topics)
  * Backend: KafkaEventProducer, KafkaEventConsumer
  * Mobile: WebSocketService (reconnection, heartbeat, typed events)
  * DashboardBloc: Auto-refresh depuis WebSocket events

- Finance Workflow: approbations + budgets (backend + mobile)
  * Backend: entities, services, resources, migrations Flyway V6
  * Mobile: features finance_workflow complète avec BLoC

- Corrections DI: interfaces IRepository partout
  * IProfileRepository, IOrganizationRepository, IMembreRepository
  * GetIt configuré avec @injectable

- Spec-Kit: constitution + templates mis à jour
  * .specify/memory/constitution.md enrichie
  * Templates agent, plan, spec, tasks, checklist

- Nettoyage: fichiers temporaires supprimés

Signed-off-by: lions dev Team
This commit is contained in:
dahoud
2026-03-15 02:12:17 +00:00
parent bbc409de9d
commit e8ad874015
635 changed files with 58160 additions and 20674 deletions

View File

@@ -1,71 +0,0 @@
/// Gestionnaire de cache pour le dashboard
/// Cache intelligent basé sur les rôles utilisateurs
library dashboard_cache_manager;
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../models/user_role.dart';
/// Gestionnaire de cache pour optimiser les performances du dashboard
class DashboardCacheManager {
static final Map<String, dynamic> _cache = {};
static final Map<String, DateTime> _cacheTimestamps = {};
static const Duration _cacheExpiry = Duration(minutes: 15);
/// Invalide le cache pour un rôle spécifique
static Future<void> invalidateForRole(UserRole role) async {
final keysToRemove = _cache.keys
.where((key) => key.startsWith('dashboard_${role.name}'))
.toList();
for (final key in keysToRemove) {
_cache.remove(key);
_cacheTimestamps.remove(key);
}
debugPrint('🗑️ Cache invalidé pour le rôle: ${role.displayName}');
}
/// Vide complètement le cache
static Future<void> clear() async {
_cache.clear();
_cacheTimestamps.clear();
debugPrint('🧹 Cache dashboard complètement vidé');
}
/// Obtient une valeur du cache
static T? get<T>(String key) {
final timestamp = _cacheTimestamps[key];
if (timestamp == null) return null;
// Vérifier l'expiration
if (DateTime.now().difference(timestamp) > _cacheExpiry) {
_cache.remove(key);
_cacheTimestamps.remove(key);
return null;
}
return _cache[key] as T?;
}
/// Met une valeur en cache
static void set<T>(String key, T value) {
_cache[key] = value;
_cacheTimestamps[key] = DateTime.now();
}
/// Obtient les statistiques du cache
static Map<String, dynamic> getStats() {
final now = DateTime.now();
final validEntries = _cacheTimestamps.entries
.where((entry) => now.difference(entry.value) <= _cacheExpiry)
.length;
return {
'totalEntries': _cache.length,
'validEntries': validEntries,
'expiredEntries': _cache.length - validEntries,
'cacheHitRate': '${(validEntries / _cache.length * 100).toStringAsFixed(1)}%',
};
}
}

View File

@@ -1,419 +1,183 @@
/// Service d'Authentification Keycloak
/// Gère l'authentification avec votre instance Keycloak sur port 8180
library keycloak_auth_service;
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter_appauth/flutter_appauth.dart';
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:injectable/injectable.dart';
import '../models/user.dart';
import '../models/user_role.dart';
import 'keycloak_role_mapper.dart';
import 'keycloak_webview_auth_service.dart';
import '../../../../core/config/environment.dart';
import '../../../../core/utils/logger.dart';
/// Configuration Keycloak pour votre instance
/// Configuration Keycloak centralisée
class KeycloakConfig {
/// URL de base de votre Keycloak (depuis AppConfig)
static String get baseUrl => AppConfig.keycloakBaseUrl;
/// Realm UnionFlow
static const String realm = 'unionflow';
/// Client ID pour l'application mobile
static const String clientId = 'unionflow-mobile';
static const String scopes = 'openid profile email roles';
/// URL de redirection après authentification
static const String redirectUrl = 'dev.lions.unionflow-mobile://auth/callback';
/// Scopes demandés
static const List<String> scopes = ['openid', 'profile', 'email', 'roles'];
/// Endpoints calculés
static String get authorizationEndpoint =>
'$baseUrl/realms/$realm/protocol/openid-connect/auth';
static String get tokenEndpoint =>
'$baseUrl/realms/$realm/protocol/openid-connect/token';
static String get userInfoEndpoint =>
'$baseUrl/realms/$realm/protocol/openid-connect/userinfo';
static String get logoutEndpoint =>
'$baseUrl/realms/$realm/protocol/openid-connect/logout';
static String get tokenEndpoint => '$baseUrl/realms/$realm/protocol/openid-connect/token';
static String get logoutEndpoint => '$baseUrl/realms/$realm/protocol/openid-connect/logout';
}
/// Service d'authentification Keycloak ultra-sophistiqué
/// Service d'Authentification Keycloak Épuré & DRY
@lazySingleton
class KeycloakAuthService {
static const FlutterAppAuth _appAuth = FlutterAppAuth();
static const FlutterSecureStorage _secureStorage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
),
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock_this_device,
),
final Dio _dio = Dio();
final FlutterSecureStorage _storage = const FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock_this_device),
);
// Clés de stockage sécurisé
static const String _accessTokenKey = 'keycloak_access_token';
static const String _refreshTokenKey = 'keycloak_refresh_token';
static const String _idTokenKey = 'keycloak_id_token';
static const String _userInfoKey = 'keycloak_user_info';
/// Authentification avec Keycloak via WebView (solution HTTP compatible)
///
/// Cette méthode utilise maintenant KeycloakWebViewAuthService pour contourner
/// les limitations HTTPS de flutter_appauth
static Future<AuthorizationTokenResponse?> authenticate() async {
static const String _accessK = 'kc_access';
static const String _refreshK = 'kc_refresh';
static const String _idK = 'kc_id';
/// Login via Direct Access Grant (Username/Password)
Future<User?> login(String username, String password) async {
try {
debugPrint('🔐 Démarrage authentification Keycloak via WebView...');
final response = await _dio.post(
KeycloakConfig.tokenEndpoint,
data: {
'client_id': KeycloakConfig.clientId,
'grant_type': 'password',
'username': username,
'password': password,
'scope': KeycloakConfig.scopes,
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
// Utiliser le service WebView pour l'authentification
// Cette méthode retourne null car l'authentification WebView
// est gérée différemment (via callback)
debugPrint('💡 Authentification WebView - utilisez authenticateWithWebView() à la place');
if (response.statusCode == 200) {
await _saveTokens(response.data);
return await getCurrentUser();
}
} catch (e, st) {
AppLogger.error('KeycloakAuthService: auth error', error: e, stackTrace: st);
}
return null;
}
return null;
static Future<String?>? _refreshFuture;
} catch (e, stackTrace) {
debugPrint('💥 Erreur authentification Keycloak: $e');
debugPrint('Stack trace: $stackTrace');
return null;
/// Rafraîchissement automatique du token avec verrouillage global
Future<String?> refreshToken() async {
if (_refreshFuture != null) {
AppLogger.info('KeycloakAuthService: waiting for ongoing refresh');
return await _refreshFuture;
}
_refreshFuture = _performRefresh();
try {
return await _refreshFuture;
} finally {
_refreshFuture = null;
}
}
/// Rafraîchit le token d'accès
static Future<TokenResponse?> refreshToken() async {
Future<String?> _performRefresh() async {
final refresh = await _storage.read(key: _refreshK);
if (refresh == null) {
AppLogger.info('KeycloakAuthService: no refresh token available');
return null;
}
try {
final String? refreshToken = await _secureStorage.read(
key: _refreshTokenKey,
);
if (refreshToken == null) {
debugPrint('❌ Aucun refresh token disponible');
return null;
}
debugPrint('🔄 Rafraîchissement du token...');
final TokenRequest request = TokenRequest(
KeycloakConfig.clientId,
KeycloakConfig.redirectUrl,
refreshToken: refreshToken,
serviceConfiguration: AuthorizationServiceConfiguration(
authorizationEndpoint: KeycloakConfig.authorizationEndpoint,
tokenEndpoint: KeycloakConfig.tokenEndpoint,
AppLogger.info('KeycloakAuthService: attempting token refresh');
final response = await _dio.post(
KeycloakConfig.tokenEndpoint,
data: {
'client_id': KeycloakConfig.clientId,
'grant_type': 'refresh_token',
'refresh_token': refresh,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
validateStatus: (status) => status == 200,
),
);
final TokenResponse? result = await _appAuth.token(request);
if (result != null) {
await _storeTokens(result);
debugPrint('✅ Token rafraîchi avec succès');
return result;
if (response.statusCode == 200) {
await _saveTokens(response.data);
AppLogger.info('KeycloakAuthService: token refreshed successfully');
return response.data['access_token'];
}
debugPrint('❌ Échec du rafraîchissement du token');
return null;
} catch (e, stackTrace) {
debugPrint('💥 Erreur rafraîchissement token: $e');
debugPrint('Stack trace: $stackTrace');
return null;
} on DioException catch (e, st) {
AppLogger.error('KeycloakAuthService: refresh error ${e.response?.statusCode}', error: e, stackTrace: st);
if (e.response?.statusCode == 400) {
AppLogger.info('KeycloakAuthService: refresh token invalid or expired, logging out');
await logout();
}
} catch (e, st) {
AppLogger.error('KeycloakAuthService: critical refresh error', error: e, stackTrace: st);
}
return null;
}
/// Récupère l'utilisateur authentifié depuis les tokens
static Future<User?> getCurrentUser() async {
/// Récupération de l'utilisateur courant + Mapage Rôles
Future<User?> getCurrentUser() async {
String? token = await _storage.read(key: _accessK);
final idToken = await _storage.read(key: _idK);
if (token == null || idToken == null) return null;
if (JwtDecoder.isExpired(token)) {
token = await refreshToken();
if (token == null) return null;
}
try {
final String? accessToken = await _secureStorage.read(
key: _accessTokenKey,
);
final String? idToken = await _secureStorage.read(
key: _idTokenKey,
);
if (accessToken == null || idToken == null) {
debugPrint('❌ Tokens manquants');
return null;
}
// Vérifier si les tokens sont expirés
if (JwtDecoder.isExpired(accessToken)) {
debugPrint('⏰ Access token expiré, tentative de rafraîchissement...');
final TokenResponse? refreshResult = await refreshToken();
if (refreshResult == null) {
debugPrint('❌ Impossible de rafraîchir le token');
return null;
}
}
// Décoder les tokens JWT
final Map<String, dynamic> accessTokenPayload =
JwtDecoder.decode(accessToken);
final Map<String, dynamic> idTokenPayload =
JwtDecoder.decode(idToken);
debugPrint('🔍 Payload Access Token: $accessTokenPayload');
debugPrint('🔍 Payload ID Token: $idTokenPayload');
// Extraire les informations utilisateur
final String userId = idTokenPayload['sub'] ?? '';
final String email = idTokenPayload['email'] ?? '';
final String firstName = idTokenPayload['given_name'] ?? '';
final String lastName = idTokenPayload['family_name'] ?? '';
final payload = JwtDecoder.decode(token);
final idPayload = JwtDecoder.decode(idToken);
// Extraire les rôles Keycloak
final List<String> keycloakRoles = _extractKeycloakRoles(accessTokenPayload);
debugPrint('🎭 Rôles Keycloak extraits: $keycloakRoles');
// Si aucun rôle, assigner un rôle par défaut
if (keycloakRoles.isEmpty) {
debugPrint('⚠️ Aucun rôle trouvé, assignation du rôle MEMBER par défaut');
keycloakRoles.add('member');
}
// Mapper vers notre système de rôles
final UserRole primaryRole = KeycloakRoleMapper.mapToUserRole(keycloakRoles);
final List<String> permissions = KeycloakRoleMapper.mapToPermissions(keycloakRoles);
debugPrint('🎯 Rôle principal mappé: ${primaryRole.displayName}');
debugPrint('🔐 Permissions mappées: ${permissions.length} permissions');
debugPrint('📋 Permissions détaillées: $permissions');
// Créer l'utilisateur
final User user = User(
id: userId,
email: email,
firstName: firstName,
lastName: lastName,
final roles = _extractRoles(payload);
final primaryRole = KeycloakRoleMapper.mapToUserRole(roles);
AppLogger.info('KeycloakAuthService: roles mapped', tag: '${primaryRole.name}');
return User(
id: idPayload['sub'] ?? '',
email: idPayload['email'] ?? '',
firstName: idPayload['given_name'] ?? '',
lastName: idPayload['family_name'] ?? '',
primaryRole: primaryRole,
organizationContexts: const [], // À implémenter selon vos besoins
additionalPermissions: permissions,
revokedPermissions: const [],
preferences: const UserPreferences(),
lastLoginAt: DateTime.now(),
createdAt: DateTime.now(), // À récupérer depuis Keycloak si disponible
additionalPermissions: KeycloakRoleMapper.mapToPermissions(roles),
isActive: true,
lastLoginAt: DateTime.now(),
createdAt: DateTime.now(),
);
// Stocker les informations utilisateur
await _secureStorage.write(
key: _userInfoKey,
value: jsonEncode(user.toJson()),
);
debugPrint('✅ Utilisateur récupéré: ${user.fullName} (${user.primaryRole.displayName})');
return user;
} catch (e, stackTrace) {
debugPrint('💥 Erreur récupération utilisateur: $e');
debugPrint('Stack trace: $stackTrace');
return null;
}
}
/// Déconnexion complète
static Future<bool> logout() async {
try {
debugPrint('🚪 Déconnexion Keycloak...');
final String? idToken = await _secureStorage.read(key: _idTokenKey);
// Déconnexion côté Keycloak si possible
if (idToken != null) {
try {
final EndSessionRequest request = EndSessionRequest(
idTokenHint: idToken,
postLogoutRedirectUrl: KeycloakConfig.redirectUrl,
serviceConfiguration: AuthorizationServiceConfiguration(
authorizationEndpoint: KeycloakConfig.authorizationEndpoint,
tokenEndpoint: KeycloakConfig.tokenEndpoint,
endSessionEndpoint: KeycloakConfig.logoutEndpoint,
),
);
await _appAuth.endSession(request);
debugPrint('✅ Déconnexion Keycloak côté serveur réussie');
} catch (e) {
debugPrint('⚠️ Déconnexion côté serveur échouée: $e');
// Continue quand même avec la déconnexion locale
}
}
// Nettoyage local des tokens
await _clearTokens();
debugPrint('✅ Déconnexion locale terminée');
return true;
} catch (e, stackTrace) {
debugPrint('💥 Erreur déconnexion: $e');
debugPrint('Stack trace: $stackTrace');
return false;
}
}
/// Vérifie si l'utilisateur est authentifié
static Future<bool> isAuthenticated() async {
try {
final String? accessToken = await _secureStorage.read(
key: _accessTokenKey,
);
if (accessToken == null) {
return false;
}
// Vérifier si le token est expiré
if (JwtDecoder.isExpired(accessToken)) {
// Tenter de rafraîchir
final TokenResponse? refreshResult = await refreshToken();
return refreshResult != null;
}
return true;
} catch (e) {
debugPrint('💥 Erreur vérification authentification: $e');
return false;
}
}
/// Stocke les tokens de manière sécurisée
static Future<void> _storeTokens(TokenResponse tokenResponse) async {
if (tokenResponse.accessToken != null) {
await _secureStorage.write(
key: _accessTokenKey,
value: tokenResponse.accessToken!,
);
}
if (tokenResponse.refreshToken != null) {
await _secureStorage.write(
key: _refreshTokenKey,
value: tokenResponse.refreshToken!,
);
}
if (tokenResponse.idToken != null) {
await _secureStorage.write(
key: _idTokenKey,
value: tokenResponse.idToken!,
);
}
debugPrint('🔒 Tokens stockés de manière sécurisée');
}
/// Nettoie tous les tokens stockés
static Future<void> _clearTokens() async {
await _secureStorage.delete(key: _accessTokenKey);
await _secureStorage.delete(key: _refreshTokenKey);
await _secureStorage.delete(key: _idTokenKey);
await _secureStorage.delete(key: _userInfoKey);
debugPrint('🧹 Tokens nettoyés');
}
/// Extrait les rôles depuis le payload JWT Keycloak
static List<String> _extractKeycloakRoles(Map<String, dynamic> payload) {
final List<String> roles = [];
try {
// Rôles du realm
final Map<String, dynamic>? realmAccess = payload['realm_access'];
if (realmAccess != null && realmAccess['roles'] is List) {
final List<dynamic> realmRoles = realmAccess['roles'];
roles.addAll(realmRoles.cast<String>());
}
// Rôles des clients
final Map<String, dynamic>? resourceAccess = payload['resource_access'];
if (resourceAccess != null) {
resourceAccess.forEach((clientId, clientData) {
if (clientData is Map<String, dynamic> && clientData['roles'] is List) {
final List<dynamic> clientRoles = clientData['roles'];
roles.addAll(clientRoles.cast<String>());
}
});
}
// Filtrer les rôles système Keycloak
return roles.where((role) =>
!role.startsWith('default-roles-') &&
role != 'offline_access' &&
role != 'uma_authorization'
).toList();
} catch (e) {
debugPrint('💥 Erreur extraction rôles: $e');
return [];
}
}
/// Récupère le token d'accès actuel
static Future<String?> getAccessToken() async {
try {
final String? accessToken = await _secureStorage.read(
key: _accessTokenKey,
);
if (accessToken != null && !JwtDecoder.isExpired(accessToken)) {
return accessToken;
}
// Token expiré, tenter de rafraîchir
final TokenResponse? refreshResult = await refreshToken();
return refreshResult?.accessToken;
} catch (e) {
debugPrint('💥 Erreur récupération access token: $e');
return null;
} catch (e, st) {
AppLogger.error('KeycloakAuthService: user parse error', error: e, stackTrace: st);
}
return null;
}
// ═══════════════════════════════════════════════════════════════════════════
// MÉTHODES WEBVIEW - Délégation vers KeycloakWebViewAuthService
// ═══════════════════════════════════════════════════════════════════════════
/// Prépare l'authentification WebView
///
/// Retourne les paramètres nécessaires pour lancer la WebView d'authentification
static Future<Map<String, String>> prepareWebViewAuthentication() async {
return KeycloakWebViewAuthService.prepareAuthentication();
Future<void> logout() async {
await _storage.deleteAll();
AppLogger.info('KeycloakAuthService: session cleared');
}
/// Traite le callback WebView et finalise l'authentification
///
/// Cette méthode doit être appelée quand l'URL de callback est interceptée
static Future<User> handleWebViewCallback(String callbackUrl) async {
return KeycloakWebViewAuthService.handleAuthCallback(callbackUrl);
Future<void> _saveTokens(Map<String, dynamic> data) async {
if (data['access_token'] != null) await _storage.write(key: _accessK, value: data['access_token']);
if (data['refresh_token'] != null) await _storage.write(key: _refreshK, value: data['refresh_token']);
if (data['id_token'] != null) await _storage.write(key: _idK, value: data['id_token']);
}
/// Vérifie si l'utilisateur est authentifié (compatible WebView)
static Future<bool> isWebViewAuthenticated() async {
return KeycloakWebViewAuthService.isAuthenticated();
List<String> _extractRoles(Map<String, dynamic> payload) {
final roles = <String>[];
if (payload['realm_access']?['roles'] != null) {
roles.addAll((payload['realm_access']['roles'] as List).cast<String>());
}
if (payload['resource_access'] != null) {
(payload['resource_access'] as Map).values.forEach((v) {
if (v['roles'] != null) roles.addAll((v['roles'] as List).cast<String>());
});
}
return roles.where((r) => !r.startsWith('default-roles-') && r != 'offline_access').toList();
}
/// Récupère l'utilisateur authentifié (compatible WebView)
static Future<User?> getCurrentWebViewUser() async {
return KeycloakWebViewAuthService.getCurrentUser();
}
/// Déconnecte l'utilisateur (compatible WebView)
static Future<bool> logoutWebView() async {
return KeycloakWebViewAuthService.logout();
}
/// Nettoie les données d'authentification WebView
static Future<void> clearWebViewAuthData() async {
return KeycloakWebViewAuthService.clearAuthData();
Future<String?> getValidToken() async {
final token = await _storage.read(key: _accessK);
if (token != null && !JwtDecoder.isExpired(token)) return token;
return await refreshToken();
}
}

View File

@@ -13,6 +13,7 @@ class KeycloakRoleMapper {
// Rôles administratifs
'SUPER_ADMINISTRATEUR': UserRole.superAdmin,
'ADMIN': UserRole.superAdmin,
'ADMIN_ORGANISATION': UserRole.orgAdmin, // Rôle Keycloak (backend)
'ADMINISTRATEUR_ORGANISATION': UserRole.orgAdmin,
'PRESIDENT': UserRole.orgAdmin,
@@ -23,6 +24,9 @@ class KeycloakRoleMapper {
'SECRETAIRE': UserRole.moderator,
'GESTIONNAIRE_MEMBRE': UserRole.moderator,
'ORGANISATEUR_EVENEMENT': UserRole.moderator,
'CONSULTANT': UserRole.consultant,
'GESTIONNAIRE_RH': UserRole.hrManager,
'HR_MANAGER': UserRole.hrManager,
// Rôles membres
'MEMBRE_ACTIF': UserRole.activeMember,
@@ -72,6 +76,21 @@ class KeycloakRoleMapper {
PermissionMatrix.REPORTS_GENERATE,
PermissionMatrix.DASHBOARD_ANALYTICS,
],
'ADMIN_ORGANISATION': [
// Permissions Admin Organisation (rôle Keycloak backend)
PermissionMatrix.ORG_CONFIG,
PermissionMatrix.MEMBERS_VIEW_ALL,
PermissionMatrix.MEMBERS_EDIT_ALL,
PermissionMatrix.FINANCES_VIEW_ALL,
PermissionMatrix.FINANCES_EDIT_ALL,
PermissionMatrix.EVENTS_VIEW_ALL,
PermissionMatrix.EVENTS_EDIT_ALL,
PermissionMatrix.SOLIDARITY_VIEW_ALL,
PermissionMatrix.SOLIDARITY_EDIT_ALL,
PermissionMatrix.REPORTS_GENERATE,
PermissionMatrix.DASHBOARD_ANALYTICS,
],
'ADMINISTRATEUR_ORGANISATION': [
// Permissions Admin Organisation
PermissionMatrix.ORG_CONFIG,
@@ -172,6 +191,33 @@ class KeycloakRoleMapper {
PermissionMatrix.COMM_SEND_MEMBERS,
],
'CONSULTANT': [
PermissionMatrix.DASHBOARD_VIEW,
PermissionMatrix.DASHBOARD_ANALYTICS,
PermissionMatrix.MEMBERS_VIEW_ALL,
PermissionMatrix.EVENTS_VIEW_ALL,
PermissionMatrix.REPORTS_VIEW_ALL,
PermissionMatrix.REPORTS_GENERATE,
],
'GESTIONNAIRE_RH': [
PermissionMatrix.DASHBOARD_VIEW,
PermissionMatrix.MEMBERS_VIEW_ALL,
PermissionMatrix.MEMBERS_EDIT_BASIC,
PermissionMatrix.MEMBERS_APPROVE,
PermissionMatrix.EVENTS_VIEW_ALL,
PermissionMatrix.MODERATION_USERS,
],
'HR_MANAGER': [
PermissionMatrix.DASHBOARD_VIEW,
PermissionMatrix.MEMBERS_VIEW_ALL,
PermissionMatrix.MEMBERS_EDIT_BASIC,
PermissionMatrix.MEMBERS_APPROVE,
PermissionMatrix.EVENTS_VIEW_ALL,
PermissionMatrix.MODERATION_USERS,
],
'MEMBRE_ACTIF': [
// Permissions Membre Actif
PermissionMatrix.MEMBERS_VIEW_OWN,
@@ -214,10 +260,14 @@ class KeycloakRoleMapper {
/// Mappe une liste de rôles Keycloak vers le UserRole principal
static UserRole mapToUserRole(List<String> keycloakRoles) {
// Normaliser en majuscules pour éviter les écarts de casse (ex. admin_organisation)
final normalized = keycloakRoles.map((r) => r.toUpperCase()).toList();
// Priorité des rôles (du plus élevé au plus bas)
const List<String> rolePriority = [
'SUPER_ADMINISTRATEUR',
'ADMIN',
'ADMIN_ORGANISATION',
'ADMINISTRATEUR_ORGANISATION',
'PRESIDENT',
'RESPONSABLE_TECHNIQUE',
@@ -226,18 +276,21 @@ class KeycloakRoleMapper {
'SECRETAIRE',
'GESTIONNAIRE_MEMBRE',
'ORGANISATEUR_EVENEMENT',
'CONSULTANT',
'GESTIONNAIRE_RH',
'HR_MANAGER',
'MEMBRE_ACTIF',
'MEMBRE_SIMPLE',
'MEMBRE',
];
// Trouver le rôle avec la priorité la plus élevée
for (final String priorityRole in rolePriority) {
if (keycloakRoles.contains(priorityRole)) {
if (normalized.contains(priorityRole)) {
return _keycloakToUserRole[priorityRole] ?? UserRole.simpleMember;
}
}
// Par défaut, visiteur si aucun rôle reconnu
return UserRole.visitor;
}
@@ -245,9 +298,12 @@ class KeycloakRoleMapper {
/// Mappe une liste de rôles Keycloak vers les permissions
static List<String> mapToPermissions(List<String> keycloakRoles) {
final Set<String> permissions = <String>{};
// Normaliser en majuscules pour cohérence avec le mapping
final normalized = keycloakRoles.map((r) => r.toUpperCase()).toList();
// Ajouter les permissions pour chaque rôle
for (final String role in keycloakRoles) {
for (final String role in normalized) {
final List<String>? rolePermissions = _keycloakToPermissions[role];
if (rolePermissions != null) {
permissions.addAll(rolePermissions);

View File

@@ -530,6 +530,7 @@ class KeycloakWebViewAuthService {
// Mapper vers notre système de rôles
final UserRole primaryRole = KeycloakRoleMapper.mapToUserRole(keycloakRoles);
debugPrint('🔐 [AUTH WebView] Rôles: $keycloakRoles → UserRole: ${primaryRole.name} (${primaryRole.displayName})');
final List<String> permissions = KeycloakRoleMapper.mapToPermissions(keycloakRoles);
// Créer l'utilisateur

View File

@@ -239,14 +239,15 @@ class PermissionEngine {
return _checkContextualPermissions(user, permission, organizationId);
}
/// Vérifications contextuelles avancées
/// Vérifications contextuelles avancées (intégration serveur).
/// Quand le backend exposera GET /api/permissions/check avec userId, permission, organizationId,
/// remplacer le return false par l'appel API et le résultat.
static Future<bool> _checkContextualPermissions(
User user,
String permission,
String? organizationId,
) async {
// Logique contextuelle future (intégration avec le serveur)
// Pour l'instant, retourne false
// Vérification contextuelle désactivée — endpoint non disponible.
return false;
}