feat(sync): MembreRoleSyncService + count admins dynamique
- 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).
This commit is contained in:
@@ -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<MembreRole> {
|
||||
|
||||
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<MembreRole> {
|
||||
return find("membreOrganisation.membre.id = ?1 AND role.id = ?2", membreId, roleId)
|
||||
.firstResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte les administrateurs actifs d'une organisation.
|
||||
*
|
||||
* <p>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).
|
||||
*
|
||||
* <p>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<MembreRole> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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 :
|
||||
* <ol>
|
||||
* <li>Comptes créés directement dans Keycloak sans passer par
|
||||
* {@code MembreService#promouvoirAdminOrganisation}.</li>
|
||||
* <li>Bases de données en dev qui ont démarré avant la migration V30.</li>
|
||||
* </ol>
|
||||
*/
|
||||
@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é")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user