feat(auth): gestion reAuthRequired + suppression flux changement mot de passe manuel

- AuthStatusResult: nouveau champ reAuthRequired (ancien compte nécessitant UPDATE_PASSWORD)
- AuthBloc._onLoginRequested: si reAuthRequired → logout silencieux + re-déclenchement AppAuth
  automatique (Keycloak affiche l'écran de changement de mot de passe dans Chrome Custom Tab)
- AuthBloc._onStatusChecked: si reAuthRequired → logout + AuthUnauthenticated (redirection login)
- Remplacement du flux premierLoginComplet (boolean) par enum côté backend
- Suppression de AuthPasswordChangeRequired, AuthPasswordChanging, change_password_page.dart
This commit is contained in:
dahoud
2026-04-05 11:13:36 +00:00
parent 5383df6dcb
commit 65b5c79c43
3 changed files with 96 additions and 35 deletions

View File

@@ -19,11 +19,7 @@ abstract class AuthEvent extends Equatable {
}
class AuthLoginRequested extends AuthEvent {
final String email;
final String password;
const AuthLoginRequested(this.email, this.password);
@override
List<Object?> get props => [email, password];
const AuthLoginRequested();
}
class AuthLogoutRequested extends AuthEvent { const AuthLogoutRequested(); }
@@ -80,13 +76,15 @@ class AuthPendingOnboarding extends AuthState {
final String onboardingState; // NO_SUBSCRIPTION | AWAITING_PAYMENT | PAYMENT_INITIATED | AWAITING_VALIDATION
final String? souscriptionId;
final String? organisationId;
final String? typeOrganisation;
const AuthPendingOnboarding({
required this.onboardingState,
this.souscriptionId,
this.organisationId,
this.typeOrganisation,
});
@override
List<Object?> get props => [onboardingState, souscriptionId, organisationId];
List<Object?> get props => [onboardingState, souscriptionId, organisationId, typeOrganisation];
}
// === BLOC ===
@@ -104,10 +102,30 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
Future<void> _onLoginRequested(AuthLoginRequested event, Emitter<AuthState> emit) async {
emit(AuthLoading());
try {
final rawUser = await _authService.login(event.email, event.password);
var rawUser = await _authService.loginWithAppAuth();
if (rawUser != null) {
// Vérification du statut du compte UnionFlow (indépendant de Keycloak)
final status = await _authService.getAuthStatus(AppConfig.apiBaseUrl);
var status = await _authService.getAuthStatus(AppConfig.apiBaseUrl);
// Ancien compte détecté : UPDATE_PASSWORD vient d'être assigné dans Keycloak.
// Déclencher une nouvelle authentification AppAuth pour afficher l'écran de changement.
if (status != null && status.reAuthRequired) {
AppLogger.info('AuthBloc: réauthentification requise (ancien compte), re-déclenchement AppAuth');
await _authService.logout();
final reAuthUser = await _authService.loginWithAppAuth();
if (reAuthUser == null) {
emit(const AuthError('Connexion annulée.'));
return;
}
rawUser = reAuthUser;
status = await _authService.getAuthStatus(AppConfig.apiBaseUrl);
// Garde-fou : si toujours reAuthRequired après la seconde tentative → erreur de config
if (status != null && status.reAuthRequired) {
AppLogger.error('AuthBloc: reAuthRequired persistant après réauthentification');
emit(const AuthError('Erreur de configuration du compte. Contactez votre administrateur.'));
return;
}
}
if (status != null && status.isPendingOnboarding) {
// OrgAdmin en attente → rediriger vers l'onboarding (sans déconnecter)
@@ -120,6 +138,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
onboardingState: status.onboardingState,
souscriptionId: status.souscriptionId,
organisationId: orgId,
typeOrganisation: status.typeOrganisation,
));
return;
}
@@ -133,7 +152,17 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
return;
}
final user = await _enrichUserWithOrgContext(rawUser);
// Si premier login venant d'être complété, rafraîchir le token pour obtenir
// les rôles MEMBRE + MEMBRE_ACTIF assignés par le backend lors de l'activation.
User user;
if (status != null && status.premierLoginComplet) {
await _authService.refreshToken();
final refreshedRawUser = await _authService.getCurrentUser();
user = await _enrichUserWithOrgContext(refreshedRawUser ?? rawUser);
} else {
user = await _enrichUserWithOrgContext(rawUser);
}
final permissions = await PermissionEngine.getEffectivePermissions(user);
final token = await _authService.getValidToken();
await DashboardCacheManager.invalidateForRole(user.primaryRole);
@@ -175,6 +204,15 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
// Vérification du statut du compte (au redémarrage de l'app)
final status = await _authService.getAuthStatus(AppConfig.apiBaseUrl);
// Ancien compte sans UPDATE_PASSWORD : effacer la session locale et renvoyer vers le login.
// L'utilisateur sera invité à se reconnecter — Keycloak affichera l'écran de changement de mot de passe.
if (status != null && status.reAuthRequired) {
AppLogger.info('AuthBloc: réauthentification requise au démarrage (ancien compte)');
await _authService.logout();
emit(AuthUnauthenticated());
return;
}
if (status != null && status.isPendingOnboarding) {
final user = await _enrichUserWithOrgContext(rawUser);
final orgId = status.organisationId ??
@@ -185,6 +223,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
onboardingState: status.onboardingState,
souscriptionId: status.souscriptionId,
organisationId: orgId,
typeOrganisation: status.typeOrganisation ?? 'ASSOCIATION',
));
return;
}
@@ -198,7 +237,17 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
return;
}
final user = await _enrichUserWithOrgContext(rawUser);
// Si premier login venant d'être complété, rafraîchir le token pour obtenir
// les rôles MEMBRE + MEMBRE_ACTIF assignés par le backend lors de l'activation.
User user;
if (status != null && status.premierLoginComplet) {
await _authService.refreshToken();
final refreshedRawUser = await _authService.getCurrentUser();
user = await _enrichUserWithOrgContext(refreshedRawUser ?? rawUser);
} else {
user = await _enrichUserWithOrgContext(rawUser);
}
final permissions = await PermissionEngine.getEffectivePermissions(user);
final token = await _authService.getValidToken();
emit(AuthAuthenticated(