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();
|
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.
|
* 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.*;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import jakarta.ws.rs.core.Response;
|
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.net.URI;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||||
@@ -59,6 +62,8 @@ public class OrganisationResource {
|
|||||||
@Inject
|
@Inject
|
||||||
dev.lions.unionflow.server.repository.MembreOrganisationRepository membreOrganisationRepository;
|
dev.lions.unionflow.server.repository.MembreOrganisationRepository membreOrganisationRepository;
|
||||||
|
|
||||||
|
@Inject OrganisationContextHolder organisationContextHolder;
|
||||||
|
|
||||||
/** Récupère les organisations du membre connecté (pour admin d'organisation) */
|
/** Récupère les organisations du membre connecté (pour admin d'organisation) */
|
||||||
@GET
|
@GET
|
||||||
@Path("/mes")
|
@Path("/mes")
|
||||||
@@ -274,6 +279,27 @@ public class OrganisationResource {
|
|||||||
|
|
||||||
LOG.infof("Mise à jour de l'organisation ID: %s", id);
|
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 {
|
try {
|
||||||
Organisation organisationMiseAJour = organisationService.convertFromUpdateRequest(request);
|
Organisation organisationMiseAJour = organisationService.convertFromUpdateRequest(request);
|
||||||
Organisation organisation =
|
Organisation organisation =
|
||||||
@@ -487,6 +513,19 @@ public class OrganisationResource {
|
|||||||
LOG.info("Récupération des statistiques des organisations");
|
LOG.info("Récupération des statistiques des organisations");
|
||||||
|
|
||||||
try {
|
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();
|
Map<String, Object> statistiques = organisationService.obtenirStatistiques();
|
||||||
return Response.ok(statistiques).build();
|
return Response.ok(statistiques).build();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -539,6 +539,41 @@ public class OrganisationService {
|
|||||||
return map;
|
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
|
* Convertit une entité Organisation en DTO complet
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ quarkus.http.cors.origins=*
|
|||||||
|
|
||||||
# Keycloak / OIDC local
|
# Keycloak / OIDC local
|
||||||
quarkus.oidc.tenant-enabled=true
|
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
|
quarkus.oidc.client-id=unionflow-server
|
||||||
# Audience mapper configuré sur unionflow-client et unionflow-mobile dans Keycloak
|
# Audience mapper configuré sur unionflow-client et unionflow-mobile dans Keycloak
|
||||||
# → les tokens contiennent désormais "unionflow-server" dans le claim aud
|
# → 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
|
quarkus.oidc-client.admin-service.early-tokens-acquisition=true
|
||||||
|
|
||||||
# Keycloak Admin API (dev)
|
# 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.username=${KEYCLOAK_ADMIN_USERNAME:admin}
|
||||||
keycloak.admin.password=${KEYCLOAK_ADMIN_PASSWORD:admin}
|
keycloak.admin.password=${KEYCLOAK_ADMIN_PASSWORD:admin}
|
||||||
keycloak.admin.realm=unionflow
|
keycloak.admin.realm=unionflow
|
||||||
|
|||||||
Reference in New Issue
Block a user