feat(v3.0): implémentation Phases 0-8 — RBAC, lifecycle, multi-org, plans, dashboards

Phase 0 : @RolesAllowed SUPER_ADMIN sur POST/DELETE organisations ; AuthenticationFilter pages super-admin
Phase 2 : OrganisationModuleService, @RequiresModule, ModuleAccessFilter, RoleService, PermissionChecker
Phase 3 : multi-org context switching (OrganisationContextFilter, headers X-Active-Organisation-Id / X-Active-Role)
Phase 4 : feature-gating navigation par typeOrganisation (web MenuBean + mobile MorePage)
Phase 5 : MemberLifecycleService — 8 transitions (activer/suspendre/radier/archiver/inviter/accepter/expirer/rappels)
Phase 6 : FormuleAbonnement Option C (planCommercial, apiAccess, federationAccess, quotas) + SouscriptionOrganisation méthodes quota
Phase 7 : DashboardResource SUPER_ADMIN ajouté ; DashboardBean.checkAccessAndRedirect() ; dashboards distincts par rôle
Phase 8 : MembreResourceLifecycleRbacTest, SouscriptionQuotaOptionCTest, OrganisationContextHolderTest, OrganisationContextFilterMultiOrgTest, MemberLifecycleServiceTest
This commit is contained in:
dahoud
2026-04-06 16:49:47 +00:00
parent 39e98a9cb3
commit aef5548e87
34 changed files with 823 additions and 86 deletions

View File

@@ -620,7 +620,7 @@ public class MembreImportExportService {
}
if (colonnesExport.contains("ORGANISATION") || colonnesExport.isEmpty()) {
row.createCell(colNum++)
.setCellValue(membre.getAssociationNom() != null ? membre.getAssociationNom() : "");
.setCellValue(membre.getOrganisationNom() != null ? membre.getOrganisationNom() : "");
}
}
@@ -701,8 +701,8 @@ public class MembreImportExportService {
// Organisations distinctes
long organisationsDistinctes = membres.stream()
.filter(m -> m.getAssociationNom() != null)
.map(MembreResponse::getAssociationNom)
.filter(m -> m.getOrganisationNom() != null)
.map(MembreResponse::getOrganisationNom)
.distinct()
.count();
@@ -862,7 +862,7 @@ public class MembreImportExportService {
values.add(membre.getStatutCompte() != null ? membre.getStatutCompte() : "");
}
if (colonnesExport.contains("ORGANISATION") || colonnesExport.isEmpty()) {
values.add(Objects.toString(membre.getAssociationNom(), ""));
values.add(Objects.toString(membre.getOrganisationNom(), ""));
}
printer.printRecord(values);
@@ -964,7 +964,7 @@ public class MembreImportExportService {
table.addCell(createDataCell(membre.getStatutCompte() != null ? membre.getStatutCompte() : "", dataFont));
}
if (inclureOrg) {
table.addCell(createDataCell(membre.getAssociationNom() != null ? membre.getAssociationNom() : "", dataFont));
table.addCell(createDataCell(membre.getOrganisationNom() != null ? membre.getOrganisationNom() : "", dataFont));
}
}
@@ -1030,8 +1030,8 @@ public class MembreImportExportService {
.equals(m.getStatutCompte()))
.count();
long organisationsDistinctes = membres.stream()
.filter(m -> m.getAssociationNom() != null)
.map(MembreResponse::getAssociationNom)
.filter(m -> m.getOrganisationNom() != null)
.map(MembreResponse::getOrganisationNom)
.distinct()
.count();

View File

@@ -7,6 +7,7 @@ import dev.lions.unionflow.server.api.dto.membre.response.MembreSummaryResponse;
import dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria;
import dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO;
import dev.lions.unionflow.server.entity.FormuleAbonnement;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.repository.MembreRepository;
import io.quarkus.panache.common.Page;
@@ -200,6 +201,29 @@ public class MembreService {
Membre membre = membreRepository.findByIdOptional(membreId)
.orElseThrow(() -> new jakarta.ws.rs.NotFoundException("Membre non trouvé avec l'ID: " + membreId));
// Vérifier le quota d'administrateurs selon la formule souscrite
membreOrganisationRepository.findFirstByMembreId(membreId).ifPresent(mo -> {
UUID orgId = mo.getOrganisation().getId();
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", orgId)
.getResultStream().findFirst().ifPresent(souscription -> {
FormuleAbonnement formule = souscription.getFormule();
if (formule != null && formule.getMaxAdmins() != null) {
long adminCount = entityManager.createQuery(
"SELECT COUNT(mr) FROM MembreRole mr WHERE mr.organisation.id = :orgId " +
"AND mr.role.code = 'ORGADMIN' AND mr.actif = true", Long.class)
.setParameter("orgId", orgId).getSingleResult();
if (adminCount >= formule.getMaxAdmins()) {
throw new jakarta.ws.rs.ForbiddenException(
"Le quota d'administrateurs de votre plan (" + formule.getMaxAdmins() +
") est atteint. Mettez à niveau votre abonnement pour ajouter plus d'administrateurs.");
}
}
});
});
membre.setStatutCompte("ACTIF");
membre.setActif(true);
membreRepository.persist(membre);
@@ -432,7 +456,7 @@ public class MembreService {
dev.lions.unionflow.server.entity.MembreOrganisation mo = membre.getMembresOrganisations().get(0);
if (mo.getOrganisation() != null) {
dto.setOrganisationId(mo.getOrganisation().getId());
dto.setAssociationNom(mo.getOrganisation().getNom());
dto.setOrganisationNom(mo.getOrganisation().getNom());
}
dto.setDateAdhesion(mo.getDateAdhesion());
} else if (membre.getDateCreation() != null) {
@@ -498,12 +522,12 @@ public class MembreService {
}
UUID organisationId = null;
String associationNom = null;
String organisationNom = null;
if (membre.getMembresOrganisations() != null && !membre.getMembresOrganisations().isEmpty()) {
dev.lions.unionflow.server.entity.MembreOrganisation mo = membre.getMembresOrganisations().get(0);
if (mo.getOrganisation() != null) {
organisationId = mo.getOrganisation().getId();
associationNom = mo.getOrganisation().getNom();
organisationNom = mo.getOrganisation().getNom();
}
}
@@ -521,7 +545,7 @@ public class MembreService {
membre.getActif(),
rolesNames,
organisationId,
associationNom);
organisationNom);
}
/** Convertit un CreateMembreRequest en entité Membre */
@@ -1183,16 +1207,33 @@ public class MembreService {
// Assigner le rôle SIMPLEMEMBER par défaut
assignerRoleDefaut(membreOrganisation, "SIMPLEMEMBER");
// Incrémenter quota si souscription existe
// Vérifier quota et expiration avant d'incrémenter
if (souscriptionOpt.isPresent()) {
dev.lions.unionflow.server.entity.SouscriptionOrganisation souscription = souscriptionOpt.get();
// Vérifier que la souscription n'est pas expirée
if (!souscription.isActive()) {
throw new jakarta.ws.rs.ForbiddenException(
"La souscription de l'organisation est expirée ou inactive. " +
"Veuillez renouveler votre abonnement avant d'ajouter de nouveaux membres.");
}
// Vérifier que le quota n'est pas dépassé
if (souscription.isQuotaDepasse()) {
Integer max = souscription.getQuotaMax();
throw new jakarta.ws.rs.ForbiddenException(
"Le quota de membres de votre plan est atteint (" + max + "/" + max + "). " +
"Veuillez mettre à niveau votre formule d'abonnement.");
}
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);
LOG.warn("Aucune souscription active trouvée pour organisation " + organisationId +
" — ajout du membre sans vérification de quota");
}
}

View File

@@ -719,4 +719,27 @@ public class OrganisationService {
.accepteNouveauxMembres(req.accepteNouveauxMembres() != null ? req.accepteNouveauxMembres() : true)
.build();
}
/**
* Retourne la liste des organisations d'un membre (pour le sélecteur multi-org).
* Inclut les infos nécessaires au sélecteur : id, nom, type, catégorie, modules, rôle du membre.
*/
public java.util.List<java.util.Map<String, Object>> listerOrganisationsParMembre(java.util.UUID membreId) {
java.util.List<MembreOrganisation> liens = membreOrganisationRepository.findOrganisationsActivesParMembre(membreId);
return liens.stream().map(lien -> {
Organisation org = lien.getOrganisation();
java.util.Map<String, Object> entry = new java.util.LinkedHashMap<>();
entry.put("organisationId", org.getId());
entry.put("nom", org.getNom());
entry.put("nomCourt", org.getNomCourt());
entry.put("typeOrganisation", org.getTypeOrganisation());
entry.put("categorieType", org.getCategorieType());
entry.put("modulesActifs", org.getModulesActifs());
entry.put("statut", org.getStatut());
entry.put("statutMembre", lien.getStatutMembre() != null ? lien.getStatutMembre().name() : null);
entry.put("roleOrg", lien.getRoleOrg());
entry.put("dateAdhesion", lien.getDateAdhesion());
return entry;
}).toList();
}
}

View File

@@ -1,14 +1,19 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.entity.Permission;
import dev.lions.unionflow.server.entity.Role;
import dev.lions.unionflow.server.entity.RolePermission;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import dev.lions.unionflow.server.repository.PermissionRepository;
import dev.lions.unionflow.server.repository.RoleRepository;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.NotFoundException;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.jboss.logging.Logger;
/**
@@ -32,6 +37,9 @@ public class RoleService {
@Inject
KeycloakService keycloakService;
@Inject
PermissionRepository permissionRepository;
/**
* Crée un nouveau rôle
*
@@ -141,6 +149,56 @@ public class RoleService {
return roleRepository.findAllActifs();
}
/**
* Liste les rôles par catégorie (PLATEFORME, FONCTIONNEL, METIER)
*/
public List<Role> listerParCategorie(String categorie) {
return roleRepository.findByCategorie(categorie);
}
/**
* Liste les rôles assignables (FONCTIONNEL + METIER) — pour UI d'assignation.
*/
public List<Role> listerRolesAssignables() {
return roleRepository.findRolesAssignables();
}
/**
* Retourne les codes de permissions attribuées à un rôle.
*/
public Set<String> getPermissionsDuRole(UUID roleId) {
Role role = roleRepository.findRoleById(roleId)
.orElseThrow(() -> new NotFoundException("Rôle non trouvé : " + roleId));
return role.getPermissions().stream()
.map(rp -> rp.getPermission().getCode())
.collect(Collectors.toSet());
}
/**
* Assigne une permission à un rôle (si pas déjà assignée).
*/
@Transactional
public void assignerPermission(UUID roleId, UUID permissionId) {
Role role = roleRepository.findRoleById(roleId)
.orElseThrow(() -> new NotFoundException("Rôle non trouvé : " + roleId));
Permission permission = permissionRepository.findByIdOptional(permissionId)
.orElseThrow(() -> new NotFoundException("Permission non trouvée : " + permissionId));
boolean dejaAssignee = role.getPermissions().stream()
.anyMatch(rp -> rp.getPermission().getId().equals(permissionId));
if (dejaAssignee) {
return;
}
RolePermission rp = new RolePermission();
rp.setRole(role);
rp.setPermission(permission);
rp.setCreePar(keycloakService.getCurrentUserEmail());
role.getPermissions().add(rp);
roleRepository.persist(role);
LOG.infof("Permission %s assignée au rôle %s", permission.getCode(), role.getCode());
}
/**
* Supprime (désactive) un rôle
*

View File

@@ -503,6 +503,26 @@ public class SouscriptionService {
r.setOrganisationId(s.getOrganisation().getId().toString());
r.setOrganisationNom(s.getOrganisation().getNom());
}
// ── Quota & Option C ──────────────────────────────────────────────────────
r.setQuotaMax(s.getQuotaMax());
r.setQuotaUtilise(s.getQuotaUtilise() != null ? s.getQuotaUtilise() : 0);
r.setQuotaDepasse(s.isQuotaDepasse());
if (s.getQuotaMax() != null && s.getQuotaUtilise() != null) {
r.setQuotaRestant(Math.max(0, s.getQuotaMax() - s.getQuotaUtilise()));
}
if (s.getDateFin() != null) {
r.setJoursAvantExpiration(java.time.LocalDate.now().until(s.getDateFin(), java.time.temporal.ChronoUnit.DAYS));
}
if (s.getFormule() != null) {
FormuleAbonnement f = s.getFormule();
r.setPlanCommercial(f.getPlanCommercial());
r.setApiAccess(Boolean.TRUE.equals(f.getApiAccess()));
r.setFederationAccess(Boolean.TRUE.equals(f.getFederationAccess()));
r.setSupportPrioritaire(Boolean.TRUE.equals(f.getSupportPrioritaire()));
r.setSlaGaranti(f.getSlaGaranti());
r.setMaxAdmins(f.getMaxAdmins());
r.setNiveauReporting(f.getNiveauReporting());
}
return r;
}
@@ -518,6 +538,14 @@ public class SouscriptionService {
r.setPrixMensuel(f.getPrixMensuel());
r.setPrixAnnuel(f.getPrixAnnuel());
r.setOrdreAffichage(f.getOrdreAffichage() != null ? f.getOrdreAffichage() : 0);
// Champs Option C (V19)
r.setPlanCommercial(f.getPlanCommercial());
r.setNiveauReporting(f.getNiveauReporting());
r.setApiAccess(Boolean.TRUE.equals(f.getApiAccess()));
r.setFederationAccess(Boolean.TRUE.equals(f.getFederationAccess()));
r.setSupportPrioritaire(Boolean.TRUE.equals(f.getSupportPrioritaire()));
r.setSlaGaranti(f.getSlaGaranti());
r.setMaxAdmins(f.getMaxAdmins() != null ? f.getMaxAdmins() : -1);
return r;
}