fix(mobile): URL changement mdp corrigée + v3.0 — multi-org, AppAuth, sécurité prod
Auth: - profile_repository.dart: /api/auth/change-password → /api/membres/auth/change-password Multi-org (Phase 3): - OrgSelectorPage, OrgSwitcherBloc, OrgSwitcherEntry - org_context_service.dart: headers X-Active-Organisation-Id + X-Active-Role Navigation: - MorePage: navigation conditionnelle par typeOrganisation - Suppression adaptive_navigation (remplacé par main_navigation_layout) Auth AppAuth: - keycloak_webview_auth_service: fixes AppAuth Android - AuthBloc: gestion REAUTH_REQUIS + premierLoginComplet Onboarding: - Nouveaux états: payment_method_page, onboarding_shared_widgets - SouscriptionStatusModel mis à jour StatutValidationSouscription Android: - build.gradle: ProGuard/R8, network_security_config - Gradle wrapper mis à jour
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
/// BLoC pour la gestion des membres
|
||||
library membres_bloc;
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
@@ -16,6 +17,8 @@ import '../domain/usecases/delete_member.dart' as uc;
|
||||
import '../domain/usecases/search_members.dart';
|
||||
import '../domain/usecases/get_member_stats.dart';
|
||||
import '../domain/repositories/membre_repository.dart';
|
||||
import '../../../core/websocket/websocket_service.dart';
|
||||
import '../../../core/utils/logger.dart';
|
||||
|
||||
/// BLoC pour la gestion des membres (Clean Architecture)
|
||||
@injectable
|
||||
@@ -27,7 +30,10 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
|
||||
final uc.DeleteMember _deleteMember;
|
||||
final SearchMembers _searchMembers;
|
||||
final GetMemberStats _getMemberStats;
|
||||
final IMembreRepository _repository; // Pour méthodes non-couvertes par use cases
|
||||
final IMembreRepository _repository;
|
||||
final WebSocketService _webSocketService;
|
||||
|
||||
StreamSubscription<WebSocketEvent>? _webSocketSubscription;
|
||||
|
||||
MembresBloc(
|
||||
this._getMembers,
|
||||
@@ -38,6 +44,7 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
|
||||
this._searchMembers,
|
||||
this._getMemberStats,
|
||||
this._repository,
|
||||
this._webSocketService,
|
||||
) : super(const MembresInitial()) {
|
||||
on<LoadMembres>(_onLoadMembres);
|
||||
on<LoadMembreById>(_onLoadMembreById);
|
||||
@@ -50,6 +57,44 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
|
||||
on<LoadActiveMembres>(_onLoadActiveMembres);
|
||||
on<LoadBureauMembres>(_onLoadBureauMembres);
|
||||
on<LoadMembresStats>(_onLoadMembresStats);
|
||||
on<ResetMotDePasse>(_onResetMotDePasse);
|
||||
on<AffecterOrganisation>(_onAffecterOrganisation);
|
||||
on<InviterMembre>(_onInviterMembre);
|
||||
on<ActiverAdhesion>(_onActiverAdhesion);
|
||||
on<SuspendrAdhesion>(_onSuspendrAdhesion);
|
||||
on<RadierAdhesion>(_onRadierAdhesion);
|
||||
|
||||
_initWebSocketListener();
|
||||
}
|
||||
|
||||
void _initWebSocketListener() {
|
||||
_webSocketSubscription = _webSocketService.eventStream.listen(
|
||||
(event) {
|
||||
try {
|
||||
if (event is MemberEvent) {
|
||||
AppLogger.info('MembresBloc: MemberEvent reçu (${event.eventType}), refresh liste');
|
||||
final currentState = state;
|
||||
if (currentState is MembresLoaded && !isClosed) {
|
||||
add(LoadMembres(
|
||||
refresh: true,
|
||||
organisationId: currentState.organisationId,
|
||||
page: currentState.currentPage,
|
||||
size: currentState.pageSize,
|
||||
));
|
||||
}
|
||||
}
|
||||
} catch (e, s) {
|
||||
AppLogger.error('MembresBloc: erreur lors du traitement WebSocket event', error: e);
|
||||
}
|
||||
},
|
||||
onError: (error) => AppLogger.error('MembresBloc: WebSocket stream error', error: error),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_webSocketSubscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
/// Charge la liste des membres
|
||||
@@ -68,11 +113,14 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
|
||||
|
||||
final MembreSearchResult result;
|
||||
if (event.organisationId != null) {
|
||||
// OrgAdmin : scope la requête à son organisation via la recherche avancée
|
||||
// OrgAdmin : scope la requête à son organisation via la recherche avancée.
|
||||
// includeInactifs=true pour récupérer aussi les membres "En attente"
|
||||
// (actif=false) — le filtrage par statut est géré côté UI.
|
||||
result = await _searchMembers(
|
||||
criteria: MembreSearchCriteria(
|
||||
organisationIds: [event.organisationId!],
|
||||
query: event.recherche?.isNotEmpty == true ? event.recherche : null,
|
||||
includeInactifs: true,
|
||||
),
|
||||
page: event.page,
|
||||
size: event.size,
|
||||
@@ -298,6 +346,60 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Réinitialise le mot de passe d'un membre
|
||||
Future<void> _onResetMotDePasse(
|
||||
ResetMotDePasse event,
|
||||
Emitter<MembresState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(const MembresLoading());
|
||||
|
||||
final membre = await _repository.resetMotDePasse(event.id);
|
||||
|
||||
emit(MotDePasseReinitialise(membre));
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresNetworkError(
|
||||
message: _getNetworkErrorMessage(e),
|
||||
code: e.response?.statusCode.toString(),
|
||||
error: e,
|
||||
));
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresError(
|
||||
message: 'Erreur lors de la réinitialisation du mot de passe. Veuillez réessayer.',
|
||||
error: e,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Affecte un membre à une organisation (superadmin)
|
||||
Future<void> _onAffecterOrganisation(
|
||||
AffecterOrganisation event,
|
||||
Emitter<MembresState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(const MembresLoading());
|
||||
|
||||
final membre = await _repository.affecterOrganisation(event.membreId, event.organisationId);
|
||||
|
||||
emit(MembreAffecte(membre));
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresNetworkError(
|
||||
message: _getNetworkErrorMessage(e),
|
||||
code: e.response?.statusCode.toString(),
|
||||
error: e,
|
||||
));
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresError(
|
||||
message: 'Erreur lors de l\'affectation à l\'organisation. Veuillez réessayer.',
|
||||
error: e,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Recherche avancée de membres
|
||||
Future<void> _onSearchMembres(
|
||||
SearchMembres event,
|
||||
@@ -479,5 +581,116 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
|
||||
return 'Erreur réseau inattendue.';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Handlers cycle de vie des adhésions ──────────────────────────────────
|
||||
|
||||
Future<void> _onInviterMembre(
|
||||
InviterMembre event,
|
||||
Emitter<MembresState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(const MembresLoading());
|
||||
final result = await _repository.inviterMembre(
|
||||
event.membreId,
|
||||
event.organisationId,
|
||||
roleOrg: event.roleOrg,
|
||||
);
|
||||
emit(MembreInvite(
|
||||
membreId: event.membreId,
|
||||
organisationId: event.organisationId,
|
||||
nouveauStatut: result['statut']?.toString() ?? 'INVITE',
|
||||
));
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresNetworkError(
|
||||
message: _getNetworkErrorMessage(e),
|
||||
code: e.response?.statusCode.toString(),
|
||||
error: e,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(MembresError(message: 'Erreur lors de l\'invitation du membre.', error: e));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onActiverAdhesion(
|
||||
ActiverAdhesion event,
|
||||
Emitter<MembresState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(const MembresLoading());
|
||||
final result = await _repository.activerAdhesion(
|
||||
event.membreId,
|
||||
event.organisationId,
|
||||
motif: event.motif,
|
||||
);
|
||||
emit(AdhesionActivee(
|
||||
membreId: event.membreId,
|
||||
nouveauStatut: result['statut']?.toString() ?? 'ACTIF',
|
||||
));
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresNetworkError(
|
||||
message: _getNetworkErrorMessage(e),
|
||||
code: e.response?.statusCode.toString(),
|
||||
error: e,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(MembresError(message: 'Erreur lors de l\'activation de l\'adhésion.', error: e));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSuspendrAdhesion(
|
||||
SuspendrAdhesion event,
|
||||
Emitter<MembresState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(const MembresLoading());
|
||||
final result = await _repository.suspendrAdhesion(
|
||||
event.membreId,
|
||||
event.organisationId,
|
||||
motif: event.motif,
|
||||
);
|
||||
emit(AdhesionSuspendue(
|
||||
membreId: event.membreId,
|
||||
nouveauStatut: result['statut']?.toString() ?? 'SUSPENDU',
|
||||
));
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresNetworkError(
|
||||
message: _getNetworkErrorMessage(e),
|
||||
code: e.response?.statusCode.toString(),
|
||||
error: e,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(MembresError(message: 'Erreur lors de la suspension de l\'adhésion.', error: e));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onRadierAdhesion(
|
||||
RadierAdhesion event,
|
||||
Emitter<MembresState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(const MembresLoading());
|
||||
final result = await _repository.radierAdhesion(
|
||||
event.membreId,
|
||||
event.organisationId,
|
||||
motif: event.motif,
|
||||
);
|
||||
emit(MembreRadie(
|
||||
membreId: event.membreId,
|
||||
nouveauStatut: result['statut']?.toString() ?? 'RADIE',
|
||||
));
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) return;
|
||||
emit(MembresNetworkError(
|
||||
message: _getNetworkErrorMessage(e),
|
||||
code: e.response?.statusCode.toString(),
|
||||
error: e,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(MembresError(message: 'Erreur lors de la radiation du membre.', error: e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -143,3 +143,90 @@ class LoadMembresStats extends MembresEvent {
|
||||
const LoadMembresStats();
|
||||
}
|
||||
|
||||
/// Événement pour réinitialiser le mot de passe d'un membre existant
|
||||
class ResetMotDePasse extends MembresEvent {
|
||||
final String id;
|
||||
|
||||
const ResetMotDePasse(this.id);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id];
|
||||
}
|
||||
|
||||
/// Événement pour affecter un membre à une organisation (superadmin)
|
||||
class AffecterOrganisation extends MembresEvent {
|
||||
final String membreId;
|
||||
final String organisationId;
|
||||
|
||||
const AffecterOrganisation(this.membreId, this.organisationId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membreId, organisationId];
|
||||
}
|
||||
|
||||
// ── Cycle de vie des adhésions ────────────────────────────────────────────
|
||||
|
||||
/// Inviter un membre dans une organisation (admin)
|
||||
class InviterMembre extends MembresEvent {
|
||||
final String membreId;
|
||||
final String organisationId;
|
||||
final String? roleOrg;
|
||||
|
||||
const InviterMembre({
|
||||
required this.membreId,
|
||||
required this.organisationId,
|
||||
this.roleOrg,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membreId, organisationId, roleOrg];
|
||||
}
|
||||
|
||||
/// Activer l'adhésion d'un membre dans l'organisation courante
|
||||
class ActiverAdhesion extends MembresEvent {
|
||||
final String membreId;
|
||||
final String organisationId;
|
||||
final String? motif;
|
||||
|
||||
const ActiverAdhesion({
|
||||
required this.membreId,
|
||||
required this.organisationId,
|
||||
this.motif,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membreId, organisationId, motif];
|
||||
}
|
||||
|
||||
/// Suspendre l'adhésion d'un membre
|
||||
class SuspendrAdhesion extends MembresEvent {
|
||||
final String membreId;
|
||||
final String organisationId;
|
||||
final String? motif;
|
||||
|
||||
const SuspendrAdhesion({
|
||||
required this.membreId,
|
||||
required this.organisationId,
|
||||
this.motif,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membreId, organisationId, motif];
|
||||
}
|
||||
|
||||
/// Radier un membre d'une organisation
|
||||
class RadierAdhesion extends MembresEvent {
|
||||
final String membreId;
|
||||
final String organisationId;
|
||||
final String? motif;
|
||||
|
||||
const RadierAdhesion({
|
||||
required this.membreId,
|
||||
required this.organisationId,
|
||||
this.motif,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membreId, organisationId, motif];
|
||||
}
|
||||
|
||||
|
||||
@@ -141,6 +141,78 @@ class MembreDeactivated extends MembresState {
|
||||
List<Object?> get props => [membre];
|
||||
}
|
||||
|
||||
/// État de succès après affectation à une organisation
|
||||
class MembreAffecte extends MembresState {
|
||||
final MembreCompletModel membre;
|
||||
|
||||
const MembreAffecte(this.membre);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membre];
|
||||
}
|
||||
|
||||
/// État de succès après réinitialisation du mot de passe
|
||||
/// [membre] contient motDePasseTemporaire renseigné (retourné une seule fois)
|
||||
class MotDePasseReinitialise extends MembresState {
|
||||
final MembreCompletModel membre;
|
||||
|
||||
const MotDePasseReinitialise(this.membre);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membre];
|
||||
}
|
||||
|
||||
// ── États cycle de vie adhésion ──────────────────────────────────────────
|
||||
|
||||
/// Invitation envoyée avec succès
|
||||
class MembreInvite extends MembresState {
|
||||
final String membreId;
|
||||
final String organisationId;
|
||||
final String nouveauStatut;
|
||||
|
||||
const MembreInvite({
|
||||
required this.membreId,
|
||||
required this.organisationId,
|
||||
required this.nouveauStatut,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membreId, organisationId, nouveauStatut];
|
||||
}
|
||||
|
||||
/// Adhésion activée
|
||||
class AdhesionActivee extends MembresState {
|
||||
final String membreId;
|
||||
final String nouveauStatut;
|
||||
|
||||
const AdhesionActivee({required this.membreId, required this.nouveauStatut});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membreId, nouveauStatut];
|
||||
}
|
||||
|
||||
/// Adhésion suspendue
|
||||
class AdhesionSuspendue extends MembresState {
|
||||
final String membreId;
|
||||
final String nouveauStatut;
|
||||
|
||||
const AdhesionSuspendue({required this.membreId, required this.nouveauStatut});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membreId, nouveauStatut];
|
||||
}
|
||||
|
||||
/// Membre radié
|
||||
class MembreRadie extends MembresState {
|
||||
final String membreId;
|
||||
final String nouveauStatut;
|
||||
|
||||
const MembreRadie({required this.membreId, required this.nouveauStatut});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membreId, nouveauStatut];
|
||||
}
|
||||
|
||||
/// État avec statistiques
|
||||
class MembresStatsLoaded extends MembresState {
|
||||
final Map<String, dynamic> stats;
|
||||
|
||||
Reference in New Issue
Block a user