469 lines
14 KiB
Dart
469 lines
14 KiB
Dart
/// 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<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;
|
|
final User? user;
|
|
|
|
const AuthWebViewCallback(this.callbackUrl, {this.user});
|
|
|
|
@override
|
|
List<Object?> get props => [callbackUrl, user];
|
|
}
|
|
|
|
// === É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,
|
|
];
|
|
}
|
|
|
|
/// É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<AuthStatusChecked>(_onStatusChecked);
|
|
on<AuthUserProfileUpdated>(_onUserProfileUpdated);
|
|
on<AuthWebViewCallback>(_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<void> _onLoginRequested(
|
|
AuthLoginRequested event,
|
|
Emitter<AuthState> emit,
|
|
) async {
|
|
emit(const AuthLoading());
|
|
|
|
try {
|
|
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'));
|
|
}
|
|
}
|
|
|
|
/// 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...');
|
|
|
|
// 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<void> _onLogoutRequested(
|
|
AuthLogoutRequested event,
|
|
Emitter<AuthState> 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<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 {
|
|
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<void> _onStatusChecked(
|
|
AuthStatusChecked event,
|
|
Emitter<AuthState> 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<void> _onUserProfileUpdated(
|
|
AuthUserProfileUpdated event,
|
|
Emitter<AuthState> 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'));
|
|
}
|
|
}
|
|
|
|
|
|
}
|