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());
}
}

View File

@@ -0,0 +1,160 @@
import 'package:equatable/equatable.dart';
import '../../../../core/models/evenement_model.dart';
/// Événements du BLoC Evenement
abstract class EvenementEvent extends Equatable {
const EvenementEvent();
@override
List<Object?> get props => [];
}
/// Charge les événements à venir
class LoadEvenementsAVenir extends EvenementEvent {
final int page;
final int size;
final bool refresh;
const LoadEvenementsAVenir({
this.page = 0,
this.size = 10,
this.refresh = false,
});
@override
List<Object?> get props => [page, size, refresh];
}
/// Charge les événements publics
class LoadEvenementsPublics extends EvenementEvent {
final int page;
final int size;
final bool refresh;
const LoadEvenementsPublics({
this.page = 0,
this.size = 20,
this.refresh = false,
});
@override
List<Object?> get props => [page, size, refresh];
}
/// Charge tous les événements
class LoadEvenements extends EvenementEvent {
final int page;
final int size;
final String sortField;
final String sortDirection;
final bool refresh;
const LoadEvenements({
this.page = 0,
this.size = 20,
this.sortField = 'dateDebut',
this.sortDirection = 'asc',
this.refresh = false,
});
@override
List<Object?> get props => [page, size, sortField, sortDirection, refresh];
}
/// Charge un événement par ID
class LoadEvenementById extends EvenementEvent {
final String id;
const LoadEvenementById(this.id);
@override
List<Object?> get props => [id];
}
/// Recherche d'événements
class SearchEvenements extends EvenementEvent {
final String terme;
final int page;
final int size;
final bool refresh;
const SearchEvenements({
required this.terme,
this.page = 0,
this.size = 20,
this.refresh = false,
});
@override
List<Object?> get props => [terme, page, size, refresh];
}
/// Filtre par type d'événement
class FilterEvenementsByType extends EvenementEvent {
final TypeEvenement type;
final int page;
final int size;
final bool refresh;
const FilterEvenementsByType({
required this.type,
this.page = 0,
this.size = 20,
this.refresh = false,
});
@override
List<Object?> get props => [type, page, size, refresh];
}
/// Crée un nouvel événement
class CreateEvenement extends EvenementEvent {
final EvenementModel evenement;
const CreateEvenement(this.evenement);
@override
List<Object?> get props => [evenement];
}
/// Met à jour un événement
class UpdateEvenement extends EvenementEvent {
final String id;
final EvenementModel evenement;
const UpdateEvenement(this.id, this.evenement);
@override
List<Object?> get props => [id, evenement];
}
/// Supprime un événement
class DeleteEvenement extends EvenementEvent {
final String id;
const DeleteEvenement(this.id);
@override
List<Object?> get props => [id];
}
/// Change le statut d'un événement
class ChangeStatutEvenement extends EvenementEvent {
final String id;
final StatutEvenement nouveauStatut;
const ChangeStatutEvenement(this.id, this.nouveauStatut);
@override
List<Object?> get props => [id, nouveauStatut];
}
/// Charge les statistiques
class LoadStatistiquesEvenements extends EvenementEvent {
const LoadStatistiquesEvenements();
}
/// Réinitialise l'état
class ResetEvenementState extends EvenementEvent {
const ResetEvenementState();
}

View File

@@ -0,0 +1,142 @@
import 'package:equatable/equatable.dart';
import '../../../../core/models/evenement_model.dart';
/// États du BLoC Evenement
abstract class EvenementState extends Equatable {
const EvenementState();
@override
List<Object?> get props => [];
}
/// État initial
class EvenementInitial extends EvenementState {
const EvenementInitial();
}
/// État de chargement
class EvenementLoading extends EvenementState {
const EvenementLoading();
}
/// État de chargement avec données existantes (pour pagination)
class EvenementLoadingMore extends EvenementState {
final List<EvenementModel> evenements;
const EvenementLoadingMore(this.evenements);
@override
List<Object?> get props => [evenements];
}
/// État de succès avec liste d'événements
class EvenementLoaded extends EvenementState {
final List<EvenementModel> evenements;
final bool hasReachedMax;
final int currentPage;
final String? searchTerm;
final TypeEvenement? filterType;
const EvenementLoaded({
required this.evenements,
this.hasReachedMax = false,
this.currentPage = 0,
this.searchTerm,
this.filterType,
});
EvenementLoaded copyWith({
List<EvenementModel>? evenements,
bool? hasReachedMax,
int? currentPage,
String? searchTerm,
TypeEvenement? filterType,
}) {
return EvenementLoaded(
evenements: evenements ?? this.evenements,
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
currentPage: currentPage ?? this.currentPage,
searchTerm: searchTerm ?? this.searchTerm,
filterType: filterType ?? this.filterType,
);
}
@override
List<Object?> get props => [
evenements,
hasReachedMax,
currentPage,
searchTerm,
filterType,
];
}
/// État de succès avec un événement spécifique
class EvenementDetailLoaded extends EvenementState {
final EvenementModel evenement;
const EvenementDetailLoaded(this.evenement);
@override
List<Object?> get props => [evenement];
}
/// État de succès avec statistiques
class EvenementStatistiquesLoaded extends EvenementState {
final Map<String, dynamic> statistiques;
const EvenementStatistiquesLoaded(this.statistiques);
@override
List<Object?> get props => [statistiques];
}
/// État de succès après création/modification
class EvenementOperationSuccess extends EvenementState {
final String message;
final EvenementModel? evenement;
const EvenementOperationSuccess({
required this.message,
this.evenement,
});
@override
List<Object?> get props => [message, evenement];
}
/// État d'erreur
class EvenementError extends EvenementState {
final String message;
final List<EvenementModel>? evenements; // Pour conserver les données en cas d'erreur de pagination
const EvenementError({
required this.message,
this.evenements,
});
@override
List<Object?> get props => [message, evenements];
}
/// État de recherche vide
class EvenementSearchEmpty extends EvenementState {
final String searchTerm;
const EvenementSearchEmpty(this.searchTerm);
@override
List<Object?> get props => [searchTerm];
}
/// État de liste vide
class EvenementEmpty extends EvenementState {
final String message;
const EvenementEmpty({
this.message = 'Aucun événement trouvé',
});
@override
List<Object?> get props => [message];
}