feat(auth): premier login via AppAuth — remédiation automatique des anciens comptes

- MembreKeycloakSyncService: completerPremierLogin retourne un enum PremierLoginResultat
  (COMPLETE / REAUTH_REQUIS / NON_APPLICABLE) au lieu d'un boolean
- Détection automatique des anciens comptes (sans UPDATE_PASSWORD ni marqueur KC):
  assigne UPDATE_PASSWORD + attribut premiere_password_pending dans Keycloak
  et retourne REAUTH_REQUIS pour que Flutter re-déclenche AppAuth
- Détection du mot de passe changé: marqueur présent + UPDATE_PASSWORD absent → COMPLETE
- createUserDTOFromMembre: ajoute l'attribut premiere_password_pending=true à la création
- CompteAdherentResource.getMonStatut: retourne reAuthRequired=true quand REAUTH_REQUIS
This commit is contained in:
dahoud
2026-04-05 11:11:53 +00:00
parent 6bcec363ce
commit 93fc69ec22
2 changed files with 172 additions and 15 deletions

View File

@@ -114,8 +114,32 @@ public class CompteAdherentResource {
.filter(s -> s != null && !s.isBlank())
.orElse("ACTIF");
// Auto-activer si le membre a été créé par un admin dont l'org a une souscription active.
// Couvre les membres créés avant l'auto-activation à la création ET les cas limites futurs.
// Premier login : Keycloak a déjà forcé UPDATE_PASSWORD dans le Chrome Custom Tab.
// Si l'utilisateur possède un token valide (@Authenticated), c'est la preuve que
// le changement de mot de passe est complété — mettre à jour la DB et assigner les rôles.
// Pour les anciens comptes (sans UPDATE_PASSWORD assigné), le service assigne automatiquement
// la required action et retourne REAUTH_REQUIS pour que Flutter déclenche une nouvelle auth.
boolean premierLoginComplet = false;
boolean reAuthRequired = false;
if (membreOpt.isPresent() && Boolean.TRUE.equals(membreOpt.get().getPremiereConnexion())) {
Membre m = membreOpt.get();
MembreKeycloakSyncService.PremierLoginResultat resultat =
membreKeycloakSyncService.completerPremierLogin(m.getId());
if (resultat == MembreKeycloakSyncService.PremierLoginResultat.COMPLETE) {
premierLoginComplet = true;
// Relire le statutCompte après activation éventuelle
statutCompte = membreRepository.findByIdOptional(m.getId())
.map(Membre::getStatutCompte)
.orElse(statutCompte);
LOG.infof("Premier login complété au statut pour %s → %s", m.getEmail(), statutCompte);
} else if (resultat == MembreKeycloakSyncService.PremierLoginResultat.REAUTH_REQUIS) {
reAuthRequired = true;
LOG.infof("Réauthentification requise pour %s (UPDATE_PASSWORD assigné)", m.getEmail());
}
}
// Fallback : auto-activer si EN_ATTENTE_VALIDATION et org a souscription active
// (membres sans premiereConnexion=true ou créés avant cette logique)
if ("EN_ATTENTE_VALIDATION".equals(statutCompte) && membreOpt.isPresent()) {
Membre m = membreOpt.get();
UUID orgId = membreOrganisationRepo.findFirstByMembreId(m.getId())
@@ -136,12 +160,13 @@ public class CompteAdherentResource {
Map<String, Object> response = new HashMap<>();
response.put("statutCompte", statutCompte);
// Signaler si le membre doit changer son mot de passe (premier login)
boolean changerMotDePasseRequis = membreOpt
.map(m -> Boolean.TRUE.equals(m.getPremiereConnexion()))
.orElse(false);
response.put("changerMotDePasseRequis", changerMotDePasseRequis);
// changerMotDePasseRequis = false : Keycloak gère nativement le changement de mot de passe
// via la required action UPDATE_PASSWORD dans le Chrome Custom Tab (AppAuth).
response.put("changerMotDePasseRequis", false);
// Indique à Flutter que le token doit être rafraîchi (nouveaux rôles MEMBRE/MEMBRE_ACTIF)
response.put("premierLoginComplet", premierLoginComplet);
// Indique à Flutter qu'une réauthentification est nécessaire (UPDATE_PASSWORD vient d'être assigné)
response.put("reAuthRequired", reAuthRequired);
// Enrichir avec l'état d'onboarding pour les comptes en attente
if ("EN_ATTENTE_VALIDATION".equals(statutCompte)) {