import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:injectable/injectable.dart'; import '../../data/models/user.dart'; import '../../data/models/user_role.dart'; import '../../data/datasources/keycloak_auth_service.dart'; import '../../data/datasources/permission_engine.dart'; import '../../../../core/config/environment.dart'; import '../../../../core/storage/dashboard_cache_manager.dart'; import '../../../../core/utils/logger.dart'; import '../../../../core/di/injection.dart'; import '../../../organizations/domain/repositories/organization_repository.dart'; // === ÉVÉNEMENTS === abstract class AuthEvent extends Equatable { const AuthEvent(); @override List get props => []; } class AuthLoginRequested extends AuthEvent { final String email; final String password; const AuthLoginRequested(this.email, this.password); @override List get props => [email, password]; } class AuthLogoutRequested extends AuthEvent { const AuthLogoutRequested(); } class AuthStatusChecked extends AuthEvent { const AuthStatusChecked(); } class AuthTokenRefreshRequested extends AuthEvent { const AuthTokenRefreshRequested(); } // === ÉTATS === abstract class AuthState extends Equatable { const AuthState(); @override List get props => []; } class AuthInitial extends AuthState {} class AuthLoading extends AuthState {} class AuthUnauthenticated extends AuthState {} class AuthAuthenticated extends AuthState { final User user; final UserRole effectiveRole; final List effectivePermissions; final String accessToken; const AuthAuthenticated({ required this.user, required this.effectiveRole, required this.effectivePermissions, required this.accessToken, }); @override List get props => [user, effectiveRole, effectivePermissions, accessToken]; } class AuthError extends AuthState { final String message; const AuthError(this.message); @override List get props => [message]; } /// Compte bloqué (SUSPENDU ou DESACTIVE) — déconnexion + message. class AuthAccountNotActive extends AuthState { final String statutCompte; final String message; const AuthAccountNotActive({required this.statutCompte, required this.message}); @override List get props => [statutCompte, message]; } /// Compte EN_ATTENTE_VALIDATION — l'OrgAdmin doit compléter l'onboarding. /// On ne déconnecte PAS pour permettre les appels API de souscription. class AuthPendingOnboarding extends AuthState { final String onboardingState; // NO_SUBSCRIPTION | AWAITING_PAYMENT | PAYMENT_INITIATED | AWAITING_VALIDATION final String? souscriptionId; final String? organisationId; const AuthPendingOnboarding({ required this.onboardingState, this.souscriptionId, this.organisationId, }); @override List get props => [onboardingState, souscriptionId, organisationId]; } // === BLOC === @lazySingleton class AuthBloc extends Bloc { final KeycloakAuthService _authService; AuthBloc(this._authService) : super(AuthInitial()) { on(_onLoginRequested); on(_onLogoutRequested); on(_onStatusChecked); on(_onTokenRefreshRequested); } Future _onLoginRequested(AuthLoginRequested event, Emitter emit) async { emit(AuthLoading()); try { final rawUser = await _authService.login(event.email, event.password); if (rawUser != null) { // Vérification du statut du compte UnionFlow (indépendant de Keycloak) final status = await _authService.getAuthStatus(AppConfig.apiBaseUrl); if (status != null && status.isPendingOnboarding) { // OrgAdmin en attente → rediriger vers l'onboarding (sans déconnecter) final user = await _enrichUserWithOrgContext(rawUser); final orgId = status.organisationId ?? (user.organizationContexts.isNotEmpty ? user.organizationContexts.first.organizationId : null); emit(AuthPendingOnboarding( onboardingState: status.onboardingState, souscriptionId: status.souscriptionId, organisationId: orgId, )); return; } if (status != null && status.isBlocked) { await _authService.logout(); emit(AuthAccountNotActive( statutCompte: status.statutCompte, message: _messageForStatut(status.statutCompte), )); return; } final user = await _enrichUserWithOrgContext(rawUser); final permissions = await PermissionEngine.getEffectivePermissions(user); final token = await _authService.getValidToken(); await DashboardCacheManager.invalidateForRole(user.primaryRole); emit(AuthAuthenticated( user: user, effectiveRole: user.primaryRole, effectivePermissions: permissions, accessToken: token ?? '', )); } else { emit(const AuthError('Identifiants incorrects.')); } } catch (e) { emit(AuthError('Erreur de connexion: $e')); } } Future _onLogoutRequested(AuthLogoutRequested event, Emitter emit) async { emit(AuthLoading()); await _authService.logout(); await DashboardCacheManager.clear(); emit(AuthUnauthenticated()); } Future _onStatusChecked(AuthStatusChecked event, Emitter emit) async { final tokenValid = await _authService.getValidToken(); final isAuth = tokenValid != null; if (!isAuth) { emit(AuthUnauthenticated()); return; } final rawUser = await _authService.getCurrentUser(); if (rawUser == null) { emit(AuthUnauthenticated()); return; } // Vérification du statut du compte (au redémarrage de l'app) final status = await _authService.getAuthStatus(AppConfig.apiBaseUrl); if (status != null && status.isPendingOnboarding) { final user = await _enrichUserWithOrgContext(rawUser); final orgId = status.organisationId ?? (user.organizationContexts.isNotEmpty ? user.organizationContexts.first.organizationId : null); emit(AuthPendingOnboarding( onboardingState: status.onboardingState, souscriptionId: status.souscriptionId, organisationId: orgId, )); return; } if (status != null && status.isBlocked) { await _authService.logout(); emit(AuthAccountNotActive( statutCompte: status.statutCompte, message: _messageForStatut(status.statutCompte), )); return; } final user = await _enrichUserWithOrgContext(rawUser); final permissions = await PermissionEngine.getEffectivePermissions(user); final token = await _authService.getValidToken(); emit(AuthAuthenticated( user: user, effectiveRole: user.primaryRole, effectivePermissions: permissions, accessToken: token ?? '', )); } /// Retourne un message lisible selon le statut du compte. String _messageForStatut(String statut) { switch (statut) { case 'EN_ATTENTE_VALIDATION': return 'Votre compte est en attente de validation par un administrateur. Vous serez notifié dès que votre accès sera activé.'; case 'SUSPENDU': return 'Votre compte a été suspendu temporairement. Contactez votre administrateur pour plus d\'informations.'; case 'DESACTIVE': return 'Votre compte a été désactivé. Contactez votre administrateur.'; default: return 'Votre compte n\'est pas encore actif. Contactez votre administrateur.'; } } /// Enrichit le contexte organisationnel pour les AdminOrganisation. /// /// Si le rôle est [UserRole.orgAdmin] et que [organizationContexts] est vide, /// appelle GET /api/organisations/mes pour récupérer les organisations de l'admin. Future _enrichUserWithOrgContext(User user) async { if (user.primaryRole != UserRole.orgAdmin || user.organizationContexts.isNotEmpty) { return user; } try { final orgRepo = getIt(); final orgs = await orgRepo.getMesOrganisations(); if (orgs.isEmpty) return user; final contexts = orgs .where((o) => o.id != null && o.id!.isNotEmpty) .map( (o) => UserOrganizationContext( organizationId: o.id!, organizationName: o.nom, role: UserRole.orgAdmin, joinedAt: DateTime.now(), ), ) .toList(); return contexts.isEmpty ? user : user.copyWith(organizationContexts: contexts); } catch (e) { AppLogger.warning('AuthBloc: impossible de charger le contexte org: $e'); return user; } } Future _onTokenRefreshRequested(AuthTokenRefreshRequested event, Emitter emit) async { if (state is AuthAuthenticated) { final newToken = await _authService.refreshToken(); final success = newToken != null; if (success) { add(AuthStatusChecked()); } else { add(AuthLogoutRequested()); } } } }