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:
391
unionflow-mobile-apps/lib/core/models/evenement_model.dart
Normal file
391
unionflow-mobile-apps/lib/core/models/evenement_model.dart
Normal 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é
|
||||
}
|
||||
}
|
||||
}
|
||||
94
unionflow-mobile-apps/lib/core/models/evenement_model.g.dart
Normal file
94
unionflow-mobile-apps/lib/core/models/evenement_model.g.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'evenement_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
EvenementModel _$EvenementModelFromJson(Map<String, dynamic> json) =>
|
||||
EvenementModel(
|
||||
id: json['id'] as String?,
|
||||
titre: json['titre'] as String,
|
||||
description: json['description'] as String?,
|
||||
dateDebut: DateTime.parse(json['dateDebut'] as String),
|
||||
dateFin: json['dateFin'] == null
|
||||
? null
|
||||
: DateTime.parse(json['dateFin'] as String),
|
||||
lieu: json['lieu'] as String?,
|
||||
adresse: json['adresse'] as String?,
|
||||
typeEvenement: $enumDecode(_$TypeEvenementEnumMap, json['typeEvenement']),
|
||||
statut: $enumDecode(_$StatutEvenementEnumMap, json['statut']),
|
||||
capaciteMax: (json['capaciteMax'] as num?)?.toInt(),
|
||||
prix: (json['prix'] as num?)?.toDouble(),
|
||||
inscriptionRequise: json['inscriptionRequise'] as bool,
|
||||
dateLimiteInscription: json['dateLimiteInscription'] == null
|
||||
? null
|
||||
: DateTime.parse(json['dateLimiteInscription'] as String),
|
||||
instructionsParticulieres: json['instructionsParticulieres'] as String?,
|
||||
contactOrganisateur: json['contactOrganisateur'] as String?,
|
||||
materielRequis: json['materielRequis'] as String?,
|
||||
visiblePublic: json['visiblePublic'] as bool,
|
||||
actif: json['actif'] as bool,
|
||||
creePar: json['creePar'] as String?,
|
||||
dateCreation: json['dateCreation'] == null
|
||||
? null
|
||||
: DateTime.parse(json['dateCreation'] as String),
|
||||
modifiePar: json['modifiePar'] as String?,
|
||||
dateModification: json['dateModification'] == null
|
||||
? null
|
||||
: DateTime.parse(json['dateModification'] as String),
|
||||
organisationId: json['organisationId'] as String?,
|
||||
organisateurId: json['organisateurId'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$EvenementModelToJson(EvenementModel instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'titre': instance.titre,
|
||||
'description': instance.description,
|
||||
'dateDebut': instance.dateDebut.toIso8601String(),
|
||||
'dateFin': instance.dateFin?.toIso8601String(),
|
||||
'lieu': instance.lieu,
|
||||
'adresse': instance.adresse,
|
||||
'typeEvenement': _$TypeEvenementEnumMap[instance.typeEvenement]!,
|
||||
'statut': _$StatutEvenementEnumMap[instance.statut]!,
|
||||
'capaciteMax': instance.capaciteMax,
|
||||
'prix': instance.prix,
|
||||
'inscriptionRequise': instance.inscriptionRequise,
|
||||
'dateLimiteInscription':
|
||||
instance.dateLimiteInscription?.toIso8601String(),
|
||||
'instructionsParticulieres': instance.instructionsParticulieres,
|
||||
'contactOrganisateur': instance.contactOrganisateur,
|
||||
'materielRequis': instance.materielRequis,
|
||||
'visiblePublic': instance.visiblePublic,
|
||||
'actif': instance.actif,
|
||||
'creePar': instance.creePar,
|
||||
'dateCreation': instance.dateCreation?.toIso8601String(),
|
||||
'modifiePar': instance.modifiePar,
|
||||
'dateModification': instance.dateModification?.toIso8601String(),
|
||||
'organisationId': instance.organisationId,
|
||||
'organisateurId': instance.organisateurId,
|
||||
};
|
||||
|
||||
const _$TypeEvenementEnumMap = {
|
||||
TypeEvenement.assembleeGenerale: 'ASSEMBLEE_GENERALE',
|
||||
TypeEvenement.reunion: 'REUNION',
|
||||
TypeEvenement.formation: 'FORMATION',
|
||||
TypeEvenement.conference: 'CONFERENCE',
|
||||
TypeEvenement.atelier: 'ATELIER',
|
||||
TypeEvenement.seminaire: 'SEMINAIRE',
|
||||
TypeEvenement.evenementSocial: 'EVENEMENT_SOCIAL',
|
||||
TypeEvenement.manifestation: 'MANIFESTATION',
|
||||
TypeEvenement.celebration: 'CELEBRATION',
|
||||
TypeEvenement.autre: 'AUTRE',
|
||||
};
|
||||
|
||||
const _$StatutEvenementEnumMap = {
|
||||
StatutEvenement.planifie: 'PLANIFIE',
|
||||
StatutEvenement.confirme: 'CONFIRME',
|
||||
StatutEvenement.enCours: 'EN_COURS',
|
||||
StatutEvenement.termine: 'TERMINE',
|
||||
StatutEvenement.annule: 'ANNULE',
|
||||
StatutEvenement.reporte: 'REPORTE',
|
||||
};
|
||||
@@ -115,6 +115,32 @@ class MembreModel extends Equatable {
|
||||
return parts.join(', ');
|
||||
}
|
||||
|
||||
/// Libellé du statut formaté
|
||||
String get statutLibelle {
|
||||
switch (statut.toUpperCase()) {
|
||||
case 'ACTIF':
|
||||
return 'Actif';
|
||||
case 'INACTIF':
|
||||
return 'Inactif';
|
||||
case 'SUSPENDU':
|
||||
return 'Suspendu';
|
||||
default:
|
||||
return statut;
|
||||
}
|
||||
}
|
||||
|
||||
/// Âge calculé à partir de la date de naissance
|
||||
int get age {
|
||||
if (dateNaissance == null) return 0;
|
||||
final now = DateTime.now();
|
||||
int age = now.year - dateNaissance!.year;
|
||||
if (now.month < dateNaissance!.month ||
|
||||
(now.month == dateNaissance!.month && now.day < dateNaissance!.day)) {
|
||||
age--;
|
||||
}
|
||||
return age;
|
||||
}
|
||||
|
||||
/// Copie avec modifications
|
||||
MembreModel copyWith({
|
||||
String? id,
|
||||
|
||||
Reference in New Issue
Block a user