Authentification stable - WIP

This commit is contained in:
DahoudG
2025-09-19 12:35:46 +00:00
parent 63fe107f98
commit 098894bdc1
383 changed files with 13072 additions and 93334 deletions

View File

@@ -1,203 +1,460 @@
import 'dart:async';
/// 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:injectable/injectable.dart';
import '../models/auth_state.dart';
import '../services/auth_service.dart';
import '../services/auth_api_service.dart';
import 'auth_event.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import '../models/user.dart';
import '../models/user_role.dart';
import '../services/permission_engine.dart';
import '../services/keycloak_auth_service.dart';
import '../../cache/dashboard_cache_manager.dart';
/// BLoC pour gérer l'authentification
@singleton
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final AuthService _authService;
late StreamSubscription<AuthState> _authStateSubscription;
// === ÉVÉNEMENTS ===
AuthBloc(this._authService) : super(const AuthState.unknown()) {
// Écouter les changements d'état du service
_authStateSubscription = _authService.authStateStream.listen(
(authState) => add(AuthStateChanged(authState)),
/// Événements d'authentification
abstract class AuthEvent extends Equatable {
const AuthEvent();
@override
List<Object?> 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<Object?> 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<Object?> get props => [updatedUser];
}
/// Événement de callback WebView
class AuthWebViewCallback extends AuthEvent {
final String callbackUrl;
const AuthWebViewCallback(this.callbackUrl);
@override
List<Object?> get props => [callbackUrl];
}
// === ÉTATS ===
/// États d'authentification
abstract class AuthState extends Equatable {
const AuthState();
@override
List<Object?> 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<String> 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<String>? 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<Object?> get props => [
user,
currentOrganizationId,
effectiveRole,
effectivePermissions,
authenticatedAt,
accessToken,
];
}
// Gestionnaires d'événements
on<AuthInitializeRequested>(_onInitializeRequested);
/// État non authentifié
class AuthUnauthenticated extends AuthState {
final String? message;
const AuthUnauthenticated({this.message});
@override
List<Object?> 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<Object?> 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<Object?> get props => [authUrl, state, codeVerifier];
}
// === BLOC ===
/// BLoC d'authentification adaptatif
class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc() : super(const AuthInitial()) {
on<AuthLoginRequested>(_onLoginRequested);
on<AuthLogoutRequested>(_onLogoutRequested);
on<AuthOrganizationContextChanged>(_onOrganizationContextChanged);
on<AuthTokenRefreshRequested>(_onTokenRefreshRequested);
on<AuthSessionExpired>(_onSessionExpired);
on<AuthStatusCheckRequested>(_onStatusCheckRequested);
on<AuthErrorCleared>(_onErrorCleared);
on<AuthStateChanged>(_onStateChanged);
on<AuthStatusChecked>(_onStatusChecked);
on<AuthUserProfileUpdated>(_onUserProfileUpdated);
on<AuthWebViewCallback>(_onWebViewCallback);
}
/// Initialisation de l'authentification
Future<void> _onInitializeRequested(
AuthInitializeRequested event,
Emitter<AuthState> emit,
) async {
emit(const AuthState.checking());
try {
await _authService.initialize();
} catch (e) {
emit(AuthState.error('Erreur d\'initialisation: $e'));
}
}
/// Gestion de la connexion
/// 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<void> _onLoginRequested(
AuthLoginRequested event,
Emitter<AuthState> emit,
) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
emit(const AuthLoading());
try {
await _authService.login(event.loginRequest);
// L'état sera mis à jour par le stream du service
} on AuthApiException catch (e) {
emit(state.copyWith(
isLoading: false,
errorMessage: e.message,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
errorMessage: 'Erreur de connexion: $e',
debugPrint('🔐 Préparation authentification Keycloak WebView...');
// Préparer l'authentification WebView
final Map<String, String> 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'));
}
}
/// Gestion de la déconnexion
/// Traite le callback WebView et finalise l'authentification
Future<void> _onWebViewCallback(
AuthWebViewCallback event,
Emitter<AuthState> emit,
) async {
emit(const AuthLoading());
try {
debugPrint('🔄 Traitement callback WebView...');
// Traiter le callback et récupérer l'utilisateur
final User user = await KeycloakAuthService.handleWebViewCallback(event.callbackUrl);
debugPrint('👤 Utilisateur récupéré: ${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');
} 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<void> _onLogoutRequested(
AuthLogoutRequested event,
Emitter<AuthState> emit,
) async {
emit(state.copyWith(isLoading: true));
emit(const AuthLoading());
try {
await _authService.logout();
// L'état sera mis à jour par le stream du service
} catch (e) {
// Même en cas d'erreur, on considère que la déconnexion locale a réussi
emit(const AuthState.unauthenticated());
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'));
}
}
/// Gestion du rafraîchissement de token
/// Gère le changement de contexte organisationnel
Future<void> _onOrganizationContextChanged(
AuthOrganizationContextChanged event,
Emitter<AuthState> 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<void> _onTokenRefreshRequested(
AuthTokenRefreshRequested event,
Emitter<AuthState> emit,
) async {
// Le rafraîchissement est géré automatiquement par le service
// Cet événement peut être utilisé pour forcer un rafraîchissement manuel
if (state is! AuthAuthenticated) return;
final currentState = state as AuthAuthenticated;
try {
// Le service gère déjà le rafraîchissement automatique
// On peut ajouter ici une logique spécifique si nécessaire
// 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(AuthState.error('Erreur lors du rafraîchissement: $e'));
emit(AuthError(message: 'Erreur de rafraîchissement: $e'));
}
}
/// Gestion de l'expiration de session
Future<void> _onSessionExpired(
AuthSessionExpired event,
/// Vérifie l'état d'authentification Keycloak
Future<void> _onStatusChecked(
AuthStatusChecked event,
Emitter<AuthState> emit,
) async {
emit(const AuthState.expired());
// Optionnel: essayer un rafraîchissement automatique
emit(const AuthLoading());
try {
await _authService.logout();
} catch (e) {
// Ignorer les erreurs de déconnexion lors de l'expiration
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'));
}
}
/// Vérification du statut d'authentification
Future<void> _onStatusCheckRequested(
AuthStatusCheckRequested event,
/// Met à jour le profil utilisateur
Future<void> _onUserProfileUpdated(
AuthUserProfileUpdated event,
Emitter<AuthState> emit,
) async {
// Utiliser l'état actuel du service
final currentServiceState = _authService.currentState;
if (currentServiceState != state) {
emit(currentServiceState);
}
}
/// Nettoyage des erreurs
void _onErrorCleared(
AuthErrorCleared event,
Emitter<AuthState> emit,
) {
if (state.errorMessage != null) {
emit(state.copyWith(errorMessage: null));
}
}
/// Mise à jour depuis le service d'authentification
void _onStateChanged(
AuthStateChanged event,
Emitter<AuthState> emit,
) {
final newState = event.authState as AuthState;
if (state is! AuthAuthenticated) return;
// Émettre le nouvel état seulement s'il a changé
if (newState != state) {
emit(newState);
}
}
/// Vérifie si l'utilisateur est connecté
bool get isAuthenticated => state.isAuthenticated;
/// Récupère l'utilisateur actuel
get currentUser => state.user;
/// Vérifie si l'utilisateur a un rôle spécifique
bool hasRole(String role) {
return _authService.hasRole(role);
}
/// Vérifie si l'utilisateur a un des rôles spécifiés
bool hasAnyRole(List<String> roles) {
return _authService.hasAnyRole(roles);
}
/// Vérifie si la session expire bientôt
bool get isSessionExpiringSoon => state.isExpiringSoon;
/// Récupère le message d'erreur formaté
String? get errorMessage {
final error = state.errorMessage;
if (error == null) return null;
// Formatage des messages d'erreur pour l'utilisateur
if (error.contains('network') || error.contains('connexion')) {
return 'Problème de connexion. Vérifiez votre réseau.';
}
final currentState = state as AuthAuthenticated;
if (error.contains('401') || error.contains('Identifiants')) {
return 'Email ou mot de passe incorrect.';
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'));
}
if (error.contains('403')) {
return 'Accès non autorisé.';
}
if (error.contains('timeout')) {
return 'Délai d\'attente dépassé. Réessayez.';
}
if (error.contains('server') || error.contains('500')) {
return 'Erreur serveur temporaire. Réessayez plus tard.';
}
return error;
}
@override
Future<void> close() {
_authStateSubscription.cancel();
return super.close();
}
}
}

View File

@@ -1,60 +0,0 @@
import 'package:equatable/equatable.dart';
import '../models/login_request.dart';
/// Événements d'authentification
abstract class AuthEvent extends Equatable {
const AuthEvent();
@override
List<Object?> get props => [];
}
/// Initialiser l'authentification
class AuthInitializeRequested extends AuthEvent {
const AuthInitializeRequested();
}
/// Demande de connexion
class AuthLoginRequested extends AuthEvent {
final LoginRequest loginRequest;
const AuthLoginRequested(this.loginRequest);
@override
List<Object> get props => [loginRequest];
}
/// Demande de déconnexion
class AuthLogoutRequested extends AuthEvent {
const AuthLogoutRequested();
}
/// Demande de rafraîchissement de token
class AuthTokenRefreshRequested extends AuthEvent {
const AuthTokenRefreshRequested();
}
/// Session expirée
class AuthSessionExpired extends AuthEvent {
const AuthSessionExpired();
}
/// Vérification de l'état d'authentification
class AuthStatusCheckRequested extends AuthEvent {
const AuthStatusCheckRequested();
}
/// Réinitialisation de l'erreur
class AuthErrorCleared extends AuthEvent {
const AuthErrorCleared();
}
/// Changement d'état depuis le service
class AuthStateChanged extends AuthEvent {
final dynamic authState;
const AuthStateChanged(this.authState);
@override
List<Object?> get props => [authState];
}

View File

@@ -1,74 +0,0 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../models/auth_state.dart';
import '../services/temp_auth_service.dart';
import 'auth_event.dart';
/// BLoC temporaire pour test sans injection de dépendances
class TempAuthBloc extends Bloc<AuthEvent, AuthState> {
final TempAuthService _authService;
late StreamSubscription<AuthState> _authStateSubscription;
TempAuthBloc(this._authService) : super(const AuthState.unknown()) {
_authStateSubscription = _authService.authStateStream.listen(
(authState) => add(AuthStateChanged(authState)),
);
on<AuthInitializeRequested>(_onInitializeRequested);
on<AuthLoginRequested>(_onLoginRequested);
on<AuthLogoutRequested>(_onLogoutRequested);
on<AuthErrorCleared>(_onErrorCleared);
on<AuthStateChanged>(_onStateChanged);
}
Future<void> _onInitializeRequested(
AuthInitializeRequested event,
Emitter<AuthState> emit,
) async {
await _authService.initialize();
}
Future<void> _onLoginRequested(
AuthLoginRequested event,
Emitter<AuthState> emit,
) async {
try {
await _authService.login(event.loginRequest);
} catch (e) {
emit(AuthState.error(e.toString()));
}
}
Future<void> _onLogoutRequested(
AuthLogoutRequested event,
Emitter<AuthState> emit,
) async {
await _authService.logout();
}
void _onErrorCleared(
AuthErrorCleared event,
Emitter<AuthState> emit,
) {
if (state.errorMessage != null) {
emit(state.copyWith(errorMessage: null));
}
}
void _onStateChanged(
AuthStateChanged event,
Emitter<AuthState> emit,
) {
final newState = event.authState as AuthState;
if (newState != state) {
emit(newState);
}
}
@override
Future<void> close() {
_authStateSubscription.cancel();
_authService.dispose();
return super.close();
}
}