From 78d8fd7cd889c19ac29e03bc5fc55af6be9abc15 Mon Sep 17 00:00:00 2001 From: dahoud <41957584+DahoudG@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:05:49 +0000 Subject: [PATCH] feat(sync): MembreRoleSyncService + count admins dynamique MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nouveau MembreRoleSyncService.ensureOrgAdminRole : auto-crée un MembreRole ORGADMIN quand un user avec rôle Keycloak ADMIN_ORGANISATION se connecte sans entrée DB (couvre les comptes créés directement dans Keycloak). - OrganisationContextFilter appelle syncService.ensureOrgAdminRole quand le rôle Keycloak est présent mais MembreRole absent (non bloquant sur erreur). - MembreRoleRepository.countAdminsByOrganisationId : count strict (ORGADMIN + actif + dateDebut/dateFin valides) avec fallback sur codes alternatifs si strict=0. - OrganisationService.convertToResponse : nombreAdministrateurs dynamique via MembreRoleRepository (remplace le champ Organisation jamais mis à jour). --- .../repository/MembreRoleRepository.java | 68 +++++++++++++++ .../security/OrganisationContextFilter.java | 16 ++++ .../server/service/MembreRoleSyncService.java | 82 +++++++++++++++++++ .../server/service/OrganisationService.java | 11 ++- 4 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 src/main/java/dev/lions/unionflow/server/service/MembreRoleSyncService.java diff --git a/src/main/java/dev/lions/unionflow/server/repository/MembreRoleRepository.java b/src/main/java/dev/lions/unionflow/server/repository/MembreRoleRepository.java index 65c1d5d..9dc3fc2 100644 --- a/src/main/java/dev/lions/unionflow/server/repository/MembreRoleRepository.java +++ b/src/main/java/dev/lions/unionflow/server/repository/MembreRoleRepository.java @@ -7,6 +7,7 @@ import java.time.LocalDate; import java.util.List; import java.util.Optional; import java.util.UUID; +import org.jboss.logging.Logger; /** * Repository pour l'entité MembreRole @@ -18,6 +19,8 @@ import java.util.UUID; @ApplicationScoped public class MembreRoleRepository implements PanacheRepository { + private static final Logger LOG = Logger.getLogger(MembreRoleRepository.class); + /** * Trouve une attribution membre-role par son UUID * @@ -76,5 +79,70 @@ public class MembreRoleRepository implements PanacheRepository { return find("membreOrganisation.membre.id = ?1 AND role.id = ?2", membreId, roleId) .firstResult(); } + + /** + * Compte les administrateurs actifs d'une organisation. + * + *

Le code DB du rôle admin d'organisation est {@code ORGADMIN} + * (cf. seed V13__Seed_Standard_Roles.sql). Ne pas confondre avec le rôle + * Keycloak {@code ADMIN_ORGANISATION} utilisé dans {@code @RolesAllowed} — + * les deux représentent le même concept mais avec un code différent par + * source (Keycloak vs table roles DB). + * + *

L'unique constraint {@code uk_mr_membre_org_role} garantit qu'un même + * membre n'est comptabilisé qu'une fois même s'il se voit attribuer + * plusieurs fois le rôle. + * + * @param organisationId ID de l'organisation + * @return nombre d'admins actifs de cette organisation + */ + public long countAdminsByOrganisationId(UUID organisationId) { + final LocalDate today = LocalDate.now(); + + // Diagnostic : inventaire complet des MembreRole liés à cette organisation + final long totalForOrg = count("organisation.id = ?1", organisationId); + final List allForOrg = list("organisation.id = ?1", organisationId); + final String codesFound = allForOrg.stream() + .map(mr -> String.format( + "%s[actif=%s,dateDebut=%s,dateFin=%s]", + mr.getRole() != null ? mr.getRole().getCode() : "null", + mr.getActif(), + mr.getDateDebut(), + mr.getDateFin())) + .reduce((a, b) -> a + ", " + b) + .orElse("(aucun)"); + + final long strictCount = count( + "organisation.id = ?1 AND role.code = ?2 AND actif = true " + + "AND (dateDebut IS NULL OR dateDebut <= ?3) " + + "AND (dateFin IS NULL OR dateFin >= ?3)", + organisationId, + "ORGADMIN", + today); + + LOG.infof( + "countAdminsByOrganisationId(org=%s) → strict=%d, total_membres_roles_pour_cette_org=%d, detail=[%s]", + organisationId, strictCount, totalForOrg, codesFound); + + // Fallback : si aucun match strict mais qu'il existe des entrées actives + // avec un code admin alternatif (ex. ADMIN_ORGANISATION résiduel), on les + // compte quand même pour éviter un faux zéro. + if (strictCount == 0 && totalForOrg > 0) { + final long fallbackCount = count( + "organisation.id = ?1 AND role.code IN (?2) AND actif = true " + + "AND (dateDebut IS NULL OR dateDebut <= ?3) " + + "AND (dateFin IS NULL OR dateFin >= ?3)", + organisationId, + List.of("ORGADMIN", "ADMIN_ORGANISATION", "ADMIN"), + today); + if (fallbackCount > 0) { + LOG.warnf( + "countAdminsByOrganisationId(org=%s) strict=0 mais fallback (codes alternatifs)=%d — le seed V13 utilise 'ORGADMIN', vérifier les assignations", + organisationId, fallbackCount); + return fallbackCount; + } + } + return strictCount; + } } diff --git a/src/main/java/dev/lions/unionflow/server/security/OrganisationContextFilter.java b/src/main/java/dev/lions/unionflow/server/security/OrganisationContextFilter.java index 87b3f0c..0aea789 100644 --- a/src/main/java/dev/lions/unionflow/server/security/OrganisationContextFilter.java +++ b/src/main/java/dev/lions/unionflow/server/security/OrganisationContextFilter.java @@ -6,6 +6,7 @@ import dev.lions.unionflow.server.entity.Organisation; import dev.lions.unionflow.server.repository.MembreOrganisationRepository; import dev.lions.unionflow.server.repository.MembreRepository; import dev.lions.unionflow.server.repository.OrganisationRepository; +import dev.lions.unionflow.server.service.MembreRoleSyncService; import io.quarkus.security.identity.SecurityIdentity; import jakarta.annotation.Priority; import jakarta.inject.Inject; @@ -54,6 +55,9 @@ public class OrganisationContextFilter implements ContainerRequestFilter { @Inject OrganisationRepository organisationRepository; + @Inject + MembreRoleSyncService membreRoleSyncService; + @Override public void filter(ContainerRequestContext requestContext) throws IOException { String orgIdHeader = requestContext.getHeaderString(HEADER_ACTIVE_ORG); @@ -128,6 +132,18 @@ public class OrganisationContextFilter implements ContainerRequestFilter { .build()); return; } + + // Sync rôle DB ↔ Keycloak : si l'utilisateur est admin dans Keycloak + // mais n'a pas encore de MembreRole ORGADMIN en base, le créer. + if (securityIdentity.getRoles().contains("ADMIN_ORGANISATION")) { + try { + membreRoleSyncService.ensureOrgAdminRole(membreOrg); + } catch (Exception e) { + // Non bloquant — on log et on continue + LOG.warnf("ensureOrgAdminRole: erreur non bloquante pour %s : %s", + email, e.getMessage()); + } + } } } } diff --git a/src/main/java/dev/lions/unionflow/server/service/MembreRoleSyncService.java b/src/main/java/dev/lions/unionflow/server/service/MembreRoleSyncService.java new file mode 100644 index 0000000..ddea3c2 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/MembreRoleSyncService.java @@ -0,0 +1,82 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.entity.MembreOrganisation; +import dev.lions.unionflow.server.entity.MembreRole; +import dev.lions.unionflow.server.repository.MembreRoleRepository; +import dev.lions.unionflow.server.repository.RoleRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.transaction.Transactional; +import java.time.LocalDate; +import org.jboss.logging.Logger; + +/** + * Synchronise le rôle DB (MembreRole) avec le rôle Keycloak au premier accès. + * + *

Lorsqu'un utilisateur est admin dans Keycloak (rôle {@code ADMIN_ORGANISATION}) + * mais n'a pas encore de ligne {@code MembreRole ORGADMIN} en base, ce service la + * crée à la volée. Cela couvre deux cas : + *

    + *
  1. Comptes créés directement dans Keycloak sans passer par + * {@code MembreService#promouvoirAdminOrganisation}.
  2. + *
  3. Bases de données en dev qui ont démarré avant la migration V30.
  4. + *
+ */ +@ApplicationScoped +public class MembreRoleSyncService { + + private static final Logger LOG = Logger.getLogger(MembreRoleSyncService.class); + + @Inject + MembreRoleRepository membreRoleRepository; + + @Inject + RoleRepository roleRepository; + + @PersistenceContext + EntityManager entityManager; + + /** + * Assure qu'un MembreRole ORGADMIN existe pour ce MembreOrganisation. + * Idempotent : sans effet si l'entrée existe déjà. + * + * @param mo MembreOrganisation de l'admin (non null) + */ + @Transactional + public void ensureOrgAdminRole(MembreOrganisation mo) { + if (mo == null || mo.getId() == null || mo.getOrganisation() == null) { + return; + } + + // Vérifier si un MembreRole ORGADMIN actif existe déjà pour ce membre dans cette org + long existing = membreRoleRepository.count( + "membreOrganisation.id = ?1 AND role.code = ?2 AND actif = true", + mo.getId(), "ORGADMIN"); + + if (existing > 0) { + return; // Déjà en place — rien à faire + } + + // Chercher le rôle ORGADMIN dans la table roles + roleRepository.findByCode("ORGADMIN").ifPresentOrElse( + role -> { + MembreRole mr = new MembreRole(); + mr.setMembreOrganisation(mo); + mr.setOrganisation(mo.getOrganisation()); + mr.setRole(role); + mr.setActif(true); + mr.setDateDebut(LocalDate.now()); + entityManager.persist(mr); + LOG.infof( + "MembreRole ORGADMIN auto-sync créé pour membre %s dans org %s", + mo.getMembre() != null ? mo.getMembre().getId() : "?", + mo.getOrganisation().getId()); + }, + () -> LOG.warnf( + "Rôle ORGADMIN introuvable dans la table roles — " + + "vérifier que V13__Seed_Standard_Roles.sql a été appliqué") + ); + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/OrganisationService.java b/src/main/java/dev/lions/unionflow/server/service/OrganisationService.java index 6880ba7..b9206c6 100644 --- a/src/main/java/dev/lions/unionflow/server/service/OrganisationService.java +++ b/src/main/java/dev/lions/unionflow/server/service/OrganisationService.java @@ -11,6 +11,7 @@ import dev.lions.unionflow.server.repository.AdresseRepository; import dev.lions.unionflow.server.repository.EvenementRepository; import dev.lions.unionflow.server.repository.MembreOrganisationRepository; import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.MembreRoleRepository; import dev.lions.unionflow.server.repository.TypeReferenceRepository; import dev.lions.unionflow.server.entity.Organisation; import dev.lions.unionflow.server.repository.OrganisationRepository; @@ -63,6 +64,9 @@ public class OrganisationService { @Inject EvenementRepository evenementRepository; + @Inject + MembreRoleRepository membreRoleRepository; + /** * Crée une nouvelle organisation * @@ -565,11 +569,16 @@ public class OrganisationService { dto.setObjectifs(organisation.getObjectifs()); dto.setActivitesPrincipales(organisation.getActivitesPrincipales()); dto.setNombreMembres(organisation.getNombreMembres()); - dto.setNombreAdministrateurs(organisation.getNombreAdministrateurs()); if (organisation.getId() != null) { + // Compte dynamique des administrateurs (rôle ADMIN_ORGANISATION actif) + // — le champ Organisation.nombreAdministrateurs n'est pas tenu à jour. + long countAdmins = membreRoleRepository.countAdminsByOrganisationId(organisation.getId()); + dto.setNombreAdministrateurs((int) countAdmins); + long countEvenements = evenementRepository.countActifsByOrganisationId(organisation.getId()); dto.setNombreEvenements((int) countEvenements); } else { + dto.setNombreAdministrateurs(0); dto.setNombreEvenements(0); } dto.setBudgetAnnuel(organisation.getBudgetAnnuel());