diff --git a/src/main/java/dev/lions/unionflow/server/repository/MembreOrganisationRepository.java b/src/main/java/dev/lions/unionflow/server/repository/MembreOrganisationRepository.java index 012f77e..17be027 100644 --- a/src/main/java/dev/lions/unionflow/server/repository/MembreOrganisationRepository.java +++ b/src/main/java/dev/lions/unionflow/server/repository/MembreOrganisationRepository.java @@ -78,6 +78,13 @@ public class MembreOrganisationRepository extends BaseRepository 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. */ diff --git a/src/main/java/dev/lions/unionflow/server/resource/OrganisationResource.java b/src/main/java/dev/lions/unionflow/server/resource/OrganisationResource.java index 2226110..76d7947 100644 --- a/src/main/java/dev/lions/unionflow/server/resource/OrganisationResource.java +++ b/src/main/java/dev/lions/unionflow/server/resource/OrganisationResource.java @@ -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 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 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 statistiques = organisationService.obtenirStatistiquesParOrganisation(orgId); + return Response.ok(statistiques).build(); + } + Map statistiques = organisationService.obtenirStatistiques(); return Response.ok(statistiques).build(); } catch (Exception e) { diff --git a/src/main/java/dev/lions/unionflow/server/service/OrganisationService.java b/src/main/java/dev/lions/unionflow/server/service/OrganisationService.java index feafc6a..ec88420 100644 --- a/src/main/java/dev/lions/unionflow/server/service/OrganisationService.java +++ b/src/main/java/dev/lions/unionflow/server/service/OrganisationService.java @@ -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 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 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 */ diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index ffd9e21..bc9ec77 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -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