Refactoring - Version OK
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
/// 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)}%',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
/// 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:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:jwt_decoder/jwt_decoder.dart';
|
||||
import '../models/user.dart';
|
||||
import '../models/user_role.dart';
|
||||
import 'keycloak_role_mapper.dart';
|
||||
import 'keycloak_webview_auth_service.dart';
|
||||
|
||||
/// Configuration Keycloak pour votre instance
|
||||
class KeycloakConfig {
|
||||
/// URL de base de votre Keycloak
|
||||
static const String baseUrl = 'http://192.168.1.11:8180';
|
||||
|
||||
/// Realm UnionFlow
|
||||
static const String realm = 'unionflow';
|
||||
|
||||
/// Client ID pour l'application mobile
|
||||
static const String clientId = 'unionflow-mobile';
|
||||
|
||||
/// 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';
|
||||
}
|
||||
|
||||
/// Service d'authentification Keycloak ultra-sophistiqué
|
||||
class KeycloakAuthService {
|
||||
static const FlutterAppAuth _appAuth = FlutterAppAuth();
|
||||
static const FlutterSecureStorage _secureStorage = 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 {
|
||||
try {
|
||||
debugPrint('🔐 Démarrage authentification Keycloak via WebView...');
|
||||
|
||||
// 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');
|
||||
|
||||
return null;
|
||||
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('💥 Erreur authentification Keycloak: $e');
|
||||
debugPrint('Stack trace: $stackTrace');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Rafraîchit le token d'accès
|
||||
static Future<TokenResponse?> refreshToken() async {
|
||||
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,
|
||||
),
|
||||
);
|
||||
|
||||
final TokenResponse? result = await _appAuth.token(request);
|
||||
|
||||
if (result != null) {
|
||||
await _storeTokens(result);
|
||||
debugPrint('✅ Token rafraîchi avec succès');
|
||||
return result;
|
||||
}
|
||||
|
||||
debugPrint('❌ Échec du rafraîchissement du token');
|
||||
return null;
|
||||
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('💥 Erreur rafraîchissement token: $e');
|
||||
debugPrint('Stack trace: $stackTrace');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère l'utilisateur authentifié depuis les tokens
|
||||
static Future<User?> getCurrentUser() async {
|
||||
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'] ?? '';
|
||||
|
||||
|
||||
// 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,
|
||||
|
||||
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
|
||||
isActive: true,
|
||||
);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 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();
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// Vérifie si l'utilisateur est authentifié (compatible WebView)
|
||||
static Future<bool> isWebViewAuthenticated() async {
|
||||
return KeycloakWebViewAuthService.isAuthenticated();
|
||||
}
|
||||
|
||||
/// 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
/// Mapper de Rôles Keycloak vers UserRole
|
||||
/// Convertit les rôles Keycloak existants vers notre système de rôles sophistiqué
|
||||
library keycloak_role_mapper;
|
||||
|
||||
import '../models/user_role.dart';
|
||||
import '../models/permission_matrix.dart';
|
||||
|
||||
/// Service de mapping des rôles Keycloak
|
||||
class KeycloakRoleMapper {
|
||||
|
||||
/// Mapping des rôles Keycloak vers UserRole
|
||||
static const Map<String, UserRole> _keycloakToUserRole = {
|
||||
// Rôles administratifs
|
||||
'SUPER_ADMINISTRATEUR': UserRole.superAdmin,
|
||||
'ADMIN': UserRole.superAdmin,
|
||||
'ADMINISTRATEUR_ORGANISATION': UserRole.orgAdmin,
|
||||
'PRESIDENT': UserRole.orgAdmin,
|
||||
|
||||
// Rôles de gestion
|
||||
'RESPONSABLE_TECHNIQUE': UserRole.moderator,
|
||||
'RESPONSABLE_MEMBRES': UserRole.moderator,
|
||||
'TRESORIER': UserRole.moderator,
|
||||
'SECRETAIRE': UserRole.moderator,
|
||||
'GESTIONNAIRE_MEMBRE': UserRole.moderator,
|
||||
'ORGANISATEUR_EVENEMENT': UserRole.moderator,
|
||||
|
||||
// Rôles membres
|
||||
'MEMBRE_ACTIF': UserRole.activeMember,
|
||||
'MEMBRE_SIMPLE': UserRole.simpleMember,
|
||||
'MEMBRE': UserRole.activeMember,
|
||||
};
|
||||
|
||||
/// Mapping des rôles Keycloak vers permissions spécifiques
|
||||
static const Map<String, List<String>> _keycloakToPermissions = {
|
||||
'SUPER_ADMINISTRATEUR': [
|
||||
// Permissions Super Admin - Accès total
|
||||
PermissionMatrix.SYSTEM_ADMIN,
|
||||
PermissionMatrix.SYSTEM_CONFIG,
|
||||
PermissionMatrix.SYSTEM_SECURITY,
|
||||
PermissionMatrix.ORG_CREATE,
|
||||
PermissionMatrix.ORG_DELETE,
|
||||
PermissionMatrix.ORG_CONFIG,
|
||||
PermissionMatrix.MEMBERS_VIEW_ALL,
|
||||
PermissionMatrix.MEMBERS_EDIT_ALL,
|
||||
PermissionMatrix.MEMBERS_DELETE_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,
|
||||
],
|
||||
'ADMIN': [
|
||||
// Permissions Super Admin - Accès total (compatibilité)
|
||||
PermissionMatrix.SYSTEM_ADMIN,
|
||||
PermissionMatrix.SYSTEM_CONFIG,
|
||||
PermissionMatrix.SYSTEM_SECURITY,
|
||||
PermissionMatrix.ORG_CREATE,
|
||||
PermissionMatrix.ORG_DELETE,
|
||||
PermissionMatrix.ORG_CONFIG,
|
||||
PermissionMatrix.MEMBERS_VIEW_ALL,
|
||||
PermissionMatrix.MEMBERS_EDIT_ALL,
|
||||
PermissionMatrix.MEMBERS_DELETE_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,
|
||||
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,
|
||||
],
|
||||
|
||||
'PRESIDENT': [
|
||||
// Permissions Président - Gestion organisation
|
||||
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,
|
||||
PermissionMatrix.COMM_SEND_ALL,
|
||||
],
|
||||
|
||||
'RESPONSABLE_TECHNIQUE': [
|
||||
// Permissions Responsable Technique
|
||||
PermissionMatrix.SYSTEM_MONITORING,
|
||||
PermissionMatrix.SYSTEM_MAINTENANCE,
|
||||
PermissionMatrix.MEMBERS_VIEW_ALL,
|
||||
PermissionMatrix.MEMBERS_EDIT_BASIC,
|
||||
PermissionMatrix.EVENTS_VIEW_ALL,
|
||||
PermissionMatrix.EVENTS_EDIT_ALL,
|
||||
PermissionMatrix.DASHBOARD_VIEW,
|
||||
PermissionMatrix.REPORTS_GENERATE,
|
||||
],
|
||||
|
||||
'RESPONSABLE_MEMBRES': [
|
||||
// Permissions Responsable Membres
|
||||
PermissionMatrix.MEMBERS_VIEW_ALL,
|
||||
PermissionMatrix.MEMBERS_EDIT_ALL,
|
||||
PermissionMatrix.MEMBERS_DELETE_ALL,
|
||||
PermissionMatrix.EVENTS_VIEW_ALL,
|
||||
PermissionMatrix.EVENTS_EDIT_ALL,
|
||||
PermissionMatrix.SOLIDARITY_VIEW_ALL,
|
||||
PermissionMatrix.SOLIDARITY_EDIT_ALL,
|
||||
PermissionMatrix.DASHBOARD_VIEW,
|
||||
PermissionMatrix.REPORTS_GENERATE,
|
||||
],
|
||||
|
||||
'TRESORIER': [
|
||||
// Permissions Trésorier - Focus finances
|
||||
PermissionMatrix.FINANCES_VIEW_ALL,
|
||||
PermissionMatrix.FINANCES_EDIT_ALL,
|
||||
PermissionMatrix.MEMBERS_VIEW_ALL,
|
||||
PermissionMatrix.MEMBERS_EDIT_BASIC,
|
||||
PermissionMatrix.EVENTS_VIEW_ALL,
|
||||
PermissionMatrix.REPORTS_GENERATE,
|
||||
PermissionMatrix.DASHBOARD_VIEW,
|
||||
],
|
||||
|
||||
'SECRETAIRE': [
|
||||
// Permissions Secrétaire - Communication et membres
|
||||
PermissionMatrix.MEMBERS_VIEW_ALL,
|
||||
PermissionMatrix.MEMBERS_EDIT_BASIC,
|
||||
PermissionMatrix.EVENTS_VIEW_ALL,
|
||||
PermissionMatrix.EVENTS_EDIT_ALL,
|
||||
PermissionMatrix.COMM_SEND_ALL,
|
||||
PermissionMatrix.COMM_MODERATE,
|
||||
PermissionMatrix.DASHBOARD_VIEW,
|
||||
],
|
||||
|
||||
'GESTIONNAIRE_MEMBRE': [
|
||||
// Permissions Gestionnaire de Membres
|
||||
PermissionMatrix.MEMBERS_VIEW_ALL,
|
||||
PermissionMatrix.MEMBERS_EDIT_ALL,
|
||||
PermissionMatrix.MEMBERS_CREATE,
|
||||
PermissionMatrix.EVENTS_VIEW_ALL,
|
||||
PermissionMatrix.SOLIDARITY_VIEW_ALL,
|
||||
PermissionMatrix.DASHBOARD_VIEW,
|
||||
PermissionMatrix.COMM_SEND_MEMBERS,
|
||||
],
|
||||
|
||||
'ORGANISATEUR_EVENEMENT': [
|
||||
// Permissions Organisateur d'Événements
|
||||
PermissionMatrix.EVENTS_VIEW_ALL,
|
||||
PermissionMatrix.EVENTS_EDIT_ALL,
|
||||
PermissionMatrix.EVENTS_CREATE,
|
||||
PermissionMatrix.MEMBERS_VIEW_ALL,
|
||||
PermissionMatrix.SOLIDARITY_VIEW_ALL,
|
||||
PermissionMatrix.DASHBOARD_VIEW,
|
||||
PermissionMatrix.COMM_SEND_MEMBERS,
|
||||
],
|
||||
|
||||
'MEMBRE_ACTIF': [
|
||||
// Permissions Membre Actif
|
||||
PermissionMatrix.MEMBERS_VIEW_OWN,
|
||||
PermissionMatrix.MEMBERS_EDIT_OWN,
|
||||
PermissionMatrix.EVENTS_VIEW_ALL,
|
||||
PermissionMatrix.EVENTS_PARTICIPATE,
|
||||
PermissionMatrix.EVENTS_CREATE,
|
||||
PermissionMatrix.SOLIDARITY_VIEW_ALL,
|
||||
PermissionMatrix.SOLIDARITY_PARTICIPATE,
|
||||
PermissionMatrix.SOLIDARITY_CREATE,
|
||||
PermissionMatrix.FINANCES_VIEW_OWN,
|
||||
PermissionMatrix.DASHBOARD_VIEW,
|
||||
PermissionMatrix.COMM_SEND_MEMBERS,
|
||||
],
|
||||
|
||||
'MEMBRE_SIMPLE': [
|
||||
// Permissions Membre Simple
|
||||
PermissionMatrix.MEMBERS_VIEW_OWN,
|
||||
PermissionMatrix.MEMBERS_EDIT_OWN,
|
||||
PermissionMatrix.EVENTS_VIEW_PUBLIC,
|
||||
PermissionMatrix.EVENTS_PARTICIPATE,
|
||||
PermissionMatrix.SOLIDARITY_VIEW_PUBLIC,
|
||||
PermissionMatrix.SOLIDARITY_PARTICIPATE,
|
||||
PermissionMatrix.FINANCES_VIEW_OWN,
|
||||
PermissionMatrix.DASHBOARD_VIEW,
|
||||
],
|
||||
|
||||
'MEMBRE': [
|
||||
// Permissions Membre Standard (compatibilité)
|
||||
PermissionMatrix.MEMBERS_VIEW_OWN,
|
||||
PermissionMatrix.MEMBERS_EDIT_OWN,
|
||||
PermissionMatrix.EVENTS_VIEW_PUBLIC,
|
||||
PermissionMatrix.EVENTS_PARTICIPATE,
|
||||
PermissionMatrix.SOLIDARITY_VIEW_PUBLIC,
|
||||
PermissionMatrix.SOLIDARITY_PARTICIPATE,
|
||||
PermissionMatrix.FINANCES_VIEW_OWN,
|
||||
PermissionMatrix.DASHBOARD_VIEW,
|
||||
],
|
||||
};
|
||||
|
||||
/// Mappe une liste de rôles Keycloak vers le UserRole principal
|
||||
static UserRole mapToUserRole(List<String> keycloakRoles) {
|
||||
// Priorité des rôles (du plus élevé au plus bas)
|
||||
const List<String> rolePriority = [
|
||||
'SUPER_ADMINISTRATEUR',
|
||||
'ADMIN',
|
||||
'ADMINISTRATEUR_ORGANISATION',
|
||||
'PRESIDENT',
|
||||
'RESPONSABLE_TECHNIQUE',
|
||||
'RESPONSABLE_MEMBRES',
|
||||
'TRESORIER',
|
||||
'SECRETAIRE',
|
||||
'GESTIONNAIRE_MEMBRE',
|
||||
'ORGANISATEUR_EVENEMENT',
|
||||
'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)) {
|
||||
return _keycloakToUserRole[priorityRole] ?? UserRole.simpleMember;
|
||||
}
|
||||
}
|
||||
|
||||
// Par défaut, visiteur si aucun rôle reconnu
|
||||
return UserRole.visitor;
|
||||
}
|
||||
|
||||
/// Mappe une liste de rôles Keycloak vers les permissions
|
||||
static List<String> mapToPermissions(List<String> keycloakRoles) {
|
||||
final Set<String> permissions = <String>{};
|
||||
|
||||
// Ajouter les permissions pour chaque rôle
|
||||
for (final String role in keycloakRoles) {
|
||||
final List<String>? rolePermissions = _keycloakToPermissions[role];
|
||||
if (rolePermissions != null) {
|
||||
permissions.addAll(rolePermissions);
|
||||
}
|
||||
}
|
||||
|
||||
// Ajouter les permissions de base pour tous les utilisateurs authentifiés
|
||||
permissions.add(PermissionMatrix.DASHBOARD_VIEW);
|
||||
permissions.add(PermissionMatrix.MEMBERS_VIEW_OWN);
|
||||
|
||||
return permissions.toList();
|
||||
}
|
||||
|
||||
/// Vérifie si un rôle Keycloak est reconnu
|
||||
static bool isValidKeycloakRole(String role) {
|
||||
return _keycloakToUserRole.containsKey(role);
|
||||
}
|
||||
|
||||
/// Récupère tous les rôles Keycloak supportés
|
||||
static List<String> getSupportedKeycloakRoles() {
|
||||
return _keycloakToUserRole.keys.toList();
|
||||
}
|
||||
|
||||
/// Récupère le UserRole correspondant à un rôle Keycloak spécifique
|
||||
static UserRole? getUserRoleForKeycloakRole(String keycloakRole) {
|
||||
return _keycloakToUserRole[keycloakRole];
|
||||
}
|
||||
|
||||
/// Récupère les permissions pour un rôle Keycloak spécifique
|
||||
static List<String> getPermissionsForKeycloakRole(String keycloakRole) {
|
||||
return _keycloakToPermissions[keycloakRole] ?? [];
|
||||
}
|
||||
|
||||
/// Analyse détaillée du mapping des rôles
|
||||
static Map<String, dynamic> analyzeRoleMapping(List<String> keycloakRoles) {
|
||||
final UserRole primaryRole = mapToUserRole(keycloakRoles);
|
||||
final List<String> permissions = mapToPermissions(keycloakRoles);
|
||||
|
||||
final Map<String, List<String>> roleBreakdown = {};
|
||||
for (final String role in keycloakRoles) {
|
||||
if (isValidKeycloakRole(role)) {
|
||||
roleBreakdown[role] = getPermissionsForKeycloakRole(role);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'keycloakRoles': keycloakRoles,
|
||||
'primaryRole': primaryRole.name,
|
||||
'primaryRoleDisplayName': primaryRole.displayName,
|
||||
'totalPermissions': permissions.length,
|
||||
'permissions': permissions,
|
||||
'roleBreakdown': roleBreakdown,
|
||||
'unrecognizedRoles': keycloakRoles
|
||||
.where((role) => !isValidKeycloakRole(role))
|
||||
.toList(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Suggestions d'amélioration du mapping
|
||||
static Map<String, dynamic> getMappingSuggestions(List<String> keycloakRoles) {
|
||||
final List<String> unrecognized = keycloakRoles
|
||||
.where((role) => !isValidKeycloakRole(role))
|
||||
.toList();
|
||||
|
||||
final List<String> suggestions = [];
|
||||
|
||||
if (unrecognized.isNotEmpty) {
|
||||
suggestions.add(
|
||||
'Rôles non reconnus détectés: ${unrecognized.join(", ")}. '
|
||||
'Considérez ajouter ces rôles au mapping ou les ignorer.',
|
||||
);
|
||||
}
|
||||
|
||||
if (keycloakRoles.isEmpty) {
|
||||
suggestions.add(
|
||||
'Aucun rôle Keycloak détecté. L\'utilisateur sera traité comme visiteur.',
|
||||
);
|
||||
}
|
||||
|
||||
final UserRole primaryRole = mapToUserRole(keycloakRoles);
|
||||
if (primaryRole == UserRole.visitor && keycloakRoles.isNotEmpty) {
|
||||
suggestions.add(
|
||||
'L\'utilisateur a des rôles Keycloak mais est mappé comme visiteur. '
|
||||
'Vérifiez la configuration du mapping.',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
'unrecognizedRoles': unrecognized,
|
||||
'suggestions': suggestions,
|
||||
'mappingHealth': suggestions.isEmpty ? 'excellent' : 'needs_attention',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,671 @@
|
||||
/// Service d'Authentification Keycloak via WebView
|
||||
///
|
||||
/// Implémentation professionnelle et sécurisée de l'authentification OAuth2/OIDC
|
||||
/// avec Keycloak utilisant WebView pour contourner les limitations HTTPS de flutter_appauth.
|
||||
///
|
||||
/// Fonctionnalités :
|
||||
/// - Flow OAuth2 Authorization Code avec PKCE
|
||||
/// - Gestion sécurisée des tokens JWT
|
||||
/// - Support HTTP/HTTPS
|
||||
/// - Gestion complète des erreurs et timeouts
|
||||
/// - Validation rigoureuse des paramètres
|
||||
/// - Logging détaillé pour le debugging
|
||||
library keycloak_webview_auth_service;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:jwt_decoder/jwt_decoder.dart';
|
||||
import '../models/user.dart';
|
||||
import '../models/user_role.dart';
|
||||
import 'keycloak_role_mapper.dart';
|
||||
|
||||
/// Configuration Keycloak pour l'authentification WebView
|
||||
class KeycloakWebViewConfig {
|
||||
/// URL de base de l'instance Keycloak
|
||||
static const String baseUrl = 'http://192.168.1.11:8180';
|
||||
|
||||
/// Realm UnionFlow
|
||||
static const String realm = 'unionflow';
|
||||
|
||||
/// Client ID pour l'application mobile
|
||||
static const String clientId = 'unionflow-mobile';
|
||||
|
||||
/// URL de redirection après authentification
|
||||
static const String redirectUrl = 'dev.lions.unionflow-mobile://auth/callback';
|
||||
|
||||
/// Scopes OAuth2 demandés
|
||||
static const List<String> scopes = ['openid', 'profile', 'email', 'roles'];
|
||||
|
||||
/// Timeout pour les requêtes HTTP (en secondes)
|
||||
static const int httpTimeoutSeconds = 30;
|
||||
|
||||
/// Timeout pour l'authentification WebView (en secondes)
|
||||
static const int authTimeoutSeconds = 300; // 5 minutes
|
||||
|
||||
/// 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 jwksEndpoint =>
|
||||
'$baseUrl/realms/$realm/protocol/openid-connect/certs';
|
||||
}
|
||||
|
||||
/// Résultat de l'authentification WebView
|
||||
class WebViewAuthResult {
|
||||
final String accessToken;
|
||||
final String idToken;
|
||||
final String? refreshToken;
|
||||
final int expiresIn;
|
||||
final String tokenType;
|
||||
final List<String> scopes;
|
||||
|
||||
const WebViewAuthResult({
|
||||
required this.accessToken,
|
||||
required this.idToken,
|
||||
this.refreshToken,
|
||||
required this.expiresIn,
|
||||
required this.tokenType,
|
||||
required this.scopes,
|
||||
});
|
||||
|
||||
/// Création depuis la réponse token de Keycloak
|
||||
factory WebViewAuthResult.fromTokenResponse(Map<String, dynamic> response) {
|
||||
return WebViewAuthResult(
|
||||
accessToken: response['access_token'] ?? '',
|
||||
idToken: response['id_token'] ?? '',
|
||||
refreshToken: response['refresh_token'],
|
||||
expiresIn: response['expires_in'] ?? 3600,
|
||||
tokenType: response['token_type'] ?? 'Bearer',
|
||||
scopes: (response['scope'] as String?)?.split(' ') ?? [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Exceptions spécifiques à l'authentification WebView
|
||||
class KeycloakWebViewAuthException implements Exception {
|
||||
final String message;
|
||||
final String? code;
|
||||
final dynamic originalError;
|
||||
|
||||
const KeycloakWebViewAuthException(
|
||||
this.message, {
|
||||
this.code,
|
||||
this.originalError,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() => 'KeycloakWebViewAuthException: $message${code != null ? ' (Code: $code)' : ''}';
|
||||
}
|
||||
|
||||
/// Service d'authentification Keycloak via WebView
|
||||
///
|
||||
/// Implémentation complète et sécurisée du flow OAuth2 Authorization Code avec PKCE
|
||||
class KeycloakWebViewAuthService {
|
||||
// Stockage sécurisé des tokens
|
||||
static const FlutterSecureStorage _secureStorage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(
|
||||
encryptedSharedPreferences: true,
|
||||
),
|
||||
iOptions: IOSOptions(
|
||||
accessibility: KeychainAccessibility.first_unlock_this_device,
|
||||
),
|
||||
);
|
||||
|
||||
// Clés de stockage sécurisé
|
||||
static const String _accessTokenKey = 'keycloak_webview_access_token';
|
||||
static const String _idTokenKey = 'keycloak_webview_id_token';
|
||||
static const String _refreshTokenKey = 'keycloak_webview_refresh_token';
|
||||
static const String _userInfoKey = 'keycloak_webview_user_info';
|
||||
static const String _authStateKey = 'keycloak_webview_auth_state';
|
||||
|
||||
// Client HTTP avec timeout configuré
|
||||
static final http.Client _httpClient = http.Client();
|
||||
|
||||
/// Génère un code verifier PKCE sécurisé
|
||||
static String _generateCodeVerifier() {
|
||||
const String charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
||||
final Random random = Random.secure();
|
||||
return List.generate(128, (i) => charset[random.nextInt(charset.length)]).join();
|
||||
}
|
||||
|
||||
/// Génère le code challenge PKCE à partir du verifier
|
||||
static String _generateCodeChallenge(String verifier) {
|
||||
final List<int> bytes = utf8.encode(verifier);
|
||||
final Digest digest = sha256.convert(bytes);
|
||||
return base64Url.encode(digest.bytes).replaceAll('=', '');
|
||||
}
|
||||
|
||||
/// Génère un state sécurisé pour la protection CSRF
|
||||
static String _generateState() {
|
||||
final Random random = Random.secure();
|
||||
final List<int> bytes = List.generate(32, (i) => random.nextInt(256));
|
||||
return base64Url.encode(bytes).replaceAll('=', '');
|
||||
}
|
||||
|
||||
/// Construit l'URL d'autorisation Keycloak avec tous les paramètres
|
||||
static Future<Map<String, String>> _buildAuthorizationUrl() async {
|
||||
final String codeVerifier = _generateCodeVerifier();
|
||||
final String codeChallenge = _generateCodeChallenge(codeVerifier);
|
||||
final String state = _generateState();
|
||||
|
||||
// Stocker les paramètres pour la validation ultérieure
|
||||
await _secureStorage.write(
|
||||
key: _authStateKey,
|
||||
value: jsonEncode({
|
||||
'code_verifier': codeVerifier,
|
||||
'state': state,
|
||||
'timestamp': DateTime.now().millisecondsSinceEpoch,
|
||||
}),
|
||||
);
|
||||
|
||||
final Map<String, String> params = {
|
||||
'response_type': 'code',
|
||||
'client_id': KeycloakWebViewConfig.clientId,
|
||||
'redirect_uri': KeycloakWebViewConfig.redirectUrl,
|
||||
'scope': KeycloakWebViewConfig.scopes.join(' '),
|
||||
'state': state,
|
||||
'code_challenge': codeChallenge,
|
||||
'code_challenge_method': 'S256',
|
||||
'kc_locale': 'fr',
|
||||
'prompt': 'login',
|
||||
};
|
||||
|
||||
final String queryString = params.entries
|
||||
.map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}')
|
||||
.join('&');
|
||||
|
||||
final String authUrl = '${KeycloakWebViewConfig.authorizationEndpoint}?$queryString';
|
||||
|
||||
debugPrint('🔐 URL d\'autorisation générée: $authUrl');
|
||||
|
||||
return {
|
||||
'url': authUrl,
|
||||
'state': state,
|
||||
'code_verifier': codeVerifier,
|
||||
};
|
||||
}
|
||||
|
||||
/// Valide la réponse de redirection et extrait le code d'autorisation
|
||||
static Future<String> _validateCallbackAndExtractCode(
|
||||
String callbackUrl,
|
||||
String expectedState,
|
||||
) async {
|
||||
debugPrint('🔍 Validation du callback: $callbackUrl');
|
||||
|
||||
final Uri uri = Uri.parse(callbackUrl);
|
||||
|
||||
// Vérifier que c'est bien notre URL de redirection
|
||||
if (!callbackUrl.startsWith(KeycloakWebViewConfig.redirectUrl)) {
|
||||
throw const KeycloakWebViewAuthException(
|
||||
'URL de callback invalide',
|
||||
code: 'INVALID_CALLBACK_URL',
|
||||
);
|
||||
}
|
||||
|
||||
// Vérifier la présence d'erreurs
|
||||
final String? error = uri.queryParameters['error'];
|
||||
if (error != null) {
|
||||
final String? errorDescription = uri.queryParameters['error_description'];
|
||||
throw KeycloakWebViewAuthException(
|
||||
'Erreur d\'authentification: ${errorDescription ?? error}',
|
||||
code: error,
|
||||
);
|
||||
}
|
||||
|
||||
// Valider le state pour la protection CSRF
|
||||
final String? receivedState = uri.queryParameters['state'];
|
||||
if (receivedState != expectedState) {
|
||||
throw const KeycloakWebViewAuthException(
|
||||
'State invalide - possible attaque CSRF',
|
||||
code: 'INVALID_STATE',
|
||||
);
|
||||
}
|
||||
|
||||
// Extraire le code d'autorisation
|
||||
final String? code = uri.queryParameters['code'];
|
||||
if (code == null || code.isEmpty) {
|
||||
throw const KeycloakWebViewAuthException(
|
||||
'Code d\'autorisation manquant',
|
||||
code: 'MISSING_AUTH_CODE',
|
||||
);
|
||||
}
|
||||
|
||||
debugPrint('✅ Code d\'autorisation extrait avec succès');
|
||||
return code;
|
||||
}
|
||||
|
||||
/// Échange le code d'autorisation contre des tokens
|
||||
static Future<WebViewAuthResult> _exchangeCodeForTokens(
|
||||
String authCode,
|
||||
String codeVerifier,
|
||||
) async {
|
||||
debugPrint('🔄 Échange du code d\'autorisation contre les tokens...');
|
||||
|
||||
try {
|
||||
final Map<String, String> body = {
|
||||
'grant_type': 'authorization_code',
|
||||
'client_id': KeycloakWebViewConfig.clientId,
|
||||
'code': authCode,
|
||||
'redirect_uri': KeycloakWebViewConfig.redirectUrl,
|
||||
'code_verifier': codeVerifier,
|
||||
};
|
||||
|
||||
final http.Response response = await _httpClient
|
||||
.post(
|
||||
Uri.parse(KeycloakWebViewConfig.tokenEndpoint),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: body,
|
||||
)
|
||||
.timeout(const Duration(seconds: KeycloakWebViewConfig.httpTimeoutSeconds));
|
||||
|
||||
debugPrint('📡 Réponse token endpoint: ${response.statusCode}');
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
final String errorBody = response.body;
|
||||
debugPrint('❌ Erreur échange tokens: $errorBody');
|
||||
|
||||
Map<String, dynamic>? errorJson;
|
||||
try {
|
||||
errorJson = jsonDecode(errorBody);
|
||||
} catch (e) {
|
||||
// Ignore JSON parsing errors
|
||||
}
|
||||
|
||||
final String errorMessage = errorJson?['error_description'] ??
|
||||
errorJson?['error'] ??
|
||||
'Erreur HTTP ${response.statusCode}';
|
||||
|
||||
throw KeycloakWebViewAuthException(
|
||||
'Échec de l\'échange de tokens: $errorMessage',
|
||||
code: errorJson?['error'],
|
||||
);
|
||||
}
|
||||
|
||||
final Map<String, dynamic> tokenResponse = jsonDecode(response.body);
|
||||
|
||||
// Valider la présence des tokens requis
|
||||
if (!tokenResponse.containsKey('access_token') ||
|
||||
!tokenResponse.containsKey('id_token')) {
|
||||
throw const KeycloakWebViewAuthException(
|
||||
'Tokens manquants dans la réponse',
|
||||
code: 'MISSING_TOKENS',
|
||||
);
|
||||
}
|
||||
|
||||
debugPrint('✅ Tokens reçus avec succès');
|
||||
return WebViewAuthResult.fromTokenResponse(tokenResponse);
|
||||
|
||||
} on TimeoutException {
|
||||
throw const KeycloakWebViewAuthException(
|
||||
'Timeout lors de l\'échange des tokens',
|
||||
code: 'TIMEOUT',
|
||||
);
|
||||
} catch (e) {
|
||||
if (e is KeycloakWebViewAuthException) rethrow;
|
||||
|
||||
throw KeycloakWebViewAuthException(
|
||||
'Erreur lors de l\'échange des tokens: $e',
|
||||
originalError: e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Stocke les tokens de manière sécurisée
|
||||
static Future<void> _storeTokens(WebViewAuthResult authResult) async {
|
||||
debugPrint('💾 Stockage sécurisé des tokens...');
|
||||
|
||||
try {
|
||||
await Future.wait([
|
||||
_secureStorage.write(key: _accessTokenKey, value: authResult.accessToken),
|
||||
_secureStorage.write(key: _idTokenKey, value: authResult.idToken),
|
||||
if (authResult.refreshToken != null)
|
||||
_secureStorage.write(key: _refreshTokenKey, value: authResult.refreshToken!),
|
||||
]);
|
||||
|
||||
debugPrint('✅ Tokens stockés avec succès');
|
||||
} catch (e) {
|
||||
throw KeycloakWebViewAuthException(
|
||||
'Erreur lors du stockage des tokens: $e',
|
||||
originalError: e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Valide et parse un token JWT
|
||||
static Map<String, dynamic> _parseAndValidateJWT(String token, String tokenType) {
|
||||
try {
|
||||
// Vérifier l'expiration
|
||||
if (JwtDecoder.isExpired(token)) {
|
||||
throw KeycloakWebViewAuthException(
|
||||
'$tokenType expiré',
|
||||
code: 'TOKEN_EXPIRED',
|
||||
);
|
||||
}
|
||||
|
||||
// Parser le payload
|
||||
final Map<String, dynamic> payload = JwtDecoder.decode(token);
|
||||
|
||||
// Validations de base
|
||||
if (payload['iss'] == null) {
|
||||
throw const KeycloakWebViewAuthException(
|
||||
'Token JWT invalide: issuer manquant',
|
||||
code: 'INVALID_JWT',
|
||||
);
|
||||
}
|
||||
|
||||
// Vérifier l'issuer
|
||||
const String expectedIssuer = '${KeycloakWebViewConfig.baseUrl}/realms/${KeycloakWebViewConfig.realm}';
|
||||
if (payload['iss'] != expectedIssuer) {
|
||||
throw KeycloakWebViewAuthException(
|
||||
'Token JWT invalide: issuer incorrect (attendu: $expectedIssuer, reçu: ${payload['iss']})',
|
||||
code: 'INVALID_ISSUER',
|
||||
);
|
||||
}
|
||||
|
||||
debugPrint('✅ $tokenType validé avec succès');
|
||||
return payload;
|
||||
|
||||
} catch (e) {
|
||||
if (e is KeycloakWebViewAuthException) rethrow;
|
||||
|
||||
throw KeycloakWebViewAuthException(
|
||||
'Erreur lors de la validation du $tokenType: $e',
|
||||
originalError: e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Méthode principale d'authentification
|
||||
///
|
||||
/// Retourne les paramètres nécessaires pour lancer la WebView d'authentification
|
||||
static Future<Map<String, String>> prepareAuthentication() async {
|
||||
debugPrint('🚀 Préparation de l\'authentification WebView...');
|
||||
|
||||
try {
|
||||
// Nettoyer les données d'authentification précédentes
|
||||
await clearAuthData();
|
||||
|
||||
// Générer l'URL d'autorisation avec PKCE
|
||||
final Map<String, String> authParams = await _buildAuthorizationUrl();
|
||||
|
||||
debugPrint('✅ Authentification préparée avec succès');
|
||||
return authParams;
|
||||
|
||||
} catch (e) {
|
||||
throw KeycloakWebViewAuthException(
|
||||
'Erreur lors de la préparation de l\'authentification: $e',
|
||||
originalError: e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Traite le callback de redirection et finalise l'authentification
|
||||
static Future<User> handleAuthCallback(String callbackUrl) async {
|
||||
debugPrint('🔄 Traitement du callback d\'authentification...');
|
||||
debugPrint('📋 URL de callback: $callbackUrl');
|
||||
|
||||
try {
|
||||
// Récupérer les paramètres d'authentification stockés
|
||||
debugPrint('🔍 Récupération de l\'état d\'authentification...');
|
||||
final String? authStateJson = await _secureStorage.read(key: _authStateKey);
|
||||
if (authStateJson == null) {
|
||||
debugPrint('❌ État d\'authentification manquant');
|
||||
throw const KeycloakWebViewAuthException(
|
||||
'État d\'authentification manquant',
|
||||
code: 'MISSING_AUTH_STATE',
|
||||
);
|
||||
}
|
||||
|
||||
final Map<String, dynamic> authState = jsonDecode(authStateJson);
|
||||
final String expectedState = authState['state'];
|
||||
final String codeVerifier = authState['code_verifier'];
|
||||
debugPrint('✅ État d\'authentification récupéré');
|
||||
|
||||
// Valider le callback et extraire le code
|
||||
debugPrint('🔍 Validation du callback...');
|
||||
final String authCode = await _validateCallbackAndExtractCode(
|
||||
callbackUrl,
|
||||
expectedState,
|
||||
);
|
||||
debugPrint('✅ Code d\'autorisation extrait: ${authCode.substring(0, 10)}...');
|
||||
|
||||
// Échanger le code contre des tokens
|
||||
debugPrint('🔄 Échange du code contre les tokens...');
|
||||
final WebViewAuthResult authResult = await _exchangeCodeForTokens(
|
||||
authCode,
|
||||
codeVerifier,
|
||||
);
|
||||
debugPrint('✅ Tokens reçus avec succès');
|
||||
|
||||
// Stocker les tokens
|
||||
debugPrint('💾 Stockage des tokens...');
|
||||
await _storeTokens(authResult);
|
||||
debugPrint('✅ Tokens stockés');
|
||||
|
||||
// Créer l'utilisateur depuis les tokens
|
||||
debugPrint('👤 Création de l\'utilisateur...');
|
||||
final User user = await _createUserFromTokens(authResult);
|
||||
debugPrint('✅ Utilisateur créé: ${user.fullName}');
|
||||
|
||||
// Nettoyer l'état d'authentification temporaire
|
||||
await _secureStorage.delete(key: _authStateKey);
|
||||
|
||||
debugPrint('🎉 Authentification WebView terminée avec succès');
|
||||
return user;
|
||||
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('💥 Erreur lors du traitement du callback: $e');
|
||||
debugPrint('📋 Stack trace: $stackTrace');
|
||||
|
||||
// Nettoyer en cas d'erreur
|
||||
await _secureStorage.delete(key: _authStateKey);
|
||||
|
||||
if (e is KeycloakWebViewAuthException) rethrow;
|
||||
|
||||
throw KeycloakWebViewAuthException(
|
||||
'Erreur lors du traitement du callback: $e',
|
||||
originalError: e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée un utilisateur depuis les tokens JWT
|
||||
static Future<User> _createUserFromTokens(WebViewAuthResult authResult) async {
|
||||
debugPrint('👤 Création de l\'utilisateur depuis les tokens...');
|
||||
|
||||
try {
|
||||
// Parser et valider les tokens
|
||||
final Map<String, dynamic> accessTokenPayload = _parseAndValidateJWT(
|
||||
authResult.accessToken,
|
||||
'Access Token',
|
||||
);
|
||||
final Map<String, dynamic> idTokenPayload = _parseAndValidateJWT(
|
||||
authResult.idToken,
|
||||
'ID Token',
|
||||
);
|
||||
|
||||
// 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'] ?? '';
|
||||
|
||||
if (userId.isEmpty || email.isEmpty) {
|
||||
throw const KeycloakWebViewAuthException(
|
||||
'Informations utilisateur manquantes dans les tokens',
|
||||
code: 'MISSING_USER_INFO',
|
||||
);
|
||||
}
|
||||
|
||||
// Extraire les rôles Keycloak
|
||||
final List<String> keycloakRoles = _extractKeycloakRoles(accessTokenPayload);
|
||||
|
||||
// Mapper vers notre système de rôles
|
||||
final UserRole primaryRole = KeycloakRoleMapper.mapToUserRole(keycloakRoles);
|
||||
final List<String> permissions = KeycloakRoleMapper.mapToPermissions(keycloakRoles);
|
||||
|
||||
// Créer l'utilisateur
|
||||
final User user = User(
|
||||
id: userId,
|
||||
email: email,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
primaryRole: primaryRole,
|
||||
organizationContexts: const [],
|
||||
additionalPermissions: permissions,
|
||||
revokedPermissions: const [],
|
||||
preferences: const UserPreferences(
|
||||
language: 'fr',
|
||||
theme: 'system',
|
||||
notificationsEnabled: true,
|
||||
emailNotifications: true,
|
||||
pushNotifications: true,
|
||||
dashboardLayout: 'adaptive',
|
||||
timezone: 'Europe/Paris',
|
||||
),
|
||||
lastLoginAt: DateTime.now(),
|
||||
createdAt: DateTime.now(),
|
||||
isActive: true,
|
||||
);
|
||||
|
||||
// Stocker les informations utilisateur
|
||||
await _secureStorage.write(
|
||||
key: _userInfoKey,
|
||||
value: jsonEncode(user.toJson()),
|
||||
);
|
||||
|
||||
debugPrint('✅ Utilisateur créé: ${user.fullName} (${user.primaryRole.displayName})');
|
||||
return user;
|
||||
|
||||
} catch (e) {
|
||||
if (e is KeycloakWebViewAuthException) rethrow;
|
||||
|
||||
throw KeycloakWebViewAuthException(
|
||||
'Erreur lors de la création de l\'utilisateur: $e',
|
||||
originalError: e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Extrait les rôles Keycloak depuis le payload du token
|
||||
static List<String> _extractKeycloakRoles(Map<String, dynamic> tokenPayload) {
|
||||
try {
|
||||
final List<String> roles = <String>[];
|
||||
|
||||
// Rôles realm
|
||||
final Map<String, dynamic>? realmAccess = tokenPayload['realm_access'];
|
||||
if (realmAccess != null && realmAccess['roles'] is List) {
|
||||
roles.addAll(List<String>.from(realmAccess['roles']));
|
||||
}
|
||||
|
||||
// Rôles client
|
||||
final Map<String, dynamic>? resourceAccess = tokenPayload['resource_access'];
|
||||
if (resourceAccess != null) {
|
||||
final Map<String, dynamic>? clientAccess = resourceAccess[KeycloakWebViewConfig.clientId];
|
||||
if (clientAccess != null && clientAccess['roles'] is List) {
|
||||
roles.addAll(List<String>.from(clientAccess['roles']));
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer les rôles système
|
||||
return roles.where((role) =>
|
||||
!role.startsWith('default-roles-') &&
|
||||
role != 'offline_access' &&
|
||||
role != 'uma_authorization'
|
||||
).toList();
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('💥 Erreur extraction rôles: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Nettoie toutes les données d'authentification
|
||||
static Future<void> clearAuthData() async {
|
||||
debugPrint('🧹 Nettoyage des données d\'authentification...');
|
||||
|
||||
try {
|
||||
await Future.wait([
|
||||
_secureStorage.delete(key: _accessTokenKey),
|
||||
_secureStorage.delete(key: _idTokenKey),
|
||||
_secureStorage.delete(key: _refreshTokenKey),
|
||||
_secureStorage.delete(key: _userInfoKey),
|
||||
_secureStorage.delete(key: _authStateKey),
|
||||
]);
|
||||
|
||||
debugPrint('✅ Données d\'authentification nettoyées');
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur lors du nettoyage: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 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é
|
||||
return !JwtDecoder.isExpired(accessToken);
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('💥 Erreur vérification authentification: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère l'utilisateur authentifié
|
||||
static Future<User?> getCurrentUser() async {
|
||||
try {
|
||||
final String? userInfoJson = await _secureStorage.read(key: _userInfoKey);
|
||||
|
||||
if (userInfoJson == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final Map<String, dynamic> userJson = jsonDecode(userInfoJson);
|
||||
return User.fromJson(userJson);
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('💥 Erreur récupération utilisateur: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Déconnecte l'utilisateur
|
||||
static Future<bool> logout() async {
|
||||
debugPrint('🚪 Déconnexion de l\'utilisateur...');
|
||||
|
||||
try {
|
||||
// Nettoyer les données locales
|
||||
await clearAuthData();
|
||||
|
||||
debugPrint('✅ Déconnexion réussie');
|
||||
return true;
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('💥 Erreur déconnexion: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
/// Moteur de permissions ultra-performant avec cache intelligent
|
||||
/// Vérifications contextuelles et audit trail intégré
|
||||
library permission_engine;
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../models/user.dart';
|
||||
import '../models/user_role.dart';
|
||||
import '../models/permission_matrix.dart';
|
||||
|
||||
/// Moteur de permissions haute performance avec cache multi-niveaux
|
||||
///
|
||||
/// Fonctionnalités :
|
||||
/// - Cache mémoire ultra-rapide avec TTL
|
||||
/// - Vérifications contextuelles avancées
|
||||
/// - Audit trail automatique
|
||||
/// - Support des permissions héritées
|
||||
/// - Invalidation intelligente du cache
|
||||
class PermissionEngine {
|
||||
static final PermissionEngine _instance = PermissionEngine._internal();
|
||||
factory PermissionEngine() => _instance;
|
||||
PermissionEngine._internal();
|
||||
|
||||
/// Cache mémoire des permissions avec TTL
|
||||
static final Map<String, _CachedPermission> _permissionCache = {};
|
||||
|
||||
/// Cache des permissions effectives par utilisateur
|
||||
static final Map<String, _CachedUserPermissions> _userPermissionsCache = {};
|
||||
|
||||
/// Durée de vie du cache (5 minutes par défaut)
|
||||
static const Duration _defaultCacheTTL = Duration(minutes: 5);
|
||||
|
||||
/// Durée de vie du cache pour les super admins (plus long)
|
||||
static const Duration _superAdminCacheTTL = Duration(minutes: 15);
|
||||
|
||||
/// Compteur de hits/miss du cache pour monitoring
|
||||
static int _cacheHits = 0;
|
||||
static int _cacheMisses = 0;
|
||||
|
||||
/// Stream pour les événements d'audit
|
||||
static final StreamController<PermissionAuditEvent> _auditController =
|
||||
StreamController<PermissionAuditEvent>.broadcast();
|
||||
|
||||
/// Stream des événements d'audit
|
||||
static Stream<PermissionAuditEvent> get auditStream => _auditController.stream;
|
||||
|
||||
/// Vérifie si un utilisateur a une permission spécifique
|
||||
///
|
||||
/// [user] - Utilisateur à vérifier
|
||||
/// [permission] - Permission à vérifier
|
||||
/// [organizationId] - Contexte organisationnel optionnel
|
||||
/// [auditLog] - Activer l'audit trail (défaut: true)
|
||||
static Future<bool> hasPermission(
|
||||
User user,
|
||||
String permission, {
|
||||
String? organizationId,
|
||||
bool auditLog = true,
|
||||
}) async {
|
||||
final cacheKey = _generateCacheKey(user.id, permission, organizationId);
|
||||
|
||||
// Vérification du cache
|
||||
final cachedResult = _getCachedPermission(cacheKey);
|
||||
if (cachedResult != null) {
|
||||
_cacheHits++;
|
||||
if (auditLog && !cachedResult.result) {
|
||||
_logAuditEvent(user, permission, false, 'CACHED_DENIED', organizationId);
|
||||
}
|
||||
return cachedResult.result;
|
||||
}
|
||||
|
||||
_cacheMisses++;
|
||||
|
||||
// Calcul de la permission
|
||||
final result = await _computePermission(user, permission, organizationId);
|
||||
|
||||
// Mise en cache
|
||||
_cachePermission(cacheKey, result, user.primaryRole);
|
||||
|
||||
// Audit trail
|
||||
if (auditLog) {
|
||||
_logAuditEvent(
|
||||
user,
|
||||
permission,
|
||||
result,
|
||||
result ? 'GRANTED' : 'DENIED',
|
||||
organizationId,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Vérifie plusieurs permissions en une seule fois
|
||||
static Future<Map<String, bool>> hasPermissions(
|
||||
User user,
|
||||
List<String> permissions, {
|
||||
String? organizationId,
|
||||
bool auditLog = true,
|
||||
}) async {
|
||||
final results = <String, bool>{};
|
||||
|
||||
// Traitement en parallèle pour les performances
|
||||
final futures = permissions.map((permission) =>
|
||||
hasPermission(user, permission, organizationId: organizationId, auditLog: auditLog)
|
||||
.then((result) => MapEntry(permission, result))
|
||||
);
|
||||
|
||||
final entries = await Future.wait(futures);
|
||||
for (final entry in entries) {
|
||||
results[entry.key] = entry.value;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// Obtient toutes les permissions effectives d'un utilisateur
|
||||
static Future<List<String>> getEffectivePermissions(
|
||||
User user, {
|
||||
String? organizationId,
|
||||
}) async {
|
||||
final cacheKey = '${user.id}_effective_${organizationId ?? 'global'}';
|
||||
|
||||
// Vérification du cache utilisateur
|
||||
final cachedUserPermissions = _getCachedUserPermissions(cacheKey);
|
||||
if (cachedUserPermissions != null) {
|
||||
_cacheHits++;
|
||||
return cachedUserPermissions.permissions;
|
||||
}
|
||||
|
||||
_cacheMisses++;
|
||||
|
||||
// Calcul des permissions effectives
|
||||
final permissions = user.getEffectivePermissions(organizationId: organizationId);
|
||||
|
||||
// Mise en cache
|
||||
_cacheUserPermissions(cacheKey, permissions, user.primaryRole);
|
||||
|
||||
return permissions;
|
||||
}
|
||||
|
||||
/// Vérifie si un utilisateur peut effectuer une action sur un domaine
|
||||
static Future<bool> canPerformAction(
|
||||
User user,
|
||||
String domain,
|
||||
String action, {
|
||||
String scope = 'own',
|
||||
String? organizationId,
|
||||
}) async {
|
||||
final permission = '$domain.$action.$scope';
|
||||
return hasPermission(user, permission, organizationId: organizationId);
|
||||
}
|
||||
|
||||
/// Invalide le cache pour un utilisateur spécifique
|
||||
static void invalidateUserCache(String userId) {
|
||||
final keysToRemove = <String>[];
|
||||
|
||||
// Invalider le cache des permissions
|
||||
for (final key in _permissionCache.keys) {
|
||||
if (key.startsWith('${userId}_')) {
|
||||
keysToRemove.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (final key in keysToRemove) {
|
||||
_permissionCache.remove(key);
|
||||
}
|
||||
|
||||
// Invalider le cache des permissions utilisateur
|
||||
final userKeysToRemove = <String>[];
|
||||
for (final key in _userPermissionsCache.keys) {
|
||||
if (key.startsWith('${userId}_')) {
|
||||
userKeysToRemove.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (final key in userKeysToRemove) {
|
||||
_userPermissionsCache.remove(key);
|
||||
}
|
||||
|
||||
debugPrint('Cache invalidé pour l\'utilisateur: $userId');
|
||||
}
|
||||
|
||||
/// Invalide tout le cache
|
||||
static void invalidateAllCache() {
|
||||
_permissionCache.clear();
|
||||
_userPermissionsCache.clear();
|
||||
debugPrint('Cache complet invalidé');
|
||||
}
|
||||
|
||||
/// Obtient les statistiques du cache
|
||||
static Map<String, dynamic> getCacheStats() {
|
||||
final totalRequests = _cacheHits + _cacheMisses;
|
||||
final hitRate = totalRequests > 0 ? (_cacheHits / totalRequests * 100) : 0.0;
|
||||
|
||||
return {
|
||||
'cacheHits': _cacheHits,
|
||||
'cacheMisses': _cacheMisses,
|
||||
'hitRate': hitRate.toStringAsFixed(2),
|
||||
'permissionCacheSize': _permissionCache.length,
|
||||
'userPermissionsCacheSize': _userPermissionsCache.length,
|
||||
};
|
||||
}
|
||||
|
||||
/// Nettoie le cache expiré
|
||||
static void cleanExpiredCache() {
|
||||
final now = DateTime.now();
|
||||
|
||||
// Nettoyer le cache des permissions
|
||||
_permissionCache.removeWhere((key, cached) => cached.expiresAt.isBefore(now));
|
||||
|
||||
// Nettoyer le cache des permissions utilisateur
|
||||
_userPermissionsCache.removeWhere((key, cached) => cached.expiresAt.isBefore(now));
|
||||
|
||||
debugPrint('Cache expiré nettoyé');
|
||||
}
|
||||
|
||||
// === MÉTHODES PRIVÉES ===
|
||||
|
||||
/// Calcule une permission sans cache
|
||||
static Future<bool> _computePermission(
|
||||
User user,
|
||||
String permission,
|
||||
String? organizationId,
|
||||
) async {
|
||||
// Vérification des permissions publiques
|
||||
if (PermissionMatrix.isPublicPermission(permission)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Vérification utilisateur actif
|
||||
if (!user.isActive) return false;
|
||||
|
||||
// Vérification directe de l'utilisateur
|
||||
if (user.hasPermission(permission, organizationId: organizationId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Vérifications contextuelles avancées
|
||||
return _checkContextualPermissions(user, permission, organizationId);
|
||||
}
|
||||
|
||||
/// Vérifications contextuelles avancées
|
||||
static Future<bool> _checkContextualPermissions(
|
||||
User user,
|
||||
String permission,
|
||||
String? organizationId,
|
||||
) async {
|
||||
// Logique contextuelle future (intégration avec le serveur)
|
||||
// Pour l'instant, retourne false
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Génère une clé de cache unique
|
||||
static String _generateCacheKey(String userId, String permission, String? organizationId) {
|
||||
return '${userId}_${permission}_${organizationId ?? 'global'}';
|
||||
}
|
||||
|
||||
/// Obtient une permission depuis le cache
|
||||
static _CachedPermission? _getCachedPermission(String key) {
|
||||
final cached = _permissionCache[key];
|
||||
if (cached != null && cached.expiresAt.isAfter(DateTime.now())) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
if (cached != null) {
|
||||
_permissionCache.remove(key);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Met en cache une permission
|
||||
static void _cachePermission(String key, bool result, UserRole userRole) {
|
||||
final ttl = userRole == UserRole.superAdmin ? _superAdminCacheTTL : _defaultCacheTTL;
|
||||
|
||||
_permissionCache[key] = _CachedPermission(
|
||||
result: result,
|
||||
expiresAt: DateTime.now().add(ttl),
|
||||
);
|
||||
}
|
||||
|
||||
/// Obtient les permissions utilisateur depuis le cache
|
||||
static _CachedUserPermissions? _getCachedUserPermissions(String key) {
|
||||
final cached = _userPermissionsCache[key];
|
||||
if (cached != null && cached.expiresAt.isAfter(DateTime.now())) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
if (cached != null) {
|
||||
_userPermissionsCache.remove(key);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Met en cache les permissions utilisateur
|
||||
static void _cacheUserPermissions(String key, List<String> permissions, UserRole userRole) {
|
||||
final ttl = userRole == UserRole.superAdmin ? _superAdminCacheTTL : _defaultCacheTTL;
|
||||
|
||||
_userPermissionsCache[key] = _CachedUserPermissions(
|
||||
permissions: permissions,
|
||||
expiresAt: DateTime.now().add(ttl),
|
||||
);
|
||||
}
|
||||
|
||||
/// Enregistre un événement d'audit
|
||||
static void _logAuditEvent(
|
||||
User user,
|
||||
String permission,
|
||||
bool granted,
|
||||
String reason,
|
||||
String? organizationId,
|
||||
) {
|
||||
final event = PermissionAuditEvent(
|
||||
userId: user.id,
|
||||
userEmail: user.email,
|
||||
permission: permission,
|
||||
granted: granted,
|
||||
reason: reason,
|
||||
organizationId: organizationId,
|
||||
timestamp: DateTime.now(),
|
||||
);
|
||||
|
||||
_auditController.add(event);
|
||||
}
|
||||
}
|
||||
|
||||
/// Classe pour les permissions mises en cache
|
||||
class _CachedPermission {
|
||||
final bool result;
|
||||
final DateTime expiresAt;
|
||||
|
||||
_CachedPermission({required this.result, required this.expiresAt});
|
||||
}
|
||||
|
||||
/// Classe pour les permissions utilisateur mises en cache
|
||||
class _CachedUserPermissions {
|
||||
final List<String> permissions;
|
||||
final DateTime expiresAt;
|
||||
|
||||
_CachedUserPermissions({required this.permissions, required this.expiresAt});
|
||||
}
|
||||
|
||||
/// Événement d'audit des permissions
|
||||
class PermissionAuditEvent {
|
||||
final String userId;
|
||||
final String userEmail;
|
||||
final String permission;
|
||||
final bool granted;
|
||||
final String reason;
|
||||
final String? organizationId;
|
||||
final DateTime timestamp;
|
||||
|
||||
PermissionAuditEvent({
|
||||
required this.userId,
|
||||
required this.userEmail,
|
||||
required this.permission,
|
||||
required this.granted,
|
||||
required this.reason,
|
||||
this.organizationId,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'userId': userId,
|
||||
'userEmail': userEmail,
|
||||
'permission': permission,
|
||||
'granted': granted,
|
||||
'reason': reason,
|
||||
'organizationId': organizationId,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user