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:
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user