feat: sécuriser endpoints organisations + stats par org + fix IP dev

- PUT /{id}: check appartenance ADMIN_ORGANISATION (403 si pas membre)
- GET /statistiques: stats scoped à l'org active pour ADMIN_ORGANISATION
- OrganisationService.obtenirStatistiquesParOrganisation(): stats mono-org
- MembreOrganisationRepository.findByMembreEmailAndOrganisationId()
- application-dev: IP 192.168.1.145→localhost pour Keycloak
This commit is contained in:
dahoud
2026-04-18 08:07:04 +00:00
parent 9f14c2e345
commit 9a53ce4077
4 changed files with 83 additions and 2 deletions

View File

@@ -78,6 +78,13 @@ public class MembreOrganisationRepository extends BaseRepository<MembreOrganisat
organisationId, StatutMembre.ACTIF).list();
}
/**
* Trouve le lien membre-organisation par email du membre et ID de l'organisation.
*/
public Optional<MembreOrganisation> findByMembreEmailAndOrganisationId(String email, UUID organisationId) {
return find("membre.email = ?1 and organisation.id = ?2", email, organisationId).firstResultOptional();
}
/**
* Trouve les membres en attente de validation depuis plus de N jours.
*/

View File

@@ -17,9 +17,12 @@ import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import dev.lions.unionflow.server.api.enums.membre.StatutMembre;
import dev.lions.unionflow.server.security.OrganisationContextHolder;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.eclipse.microprofile.openapi.annotations.Operation;
@@ -59,6 +62,8 @@ public class OrganisationResource {
@Inject
dev.lions.unionflow.server.repository.MembreOrganisationRepository membreOrganisationRepository;
@Inject OrganisationContextHolder organisationContextHolder;
/** Récupère les organisations du membre connecté (pour admin d'organisation) */
@GET
@Path("/mes")
@@ -274,6 +279,27 @@ public class OrganisationResource {
LOG.infof("Mise à jour de l'organisation ID: %s", id);
// Ownership check: ADMIN_ORGANISATION can only update their own org
Set<String> roles = securityIdentity.getRoles();
boolean isOrgAdmin = roles.contains("ADMIN_ORGANISATION")
&& !roles.contains("ADMIN")
&& !roles.contains("SUPER_ADMIN");
if (isOrgAdmin) {
String email = securityIdentity.getPrincipal() != null
? securityIdentity.getPrincipal().getName()
: null;
boolean belongsToOrg = email != null
&& membreOrganisationRepository.findByMembreEmailAndOrganisationId(email, id)
.filter(mo -> StatutMembre.ACTIF.equals(mo.getStatutMembre()))
.isPresent();
if (!belongsToOrg) {
LOG.warnf("ADMIN_ORGANISATION %s attempted to update org %s without membership", email, id);
return Response.status(Response.Status.FORBIDDEN)
.entity(Map.of("error", "Vous n'êtes pas membre actif de cette organisation"))
.build();
}
}
try {
Organisation organisationMiseAJour = organisationService.convertFromUpdateRequest(request);
Organisation organisation =
@@ -487,6 +513,19 @@ public class OrganisationResource {
LOG.info("Récupération des statistiques des organisations");
try {
// ADMIN_ORGANISATION (without ADMIN/SUPER_ADMIN) → scope to their active org
Set<String> roles = securityIdentity.getRoles();
boolean isOrgAdmin = roles.contains("ADMIN_ORGANISATION")
&& !roles.contains("ADMIN")
&& !roles.contains("SUPER_ADMIN");
if (isOrgAdmin && organisationContextHolder.hasContext()) {
UUID orgId = organisationContextHolder.getOrganisationId();
LOG.infof("ADMIN_ORGANISATION: scoping statistiques to org %s", orgId);
Map<String, Object> statistiques = organisationService.obtenirStatistiquesParOrganisation(orgId);
return Response.ok(statistiques).build();
}
Map<String, Object> statistiques = organisationService.obtenirStatistiques();
return Response.ok(statistiques).build();
} catch (Exception e) {

View File

@@ -539,6 +539,41 @@ public class OrganisationService {
return map;
}
/**
* Obtient les statistiques scopées à une seule organisation (pour ADMIN_ORGANISATION).
*
* @param organisationId ID de l'organisation
* @return map contenant les statistiques de l'organisation
*/
public Map<String, Object> obtenirStatistiquesParOrganisation(UUID organisationId) {
LOG.infof("Calcul des statistiques pour l'organisation %s", organisationId);
Organisation org = organisationRepository.findById(organisationId);
if (org == null) {
return Map.of("error", "Organisation non trouvée");
}
long membresActifs = membreOrganisationRepository
.findMembresActifsParOrganisation(organisationId).size();
long totalMembres = membreOrganisationRepository
.findAllByOrganisationId(organisationId).size();
Map<String, Object> map = new HashMap<>();
map.put("totalAssociations", 1L);
map.put("associationsActives", org.getActif() != null && org.getActif() ? 1L : 0L);
map.put("associationsInactives", org.getActif() != null && org.getActif() ? 0L : 1L);
map.put("associationsSuspendues", "SUSPENDUE".equals(org.getStatut()) ? 1L : 0L);
map.put("associationsDissoutes", "DISSOLUE".equals(org.getStatut()) ? 1L : 0L);
map.put("nouvellesAssociations30Jours", 0L);
map.put("tauxActivite", org.getActif() != null && org.getActif() ? 100.0 : 0.0);
map.put("totalMembres", totalMembres);
map.put("membresActifs", membresActifs);
map.put("repartitionParType", Map.of(
org.getTypeOrganisation() != null ? org.getTypeOrganisation() : "NON_DEFINI", 1L));
map.put("repartitionParRegion", Map.of());
return map;
}
/**
* Convertit une entité Organisation en DTO complet
*/

View File

@@ -24,7 +24,7 @@ quarkus.http.cors.origins=*
# Keycloak / OIDC local
quarkus.oidc.tenant-enabled=true
quarkus.oidc.auth-server-url=http://192.168.1.145:8180/realms/unionflow
quarkus.oidc.auth-server-url=http://localhost:8180/realms/unionflow
quarkus.oidc.client-id=unionflow-server
# Audience mapper configuré sur unionflow-client et unionflow-mobile dans Keycloak
# → les tokens contiennent désormais "unionflow-server" dans le claim aud
@@ -65,7 +65,7 @@ quarkus.oidc-client.admin-service.tls.verification=none
quarkus.oidc-client.admin-service.early-tokens-acquisition=true
# Keycloak Admin API (dev)
keycloak.admin.url=http://192.168.1.145:8180
keycloak.admin.url=http://localhost:8180
keycloak.admin.username=${KEYCLOAK_ADMIN_USERNAME:admin}
keycloak.admin.password=${KEYCLOAK_ADMIN_PASSWORD:admin}
keycloak.admin.realm=unionflow