fix(security): audit RBAC complet v3.0 — rôles normalisés, lifecycle, changement mdp mobile

RBAC:
- HealthResource: @PermitAll
- RoleResource: @RolesAllowed ADMIN/SUPER_ADMIN/ADMIN_ORGANISATION class-level
- PropositionAideResource: @RolesAllowed MEMBRE/USER class-level
- AuthCallbackResource: @PermitAll
- EvenementResource: @PermitAll /publics et /test, count restreint
- BackupResource/LogsMonitoringResource/SystemResource: MODERATOR → MODERATEUR
- AnalyticsResource: MANAGER/MEMBER → ADMIN_ORGANISATION/MEMBRE
- RoleConstant.java: constantes de rôles centralisées

Cycle de vie membres:
- MemberLifecycleService: ajouterMembre()/retirerMembre() sur activation/radiation/archivage
- MembreResource: endpoint GET /numero/{numeroMembre}
- MembreService: méthode trouverParNumeroMembre()

Changement mot de passe:
- CompteAdherentResource: endpoint POST /auth/change-password (mobile)
- MembreKeycloakSyncService: changerMotDePasseDirectKeycloak() via API Admin Keycloak directe
- Fallback automatique si lions-user-manager indisponible

Workflow:
- Flyway V17-V23: rôles, types org, formules Option C, lifecycle columns, bareme cotisation
- Nouvelles classes: MemberLifecycleService, OrganisationModuleService, scheduler
- Security: OrganisationContextFilter, OrganisationContextHolder, ModuleAccessFilter
This commit is contained in:
dahoud
2026-04-07 20:52:26 +00:00
parent c74ae25ad6
commit a2dfae9a0b
78 changed files with 5637 additions and 271 deletions

View File

@@ -50,7 +50,7 @@ public class AnalyticsResource {
/** Calcule une métrique analytics pour une période donnée */
@GET
@Path("/metriques/{typeMetrique}")
@RolesAllowed({"ADMIN", "MANAGER", "MEMBER"})
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE"})
@Operation(
summary = "Calculer une métrique analytics",
description = "Calcule une métrique spécifique pour une période et organisation données")
@@ -88,7 +88,7 @@ public class AnalyticsResource {
/** Calcule les tendances d'un KPI sur une période */
@GET
@Path("/tendances/{typeMetrique}")
@RolesAllowed({"ADMIN", "MANAGER", "MEMBER"})
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE"})
@Operation(
summary = "Calculer la tendance d'un KPI",
description = "Calcule l'évolution et les tendances d'un KPI sur une période donnée")
@@ -127,7 +127,7 @@ public class AnalyticsResource {
/** Obtient tous les KPI pour une organisation */
@GET
@Path("/kpis")
@RolesAllowed({"ADMIN", "MANAGER", "MEMBER"})
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE"})
@Operation(
summary = "Obtenir tous les KPI",
description = "Récupère tous les KPI calculés pour une organisation et période données")
@@ -163,7 +163,7 @@ public class AnalyticsResource {
/** Calcule le KPI de performance globale */
@GET
@Path("/performance-globale")
@RolesAllowed({"ADMIN", "MANAGER"})
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "SUPER_ADMIN"})
@Operation(
summary = "Calculer la performance globale",
description = "Calcule le score de performance globale de l'organisation")
@@ -208,7 +208,7 @@ public class AnalyticsResource {
/** Obtient les évolutions des KPI par rapport à la période précédente */
@GET
@Path("/evolutions")
@RolesAllowed({"ADMIN", "MANAGER", "MEMBER"})
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE"})
@Operation(
summary = "Obtenir les évolutions des KPI",
description = "Récupère les évolutions des KPI par rapport à la période précédente")
@@ -248,7 +248,7 @@ public class AnalyticsResource {
/** Obtient les widgets du tableau de bord pour un utilisateur */
@GET
@Path("/dashboard/widgets")
@RolesAllowed({"ADMIN", "MANAGER", "MEMBER"})
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE"})
@Operation(
summary = "Obtenir les widgets du tableau de bord",
description = "Récupère tous les widgets configurés pour le tableau de bord de l'utilisateur")
@@ -285,7 +285,7 @@ public class AnalyticsResource {
/** Obtient les types de métriques disponibles */
@GET
@Path("/types-metriques")
@RolesAllowed({"ADMIN", "MANAGER", "MEMBER"})
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE"})
@Operation(
summary = "Obtenir les types de métriques disponibles",
description = "Récupère la liste de tous les types de métriques disponibles")
@@ -300,7 +300,7 @@ public class AnalyticsResource {
/** Obtient les périodes d'analyse disponibles */
@GET
@Path("/periodes-analyse")
@RolesAllowed({"ADMIN", "MANAGER", "MEMBER"})
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE"})
@Operation(
summary = "Obtenir les périodes d'analyse disponibles",
description = "Récupère la liste de toutes les périodes d'analyse disponibles")

View File

@@ -1,6 +1,7 @@
package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.service.ApprovalService;
import dev.lions.unionflow.server.api.dto.common.ErrorResponse;
import dev.lions.unionflow.server.api.dto.finance_workflow.request.ApproveTransactionRequest;
import dev.lions.unionflow.server.api.dto.finance_workflow.request.RejectTransactionRequest;
import dev.lions.unionflow.server.api.dto.finance_workflow.response.TransactionApprovalResponse;
@@ -53,7 +54,7 @@ public class ApprovalResource {
if (transactionId == null || transactionType == null || amount == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("transactionId, transactionType et amount sont requis"))
.entity(ErrorResponse.of("transactionId, transactionType et amount sont requis"))
.build();
}
@@ -64,12 +65,12 @@ public class ApprovalResource {
return Response.status(Response.Status.CREATED).entity(approval).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
} catch (Exception e) {
LOG.error("Erreur lors de la création de la demande d'approbation", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
}
}
@@ -88,7 +89,7 @@ public class ApprovalResource {
} catch (Exception e) {
LOG.error("Erreur lors de la récupération des approbations en attente", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
}
}
@@ -106,12 +107,12 @@ public class ApprovalResource {
return Response.ok(approval).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
} catch (Exception e) {
LOG.error("Erreur lors de la récupération de l'approbation", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
}
}
@@ -131,16 +132,16 @@ public class ApprovalResource {
return Response.ok(approval).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
} catch (ForbiddenException e) {
return Response.status(Response.Status.FORBIDDEN)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
} catch (Exception e) {
LOG.error("Erreur lors de l'approbation de la transaction", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
}
}
@@ -160,16 +161,16 @@ public class ApprovalResource {
return Response.ok(approval).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
} catch (ForbiddenException e) {
return Response.status(Response.Status.FORBIDDEN)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
} catch (Exception e) {
LOG.error("Erreur lors du rejet de la transaction", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
}
}
@@ -197,12 +198,12 @@ public class ApprovalResource {
return Response.ok(approvals).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
} catch (Exception e) {
LOG.error("Erreur lors de la récupération de l'historique", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
}
}
@@ -221,12 +222,11 @@ public class ApprovalResource {
} catch (Exception e) {
LOG.error("Erreur lors du comptage des approbations", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
}
}
// Classes internes pour les réponses
record ErrorResponse(String message) {}
// Classe interne pour le comptage
record CountResponse(long count) {}
}

View File

@@ -36,7 +36,7 @@ public class BackupResource {
* Lister toutes les sauvegardes
*/
@GET
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATOR"})
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
@Operation(summary = "Lister toutes les sauvegardes disponibles")
public List<BackupResponse> getAllBackups() {
log.info("GET /api/backups");
@@ -48,7 +48,7 @@ public class BackupResource {
*/
@GET
@Path("/{id}")
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATOR"})
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
@Operation(summary = "Récupérer une sauvegarde par ID")
public BackupResponse getBackupById(@PathParam("id") UUID id) {
log.info("GET /api/backups/{}", id);
@@ -98,7 +98,7 @@ public class BackupResource {
*/
@GET
@Path("/config")
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATOR"})
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
@Operation(summary = "Récupérer la configuration des sauvegardes automatiques")
public BackupConfigResponse getBackupConfig() {
log.info("GET /api/backups/config");

View File

@@ -1,6 +1,7 @@
package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.service.BudgetService;
import dev.lions.unionflow.server.api.dto.common.ErrorResponse;
import dev.lions.unionflow.server.api.dto.finance_workflow.request.CreateBudgetRequest;
import dev.lions.unionflow.server.api.dto.finance_workflow.response.BudgetResponse;
import jakarta.annotation.security.RolesAllowed;
@@ -51,12 +52,12 @@ public class BudgetResource {
return Response.ok(budgets).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
} catch (Exception e) {
LOG.error("Erreur lors de la récupération des budgets", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
}
}
@@ -74,12 +75,12 @@ public class BudgetResource {
return Response.ok(budget).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
} catch (Exception e) {
LOG.error("Erreur lors de la récupération du budget", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
}
}
@@ -98,16 +99,16 @@ public class BudgetResource {
.build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
} catch (Exception e) {
LOG.error("Erreur lors de la création du budget", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
}
}
@@ -125,12 +126,12 @@ public class BudgetResource {
return Response.ok(tracking).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
} catch (Exception e) {
LOG.error("Erreur lors de la récupération du suivi budgétaire", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
}
}
@@ -150,16 +151,16 @@ public class BudgetResource {
return Response.ok(budget).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
} catch (Exception e) {
LOG.error("Erreur lors de la mise à jour du budget", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
}
}
@@ -177,16 +178,15 @@ public class BudgetResource {
return Response.noContent().build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
} catch (Exception e) {
LOG.error("Erreur lors de la suppression du budget", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.of(e.getMessage()))
.build();
}
}
// Classe interne pour les réponses d'erreur
record ErrorResponse(String message) {}
}

View File

@@ -2,6 +2,7 @@ package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.api.dto.comptabilite.request.*;
import dev.lions.unionflow.server.api.dto.comptabilite.response.*;
import dev.lions.unionflow.server.api.dto.common.ErrorResponse;
import dev.lions.unionflow.server.service.ComptabiliteService;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
@@ -52,12 +53,12 @@ public class ComptabiliteResource {
return Response.status(Response.Status.CREATED).entity(result).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.ofError(e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la création du compte comptable");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la création du compte comptable: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la création du compte comptable: " + e.getMessage()))
.build();
}
}
@@ -76,12 +77,12 @@ public class ComptabiliteResource {
return Response.ok(result).build();
} catch (jakarta.ws.rs.NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse("Compte comptable non trouvé"))
.entity(ErrorResponse.ofError("Compte comptable non trouvé"))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la recherche du compte comptable");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la recherche du compte comptable: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la recherche du compte comptable: " + e.getMessage()))
.build();
}
}
@@ -100,7 +101,7 @@ public class ComptabiliteResource {
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la liste des comptes comptables");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la liste des comptes comptables: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la liste des comptes comptables: " + e.getMessage()))
.build();
}
}
@@ -124,12 +125,12 @@ public class ComptabiliteResource {
return Response.status(Response.Status.CREATED).entity(result).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.ofError(e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la création du journal comptable");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la création du journal comptable: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la création du journal comptable: " + e.getMessage()))
.build();
}
}
@@ -148,12 +149,12 @@ public class ComptabiliteResource {
return Response.ok(result).build();
} catch (jakarta.ws.rs.NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse("Journal comptable non trouvé"))
.entity(ErrorResponse.ofError("Journal comptable non trouvé"))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la recherche du journal comptable");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la recherche du journal comptable: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la recherche du journal comptable: " + e.getMessage()))
.build();
}
}
@@ -172,7 +173,7 @@ public class ComptabiliteResource {
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la liste des journaux comptables");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la liste des journaux comptables: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la liste des journaux comptables: " + e.getMessage()))
.build();
}
}
@@ -196,12 +197,12 @@ public class ComptabiliteResource {
return Response.status(Response.Status.CREATED).entity(result).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.ofError(e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la création de l'écriture comptable");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la création de l'écriture comptable: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la création de l'écriture comptable: " + e.getMessage()))
.build();
}
}
@@ -220,12 +221,12 @@ public class ComptabiliteResource {
return Response.ok(result).build();
} catch (jakarta.ws.rs.NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse("Écriture comptable non trouvée"))
.entity(ErrorResponse.ofError("Écriture comptable non trouvée"))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la recherche de l'écriture comptable");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la recherche de l'écriture comptable: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la recherche de l'écriture comptable: " + e.getMessage()))
.build();
}
}
@@ -245,7 +246,7 @@ public class ComptabiliteResource {
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la liste des écritures");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la liste des écritures: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la liste des écritures: " + e.getMessage()))
.build();
}
}
@@ -265,17 +266,10 @@ public class ComptabiliteResource {
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la liste des écritures");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la liste des écritures: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la liste des écritures: " + e.getMessage()))
.build();
}
}
/** Classe interne pour les réponses d'erreur */
public static class ErrorResponse {
public String error;
public ErrorResponse(String error) {
this.error = error;
}
}
}

View File

@@ -247,4 +247,55 @@ public class CompteAdherentResource {
membreKeycloakSyncService.changerMotDePassePremierLogin(membreOpt.get().getId(), nouveauMotDePasse);
return Response.ok(Map.of("message", "Mot de passe mis à jour avec succès.")).build();
}
/**
* Endpoint mobile : changement de mot de passe depuis l'app Flutter.
* Bypass lions-user-manager — appel direct à l'API Admin Keycloak.
*
* <p>Body attendu : {@code { "userId": "...", "oldPassword": "...", "newPassword": "..." }}
*/
@POST
@Path("/auth/change-password")
@Authenticated
@Operation(
summary = "Changer le mot de passe (mobile)",
description = "Endpoint dédié à l'application mobile. Bypass lions-user-manager via API Admin Keycloak directe."
)
public Response changerMotDePasseMobile(Map<String, String> body) {
String email = securiteHelper.resolveEmail();
if (email == null || email.isBlank()) {
return Response.status(Response.Status.UNAUTHORIZED).build();
}
String newPassword = body == null ? null : body.get("newPassword");
if (newPassword == null || newPassword.isBlank()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("message", "Le champ 'newPassword' est requis."))
.build();
}
if (newPassword.length() < 8) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("message", "Le mot de passe doit contenir au moins 8 caractères."))
.build();
}
Optional<Membre> membreOpt = membreRepository.findByEmail(email.trim())
.or(() -> membreRepository.findByEmail(email.trim().toLowerCase()));
if (membreOpt.isEmpty()) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("message", "Aucun membre trouvé pour ce compte."))
.build();
}
try {
membreKeycloakSyncService.changerMotDePasseDirectKeycloak(membreOpt.get().getId(), newPassword);
return Response.ok(Map.of("message", "Mot de passe mis à jour avec succès.")).build();
} catch (Exception e) {
LOG.errorf(e, "Erreur changement mot de passe pour %s", email);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("message", "Erreur lors du changement de mot de passe: " + e.getMessage()))
.build();
}
}
}

View File

@@ -60,19 +60,19 @@ public class CotisationResource {
try {
log.info("GET /api/cotisations/public - page: {}, size: {}", page, size);
List<CotisationSummaryResponse> cotisations = cotisationService.getAllCotisations(page, size);
List<CotisationResponse> cotisations = cotisationService.getAllCotisations(page, size);
List<Map<String, Object>> content = cotisations.stream()
.map(c -> {
Map<String, Object> map = new java.util.HashMap<>();
map.put("id", c.id().toString());
map.put("reference", c.numeroReference());
map.put("nomMembre", c.nomMembre());
map.put("montantDu", c.montantDu());
map.put("montantPaye", c.montantPaye());
map.put("statut", c.statut());
map.put("statutLibelle", c.statutLibelle());
map.put("dateEcheance", c.dateEcheance().toString());
map.put("id", c.getId() != null ? c.getId().toString() : null);
map.put("reference", c.getNumeroReference());
map.put("nomMembre", c.getNomMembre());
map.put("montantDu", c.getMontantDu());
map.put("montantPaye", c.getMontantPaye());
map.put("statut", c.getStatut());
map.put("statutLibelle", c.getStatutLibelle());
map.put("dateEcheance", c.getDateEcheance() != null ? c.getDateEcheance().toString() : null);
return map;
})
.collect(Collectors.toList());
@@ -113,7 +113,7 @@ public class CotisationResource {
try {
log.info("GET /api/cotisations - page: {}, size: {}", page, size);
List<CotisationSummaryResponse> cotisations = cotisationService.getAllCotisations(page, size);
List<CotisationResponse> cotisations = cotisationService.getAllCotisations(page, size);
return Response.ok(cotisations).build();
} catch (Exception e) {
log.error("Erreur lister cotisations", e);
@@ -233,7 +233,7 @@ public class CotisationResource {
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size) {
try {
List<CotisationSummaryResponse> results = cotisationService.getCotisationsByMembre(membreId, page, size);
List<CotisationResponse> results = cotisationService.getCotisationsByMembre(membreId, page, size);
return Response.ok(results).build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(Map.of("error", e.getMessage())).build();
@@ -251,7 +251,7 @@ public class CotisationResource {
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size) {
try {
List<CotisationSummaryResponse> results = cotisationService.getCotisationsByStatut(statut, page, size);
List<CotisationResponse> results = cotisationService.getCotisationsByStatut(statut, page, size);
return Response.ok(results).build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(Map.of("error", e.getMessage())).build();
@@ -268,7 +268,7 @@ public class CotisationResource {
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size) {
try {
List<CotisationSummaryResponse> results = cotisationService.getCotisationsEnRetard(page, size);
List<CotisationResponse> results = cotisationService.getCotisationsEnRetard(page, size);
return Response.ok(results).build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(Map.of("error", e.getMessage())).build();
@@ -290,7 +290,7 @@ public class CotisationResource {
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size) {
try {
List<CotisationSummaryResponse> results = cotisationService.rechercherCotisations(
List<CotisationResponse> results = cotisationService.rechercherCotisations(
membreId, statut, typeCotisation, annee, mois, page, size);
return Response.ok(results).build();
} catch (Exception e) {
@@ -399,7 +399,7 @@ public class CotisationResource {
@QueryParam("size") @DefaultValue("50") int size) {
try {
log.info("GET /api/cotisations/mes-cotisations");
List<CotisationSummaryResponse> results = cotisationService.getMesCotisations(page, size);
List<CotisationResponse> results = cotisationService.getMesCotisations(page, size);
return Response.ok(results).build();
} catch (Exception e) {
log.error("Erreur récupération mes cotisations", e);
@@ -423,7 +423,7 @@ public class CotisationResource {
public Response getMesCotisationsEnAttente() {
try {
log.info("GET /api/cotisations/mes-cotisations/en-attente");
List<CotisationSummaryResponse> results = cotisationService.getMesCotisationsEnAttente();
List<CotisationResponse> results = cotisationService.getMesCotisationsEnAttente();
return Response.ok(results).build();
} catch (Exception e) {
log.error("Erreur récupération mes cotisations en attente", e);

View File

@@ -1,5 +1,6 @@
package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.api.dto.common.ErrorResponse;
import dev.lions.unionflow.server.api.dto.document.request.CreateDocumentRequest;
import dev.lions.unionflow.server.api.dto.document.response.DocumentResponse;
import dev.lions.unionflow.server.api.dto.document.request.CreatePieceJointeRequest;
@@ -62,7 +63,7 @@ public class DocumentResource {
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la création du document");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la création du document: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la création du document: " + e.getMessage()))
.build();
}
}
@@ -87,7 +88,7 @@ public class DocumentResource {
try {
if (file == null || file.fileName() == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Aucun fichier fourni"))
.entity(ErrorResponse.ofError("Aucun fichier fourni"))
.build();
}
@@ -135,12 +136,31 @@ public class DocumentResource {
} catch (IllegalArgumentException e) {
LOG.warnf("Validation échouée pour upload: %s", e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.ofError(e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'upload du fichier");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse("Erreur lors de l'upload: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de l'upload: " + e.getMessage()))
.build();
}
}
/**
* Liste les documents de l'utilisateur connecté
*
* @return Liste des documents du membre connecté
*/
@GET
@Path("/mes-documents")
public Response listerMesDocuments() {
try {
List<DocumentResponse> result = documentService.listerMesDocuments();
return Response.ok(result).build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la liste des documents du membre connecté");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(ErrorResponse.ofError("Erreur lors de la récupération des documents: " + e.getMessage()))
.build();
}
}
@@ -159,12 +179,12 @@ public class DocumentResource {
return Response.ok(result).build();
} catch (jakarta.ws.rs.NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse("Document non trouvé"))
.entity(ErrorResponse.ofError("Document non trouvé"))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la recherche du document");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la recherche du document: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la recherche du document: " + e.getMessage()))
.build();
}
}
@@ -184,14 +204,12 @@ public class DocumentResource {
return Response.ok().build();
} catch (jakarta.ws.rs.NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse("Document non trouvé"))
.entity(ErrorResponse.ofError("Document non trouvé"))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'enregistrement du téléchargement");
return Response.status(Response.Status.BAD_REQUEST)
.entity(
new ErrorResponse(
"Erreur lors de l'enregistrement du téléchargement: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de l'enregistrement du téléchargement: " + e.getMessage()))
.build();
}
}
@@ -211,12 +229,12 @@ public class DocumentResource {
return Response.status(Response.Status.CREATED).entity(result).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.ofError(e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la création de la pièce jointe");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la création de la pièce jointe: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la création de la pièce jointe: " + e.getMessage()))
.build();
}
}
@@ -236,17 +254,10 @@ public class DocumentResource {
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la liste des pièces jointes");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la liste des pièces jointes: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la liste des pièces jointes: " + e.getMessage()))
.build();
}
}
/** Classe interne pour les réponses d'erreur */
public static class ErrorResponse {
public String error;
public ErrorResponse(String error) {
this.error = error;
}
}
}

View File

@@ -8,6 +8,7 @@ import dev.lions.unionflow.server.entity.InscriptionEvenement;
import dev.lions.unionflow.server.service.EvenementService;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import io.quarkus.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
@@ -53,6 +54,7 @@ public class EvenementResource {
/** Endpoint de test public pour vérifier la connectivité */
@GET
@Path("/test")
@PermitAll
@Operation(summary = "Test de connectivité", description = "Endpoint public pour tester la connectivité")
@APIResponse(responseCode = "200", description = "Test réussi")
public Response testConnectivity() {
@@ -69,6 +71,7 @@ public class EvenementResource {
/** Endpoint de debug pour vérifier le chargement des données */
@GET
@Path("/count")
@RolesAllowed({ "ADMIN", "ADMIN_ORGANISATION", "SUPER_ADMIN" })
@Operation(summary = "Compter les événements", description = "Compte le nombre d'événements dans la base")
@APIResponse(responseCode = "200", description = "Nombre d'événements")
public Response countEvenements() {
@@ -194,6 +197,7 @@ public class EvenementResource {
/** Liste les événements publics */
@GET
@Path("/publics")
@PermitAll
@Operation(summary = "Événements publics")
public Response evenementsPublics(
@QueryParam("page") @DefaultValue("0") int page,
@@ -379,6 +383,7 @@ public class EvenementResource {
/** Liste des feedbacks d'un événement */
@GET
@Path("/{id}/feedbacks")
@RolesAllowed({ "ADMIN", "ADMIN_ORGANISATION", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT", "MEMBRE", "USER" })
@Operation(summary = "Liste des feedbacks de l'événement")
@APIResponse(responseCode = "200", description = "Liste des feedbacks")
public Response getFeedbacks(@PathParam("id") UUID evenementId) {

View File

@@ -1,5 +1,6 @@
package dev.lions.unionflow.server.resource;
import io.quarkus.security.PermitAll;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@@ -15,6 +16,7 @@ import org.eclipse.microprofile.openapi.annotations.tags.Tag;
@Path("/api/status")
@Produces(MediaType.APPLICATION_JSON)
@ApplicationScoped
@PermitAll
@Tag(name = "Status", description = "API de statut du serveur")
public class HealthResource {

View File

@@ -38,7 +38,7 @@ public class LogsMonitoringResource {
*/
@POST
@Path("/logs/search")
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATOR"})
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
@Operation(summary = "Rechercher dans les logs système", description = "Recherche avec filtres (niveau, source, texte, dates)")
public List<SystemLogResponse> searchLogs(@Valid LogSearchRequest request) {
log.info("POST /api/logs/search - level={}, source={}", request.getLevel(), request.getSource());
@@ -90,7 +90,7 @@ public class LogsMonitoringResource {
*/
@GET
@Path("/monitoring/metrics")
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATOR", "HR_MANAGER", "ACTIVE_MEMBER"})
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
@Operation(summary = "Récupérer les métriques système en temps réel")
public SystemMetricsResponse getSystemMetrics() {
log.debug("GET /api/monitoring/metrics");
@@ -102,7 +102,7 @@ public class LogsMonitoringResource {
*/
@GET
@Path("/alerts")
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATOR"})
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
@Operation(summary = "Récupérer toutes les alertes actives")
public List<SystemAlertResponse> getActiveAlerts() {
log.info("GET /api/alerts");
@@ -114,7 +114,7 @@ public class LogsMonitoringResource {
*/
@POST
@Path("/alerts/{id}/acknowledge")
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATOR"})
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
@Operation(summary = "Acquitter une alerte")
public Response acknowledgeAlert(@PathParam("id") UUID id) {
log.info("POST /api/alerts/{}/acknowledge", id);
@@ -127,7 +127,7 @@ public class LogsMonitoringResource {
*/
@GET
@Path("/alerts/config")
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATOR"})
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
@Operation(summary = "Récupérer la configuration des alertes système")
public AlertConfigResponse getAlertConfig() {
log.info("GET /api/alerts/config");

View File

@@ -421,6 +421,55 @@ public class MembreResource {
}
}
/**
* Liste tous les membres actifs (statut compte = ACTIF).
* Utilisé notamment pour la création de campagnes de cotisations.
*/
@GET
@Path("/actifs")
@RolesAllowed({ "ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "TRESORIER" })
@Operation(summary = "Membres actifs", description = "Liste tous les membres dont le compte est actif")
@APIResponse(responseCode = "200", description = "Liste des membres actifs")
public Response getMembresActifs() {
try {
LOG.info("GET /api/membres/actifs");
List<Membre> membres = membreService.listerMembresActifs();
List<MembreResponse> membresDTO = membreService.convertToResponseList(membres);
return Response.ok(membresDTO).build();
} catch (Exception e) {
LOG.errorf(e, "Erreur récupération membres actifs");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", e.getMessage())).build();
}
}
/**
* Liste les membres d'une organisation spécifique (statut ACTIF dans l'organisation).
* Utilisé pour la création de campagnes ciblées.
*/
@GET
@Path("/organisation/{organisationId}")
@RolesAllowed({ "ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "TRESORIER" })
@Operation(summary = "Membres d'une organisation", description = "Liste les membres actifs d'une organisation")
@APIResponse(responseCode = "200", description = "Liste des membres")
public Response getMembresParOrganisation(
@Parameter(description = "UUID de l'organisation") @PathParam("organisationId") UUID organisationId) {
try {
LOG.infof("GET /api/membres/organisation/%s", organisationId);
List<dev.lions.unionflow.server.entity.MembreOrganisation> liens =
membreOrgRepository.findMembresActifsParOrganisation(organisationId);
List<MembreResponse> membresDTO = liens.stream()
.filter(mo -> mo.getMembre() != null)
.map(mo -> membreService.convertToResponse(mo.getMembre()))
.collect(java.util.stream.Collectors.toList());
return Response.ok(membresDTO).build();
} catch (Exception e) {
LOG.errorf(e, "Erreur récupération membres organisation %s", organisationId);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", e.getMessage())).build();
}
}
@GET
@Path("/recherche")
@Operation(summary = "Rechercher des membres par nom ou prénom")
@@ -1248,6 +1297,23 @@ public class MembreResource {
return Response.ok(Map.of("statut", updated.getStatutMembre())).build();
}
/**
* Trouve un membre par son numéro de membre (ex: MBR-0001).
* Utilisé notamment pour la recherche de parrain lors de l'inscription.
*/
@GET
@Path("/numero/{numeroMembre}")
@RolesAllowed({ "ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "MODERATEUR", "MEMBRE", "USER" })
@Operation(summary = "Trouver un membre par son numéro")
@APIResponse(responseCode = "200", description = "Membre trouvé")
@APIResponse(responseCode = "404", description = "Membre non trouvé")
public Response obtenirParNumero(@PathParam("numeroMembre") String numeroMembre) {
LOG.infof("GET /api/membres/numero/%s", numeroMembre);
Membre membre = membreService.trouverParNumeroMembre(numeroMembre)
.orElseThrow(() -> new NotFoundException("Membre non trouvé avec le numéro: " + numeroMembre));
return Response.ok(membreService.convertToResponse(membre)).build();
}
/** Résout l'UUID de l'admin connecté depuis le JWT subject. */
private UUID resolveCurrentAdminId() {
try {

View File

@@ -1,5 +1,6 @@
package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.api.dto.common.ErrorResponse;
import dev.lions.unionflow.server.api.dto.notification.request.CreateNotificationRequest;
import dev.lions.unionflow.server.api.dto.notification.request.CreateTemplateNotificationRequest;
import dev.lions.unionflow.server.api.dto.notification.response.NotificationResponse;
@@ -61,7 +62,7 @@ public class NotificationResource {
} catch (Exception e) {
LOG.errorf(e, "Erreur liste notifications membre connecté");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur: " + e.getMessage()))
.build();
}
}
@@ -83,7 +84,7 @@ public class NotificationResource {
} catch (Exception e) {
LOG.errorf(e, "Erreur liste notifications non lues");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur: " + e.getMessage()))
.build();
}
}
@@ -107,12 +108,12 @@ public class NotificationResource {
return Response.status(Response.Status.CREATED).entity(result).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.ofError(e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la création du template");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la création du template: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la création du template: " + e.getMessage()))
.build();
}
}
@@ -136,7 +137,7 @@ public class NotificationResource {
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la création de la notification");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la création de la notification: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la création de la notification: " + e.getMessage()))
.build();
}
}
@@ -156,12 +157,12 @@ public class NotificationResource {
return Response.ok(result).build();
} catch (jakarta.ws.rs.NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse("Notification non trouvée"))
.entity(ErrorResponse.ofError("Notification non trouvée"))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors du marquage de la notification");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors du marquage de la notification: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors du marquage de la notification: " + e.getMessage()))
.build();
}
}
@@ -180,12 +181,12 @@ public class NotificationResource {
return Response.ok(result).build();
} catch (jakarta.ws.rs.NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse("Notification non trouvée"))
.entity(ErrorResponse.ofError("Notification non trouvée"))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la recherche de la notification");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la recherche de la notification: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la recherche de la notification: " + e.getMessage()))
.build();
}
}
@@ -205,7 +206,7 @@ public class NotificationResource {
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la liste des notifications");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la liste des notifications: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la liste des notifications: " + e.getMessage()))
.build();
}
}
@@ -225,9 +226,7 @@ public class NotificationResource {
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la liste des notifications non lues");
return Response.status(Response.Status.BAD_REQUEST)
.entity(
new ErrorResponse(
"Erreur lors de la liste des notifications non lues: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la liste des notifications non lues: " + e.getMessage()))
.build();
}
}
@@ -246,9 +245,7 @@ public class NotificationResource {
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la liste des notifications en attente");
return Response.status(Response.Status.BAD_REQUEST)
.entity(
new ErrorResponse(
"Erreur lors de la liste des notifications en attente: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la liste des notifications en attente: " + e.getMessage()))
.build();
}
}
@@ -269,27 +266,16 @@ public class NotificationResource {
return Response.ok(Map.of("notificationsCreees", notificationsCreees)).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.ofError(e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'envoi des notifications groupées");
return Response.status(Response.Status.BAD_REQUEST)
.entity(
new ErrorResponse(
"Erreur lors de l'envoi des notifications groupées: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de l'envoi des notifications groupées: " + e.getMessage()))
.build();
}
}
/** Classe interne pour les réponses d'erreur */
public static class ErrorResponse {
public String error;
public ErrorResponse(String error) {
this.error = error;
}
}
/** Classe interne pour les requêtes de notifications groupées (WOU/DRY) */
public static class NotificationGroupeeRequest {
public List<UUID> membreIds;

View File

@@ -170,6 +170,24 @@ public class PaiementResource {
return Response.status(Response.Status.CREATED).entity(result).build();
}
/**
* Polling du statut d'une IntentionPaiement Wave.
* Si Wave a confirmé le paiement, réconcilie automatiquement la cotisation (PAYEE) et retourne COMPLETEE.
* Le client web appelle cet endpoint toutes les 3 secondes pendant l'affichage du QR code.
*
* @param intentionId UUID de l'intention (clientReference retourné par initier-paiement-en-ligne)
* @return Statut courant + waveLaunchUrl (pour re-générer le QR si besoin) + message
*/
@GET
@Path("/statut-intention/{intentionId}")
@RolesAllowed({ "MEMBRE", "MEMBRE_ACTIF", "ADMIN", "ADMIN_ORGANISATION", "USER" })
public Response getStatutIntention(@PathParam("intentionId") java.util.UUID intentionId) {
LOG.infof("GET /api/paiements/statut-intention/%s", intentionId);
dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse result =
paiementService.verifierStatutIntention(intentionId);
return Response.ok(result).build();
}
/**
* Déclare un paiement manuel (espèces, virement, chèque).
* Le paiement est créé avec le statut EN_ATTENTE_VALIDATION.

View File

@@ -4,6 +4,7 @@ import dev.lions.unionflow.server.api.dto.solidarite.request.CreatePropositionAi
import dev.lions.unionflow.server.api.dto.solidarite.request.UpdatePropositionAideRequest;
import dev.lions.unionflow.server.api.dto.solidarite.response.PropositionAideResponse;
import dev.lions.unionflow.server.service.PropositionAideService;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.*;
@@ -21,6 +22,7 @@ import java.util.List;
@Path("/api/propositions-aide")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@RolesAllowed({"ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "MEMBRE", "USER"})
@Tag(name = "Propositions d'aide", description = "Gestion des propositions d'aide solidarité")
public class PropositionAideResource {

View File

@@ -3,6 +3,7 @@ package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.api.dto.role.response.RoleResponse;
import dev.lions.unionflow.server.entity.Role;
import dev.lions.unionflow.server.service.RoleService;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@@ -19,6 +20,7 @@ import java.util.stream.Collectors;
*/
@Path("/api/roles")
@Produces(MediaType.APPLICATION_JSON)
@RolesAllowed({"ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION"})
@Tag(name = "Rôles", description = "Gestion des rôles et permissions")
public class RoleResource {

View File

@@ -18,6 +18,7 @@ import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
@@ -158,6 +159,36 @@ public class SouscriptionResource {
// ── SuperAdmin ────────────────────────────────────────────────────────────
/**
* Liste toutes les souscriptions (SuperAdmin dashboard), avec filtre optionnel par org.
*/
@GET
@Path("/admin/toutes")
@RolesAllowed({"SUPER_ADMIN"})
public Response getSouscriptionsToutes(
@QueryParam("organisationId") UUID organisationId,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("1000") int size) {
LOG.debug("GET /api/souscriptions/admin/toutes");
List<SouscriptionStatutResponse> liste = souscriptionService.listerToutes(organisationId, page, size);
return Response.ok(liste).build();
}
/**
* Retourne la souscription active d'une organisation (SuperAdmin).
*/
@GET
@Path("/admin/organisation/{organisationId}/active")
@RolesAllowed({"SUPER_ADMIN"})
public Response getActiveParOrganisation(@PathParam("organisationId") UUID organisationId) {
LOG.debugf("GET /api/souscriptions/admin/organisation/%s/active", organisationId);
SouscriptionStatutResponse resp = souscriptionService.obtenirActiveParOrganisation(organisationId);
if (resp == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
return Response.ok(resp).build();
}
/**
* Liste les souscriptions en attente de validation SuperAdmin (statut PAIEMENT_CONFIRME).
*/

View File

@@ -38,7 +38,7 @@ public class SystemResource {
*/
@GET
@Path("/config")
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATOR"})
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
@Operation(summary = "Récupérer la configuration système", description = "Retourne la configuration système complète")
public SystemConfigResponse getSystemConfig() {
log.info("GET /api/system/config");
@@ -62,7 +62,7 @@ public class SystemResource {
*/
@GET
@Path("/cache/stats")
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATOR"})
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
@Operation(summary = "Récupérer les statistiques du cache système")
public CacheStatsResponse getCacheStats() {
log.info("GET /api/system/cache/stats");
@@ -111,7 +111,7 @@ public class SystemResource {
*/
@GET
@Path("/metrics")
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATOR"})
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
@Operation(
summary = "Récupérer les métriques système en temps réel",
description = "Retourne toutes les métriques système (CPU, RAM, disque, utilisateurs actifs, etc.)"

View File

@@ -4,12 +4,13 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.lions.unionflow.server.api.dto.mutuelle.epargne.TransactionEpargneRequest;
import dev.lions.unionflow.server.api.enums.mutuelle.epargne.TypeTransactionEpargne;
import dev.lions.unionflow.server.api.enums.paiement.StatutIntentionPaiement;
import dev.lions.unionflow.server.entity.Cotisation;
import dev.lions.unionflow.server.entity.IntentionPaiement;
import dev.lions.unionflow.server.repository.IntentionPaiementRepository;
import dev.lions.unionflow.server.service.PaiementService;
import dev.lions.unionflow.server.service.mutuelle.epargne.TransactionEpargneService;
import jakarta.annotation.security.PermitAll;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.GET;
@@ -21,7 +22,6 @@ import org.jboss.logging.Logger;
import java.math.BigDecimal;
import java.net.URI;
import java.time.LocalDateTime;
import java.util.UUID;
/**
@@ -50,18 +50,65 @@ public class WaveRedirectResource {
@Inject
TransactionEpargneService transactionEpargneService;
@Inject
PaiementService paiementService;
@GET
@Path("/success")
@Transactional
public Response success(@QueryParam("ref") String ref) {
LOG.infof("Wave redirect success, ref=%s", ref);
if (mockEnabled && ref != null && !ref.isBlank()) {
applyMockCompletion(ref);
LOG.infof("Wave redirect success (mobile), ref=%s", ref);
if (ref != null && !ref.isBlank()) {
applyCompletion(ref);
}
String location = buildDeepLink("success", ref);
return Response.seeOther(URI.create(location)).build();
}
/**
* Endpoint de redirection Wave pour le flux web QR code.
* Appelé par Wave sur le téléphone du membre après paiement confirmé.
* Marque la cotisation PAYEE et affiche une page HTML de confirmation.
*/
@GET
@Path("/web-success")
@Produces(MediaType.TEXT_HTML)
@Transactional
public Response webSuccess(@QueryParam("ref") String ref) {
LOG.infof("Wave redirect web-success, ref=%s", ref);
if (ref != null && !ref.isBlank()) {
applyCompletion(ref);
}
String html = """
<!DOCTYPE html>
<html lang="fr">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Paiement confirmé</title>
<style>
body{font-family:sans-serif;display:flex;flex-direction:column;align-items:center;
justify-content:center;min-height:100vh;margin:0;background:#f0fdf4;}
.card{background:#fff;border-radius:16px;padding:2rem 2.5rem;text-align:center;
box-shadow:0 4px 24px #0001;max-width:360px;}
.icon{font-size:3.5rem;margin-bottom:1rem;}
h2{color:#16a34a;margin:.5rem 0;}
p{color:#555;margin:.5rem 0;}
</style>
</head>
<body>
<div class="card">
<div class="icon">✅</div>
<h2>Paiement confirmé !</h2>
<p>Votre cotisation a été enregistrée avec succès.</p>
<p style="font-size:.85rem;color:#888;margin-top:1rem;">
Vous pouvez fermer cette page et revenir sur UnionFlow.
</p>
</div>
</body>
</html>
""";
return Response.ok(html).build();
}
@GET
@Path("/error")
public Response error(@QueryParam("ref") String ref) {
@@ -85,40 +132,31 @@ public class WaveRedirectResource {
if (ref == null || ref.isBlank()) {
return Response.status(Response.Status.BAD_REQUEST).entity("ref requis").build();
}
applyMockCompletion(ref);
applyCompletion(ref);
return Response.seeOther(URI.create(buildDeepLink("success", ref))).build();
}
/** En mode mock : marque l'intention COMPLETEE et les cotisations liées PAYEE (simulation Wave). */
private void applyMockCompletion(String ref) {
/**
* Marque l'intention comme complétée et réconcilie les cotisations/dépôts liés.
* Délègue au PaiementService pour les cotisations ; gère les dépôts épargne localement.
*/
private void applyCompletion(String ref) {
try {
UUID intentionId = UUID.fromString(ref.trim());
IntentionPaiement intention = intentionPaiementRepository.findById(intentionId);
if (intention == null) {
LOG.warnf("Intention non trouvée pour mock: %s", ref);
LOG.warnf("Intention non trouvée: %s", ref);
return;
}
intention.setStatut(StatutIntentionPaiement.COMPLETEE);
intention.setDateCompletion(LocalDateTime.now());
intentionPaiementRepository.persist(intention);
// Gérer les dépôts épargne (non couverts par PaiementService)
String objetsCibles = intention.getObjetsCibles();
if (objetsCibles != null && !objetsCibles.isBlank()) {
JsonNode arr = OBJECT_MAPPER.readTree(objetsCibles);
if (arr.isArray()) {
for (JsonNode node : arr) {
if (node.has("type") && "COTISATION".equals(node.get("type").asText()) && node.has("id")) {
UUID cotisationId = UUID.fromString(node.get("id").asText());
Cotisation cotisation = intentionPaiementRepository.getEntityManager().find(Cotisation.class, cotisationId);
if (cotisation != null) {
BigDecimal montant = node.has("montant") ? new BigDecimal(node.get("montant").asText()) : cotisation.getMontantDu();
cotisation.setMontantPaye(montant);
cotisation.setStatut("PAYEE");
cotisation.setDatePaiement(LocalDateTime.now());
intentionPaiementRepository.getEntityManager().merge(cotisation);
LOG.infof("Mock Wave: cotisation %s marquée PAYEE", cotisationId);
}
} else if (node.has("type") && "DEPOT_EPARGNE".equals(node.get("type").asText()) && node.has("compteId") && node.has("montant")) {
if ("DEPOT_EPARGNE".equals(node.path("type").asText())
&& node.has("compteId") && node.has("montant")) {
String compteId = node.get("compteId").asText();
BigDecimal montant = new BigDecimal(node.get("montant").asText());
TransactionEpargneRequest req = TransactionEpargneRequest.builder()
@@ -128,14 +166,17 @@ public class WaveRedirectResource {
.motif("Dépôt via Wave (mobile money)")
.build();
transactionEpargneService.executerTransaction(req);
LOG.infof("Mock Wave: dépôt épargne %s XOF sur compte %s", montant, compteId);
LOG.infof("Wave: dépôt épargne %s XOF sur compte %s", montant, compteId);
}
}
}
}
LOG.infof("Mock Wave: intention %s complétée (validation simulée)", ref);
// Déléguer la complétion cotisations au service
paiementService.completerIntention(intention, null);
LOG.infof("Wave: intention %s complétée", ref);
} catch (Exception e) {
LOG.errorf(e, "Mock Wave: erreur applyMockCompletion ref=%s", ref);
LOG.errorf(e, "Wave: erreur applyCompletion ref=%s", ref);
}
}

View File

@@ -1,5 +1,6 @@
package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.api.dto.common.ErrorResponse;
import dev.lions.unionflow.server.api.dto.wave.CompteWaveDTO;
import dev.lions.unionflow.server.api.dto.wave.TransactionWaveDTO;
import dev.lions.unionflow.server.api.enums.wave.StatutTransactionWave;
@@ -56,12 +57,12 @@ public class WaveResource {
return Response.status(Response.Status.CREATED).entity(result).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse(e.getMessage()))
.entity(ErrorResponse.ofError(e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la création du compte Wave");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la création du compte Wave: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la création du compte Wave: " + e.getMessage()))
.build();
}
}
@@ -83,12 +84,12 @@ public class WaveResource {
return Response.ok(result).build();
} catch (jakarta.ws.rs.NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse("Compte Wave non trouvé"))
.entity(ErrorResponse.ofError("Compte Wave non trouvé"))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la mise à jour du compte Wave");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la mise à jour du compte Wave: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la mise à jour du compte Wave: " + e.getMessage()))
.build();
}
}
@@ -109,12 +110,12 @@ public class WaveResource {
return Response.ok(result).build();
} catch (jakarta.ws.rs.NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse("Compte Wave non trouvé"))
.entity(ErrorResponse.ofError("Compte Wave non trouvé"))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la vérification du compte Wave");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la vérification du compte Wave: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la vérification du compte Wave: " + e.getMessage()))
.build();
}
}
@@ -133,12 +134,12 @@ public class WaveResource {
return Response.ok(result).build();
} catch (jakarta.ws.rs.NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse("Compte Wave non trouvé"))
.entity(ErrorResponse.ofError("Compte Wave non trouvé"))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la recherche du compte Wave");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la recherche du compte Wave: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la recherche du compte Wave: " + e.getMessage()))
.build();
}
}
@@ -156,14 +157,14 @@ public class WaveResource {
CompteWaveDTO result = waveService.trouverCompteWaveParTelephone(numeroTelephone);
if (result == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse("Compte Wave non trouvé"))
.entity(ErrorResponse.ofError("Compte Wave non trouvé"))
.build();
}
return Response.ok(result).build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la recherche du compte Wave");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la recherche du compte Wave: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la recherche du compte Wave: " + e.getMessage()))
.build();
}
}
@@ -183,7 +184,7 @@ public class WaveResource {
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la liste des comptes Wave");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la liste des comptes Wave: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la liste des comptes Wave: " + e.getMessage()))
.build();
}
}
@@ -207,7 +208,7 @@ public class WaveResource {
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la création de la transaction Wave");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la création de la transaction Wave: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la création de la transaction Wave: " + e.getMessage()))
.build();
}
}
@@ -229,14 +230,12 @@ public class WaveResource {
return Response.ok(result).build();
} catch (jakarta.ws.rs.NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse("Transaction Wave non trouvée"))
.entity(ErrorResponse.ofError("Transaction Wave non trouvée"))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la mise à jour du statut de la transaction Wave");
return Response.status(Response.Status.BAD_REQUEST)
.entity(
new ErrorResponse(
"Erreur lors de la mise à jour du statut de la transaction Wave: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la mise à jour du statut de la transaction Wave: " + e.getMessage()))
.build();
}
}
@@ -255,22 +254,15 @@ public class WaveResource {
return Response.ok(result).build();
} catch (jakarta.ws.rs.NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse("Transaction Wave non trouvée"))
.entity(ErrorResponse.ofError("Transaction Wave non trouvée"))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la recherche de la transaction Wave");
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Erreur lors de la recherche de la transaction Wave: " + e.getMessage()))
.entity(ErrorResponse.ofError("Erreur lors de la recherche de la transaction Wave: " + e.getMessage()))
.build();
}
}
/** Classe interne pour les réponses d'erreur */
public static class ErrorResponse {
public String error;
public ErrorResponse(String error) {
this.error = error;
}
}
}