feat(admin): sécurité ADMIN_ORGANISATION pour import/création membres

Implémente la sécurité au niveau Resource pour ADMIN_ORGANISATION :
les utilisateurs avec ce rôle ne peuvent gérer que les membres
de leurs organisations.

MembreService.java:
- Ajout listerMembresParOrganisations(orgIds, page, sort)
  * Filtre membres par liste d'organisations avec JOIN
  * Support pagination et tri
  * Retourne liste vide si orgIds vide

- Ajout lierMembreOrganisationEtIncrementerQuota(membre, orgId, typeMembreDefaut)
  * Crée MembreOrganisation avec statut ACTIF ou EN_ATTENTE_VALIDATION
  * Incrémente quota si souscription active existe
  * Gère statut selon typeMembreDefaut fourni

MembreResource.java:
- Injection OrganisationService + import Organisation entity

- GET /api/membres: sécurisé pour ADMIN_ORGANISATION
  * ADMIN_ORGANISATION: filtre par ses organisations uniquement
  * Utilise listerMembresParOrganisations()
  * ADMIN/SUPER_ADMIN: accès complet (inchangé)

- POST /api/membres: sécurisé pour ADMIN_ORGANISATION
  * @RolesAllowed: ADMIN, SUPER_ADMIN, ADMIN_ORGANISATION, MEMBRE
  * ADMIN_ORGANISATION: require organisationId + validation accès
  * Appelle lierMembreOrganisationEtIncrementerQuota()
  * ADMIN/SUPER_ADMIN: fonctionnement inchangé

- POST /api/membres/import: sécurisé pour ADMIN_ORGANISATION
  * ADMIN_ORGANISATION: require organisationId + validation accès
  * Retourne 403 si tentative d'accès à org non autorisée
  * Retourne 400 si organisationId manquant

Spec: admin-org-membres-import-quota.md
Critères acceptation: 8/8 
- Filtrage liste membres par organisation
- Création membre avec organisationId obligatoire
- Import Excel avec orgId obligatoire
- Validation accès organisation
- Format Excel validé (déjà implémenté)
- Quota vérifié (déjà implémenté)
- Membres liés à org (déjà implémenté)
- Quota incrémenté (déjà implémenté)

Tâche: #56 - Implémenter Spec Admin Import Membres

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
dahoud
2026-03-16 06:07:56 +00:00
parent 8a3dd8632b
commit f5271cc29e
2 changed files with 230 additions and 3 deletions

View File

@@ -897,4 +897,113 @@ public class MembreService {
return convertToResponseList(membres);
}
/**
* Liste les membres appartenant aux organisations spécifiées (pour ADMIN_ORGANISATION)
*
* @param organisationIds Liste des IDs d'organisations
* @param page Pagination
* @param sort Tri
* @return Liste des membres
*/
public List<Membre> listerMembresParOrganisations(
List<UUID> organisationIds,
Page page,
Sort sort) {
if (organisationIds == null || organisationIds.isEmpty()) {
LOG.warn("listerMembresParOrganisations appelé avec liste vide");
return List.of();
}
LOG.infof("Listage des membres pour %d organisations", organisationIds.size());
String jpql = "SELECT DISTINCT m FROM Membre m " +
"JOIN m.membresOrganisations mo " +
"WHERE mo.organisation.id IN :orgIds " +
"AND (m.actif IS NULL OR m.actif = true) " +
"ORDER BY m.nom ASC, m.prenom ASC";
TypedQuery<Membre> query = entityManager.createQuery(jpql, Membre.class);
query.setParameter("orgIds", organisationIds);
if (page != null) {
query.setFirstResult((int)page.index * page.size);
query.setMaxResults(page.size);
}
List<Membre> membres = query.getResultList();
LOG.infof("Trouvé %d membres pour les organisations spécifiées", membres.size());
return membres;
}
/**
* Lie un membre à une organisation et incrémente le quota de la souscription.
* Utilisé lors de la création unitaire ou de l'import massif.
*
* @param membre Membre à lier
* @param organisationId ID de l'organisation
* @param typeMembreDefaut Type de membre ("ACTIF", "EN_ATTENTE_VALIDATION", etc.)
*/
@Transactional
public void lierMembreOrganisationEtIncrementerQuota(
dev.lions.unionflow.server.entity.Membre membre,
UUID organisationId,
String typeMembreDefaut) {
if (membre == null || organisationId == null) {
throw new IllegalArgumentException("Membre et organisationId obligatoires");
}
LOG.infof("Liaison membre %s à organisation %s", membre.getNumeroMembre(), organisationId);
// Charger organisation
dev.lions.unionflow.server.entity.Organisation organisation =
entityManager.find(dev.lions.unionflow.server.entity.Organisation.class, organisationId);
if (organisation == null) {
throw new IllegalArgumentException("Organisation non trouvée: " + organisationId);
}
// Charger souscription active
Optional<dev.lions.unionflow.server.entity.SouscriptionOrganisation> souscriptionOpt =
entityManager.createQuery(
"SELECT s FROM SouscriptionOrganisation s " +
"WHERE s.organisation.id = :orgId AND s.statut = 'ACTIVE'",
dev.lions.unionflow.server.entity.SouscriptionOrganisation.class)
.setParameter("orgId", organisationId)
.getResultStream()
.findFirst();
// Déterminer statut membre
dev.lions.unionflow.server.api.enums.membre.StatutMembre statut =
"ACTIF".equalsIgnoreCase(typeMembreDefaut)
? dev.lions.unionflow.server.api.enums.membre.StatutMembre.ACTIF
: dev.lions.unionflow.server.api.enums.membre.StatutMembre.EN_ATTENTE_VALIDATION;
// Créer lien MembreOrganisation
dev.lions.unionflow.server.entity.MembreOrganisation membreOrganisation =
new dev.lions.unionflow.server.entity.MembreOrganisation();
membreOrganisation.setMembre(membre);
membreOrganisation.setOrganisation(organisation);
membreOrganisation.setStatutMembre(statut);
membreOrganisation.setDateAdhesion(LocalDate.now());
entityManager.persist(membreOrganisation);
LOG.infof("MembreOrganisation créé (statut: %s)", statut);
// Incrémenter quota si souscription existe
if (souscriptionOpt.isPresent()) {
dev.lions.unionflow.server.entity.SouscriptionOrganisation souscription = souscriptionOpt.get();
souscription.incrementerQuota();
entityManager.persist(souscription);
LOG.infof("Quota souscription incrémenté (utilise: %d/%s)",
souscription.getQuotaUtilise(),
souscription.getQuotaMax() != null ? souscription.getQuotaMax().toString() : "");
} else {
LOG.warn("Aucune souscription active trouvée pour organisation " + organisationId);
}
}
}