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,391 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
part 'evenement_model.g.dart';
/// Modèle de données pour un événement UnionFlow
/// Aligné avec l'entité Evenement du serveur API
@JsonSerializable()
class EvenementModel extends Equatable {
/// ID unique de l'événement
final String? id;
/// Titre de l'événement
final String titre;
/// Description détaillée
final String? description;
/// Date et heure de début
@JsonKey(name: 'dateDebut')
final DateTime dateDebut;
/// Date et heure de fin
@JsonKey(name: 'dateFin')
final DateTime? dateFin;
/// Lieu de l'événement
final String? lieu;
/// Adresse complète
final String? adresse;
/// Type d'événement
@JsonKey(name: 'typeEvenement')
final TypeEvenement typeEvenement;
/// Statut de l'événement
final StatutEvenement statut;
/// Capacité maximale
@JsonKey(name: 'capaciteMax')
final int? capaciteMax;
/// Prix de participation
final double? prix;
/// Inscription requise
@JsonKey(name: 'inscriptionRequise')
final bool inscriptionRequise;
/// Date limite d'inscription
@JsonKey(name: 'dateLimiteInscription')
final DateTime? dateLimiteInscription;
/// Instructions particulières
@JsonKey(name: 'instructionsParticulieres')
final String? instructionsParticulieres;
/// Contact organisateur
@JsonKey(name: 'contactOrganisateur')
final String? contactOrganisateur;
/// Matériel requis
@JsonKey(name: 'materielRequis')
final String? materielRequis;
/// Visible au public
@JsonKey(name: 'visiblePublic')
final bool visiblePublic;
/// Événement actif
final bool actif;
/// Créé par
@JsonKey(name: 'creePar')
final String? creePar;
/// Date de création
@JsonKey(name: 'dateCreation')
final DateTime? dateCreation;
/// Modifié par
@JsonKey(name: 'modifiePar')
final String? modifiePar;
/// Date de modification
@JsonKey(name: 'dateModification')
final DateTime? dateModification;
/// Organisation associée (ID)
@JsonKey(name: 'organisationId')
final String? organisationId;
/// Organisateur (ID)
@JsonKey(name: 'organisateurId')
final String? organisateurId;
const EvenementModel({
this.id,
required this.titre,
this.description,
required this.dateDebut,
this.dateFin,
this.lieu,
this.adresse,
required this.typeEvenement,
required this.statut,
this.capaciteMax,
this.prix,
required this.inscriptionRequise,
this.dateLimiteInscription,
this.instructionsParticulieres,
this.contactOrganisateur,
this.materielRequis,
required this.visiblePublic,
required this.actif,
this.creePar,
this.dateCreation,
this.modifiePar,
this.dateModification,
this.organisationId,
this.organisateurId,
});
/// Factory pour créer depuis JSON
factory EvenementModel.fromJson(Map<String, dynamic> json) =>
_$EvenementModelFromJson(json);
/// Convertir vers JSON
Map<String, dynamic> toJson() => _$EvenementModelToJson(this);
/// Copie avec modifications
EvenementModel copyWith({
String? id,
String? titre,
String? description,
DateTime? dateDebut,
DateTime? dateFin,
String? lieu,
String? adresse,
TypeEvenement? typeEvenement,
StatutEvenement? statut,
int? capaciteMax,
double? prix,
bool? inscriptionRequise,
DateTime? dateLimiteInscription,
String? instructionsParticulieres,
String? contactOrganisateur,
String? materielRequis,
bool? visiblePublic,
bool? actif,
String? creePar,
DateTime? dateCreation,
String? modifiePar,
DateTime? dateModification,
String? organisationId,
String? organisateurId,
}) {
return EvenementModel(
id: id ?? this.id,
titre: titre ?? this.titre,
description: description ?? this.description,
dateDebut: dateDebut ?? this.dateDebut,
dateFin: dateFin ?? this.dateFin,
lieu: lieu ?? this.lieu,
adresse: adresse ?? this.adresse,
typeEvenement: typeEvenement ?? this.typeEvenement,
statut: statut ?? this.statut,
capaciteMax: capaciteMax ?? this.capaciteMax,
prix: prix ?? this.prix,
inscriptionRequise: inscriptionRequise ?? this.inscriptionRequise,
dateLimiteInscription: dateLimiteInscription ?? this.dateLimiteInscription,
instructionsParticulieres: instructionsParticulieres ?? this.instructionsParticulieres,
contactOrganisateur: contactOrganisateur ?? this.contactOrganisateur,
materielRequis: materielRequis ?? this.materielRequis,
visiblePublic: visiblePublic ?? this.visiblePublic,
actif: actif ?? this.actif,
creePar: creePar ?? this.creePar,
dateCreation: dateCreation ?? this.dateCreation,
modifiePar: modifiePar ?? this.modifiePar,
dateModification: dateModification ?? this.dateModification,
organisationId: organisationId ?? this.organisationId,
organisateurId: organisateurId ?? this.organisateurId,
);
}
/// Méthodes utilitaires
/// Vérifie si l'événement est à venir
bool get estAVenir => dateDebut.isAfter(DateTime.now());
/// Vérifie si l'événement est en cours
bool get estEnCours {
final maintenant = DateTime.now();
return dateDebut.isBefore(maintenant) &&
(dateFin?.isAfter(maintenant) ?? false);
}
/// Vérifie si l'événement est terminé
bool get estTermine {
final maintenant = DateTime.now();
return dateFin?.isBefore(maintenant) ?? dateDebut.isBefore(maintenant);
}
/// Vérifie si les inscriptions sont ouvertes
bool get inscriptionsOuvertes {
if (!inscriptionRequise) return false;
if (dateLimiteInscription == null) return estAVenir;
return dateLimiteInscription!.isAfter(DateTime.now()) && estAVenir;
}
/// Durée de l'événement
Duration? get duree {
if (dateFin == null) return null;
return dateFin!.difference(dateDebut);
}
/// Formatage de la durée
String get dureeFormatee {
final d = duree;
if (d == null) return 'Non spécifiée';
if (d.inDays > 0) {
return '${d.inDays} jour${d.inDays > 1 ? 's' : ''}';
} else if (d.inHours > 0) {
return '${d.inHours}h${d.inMinutes.remainder(60) > 0 ? '${d.inMinutes.remainder(60)}' : ''}';
} else {
return '${d.inMinutes} min';
}
}
@override
List<Object?> get props => [
id,
titre,
description,
dateDebut,
dateFin,
lieu,
adresse,
typeEvenement,
statut,
capaciteMax,
prix,
inscriptionRequise,
dateLimiteInscription,
instructionsParticulieres,
contactOrganisateur,
materielRequis,
visiblePublic,
actif,
creePar,
dateCreation,
modifiePar,
dateModification,
organisationId,
organisateurId,
];
}
/// Types d'événements disponibles
@JsonEnum()
enum TypeEvenement {
@JsonValue('ASSEMBLEE_GENERALE')
assembleeGenerale,
@JsonValue('REUNION')
reunion,
@JsonValue('FORMATION')
formation,
@JsonValue('CONFERENCE')
conference,
@JsonValue('ATELIER')
atelier,
@JsonValue('SEMINAIRE')
seminaire,
@JsonValue('EVENEMENT_SOCIAL')
evenementSocial,
@JsonValue('MANIFESTATION')
manifestation,
@JsonValue('CELEBRATION')
celebration,
@JsonValue('AUTRE')
autre,
}
/// Extension pour les libellés des types
extension TypeEvenementExtension on TypeEvenement {
String get libelle {
switch (this) {
case TypeEvenement.assembleeGenerale:
return 'Assemblée Générale';
case TypeEvenement.reunion:
return 'Réunion';
case TypeEvenement.formation:
return 'Formation';
case TypeEvenement.conference:
return 'Conférence';
case TypeEvenement.atelier:
return 'Atelier';
case TypeEvenement.seminaire:
return 'Séminaire';
case TypeEvenement.evenementSocial:
return 'Événement Social';
case TypeEvenement.manifestation:
return 'Manifestation';
case TypeEvenement.celebration:
return 'Célébration';
case TypeEvenement.autre:
return 'Autre';
}
}
String get icone {
switch (this) {
case TypeEvenement.assembleeGenerale:
return '🏛️';
case TypeEvenement.reunion:
return '👥';
case TypeEvenement.formation:
return '📚';
case TypeEvenement.conference:
return '🎤';
case TypeEvenement.atelier:
return '🔧';
case TypeEvenement.seminaire:
return '🎓';
case TypeEvenement.evenementSocial:
return '🎉';
case TypeEvenement.manifestation:
return '📢';
case TypeEvenement.celebration:
return '🎊';
case TypeEvenement.autre:
return '📅';
}
}
}
/// Statuts d'événements disponibles
@JsonEnum()
enum StatutEvenement {
@JsonValue('PLANIFIE')
planifie,
@JsonValue('CONFIRME')
confirme,
@JsonValue('EN_COURS')
enCours,
@JsonValue('TERMINE')
termine,
@JsonValue('ANNULE')
annule,
@JsonValue('REPORTE')
reporte,
}
/// Extension pour les libellés des statuts
extension StatutEvenementExtension on StatutEvenement {
String get libelle {
switch (this) {
case StatutEvenement.planifie:
return 'Planifié';
case StatutEvenement.confirme:
return 'Confirmé';
case StatutEvenement.enCours:
return 'En cours';
case StatutEvenement.termine:
return 'Terminé';
case StatutEvenement.annule:
return 'Annulé';
case StatutEvenement.reporte:
return 'Reporté';
}
}
String get couleur {
switch (this) {
case StatutEvenement.planifie:
return '#FFA500'; // Orange
case StatutEvenement.confirme:
return '#4CAF50'; // Vert
case StatutEvenement.enCours:
return '#2196F3'; // Bleu
case StatutEvenement.termine:
return '#9E9E9E'; // Gris
case StatutEvenement.annule:
return '#F44336'; // Rouge
case StatutEvenement.reporte:
return '#FF9800'; // Orange foncé
}
}
}