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:
@@ -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();
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user