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

@@ -16,6 +16,7 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
on<LoadMembres>(_onLoadMembres);
on<RefreshMembres>(_onRefreshMembres);
on<SearchMembres>(_onSearchMembres);
on<AdvancedSearchMembres>(_onAdvancedSearchMembres);
on<LoadMembreById>(_onLoadMembreById);
on<CreateMembre>(_onCreateMembre);
on<UpdateMembre>(_onUpdateMembre);
@@ -101,6 +102,83 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
}
}
/// Handler pour recherche avancée des membres avec filtres multiples
Future<void> _onAdvancedSearchMembres(
AdvancedSearchMembres event,
Emitter<MembresState> emit,
) async {
// Si aucun filtre n'est appliqué, recharger tous les membres
if (event.filters.isEmpty || _areFiltersEmpty(event.filters)) {
add(const LoadMembres());
return;
}
emit(const MembresLoading());
try {
final membres = await _membreRepository.advancedSearchMembres(event.filters);
emit(MembresLoaded(
membres: membres,
isSearchResult: true,
searchQuery: _buildSearchQueryFromFilters(event.filters),
));
} catch (e) {
final failure = _mapExceptionToFailure(e);
emit(MembresError(failure: failure));
}
}
/// Vérifie si tous les filtres sont vides
bool _areFiltersEmpty(Map<String, dynamic> filters) {
return filters.values.every((value) {
if (value == null) return true;
if (value is String) return value.trim().isEmpty;
if (value is List) return value.isEmpty;
return false;
});
}
/// Construit une chaîne de recherche à partir des filtres pour l'affichage
String _buildSearchQueryFromFilters(Map<String, dynamic> filters) {
final activeFilters = <String>[];
filters.forEach((key, value) {
if (value != null && value.toString().isNotEmpty) {
switch (key) {
case 'nom':
activeFilters.add('Nom: $value');
break;
case 'prenom':
activeFilters.add('Prénom: $value');
break;
case 'email':
activeFilters.add('Email: $value');
break;
case 'telephone':
activeFilters.add('Téléphone: $value');
break;
case 'actif':
activeFilters.add('Statut: ${value == true ? "Actif" : "Inactif"}');
break;
case 'profession':
activeFilters.add('Profession: $value');
break;
case 'ville':
activeFilters.add('Ville: $value');
break;
case 'ageMin':
activeFilters.add('Âge min: $value');
break;
case 'ageMax':
activeFilters.add('Âge max: $value');
break;
}
}
});
return activeFilters.join(', ');
}
/// Handler pour charger un membre par ID
Future<void> _onLoadMembreById(
LoadMembreById event,