/// BLoC d'authentification Keycloak adaptatif avec gestion des rôles /// Support Keycloak avec contextes multi-organisations et états sophistiqués library auth_bloc; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import '../../data/models/user.dart'; import '../../data/models/user_role.dart'; import '../../data/datasources/permission_engine.dart'; import '../../data/datasources/keycloak_auth_service.dart'; import '../../data/datasources/dashboard_cache_manager.dart'; // === ÉVÉNEMENTS === /// Événements d'authentification abstract class AuthEvent extends Equatable { const AuthEvent(); @override List get props => []; } /// Événement de connexion Keycloak class AuthLoginRequested extends AuthEvent { const AuthLoginRequested(); } /// Événement de déconnexion class AuthLogoutRequested extends AuthEvent { const AuthLogoutRequested(); } /// Événement de changement de contexte organisationnel class AuthOrganizationContextChanged extends AuthEvent { final String organizationId; const AuthOrganizationContextChanged(this.organizationId); @override List get props => [organizationId]; } /// Événement de rafraîchissement du token class AuthTokenRefreshRequested extends AuthEvent { const AuthTokenRefreshRequested(); } /// Événement de vérification de l'état d'authentification class AuthStatusChecked extends AuthEvent { const AuthStatusChecked(); } /// Événement de mise à jour du profil utilisateur class AuthUserProfileUpdated extends AuthEvent { final User updatedUser; const AuthUserProfileUpdated(this.updatedUser); @override List get props => [updatedUser]; } /// Événement de callback WebView class AuthWebViewCallback extends AuthEvent { final String callbackUrl; final User? user; const AuthWebViewCallback(this.callbackUrl, {this.user}); @override List get props => [callbackUrl, user]; } // === ÉTATS === /// États d'authentification abstract class AuthState extends Equatable { const AuthState(); @override List get props => []; } /// État initial class AuthInitial extends AuthState { const AuthInitial(); } /// État de chargement class AuthLoading extends AuthState { const AuthLoading(); } /// État authentifié avec contexte riche class AuthAuthenticated extends AuthState { final User user; final String? currentOrganizationId; final UserRole effectiveRole; final List effectivePermissions; final DateTime authenticatedAt; final String? accessToken; const AuthAuthenticated({ required this.user, this.currentOrganizationId, required this.effectiveRole, required this.effectivePermissions, required this.authenticatedAt, this.accessToken, }); /// Vérifie si l'utilisateur a une permission bool hasPermission(String permission) { return effectivePermissions.contains(permission); } /// Vérifie si l'utilisateur peut effectuer une action bool canPerformAction(String domain, String action, {String scope = 'own'}) { final permission = '$domain.$action.$scope'; return hasPermission(permission); } /// Obtient le contexte organisationnel actuel UserOrganizationContext? get currentOrganizationContext { if (currentOrganizationId == null) return null; return user.getOrganizationContext(currentOrganizationId!); } /// Crée une copie avec des modifications AuthAuthenticated copyWith({ User? user, String? currentOrganizationId, UserRole? effectiveRole, List? effectivePermissions, DateTime? authenticatedAt, String? accessToken, }) { return AuthAuthenticated( user: user ?? this.user, currentOrganizationId: currentOrganizationId ?? this.currentOrganizationId, effectiveRole: effectiveRole ?? this.effectiveRole, effectivePermissions: effectivePermissions ?? this.effectivePermissions, authenticatedAt: authenticatedAt ?? this.authenticatedAt, accessToken: accessToken ?? this.accessToken, ); } @override List get props => [ user, currentOrganizationId, effectiveRole, effectivePermissions, authenticatedAt, accessToken, ]; } /// État non authentifié class AuthUnauthenticated extends AuthState { final String? message; const AuthUnauthenticated({this.message}); @override List get props => [message]; } /// État d'erreur class AuthError extends AuthState { final String message; final String? errorCode; const AuthError({ required this.message, this.errorCode, }); @override List get props => [message, errorCode]; } /// État indiquant qu'une WebView d'authentification est requise class AuthWebViewRequired extends AuthState { final String authUrl; final String state; final String codeVerifier; const AuthWebViewRequired({ required this.authUrl, required this.state, required this.codeVerifier, }); @override List get props => [authUrl, state, codeVerifier]; } // === BLOC === /// BLoC d'authentification adaptatif class AuthBloc extends Bloc { AuthBloc() : super(const AuthInitial()) { on(_onLoginRequested); on(_onLogoutRequested); on(_onOrganizationContextChanged); on(_onTokenRefreshRequested); on(_onStatusChecked); on(_onUserProfileUpdated); on(_onWebViewCallback); } /// Gère la demande de connexion Keycloak via WebView /// /// Cette méthode prépare l'authentification WebView et émet un état spécial /// pour indiquer qu'une WebView doit être ouverte Future _onLoginRequested( AuthLoginRequested event, Emitter emit, ) async { emit(const AuthLoading()); try { debugPrint('🔐 Préparation authentification Keycloak WebView...'); // Préparer l'authentification WebView final Map authParams = await KeycloakAuthService.prepareWebViewAuthentication(); debugPrint('✅ Authentification WebView préparée'); // Émettre un état spécial pour indiquer qu'une WebView doit être ouverte debugPrint('🚀 Émission de l\'état AuthWebViewRequired...'); emit(AuthWebViewRequired( authUrl: authParams['url']!, state: authParams['state']!, codeVerifier: authParams['code_verifier']!, )); debugPrint('✅ État AuthWebViewRequired émis'); } catch (e, stackTrace) { debugPrint('💥 Erreur préparation authentification Keycloak: $e'); debugPrint('Stack trace: $stackTrace'); emit(AuthError(message: 'Erreur de préparation: $e')); } } /// Traite le callback WebView et finalise l'authentification Future _onWebViewCallback( AuthWebViewCallback event, Emitter emit, ) async { emit(const AuthLoading()); try { debugPrint('🔄 Traitement callback WebView...'); // Utiliser l'utilisateur fourni ou traiter le callback final User user; if (event.user != null) { debugPrint('👤 Utilisation des données utilisateur fournies: ${event.user!.fullName}'); user = event.user!; } else { debugPrint('🔄 Traitement du callback URL: ${event.callbackUrl}'); user = await KeycloakAuthService.handleWebViewCallback(event.callbackUrl); } debugPrint('👤 Utilisateur authentifié: ${user.fullName} (${user.primaryRole.displayName})'); // Calculer les permissions effectives debugPrint('🔐 Calcul des permissions effectives...'); final effectivePermissions = await PermissionEngine.getEffectivePermissions(user); debugPrint('✅ Permissions effectives calculées: ${effectivePermissions.length} permissions'); // Invalider le cache pour forcer le rechargement debugPrint('🧹 Invalidation du cache pour le rôle ${user.primaryRole.displayName}...'); await DashboardCacheManager.invalidateForRole(user.primaryRole); debugPrint('✅ Cache invalidé'); emit(AuthAuthenticated( user: user, currentOrganizationId: null, // À implémenter selon vos besoins effectiveRole: user.primaryRole, effectivePermissions: effectivePermissions, authenticatedAt: DateTime.now(), accessToken: '', // Token géré par KeycloakWebViewAuthService )); debugPrint('🎉 Authentification complète réussie - navigation vers dashboard'); } catch (e, stackTrace) { debugPrint('💥 Erreur authentification: $e'); debugPrint('Stack trace: $stackTrace'); emit(AuthError(message: 'Erreur de connexion: $e')); } } /// Gère la demande de déconnexion Keycloak Future _onLogoutRequested( AuthLogoutRequested event, Emitter emit, ) async { emit(const AuthLoading()); try { debugPrint('🚪 Démarrage déconnexion Keycloak...'); // Déconnexion Keycloak final logoutSuccess = await KeycloakAuthService.logout(); if (!logoutSuccess) { debugPrint('⚠️ Déconnexion Keycloak partielle'); } // Nettoyer le cache local await DashboardCacheManager.clear(); // Invalider le cache des permissions if (state is AuthAuthenticated) { final authState = state as AuthAuthenticated; PermissionEngine.invalidateUserCache(authState.user.id); } debugPrint('✅ Déconnexion complète réussie'); emit(const AuthUnauthenticated(message: 'Déconnexion réussie')); } catch (e, stackTrace) { debugPrint('💥 Erreur déconnexion: $e'); debugPrint('Stack trace: $stackTrace'); emit(AuthError(message: 'Erreur de déconnexion: $e')); } } /// Gère le changement de contexte organisationnel Future _onOrganizationContextChanged( AuthOrganizationContextChanged event, Emitter emit, ) async { if (state is! AuthAuthenticated) return; final currentState = state as AuthAuthenticated; emit(const AuthLoading()); try { // Recalculer le rôle effectif et les permissions final effectiveRole = currentState.user.getRoleInOrganization(event.organizationId); final effectivePermissions = await PermissionEngine.getEffectivePermissions( currentState.user, organizationId: event.organizationId, ); // Invalider le cache pour le nouveau contexte PermissionEngine.invalidateUserCache(currentState.user.id); emit(currentState.copyWith( currentOrganizationId: event.organizationId, effectiveRole: effectiveRole, effectivePermissions: effectivePermissions, )); } catch (e) { emit(AuthError(message: 'Erreur de changement de contexte: $e')); } } /// Gère le rafraîchissement du token Future _onTokenRefreshRequested( AuthTokenRefreshRequested event, Emitter emit, ) async { if (state is! AuthAuthenticated) return; final currentState = state as AuthAuthenticated; try { // Simulation du rafraîchissement (à remplacer par l'API réelle) await Future.delayed(const Duration(seconds: 1)); final newToken = 'refreshed_token_${DateTime.now().millisecondsSinceEpoch}'; emit(currentState.copyWith(accessToken: newToken)); } catch (e) { emit(AuthError(message: 'Erreur de rafraîchissement: $e')); } } /// Vérifie l'état d'authentification Keycloak Future _onStatusChecked( AuthStatusChecked event, Emitter emit, ) async { emit(const AuthLoading()); try { debugPrint('🔍 Vérification état authentification Keycloak...'); // Vérifier si l'utilisateur est authentifié avec Keycloak final bool isAuthenticated = await KeycloakAuthService.isAuthenticated(); if (!isAuthenticated) { debugPrint('❌ Utilisateur non authentifié'); emit(const AuthUnauthenticated()); return; } // Récupérer l'utilisateur actuel final User? user = await KeycloakAuthService.getCurrentUser(); if (user == null) { debugPrint('❌ Impossible de récupérer l\'utilisateur'); emit(const AuthUnauthenticated()); return; } // Calculer les permissions effectives final effectivePermissions = await PermissionEngine.getEffectivePermissions(user); // Récupérer le token d'accès final String? accessToken = await KeycloakAuthService.getAccessToken(); debugPrint('✅ Utilisateur authentifié: ${user.fullName}'); emit(AuthAuthenticated( user: user, currentOrganizationId: null, // À implémenter selon vos besoins effectiveRole: user.primaryRole, effectivePermissions: effectivePermissions, authenticatedAt: DateTime.now(), accessToken: accessToken ?? '', )); } catch (e, stackTrace) { debugPrint('💥 Erreur vérification authentification: $e'); debugPrint('Stack trace: $stackTrace'); emit(AuthError(message: 'Erreur de vérification: $e')); } } /// Met à jour le profil utilisateur Future _onUserProfileUpdated( AuthUserProfileUpdated event, Emitter emit, ) async { if (state is! AuthAuthenticated) return; final currentState = state as AuthAuthenticated; try { // Recalculer les permissions si nécessaire final effectivePermissions = await PermissionEngine.getEffectivePermissions( event.updatedUser, organizationId: currentState.currentOrganizationId, ); emit(currentState.copyWith( user: event.updatedUser, effectivePermissions: effectivePermissions, )); } catch (e) { emit(AuthError(message: 'Erreur de mise à jour: $e')); } } }