feat(mobile): Implement Keycloak WebView authentication with HTTP callback

- Replace flutter_appauth with custom WebView implementation to resolve deep link issues
- Add KeycloakWebViewAuthService with integrated WebView for seamless authentication
- Configure Android manifest for HTTP cleartext traffic support
- Add network security config for development environment (192.168.1.11)
- Update Keycloak client to use HTTP callback endpoint (http://192.168.1.11:8080/auth/callback)
- Remove obsolete keycloak_auth_service.dart and temporary scripts
- Clean up dependencies and regenerate injection configuration
- Tested successfully on multiple Android devices (Xiaomi 2201116TG, SM A725F)

BREAKING CHANGE: Authentication flow now uses WebView instead of external browser
- Users will see Keycloak login page within the app instead of browser redirect
- Resolves ERR_CLEARTEXT_NOT_PERMITTED and deep link state management issues
- Maintains full OIDC compliance with PKCE flow and secure token storage

Technical improvements:
- WebView with custom navigation delegate for callback handling
- Automatic token extraction and user info parsing from JWT
- Proper error handling and user feedback
- Consistent authentication state management across app lifecycle
This commit is contained in:
DahoudG
2025-09-15 01:44:16 +00:00
parent 73459b3092
commit f89f6167cc
290 changed files with 34563 additions and 3528 deletions

View File

@@ -0,0 +1,388 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import '../../../../core/models/evenement_model.dart';
import '../../domain/repositories/evenement_repository.dart';
import 'evenement_event.dart';
import 'evenement_state.dart';
/// BLoC pour la gestion des événements
@injectable
class EvenementBloc extends Bloc<EvenementEvent, EvenementState> {
final EvenementRepository _repository;
EvenementBloc(this._repository) : super(const EvenementInitial()) {
on<LoadEvenementsAVenir>(_onLoadEvenementsAVenir);
on<LoadEvenementsPublics>(_onLoadEvenementsPublics);
on<LoadEvenements>(_onLoadEvenements);
on<LoadEvenementById>(_onLoadEvenementById);
on<SearchEvenements>(_onSearchEvenements);
on<FilterEvenementsByType>(_onFilterEvenementsByType);
on<CreateEvenement>(_onCreateEvenement);
on<UpdateEvenement>(_onUpdateEvenement);
on<DeleteEvenement>(_onDeleteEvenement);
on<ChangeStatutEvenement>(_onChangeStatutEvenement);
on<LoadStatistiquesEvenements>(_onLoadStatistiquesEvenements);
on<ResetEvenementState>(_onResetEvenementState);
}
/// Charge les événements à venir
Future<void> _onLoadEvenementsAVenir(
LoadEvenementsAVenir event,
Emitter<EvenementState> emit,
) async {
try {
if (event.refresh || state is EvenementInitial) {
emit(const EvenementLoading());
} else if (state is EvenementLoaded) {
emit(EvenementLoadingMore((state as EvenementLoaded).evenements));
}
final evenements = await _repository.getEvenementsAVenir(
page: event.page,
size: event.size,
);
if (event.refresh || event.page == 0) {
emit(EvenementLoaded(
evenements: evenements,
hasReachedMax: evenements.length < event.size,
currentPage: event.page,
));
} else {
final currentState = state as EvenementLoaded;
final allEvenements = List<EvenementModel>.from(currentState.evenements)
..addAll(evenements);
emit(currentState.copyWith(
evenements: allEvenements,
hasReachedMax: evenements.length < event.size,
currentPage: event.page,
));
}
} catch (e) {
final currentEvenements = state is EvenementLoaded
? (state as EvenementLoaded).evenements
: null;
emit(EvenementError(
message: e.toString(),
evenements: currentEvenements,
));
}
}
/// Charge les événements publics
Future<void> _onLoadEvenementsPublics(
LoadEvenementsPublics event,
Emitter<EvenementState> emit,
) async {
try {
if (event.refresh || state is EvenementInitial) {
emit(const EvenementLoading());
} else if (state is EvenementLoaded) {
emit(EvenementLoadingMore((state as EvenementLoaded).evenements));
}
final evenements = await _repository.getEvenementsPublics(
page: event.page,
size: event.size,
);
if (event.refresh || event.page == 0) {
emit(EvenementLoaded(
evenements: evenements,
hasReachedMax: evenements.length < event.size,
currentPage: event.page,
));
} else {
final currentState = state as EvenementLoaded;
final allEvenements = List<EvenementModel>.from(currentState.evenements)
..addAll(evenements);
emit(currentState.copyWith(
evenements: allEvenements,
hasReachedMax: evenements.length < event.size,
currentPage: event.page,
));
}
} catch (e) {
final currentEvenements = state is EvenementLoaded
? (state as EvenementLoaded).evenements
: null;
emit(EvenementError(
message: e.toString(),
evenements: currentEvenements,
));
}
}
/// Charge tous les événements
Future<void> _onLoadEvenements(
LoadEvenements event,
Emitter<EvenementState> emit,
) async {
try {
if (event.refresh || state is EvenementInitial) {
emit(const EvenementLoading());
} else if (state is EvenementLoaded) {
emit(EvenementLoadingMore((state as EvenementLoaded).evenements));
}
final evenements = await _repository.getEvenements(
page: event.page,
size: event.size,
sortField: event.sortField,
sortDirection: event.sortDirection,
);
if (event.refresh || event.page == 0) {
emit(EvenementLoaded(
evenements: evenements,
hasReachedMax: evenements.length < event.size,
currentPage: event.page,
));
} else {
final currentState = state as EvenementLoaded;
final allEvenements = List<EvenementModel>.from(currentState.evenements)
..addAll(evenements);
emit(currentState.copyWith(
evenements: allEvenements,
hasReachedMax: evenements.length < event.size,
currentPage: event.page,
));
}
} catch (e) {
final currentEvenements = state is EvenementLoaded
? (state as EvenementLoaded).evenements
: null;
emit(EvenementError(
message: e.toString(),
evenements: currentEvenements,
));
}
}
/// Charge un événement par ID
Future<void> _onLoadEvenementById(
LoadEvenementById event,
Emitter<EvenementState> emit,
) async {
try {
emit(const EvenementLoading());
final evenement = await _repository.getEvenementById(event.id);
emit(EvenementDetailLoaded(evenement));
} catch (e) {
emit(EvenementError(message: e.toString()));
}
}
/// Recherche d'événements
Future<void> _onSearchEvenements(
SearchEvenements event,
Emitter<EvenementState> emit,
) async {
try {
if (event.refresh || event.page == 0) {
emit(const EvenementLoading());
} else if (state is EvenementLoaded) {
emit(EvenementLoadingMore((state as EvenementLoaded).evenements));
}
final evenements = await _repository.rechercherEvenements(
event.terme,
page: event.page,
size: event.size,
);
if (evenements.isEmpty && event.page == 0) {
emit(EvenementSearchEmpty(event.terme));
return;
}
if (event.refresh || event.page == 0) {
emit(EvenementLoaded(
evenements: evenements,
hasReachedMax: evenements.length < event.size,
currentPage: event.page,
searchTerm: event.terme,
));
} else {
final currentState = state as EvenementLoaded;
final allEvenements = List<EvenementModel>.from(currentState.evenements)
..addAll(evenements);
emit(currentState.copyWith(
evenements: allEvenements,
hasReachedMax: evenements.length < event.size,
currentPage: event.page,
searchTerm: event.terme,
));
}
} catch (e) {
final currentEvenements = state is EvenementLoaded
? (state as EvenementLoaded).evenements
: null;
emit(EvenementError(
message: e.toString(),
evenements: currentEvenements,
));
}
}
/// Filtre par type d'événement
Future<void> _onFilterEvenementsByType(
FilterEvenementsByType event,
Emitter<EvenementState> emit,
) async {
try {
if (event.refresh || event.page == 0) {
emit(const EvenementLoading());
} else if (state is EvenementLoaded) {
emit(EvenementLoadingMore((state as EvenementLoaded).evenements));
}
final evenements = await _repository.getEvenementsByType(
event.type,
page: event.page,
size: event.size,
);
if (evenements.isEmpty && event.page == 0) {
emit(const EvenementEmpty(message: 'Aucun événement de ce type trouvé'));
return;
}
if (event.refresh || event.page == 0) {
emit(EvenementLoaded(
evenements: evenements,
hasReachedMax: evenements.length < event.size,
currentPage: event.page,
filterType: event.type,
));
} else {
final currentState = state as EvenementLoaded;
final allEvenements = List<EvenementModel>.from(currentState.evenements)
..addAll(evenements);
emit(currentState.copyWith(
evenements: allEvenements,
hasReachedMax: evenements.length < event.size,
currentPage: event.page,
filterType: event.type,
));
}
} catch (e) {
final currentEvenements = state is EvenementLoaded
? (state as EvenementLoaded).evenements
: null;
emit(EvenementError(
message: e.toString(),
evenements: currentEvenements,
));
}
}
/// Crée un nouvel événement
Future<void> _onCreateEvenement(
CreateEvenement event,
Emitter<EvenementState> emit,
) async {
try {
emit(const EvenementLoading());
final evenement = await _repository.createEvenement(event.evenement);
emit(EvenementOperationSuccess(
message: 'Événement créé avec succès',
evenement: evenement,
));
} catch (e) {
emit(EvenementError(message: e.toString()));
}
}
/// Met à jour un événement
Future<void> _onUpdateEvenement(
UpdateEvenement event,
Emitter<EvenementState> emit,
) async {
try {
emit(const EvenementLoading());
final evenement = await _repository.updateEvenement(event.id, event.evenement);
emit(EvenementOperationSuccess(
message: 'Événement mis à jour avec succès',
evenement: evenement,
));
} catch (e) {
emit(EvenementError(message: e.toString()));
}
}
/// Supprime un événement
Future<void> _onDeleteEvenement(
DeleteEvenement event,
Emitter<EvenementState> emit,
) async {
try {
emit(const EvenementLoading());
await _repository.deleteEvenement(event.id);
emit(const EvenementOperationSuccess(
message: 'Événement supprimé avec succès',
));
} catch (e) {
emit(EvenementError(message: e.toString()));
}
}
/// Change le statut d'un événement
Future<void> _onChangeStatutEvenement(
ChangeStatutEvenement event,
Emitter<EvenementState> emit,
) async {
try {
emit(const EvenementLoading());
final evenement = await _repository.changerStatutEvenement(
event.id,
event.nouveauStatut,
);
emit(EvenementOperationSuccess(
message: 'Statut de l\'événement modifié avec succès',
evenement: evenement,
));
} catch (e) {
emit(EvenementError(message: e.toString()));
}
}
/// Charge les statistiques
Future<void> _onLoadStatistiquesEvenements(
LoadStatistiquesEvenements event,
Emitter<EvenementState> emit,
) async {
try {
emit(const EvenementLoading());
final statistiques = await _repository.getStatistiquesEvenements();
emit(EvenementStatistiquesLoaded(statistiques));
} catch (e) {
emit(EvenementError(message: e.toString()));
}
}
/// Réinitialise l'état
void _onResetEvenementState(
ResetEvenementState event,
Emitter<EvenementState> emit,
) {
emit(const EvenementInitial());
}
}