Refactoring - Version stable
This commit is contained in:
@@ -176,10 +176,16 @@ public class MembreResource {
|
|||||||
// Conversion DTO vers entité
|
// Conversion DTO vers entité
|
||||||
Membre membre = membreService.convertFromCreateRequest(membreDTO);
|
Membre membre = membreService.convertFromCreateRequest(membreDTO);
|
||||||
|
|
||||||
// Création du membre — statut EN_ATTENTE_VALIDATION, Keycloak provisionné à
|
// Création du membre — statut EN_ATTENTE_VALIDATION
|
||||||
// l'approbation
|
|
||||||
Membre nouveauMembre = membreService.creerMembre(membre);
|
Membre nouveauMembre = membreService.creerMembre(membre);
|
||||||
|
|
||||||
|
// Provisionner le compte Keycloak (non bloquant — l'admin peut activer manuellement)
|
||||||
|
try {
|
||||||
|
keycloakSyncService.provisionKeycloakUser(nouveauMembre.getId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.warnf("Provisionnement Keycloak échoué pour %s (non bloquant): %s", nouveauMembre.getEmail(), e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
// Validation périmètre ADMIN_ORGANISATION - lier le membre à l'organisation
|
// Validation périmètre ADMIN_ORGANISATION - lier le membre à l'organisation
|
||||||
java.util.Set<String> roles = securityIdentity.getRoles();
|
java.util.Set<String> roles = securityIdentity.getRoles();
|
||||||
boolean onlyOrgAdmin = roles != null && roles.contains("ADMIN_ORGANISATION")
|
boolean onlyOrgAdmin = roles != null && roles.contains("ADMIN_ORGANISATION")
|
||||||
@@ -668,6 +674,27 @@ public class MembreResource {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("/{id}/activer")
|
||||||
|
@RolesAllowed({"ADMIN", "SUPER_ADMIN"})
|
||||||
|
@Operation(summary = "Activer un membre", description = "Passe le membre en statut ACTIF et lui assigne le rôle MEMBRE_ACTIF dans Keycloak.")
|
||||||
|
@APIResponse(responseCode = "200", description = "Membre activé avec succès")
|
||||||
|
@APIResponse(responseCode = "404", description = "Membre non trouvé")
|
||||||
|
public Response activerMembre(@Parameter(description = "UUID du membre") @PathParam("id") UUID id) {
|
||||||
|
LOG.infof("Activation du membre ID: %s", id);
|
||||||
|
|
||||||
|
Membre membreActive = membreService.activerMembre(id);
|
||||||
|
|
||||||
|
// Assigner le rôle MEMBRE_ACTIF dans Keycloak (non bloquant)
|
||||||
|
try {
|
||||||
|
keycloakSyncService.activerMembreDansKeycloak(id);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.warnf("Activation Keycloak échouée pour %s (non bloquant): %s", membreActive.getEmail(), e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.ok(membreService.convertToResponse(membreActive)).build();
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/export/count")
|
@Path("/export/count")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
|||||||
@@ -140,6 +140,62 @@ public class MembreKeycloakSyncService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active un membre dans Keycloak en lui assignant le rôle MEMBRE_ACTIF.
|
||||||
|
* Appelé après MembreService.activerMembre() lors de la validation admin.
|
||||||
|
*
|
||||||
|
* <p>Si le membre n'a pas encore de compte Keycloak, le provisionne d'abord
|
||||||
|
* puis assigne le rôle MEMBRE_ACTIF.
|
||||||
|
*
|
||||||
|
* @param membreId UUID du membre à activer dans Keycloak
|
||||||
|
* @throws NotFoundException si le membre n'existe pas en base
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void activerMembreDansKeycloak(java.util.UUID membreId) {
|
||||||
|
LOGGER.info("Activation Keycloak (rôle MEMBRE_ACTIF) pour Membre ID: " + membreId);
|
||||||
|
|
||||||
|
Membre membre = membreRepository.findByIdOptional(membreId)
|
||||||
|
.orElseThrow(() -> new NotFoundException("Membre non trouvé avec l'ID: " + membreId));
|
||||||
|
|
||||||
|
// Provisionner le compte Keycloak s'il n'existe pas encore
|
||||||
|
if (membre.getKeycloakId() == null) {
|
||||||
|
LOGGER.info("Compte Keycloak absent — provisionnement automatique pour " + membre.getNomComplet());
|
||||||
|
provisionKeycloakUser(membreId);
|
||||||
|
// Recharger après persist dans provisionKeycloakUser
|
||||||
|
membre = membreRepository.findByIdOptional(membreId)
|
||||||
|
.orElseThrow(() -> new NotFoundException("Membre non trouvé après provisionnement: " + membreId));
|
||||||
|
}
|
||||||
|
|
||||||
|
String keycloakUserId = membre.getKeycloakId().toString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Récupérer l'utilisateur Keycloak
|
||||||
|
UserDTO user = userServiceClient.getUserById(keycloakUserId, DEFAULT_REALM);
|
||||||
|
|
||||||
|
// S'assurer que le compte est activé
|
||||||
|
if (Boolean.FALSE.equals(user.getEnabled())) {
|
||||||
|
user.setEnabled(true);
|
||||||
|
userServiceClient.updateUser(keycloakUserId, user, DEFAULT_REALM);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter le rôle MEMBRE_ACTIF s'il n'est pas déjà présent
|
||||||
|
List<String> roles = user.getRealmRoles() != null
|
||||||
|
? new java.util.ArrayList<>(user.getRealmRoles())
|
||||||
|
: new java.util.ArrayList<>();
|
||||||
|
if (!roles.contains("MEMBRE_ACTIF")) {
|
||||||
|
roles.add("MEMBRE_ACTIF");
|
||||||
|
user.setRealmRoles(roles);
|
||||||
|
userServiceClient.updateUser(keycloakUserId, user, DEFAULT_REALM);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info("✅ Rôle MEMBRE_ACTIF assigné dans Keycloak pour " + membre.getNomComplet());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.severe("❌ Erreur lors de l'activation Keycloak pour " + membre.getNomComplet() + ": " + e.getMessage());
|
||||||
|
throw new RuntimeException("Impossible d'activer le compte Keycloak: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronise les données du Membre vers le User Keycloak.
|
* Synchronise les données du Membre vers le User Keycloak.
|
||||||
* Si le membre n'a pas de compte Keycloak, le provisionne automatiquement.
|
* Si le membre n'a pas de compte Keycloak, le provisionne automatiquement.
|
||||||
|
|||||||
@@ -84,18 +84,40 @@ public class MembreService {
|
|||||||
throw new IllegalArgumentException("Un membre avec ce numéro existe déjà");
|
throw new IllegalArgumentException("Un membre avec ce numéro existe déjà");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forcer le statut d'attente — le compte est activé uniquement après validation
|
// Statut initial : en attente de validation admin
|
||||||
// admin
|
// L'activation (ACTIF + Keycloak MEMBRE_ACTIF) se fait via PUT /api/membres/{id}/activer
|
||||||
// Forcer l'activation pour les tests E2E (normalement géré par validation
|
membre.setStatutCompte("EN_ATTENTE_VALIDATION");
|
||||||
// admin)
|
membre.setActif(false);
|
||||||
membre.setStatutCompte("ACTIF");
|
|
||||||
membre.setActif(true);
|
|
||||||
|
|
||||||
membreRepository.persist(membre);
|
membreRepository.persist(membre);
|
||||||
LOG.infof("Membre créé en attente de validation: %s (ID: %s)", membre.getNomComplet(), membre.getId());
|
LOG.infof("Membre créé en attente de validation: %s (ID: %s)", membre.getNomComplet(), membre.getId());
|
||||||
return membre;
|
return membre;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active un membre : passe son statut à ACTIF et son flag actif à true.
|
||||||
|
* Doit être suivi d'un appel à MembreKeycloakSyncService.activerMembreDansKeycloak()
|
||||||
|
* pour que le rôle MEMBRE_ACTIF soit assigné dans Keycloak.
|
||||||
|
*
|
||||||
|
* @param membreId UUID du membre à activer
|
||||||
|
* @return Le membre mis à jour
|
||||||
|
* @throws jakarta.ws.rs.NotFoundException si le membre est introuvable
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Membre activerMembre(UUID membreId) {
|
||||||
|
LOG.infof("Activation du membre ID: %s", membreId);
|
||||||
|
|
||||||
|
Membre membre = membreRepository.findByIdOptional(membreId)
|
||||||
|
.orElseThrow(() -> new jakarta.ws.rs.NotFoundException("Membre non trouvé avec l'ID: " + membreId));
|
||||||
|
|
||||||
|
membre.setStatutCompte("ACTIF");
|
||||||
|
membre.setActif(true);
|
||||||
|
membreRepository.persist(membre);
|
||||||
|
|
||||||
|
LOG.infof("Membre activé avec succès: %s (ID: %s)", membre.getNomComplet(), membreId);
|
||||||
|
return membre;
|
||||||
|
}
|
||||||
|
|
||||||
/** Met à jour un membre existant */
|
/** Met à jour un membre existant */
|
||||||
@Transactional
|
@Transactional
|
||||||
public Membre mettreAJourMembre(UUID id, Membre membreModifie) {
|
public Membre mettreAJourMembre(UUID id, Membre membreModifie) {
|
||||||
|
|||||||
@@ -108,8 +108,8 @@ class MembreServiceTest {
|
|||||||
class CreerMembreTests {
|
class CreerMembreTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Happy path: numéro généré automatiquement, statut ACTIF")
|
@DisplayName("Happy path: numéro généré automatiquement, statut EN_ATTENTE_VALIDATION")
|
||||||
void creerMembre_generatesNumeroAndSetsActif() {
|
void creerMembre_generatesNumeroAndSetsEnAttenteValidation() {
|
||||||
Membre membre = new Membre();
|
Membre membre = new Membre();
|
||||||
membre.setEmail("test@unionflow.dev");
|
membre.setEmail("test@unionflow.dev");
|
||||||
membre.setNom("Doe");
|
membre.setNom("Doe");
|
||||||
@@ -123,8 +123,8 @@ class MembreServiceTest {
|
|||||||
Membre created = membreService.creerMembre(membre);
|
Membre created = membreService.creerMembre(membre);
|
||||||
|
|
||||||
assertThat(created.getNumeroMembre()).startsWith("UF");
|
assertThat(created.getNumeroMembre()).startsWith("UF");
|
||||||
assertThat(created.getStatutCompte()).isEqualTo("ACTIF");
|
assertThat(created.getStatutCompte()).isEqualTo("EN_ATTENTE_VALIDATION");
|
||||||
assertThat(created.getActif()).isTrue();
|
assertThat(created.getActif()).isFalse();
|
||||||
verify(membreRepository).persist(membre);
|
verify(membreRepository).persist(membre);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,6 +205,47 @@ class MembreServiceTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// activerMembre
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("activerMembre")
|
||||||
|
class ActiverMembreTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Happy path: membre EN_ATTENTE_VALIDATION → ACTIF + actif=true")
|
||||||
|
void activerMembre_pendingMember_setsActif() {
|
||||||
|
UUID id = UUID.randomUUID();
|
||||||
|
Membre membre = membreFixture("pending@unionflow.dev");
|
||||||
|
membre.setId(id);
|
||||||
|
membre.setStatutCompte("EN_ATTENTE_VALIDATION");
|
||||||
|
membre.setActif(false);
|
||||||
|
|
||||||
|
when(membreRepository.findByIdOptional(id)).thenReturn(Optional.of(membre));
|
||||||
|
doNothing().when(membreRepository).persist(any(Membre.class));
|
||||||
|
|
||||||
|
Membre activated = membreService.activerMembre(id);
|
||||||
|
|
||||||
|
assertThat(activated.getStatutCompte()).isEqualTo("ACTIF");
|
||||||
|
assertThat(activated.getActif()).isTrue();
|
||||||
|
verify(membreRepository).persist(membre);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Membre introuvable: lève NotFoundException")
|
||||||
|
void activerMembre_notFound_throws() {
|
||||||
|
UUID id = UUID.randomUUID();
|
||||||
|
when(membreRepository.findByIdOptional(id)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> membreService.activerMembre(id))
|
||||||
|
.isInstanceOf(jakarta.ws.rs.NotFoundException.class)
|
||||||
|
.hasMessageContaining("non trouvé");
|
||||||
|
|
||||||
|
verify(membreRepository, never()).persist(any(Membre.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// mettreAJourMembre
|
// mettreAJourMembre
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user