Refactoring - Bonne version améliorée
This commit is contained in:
@@ -2,7 +2,11 @@ package com.lions.dev.resource;
|
||||
|
||||
import com.lions.dev.dto.response.admin.AdminRevenueResponseDTO;
|
||||
import com.lions.dev.dto.response.admin.ManagerStatsResponseDTO;
|
||||
import com.lions.dev.security.Permission;
|
||||
import com.lions.dev.security.RequiresPermission;
|
||||
import com.lions.dev.service.AdminStatsService;
|
||||
import com.lions.dev.util.UserRoles;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
@@ -17,12 +21,16 @@ import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Ressource REST pour le tableau de bord Super Admin.
|
||||
* Requiert le header X-Super-Admin-Key pour toutes les opérations.
|
||||
*
|
||||
* SÉCURITÉ : Double vérification - JWT avec rôle SUPER_ADMIN + header X-Super-Admin-Key.
|
||||
*
|
||||
* @since 2.0 - Sécurité JWT production-ready
|
||||
*/
|
||||
@Path("/admin/stats")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Admin Stats", description = "Statistiques et KPIs (Super Admin)")
|
||||
@RequiresPermission(Permission.SUPER_ADMIN_ACCESS)
|
||||
public class AdminStatsResource {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(AdminStatsResource.class);
|
||||
|
||||
@@ -2,7 +2,11 @@ package com.lions.dev.resource;
|
||||
|
||||
import com.lions.dev.dto.request.booking.ReservationCreateRequestDTO;
|
||||
import com.lions.dev.dto.response.booking.ReservationResponseDTO;
|
||||
import com.lions.dev.security.Permission;
|
||||
import com.lions.dev.security.RequiresPermission;
|
||||
import com.lions.dev.service.BookingService;
|
||||
import com.lions.dev.util.UserRoles;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
@@ -18,12 +22,16 @@ import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Ressource REST pour les réservations (bookings).
|
||||
* Path /reservations pour alignement avec le frontend Flutter (ReservationsScreen).
|
||||
*
|
||||
* SÉCURITÉ : Tous les endpoints requièrent une authentification.
|
||||
*
|
||||
* @since 2.0 - Sécurité JWT production-ready
|
||||
*/
|
||||
@Path("/reservations")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Reservations", description = "Réservations d'établissements")
|
||||
@RolesAllowed({UserRoles.USER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||
public class BookingResource {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(BookingResource.class);
|
||||
@@ -33,6 +41,7 @@ public class BookingResource {
|
||||
|
||||
@GET
|
||||
@Path("/user/{userId}")
|
||||
@RequiresPermission(value = {Permission.RESERVATIONS_VIEW_OWN, Permission.RESERVATIONS_VIEW_ALL}, requireAll = false)
|
||||
@Operation(summary = "Liste des réservations d'un utilisateur")
|
||||
public Response getUserReservations(@PathParam("userId") UUID userId) {
|
||||
List<ReservationResponseDTO> list = bookingService.getReservationsByUserId(userId);
|
||||
@@ -41,6 +50,7 @@ public class BookingResource {
|
||||
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
@RequiresPermission(value = {Permission.RESERVATIONS_VIEW_OWN, Permission.RESERVATIONS_VIEW_ALL}, requireAll = false)
|
||||
@Operation(summary = "Détail d'une réservation")
|
||||
public Response getReservation(@PathParam("id") UUID id) {
|
||||
ReservationResponseDTO dto = bookingService.getReservationById(id);
|
||||
@@ -52,6 +62,7 @@ public class BookingResource {
|
||||
|
||||
@POST
|
||||
@Transactional
|
||||
@RequiresPermission(Permission.RESERVATIONS_CREATE)
|
||||
@Operation(summary = "Créer une réservation")
|
||||
public Response createReservation(@Valid ReservationCreateRequestDTO dto) {
|
||||
try {
|
||||
@@ -65,6 +76,7 @@ public class BookingResource {
|
||||
@PUT
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@RequiresPermission(value = {Permission.RESERVATIONS_UPDATE_OWN, Permission.RESERVATIONS_UPDATE_ANY}, requireAll = false)
|
||||
@Operation(summary = "Mettre à jour une réservation")
|
||||
public Response updateReservation(@PathParam("id") UUID id, @Valid ReservationCreateRequestDTO dto) {
|
||||
ReservationResponseDTO updated = bookingService.updateReservation(id, dto);
|
||||
@@ -77,6 +89,7 @@ public class BookingResource {
|
||||
@PUT
|
||||
@Path("/{id}/cancel")
|
||||
@Transactional
|
||||
@RequiresPermission(value = {Permission.RESERVATIONS_CANCEL_OWN, Permission.RESERVATIONS_CANCEL_ANY}, requireAll = false)
|
||||
@Operation(summary = "Annuler une réservation")
|
||||
public Response cancelReservation(@PathParam("id") UUID id) {
|
||||
ReservationResponseDTO dto = bookingService.cancelReservation(id);
|
||||
@@ -89,6 +102,7 @@ public class BookingResource {
|
||||
@DELETE
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@RequiresPermission(value = {Permission.RESERVATIONS_DELETE_OWN, Permission.RESERVATIONS_DELETE_ANY}, requireAll = false)
|
||||
@Operation(summary = "Supprimer une réservation")
|
||||
public Response deleteReservation(@PathParam("id") UUID id) {
|
||||
if (bookingService.getReservationById(id) == null) {
|
||||
|
||||
@@ -13,8 +13,12 @@ import com.lions.dev.repository.EstablishmentAmenityRepository;
|
||||
import com.lions.dev.repository.EstablishmentRepository;
|
||||
import com.lions.dev.repository.EventsRepository;
|
||||
import com.lions.dev.repository.UsersRepository;
|
||||
import com.lions.dev.security.Permission;
|
||||
import com.lions.dev.security.RequiresPermission;
|
||||
import com.lions.dev.service.EstablishmentService;
|
||||
import com.lions.dev.service.SecurityService;
|
||||
import com.lions.dev.util.UserRoles;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
@@ -32,13 +36,16 @@ import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Ressource REST pour la gestion des établissements dans le système AfterWork.
|
||||
* Cette classe expose des endpoints pour créer, récupérer, mettre à jour et supprimer des établissements.
|
||||
* Seuls les responsables d'établissement peuvent créer et gérer des établissements.
|
||||
*
|
||||
* SÉCURITÉ : Les lectures sont publiques, les écritures requièrent une authentification MANAGER ou plus.
|
||||
*
|
||||
* @since 2.0 - Sécurité JWT production-ready
|
||||
*/
|
||||
@Path("/establishments")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Establishments", description = "Opérations liées à la gestion des établissements")
|
||||
@jakarta.annotation.security.RolesAllowed({UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||
public class EstablishmentResource {
|
||||
|
||||
@Inject
|
||||
@@ -59,14 +66,18 @@ public class EstablishmentResource {
|
||||
@Inject
|
||||
EventsRepository eventsRepository;
|
||||
|
||||
@Inject
|
||||
SecurityService securityService;
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(EstablishmentResource.class);
|
||||
|
||||
// *********** Création d'un établissement ***********
|
||||
|
||||
@POST
|
||||
@Transactional
|
||||
@RequiresPermission(Permission.ESTABLISHMENTS_CREATE)
|
||||
@Operation(summary = "Créer un nouvel établissement",
|
||||
description = "Crée un nouvel établissement. Seuls les responsables d'établissement peuvent créer des établissements.")
|
||||
description = "Crée un nouvel établissement. Requiert la permission ESTABLISHMENTS_CREATE.")
|
||||
public Response createEstablishment(@Valid EstablishmentCreateRequestDTO requestDTO) {
|
||||
LOG.info("[LOG] Tentative de création d'un nouvel établissement : " + requestDTO.getName());
|
||||
|
||||
@@ -144,6 +155,7 @@ public class EstablishmentResource {
|
||||
// *********** Récupération de tous les établissements ***********
|
||||
|
||||
@GET
|
||||
@PermitAll
|
||||
@Operation(summary = "Récupérer tous les établissements",
|
||||
description = "Retourne la liste de tous les établissements disponibles")
|
||||
public Response getAllEstablishments() {
|
||||
@@ -166,6 +178,7 @@ public class EstablishmentResource {
|
||||
|
||||
@GET
|
||||
@Path("/{id}/business-hours")
|
||||
@PermitAll
|
||||
@Operation(summary = "Récupérer les horaires d'ouverture d'un établissement",
|
||||
description = "Retourne la liste des horaires d'ouverture pour l'établissement donné")
|
||||
public Response getBusinessHoursByEstablishmentId(@PathParam("id") UUID id) {
|
||||
@@ -190,6 +203,7 @@ public class EstablishmentResource {
|
||||
|
||||
@GET
|
||||
@Path("/{id}/amenities")
|
||||
@PermitAll
|
||||
@Operation(summary = "Récupérer les équipements d'un établissement",
|
||||
description = "Retourne la liste des équipements (amenities) pour l'établissement donné")
|
||||
public Response getAmenitiesByEstablishmentId(@PathParam("id") UUID id) {
|
||||
@@ -218,6 +232,7 @@ public class EstablishmentResource {
|
||||
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
@PermitAll
|
||||
@Operation(summary = "Récupérer un établissement par ID",
|
||||
description = "Retourne les détails de l'établissement demandé")
|
||||
public Response getEstablishmentById(@PathParam("id") UUID id) {
|
||||
@@ -244,6 +259,7 @@ public class EstablishmentResource {
|
||||
|
||||
@GET
|
||||
@Path("/search")
|
||||
@PermitAll
|
||||
@Operation(summary = "Rechercher des établissements",
|
||||
description = "Recherche des établissements par nom ou ville")
|
||||
public Response searchEstablishments(@QueryParam("q") String query) {
|
||||
@@ -271,6 +287,7 @@ public class EstablishmentResource {
|
||||
|
||||
@GET
|
||||
@Path("/filter")
|
||||
@PermitAll
|
||||
@Operation(summary = "Filtrer les établissements",
|
||||
description = "Filtre les établissements par type, fourchette de prix et/ou ville")
|
||||
public Response filterEstablishments(
|
||||
@@ -296,6 +313,7 @@ public class EstablishmentResource {
|
||||
|
||||
@GET
|
||||
@Path("/manager/{managerId}")
|
||||
@PermitAll
|
||||
@Operation(summary = "Récupérer les établissements d'un responsable",
|
||||
description = "Retourne tous les établissements gérés par un responsable")
|
||||
public Response getEstablishmentsByManager(@PathParam("managerId") UUID managerId) {
|
||||
@@ -319,8 +337,9 @@ public class EstablishmentResource {
|
||||
@PUT
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@RequiresPermission(value = {Permission.ESTABLISHMENTS_UPDATE_OWN, Permission.ESTABLISHMENTS_UPDATE_ANY}, requireAll = false)
|
||||
@Operation(summary = "Mettre à jour un établissement",
|
||||
description = "Met à jour les informations d'un établissement existant")
|
||||
description = "Met à jour les informations d'un établissement existant. Requiert ESTABLISHMENTS_UPDATE_OWN ou ESTABLISHMENTS_UPDATE_ANY.")
|
||||
public Response updateEstablishment(
|
||||
@PathParam("id") UUID id,
|
||||
@Valid EstablishmentUpdateRequestDTO requestDTO) {
|
||||
@@ -362,8 +381,9 @@ public class EstablishmentResource {
|
||||
@DELETE
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@RequiresPermission(value = {Permission.ESTABLISHMENTS_DELETE_OWN, Permission.ESTABLISHMENTS_DELETE_ANY}, requireAll = false)
|
||||
@Operation(summary = "Supprimer un établissement",
|
||||
description = "Supprime un établissement du système")
|
||||
description = "Supprime un établissement du système. Requiert ESTABLISHMENTS_DELETE_OWN ou ESTABLISHMENTS_DELETE_ANY.")
|
||||
public Response deleteEstablishment(@PathParam("id") UUID id) {
|
||||
LOG.info("[LOG] Suppression de l'établissement avec l'ID : " + id);
|
||||
try {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.lions.dev.resource;
|
||||
|
||||
import com.lions.dev.core.errors.exceptions.EventNotFoundException;
|
||||
import com.lions.dev.core.security.JwtAuthFilter;
|
||||
import com.lions.dev.core.security.RequiresAuth;
|
||||
import com.lions.dev.dto.request.events.EventCreateRequestDTO;
|
||||
import com.lions.dev.dto.request.events.EventReadManyByIdRequestDTO;
|
||||
import com.lions.dev.dto.request.events.EventUpdateRequestDTO;
|
||||
@@ -19,13 +17,17 @@ import com.lions.dev.entity.users.Users;
|
||||
import com.lions.dev.repository.EventShareRepository;
|
||||
import com.lions.dev.repository.EventsRepository;
|
||||
import com.lions.dev.repository.UsersRepository;
|
||||
import com.lions.dev.security.Permission;
|
||||
import com.lions.dev.security.RequiresPermission;
|
||||
import com.lions.dev.service.EventService;
|
||||
import com.lions.dev.service.FriendshipService;
|
||||
import com.lions.dev.service.SecurityService;
|
||||
import com.lions.dev.util.UserRoles;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.io.File;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -43,14 +45,18 @@ import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Ressource REST pour la gestion des événements dans le système AfterWork.
|
||||
* Cette classe expose des endpoints pour créer, récupérer, mettre à jour et supprimer des événements.
|
||||
*
|
||||
* SÉCURITÉ : Tous les endpoints modifiant des données sont protégés par JWT.
|
||||
* Les endpoints de lecture publique (GET) restent accessibles à tous.
|
||||
* Les modifications sont réservées au créateur de l'événement ou aux admins.
|
||||
*
|
||||
* Tous les logs nécessaires pour la traçabilité sont intégrés.
|
||||
* @since 2.0 - Sécurité JWT production-ready
|
||||
*/
|
||||
@Path("/events")
|
||||
@Produces("application/json")
|
||||
@Consumes("application/json")
|
||||
@Tag(name = "Events", description = "Opérations liées à la gestion des événements")
|
||||
@RolesAllowed({UserRoles.USER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||
public class EventsResource {
|
||||
|
||||
@Inject
|
||||
@@ -68,40 +74,31 @@ public class EventsResource {
|
||||
@Inject
|
||||
FriendshipService friendshipService;
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(EventsResource.class);
|
||||
@Inject
|
||||
SecurityService securityService;
|
||||
|
||||
/**
|
||||
* Extrait l'ID de l'utilisateur authentifié du contexte de la requête.
|
||||
*
|
||||
* @param requestContext Le contexte de la requête
|
||||
* @return L'ID de l'utilisateur authentifié
|
||||
*/
|
||||
private UUID getAuthenticatedUserId(ContainerRequestContext requestContext) {
|
||||
return (UUID) requestContext.getProperty(JwtAuthFilter.AUTHENTICATED_USER_ID);
|
||||
}
|
||||
private static final Logger LOG = Logger.getLogger(EventsResource.class);
|
||||
|
||||
// *********** Création d'un événement ***********
|
||||
|
||||
@POST
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@Operation(summary = "Créer un nouvel événement", description = "Crée un nouvel événement. Requiert une authentification JWT.")
|
||||
@RequiresPermission(Permission.EVENTS_CREATE)
|
||||
@Operation(summary = "Créer un nouvel événement", description = "Crée un nouvel événement. Requiert une authentification JWT et permission EVENTS_CREATE.")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@APIResponse(responseCode = "201", description = "Événement créé avec succès")
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "403", description = "L'utilisateur authentifié ne correspond pas au creatorId")
|
||||
public Response createEvent(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
EventCreateRequestDTO eventCreateRequestDTO) {
|
||||
@APIResponse(responseCode = "403", description = "L'utilisateur authentifié ne correspond pas au creatorId ou n'a pas la permission")
|
||||
public Response createEvent(EventCreateRequestDTO eventCreateRequestDTO) {
|
||||
LOG.info("[LOG] Tentative de création d'un nouvel événement : " + eventCreateRequestDTO.getTitle());
|
||||
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
|
||||
// Valider que creatorId est fourni
|
||||
if (eventCreateRequestDTO.getCreatorId() == null) {
|
||||
LOG.error("[ERROR] creatorId est obligatoire pour créer un événement");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity("L'identifiant du créateur (creatorId) est obligatoire")
|
||||
.entity(Map.of("message", "L'identifiant du créateur (creatorId) est obligatoire"))
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -109,7 +106,7 @@ public class EventsResource {
|
||||
if (!authenticatedUserId.equals(eventCreateRequestDTO.getCreatorId())) {
|
||||
LOG.warn("[WARN] Utilisateur " + authenticatedUserId + " tente de créer un événement pour " + eventCreateRequestDTO.getCreatorId());
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
.entity("{\"message\": \"Vous ne pouvez créer un événement que pour votre propre compte.\"}")
|
||||
.entity(Map.of("message", "Vous ne pouvez créer un événement que pour votre propre compte."))
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -118,7 +115,7 @@ public class EventsResource {
|
||||
if (creator == null) {
|
||||
LOG.error("[ERROR] Créateur non trouvé avec l'ID : " + eventCreateRequestDTO.getCreatorId());
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity("Créateur non trouvé avec l'ID fourni")
|
||||
.entity(Map.of("message", "Créateur non trouvé avec l'ID fourni"))
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -133,6 +130,7 @@ public class EventsResource {
|
||||
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
@PermitAll
|
||||
@Operation(summary = "Récupérer un événement par ID", description = "Retourne les détails de l'événement demandé")
|
||||
public Response getEventById(@PathParam("id") UUID id) {
|
||||
LOG.info("[LOG] Récupération de l'événement avec l'ID : " + id);
|
||||
@@ -151,17 +149,15 @@ public class EventsResource {
|
||||
@DELETE
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@Operation(summary = "Supprimer un événement", description = "Supprime un événement. Seul le créateur peut supprimer.")
|
||||
@RequiresPermission(value = {Permission.EVENTS_DELETE_OWN, Permission.EVENTS_DELETE_ANY}, requireAll = false)
|
||||
@Operation(summary = "Supprimer un événement", description = "Supprime un événement. Seul le créateur ou admin peut supprimer.")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@APIResponse(responseCode = "204", description = "Événement supprimé")
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "403", description = "Non autorisé à supprimer cet événement")
|
||||
@APIResponse(responseCode = "404", description = "Événement non trouvé")
|
||||
public Response deleteEvent(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("id") UUID id) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
public Response deleteEvent(@PathParam("id") UUID id) {
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("Tentative de suppression de l'événement avec l'ID : " + id + " par l'utilisateur : " + authenticatedUserId);
|
||||
|
||||
try {
|
||||
@@ -230,18 +226,17 @@ public class EventsResource {
|
||||
@PUT
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@Operation(summary = "Mettre à jour un événement", description = "Modifie un événement. Seul le créateur peut modifier.")
|
||||
@RequiresPermission(value = {Permission.EVENTS_UPDATE_OWN, Permission.EVENTS_UPDATE_ANY}, requireAll = false)
|
||||
@Operation(summary = "Mettre à jour un événement", description = "Modifie un événement. Seul le créateur ou admin peut modifier.")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@APIResponse(responseCode = "200", description = "Événement mis à jour")
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "403", description = "Non autorisé à modifier cet événement")
|
||||
@APIResponse(responseCode = "404", description = "Événement non trouvé")
|
||||
public Response updateEvent(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("id") UUID id,
|
||||
EventUpdateRequestDTO eventUpdateRequestDTO) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("[LOG] Tentative de mise à jour de l'événement avec l'ID : " + id + " par l'utilisateur : " + authenticatedUserId);
|
||||
|
||||
Events event = eventsRepository.findById(id);
|
||||
@@ -325,6 +320,7 @@ public class EventsResource {
|
||||
*/
|
||||
@GET
|
||||
@Path("/category/{category}")
|
||||
@PermitAll
|
||||
@Operation(
|
||||
summary = "Récupérer les événements par catégorie",
|
||||
description = "Retourne la liste paginée des événements correspondant à une catégorie donnée")
|
||||
@@ -358,6 +354,7 @@ public class EventsResource {
|
||||
*/
|
||||
@GET
|
||||
@Path("/between-dates")
|
||||
@PermitAll
|
||||
@Operation(
|
||||
summary = "Récupérer les événements entre deux dates",
|
||||
description = "Retourne la liste des événements qui se déroulent entre deux dates spécifiques")
|
||||
@@ -396,6 +393,7 @@ public class EventsResource {
|
||||
*/
|
||||
@GET
|
||||
@Path("/status/{status}")
|
||||
@PermitAll
|
||||
@Operation(
|
||||
summary = "Récupérer les événements par statut",
|
||||
description = "Retourne la liste des événements correspondant à un statut spécifique")
|
||||
@@ -427,6 +425,7 @@ public class EventsResource {
|
||||
*/
|
||||
@GET
|
||||
@Path("/search")
|
||||
@PermitAll
|
||||
@Operation(
|
||||
summary = "Rechercher des événements par mots-clés",
|
||||
description = "Retourne la liste paginée des événements dont le titre ou la description contient les mots-clés spécifiés")
|
||||
@@ -463,19 +462,18 @@ public class EventsResource {
|
||||
@PUT
|
||||
@Path("/{id}/status")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@RequiresPermission(value = {Permission.EVENTS_UPDATE_OWN, Permission.EVENTS_UPDATE_ANY}, requireAll = false)
|
||||
@Operation(
|
||||
summary = "Mettre à jour le statut d'un événement",
|
||||
description = "Modifie le statut d'un événement. Seul le créateur peut modifier.")
|
||||
description = "Modifie le statut d'un événement. Seul le créateur ou admin peut modifier.")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@APIResponse(responseCode = "200", description = "Statut mis à jour")
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "403", description = "Non autorisé à modifier cet événement")
|
||||
public Response updateEventStatus(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("id") UUID id,
|
||||
@QueryParam("status") String status) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("[LOG] Mise à jour du statut de l'événement avec l'ID : " + id + " par l'utilisateur : " + authenticatedUserId);
|
||||
|
||||
Events event = eventsRepository.findById(id);
|
||||
@@ -545,17 +543,16 @@ public class EventsResource {
|
||||
*/
|
||||
@PUT
|
||||
@Path("/{id}/image")
|
||||
@RequiresAuth
|
||||
@Operation(summary = "Mettre à jour l'image d'un événement", description = "Modifie l'image de l'événement. Seul le créateur peut modifier.")
|
||||
@RequiresPermission(value = {Permission.EVENTS_UPDATE_OWN, Permission.EVENTS_UPDATE_ANY}, requireAll = false)
|
||||
@Operation(summary = "Mettre à jour l'image d'un événement", description = "Modifie l'image de l'événement. Seul le créateur ou admin peut modifier.")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@APIResponse(responseCode = "200", description = "Image mise à jour")
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "403", description = "Non autorisé à modifier cet événement")
|
||||
public Response updateEventImage(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("id") UUID id,
|
||||
String imageFilePath) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("[LOG] Tentative de mise à jour de l'image pour l'événement avec l'ID : " + id + " par l'utilisateur : " + authenticatedUserId);
|
||||
|
||||
try {
|
||||
@@ -598,18 +595,17 @@ public class EventsResource {
|
||||
@PATCH
|
||||
@Path("/{id}/partial-update")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@Operation(summary = "Mettre à jour partiellement un événement", description = "Mise à jour partielle. Seul le créateur peut modifier.")
|
||||
@RequiresPermission(value = {Permission.EVENTS_UPDATE_OWN, Permission.EVENTS_UPDATE_ANY}, requireAll = false)
|
||||
@Operation(summary = "Mettre à jour partiellement un événement", description = "Mise à jour partielle. Seul le créateur ou admin peut modifier.")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@APIResponse(responseCode = "200", description = "Événement mis à jour")
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "403", description = "Non autorisé à modifier cet événement")
|
||||
@APIResponse(responseCode = "404", description = "Événement non trouvé")
|
||||
public Response partialUpdateEvent(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("id") UUID id,
|
||||
Map<String, Object> updates) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("[LOG] Tentative de mise à jour partielle de l'événement avec l'ID : " + id + " par l'utilisateur : " + authenticatedUserId);
|
||||
|
||||
Events event = eventsRepository.findById(id);
|
||||
@@ -660,6 +656,7 @@ public class EventsResource {
|
||||
// *********** Récupérer les événements à venir ***********
|
||||
@GET
|
||||
@Path("/upcoming")
|
||||
@PermitAll
|
||||
@Operation(summary = "Récupérer les événements à venir", description = "Retourne les événements futurs avec pagination.")
|
||||
public Response getUpcomingEvents(
|
||||
@QueryParam("page") @DefaultValue("0") int page,
|
||||
@@ -679,6 +676,7 @@ public class EventsResource {
|
||||
// *********** Récupérer les événements passés ***********
|
||||
@GET
|
||||
@Path("/past")
|
||||
@PermitAll
|
||||
@Operation(summary = "Récupérer les événements passés", description = "Retourne les événements déjà terminés avec pagination.")
|
||||
public Response getPastEvents(
|
||||
@QueryParam("page") @DefaultValue("0") int page,
|
||||
@@ -699,17 +697,15 @@ public class EventsResource {
|
||||
@POST
|
||||
@Path("/{id}/cancel")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@Operation(summary = "Annuler un événement", description = "Annule un événement sans le supprimer. Seul le créateur peut annuler.")
|
||||
@RequiresPermission(value = {Permission.EVENTS_UPDATE_OWN, Permission.EVENTS_UPDATE_ANY}, requireAll = false)
|
||||
@Operation(summary = "Annuler un événement", description = "Annule un événement sans le supprimer. Seul le créateur ou admin peut annuler.")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@APIResponse(responseCode = "200", description = "Événement annulé")
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "403", description = "Non autorisé à annuler cet événement")
|
||||
@APIResponse(responseCode = "404", description = "Événement non trouvé")
|
||||
public Response cancelEvent(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("id") UUID id) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
public Response cancelEvent(@PathParam("id") UUID id) {
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("[LOG] Annulation de l'événement avec l'ID : " + id + " par l'utilisateur : " + authenticatedUserId);
|
||||
|
||||
Events event = eventsRepository.findById(id);
|
||||
@@ -733,6 +729,7 @@ public class EventsResource {
|
||||
// *********** Récupérer les événements par localisation ***********
|
||||
@GET
|
||||
@Path("/location/{location}")
|
||||
@PermitAll
|
||||
@Operation(summary = "Récupérer les événements par localisation", description = "Retourne les événements situés à une localisation spécifique.")
|
||||
public Response getEventsByLocation(@PathParam("location") String location) {
|
||||
LOG.info("[LOG] Récupération des événements à la localisation : " + location);
|
||||
@@ -750,6 +747,7 @@ public class EventsResource {
|
||||
// *********** Récupérer les événements populaires ***********
|
||||
@GET
|
||||
@Path("/popular")
|
||||
@PermitAll
|
||||
@Operation(summary = "Récupérer les événements populaires", description = "Retourne les événements ayant le plus de participants.")
|
||||
public Response getPopularEvents() {
|
||||
LOG.info("[LOG] Récupération des événements populaires.");
|
||||
@@ -813,6 +811,7 @@ public class EventsResource {
|
||||
|
||||
@GET
|
||||
@Path("/{id}/participants")
|
||||
@PermitAll
|
||||
@Operation(summary = "Récupérer la liste des participants d'un événement", description = "Retourne la liste des utilisateurs participant à un événement spécifique.")
|
||||
public Response getParticipants(@PathParam("id") UUID eventId) {
|
||||
// Log d'entrée de la méthode
|
||||
@@ -850,16 +849,13 @@ public class EventsResource {
|
||||
@POST
|
||||
@Path("/{id}/favorite")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@Operation(summary = "Toggle favori d'un événement", description = "Permet à l'utilisateur authentifié d'ajouter ou retirer un événement de ses favoris.")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@APIResponse(responseCode = "200", description = "Favori modifié")
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "404", description = "Événement non trouvé")
|
||||
public Response favoriteEvent(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("id") UUID eventId) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
public Response favoriteEvent(@PathParam("id") UUID eventId) {
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("[LOG] Toggle favori de l'événement " + eventId + " pour l'utilisateur ID : " + authenticatedUserId);
|
||||
|
||||
Events event = eventsRepository.findById(eventId);
|
||||
@@ -914,6 +910,7 @@ public class EventsResource {
|
||||
|
||||
@GET
|
||||
@Path("/{id}/comments")
|
||||
@PermitAll
|
||||
@Operation(summary = "Récupérer les commentaires d'un événement", description = "Retourne la liste des commentaires associés à un événement.")
|
||||
public Response getComments(@PathParam("id") UUID eventId) {
|
||||
LOG.info("[LOG] Récupération des commentaires pour l'événement ID : " + eventId);
|
||||
@@ -940,17 +937,15 @@ public class EventsResource {
|
||||
@POST
|
||||
@Path("/{id}/comments")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@Operation(summary = "Ajouter un commentaire à un événement", description = "Crée un nouveau commentaire pour un événement. Requiert une authentification JWT.")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@APIResponse(responseCode = "201", description = "Commentaire créé")
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "404", description = "Événement non trouvé")
|
||||
public Response addComment(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("id") UUID eventId,
|
||||
Map<String, String> requestBody) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("[LOG] Ajout d'un commentaire à l'événement ID : " + eventId + " par l'utilisateur ID : " + authenticatedUserId);
|
||||
|
||||
Events event = eventsRepository.findById(eventId);
|
||||
@@ -1007,6 +1002,7 @@ public class EventsResource {
|
||||
|
||||
@GET
|
||||
@Path("/{id}/share-link")
|
||||
@PermitAll
|
||||
@Operation(summary = "Partager un événement via un lien", description = "Génère un lien de partage pour un événement.")
|
||||
public Response getShareLink(@PathParam("id") UUID eventId) {
|
||||
LOG.info("[LOG] Génération du lien de partage pour l'événement ID : " + eventId);
|
||||
@@ -1024,16 +1020,13 @@ public class EventsResource {
|
||||
@POST
|
||||
@Path("/{id}/share")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@Operation(summary = "Enregistrer un partage d'événement", description = "Enregistre que l'utilisateur authentifié a partagé l'événement.")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@APIResponse(responseCode = "201", description = "Partage enregistré")
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "404", description = "Événement non trouvé")
|
||||
public Response shareEvent(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("id") UUID eventId) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
public Response shareEvent(@PathParam("id") UUID eventId) {
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("[LOG] Partage de l'événement ID : " + eventId + " par l'utilisateur ID : " + authenticatedUserId);
|
||||
|
||||
Events event = eventsRepository.findById(eventId);
|
||||
@@ -1072,20 +1065,18 @@ public class EventsResource {
|
||||
@PATCH
|
||||
@Path("/{id}/close")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@RequiresPermission(value = {Permission.EVENTS_UPDATE_OWN, Permission.EVENTS_UPDATE_ANY}, requireAll = false)
|
||||
@Operation(
|
||||
summary = "Fermer un événement",
|
||||
description = "Ferme un événement et empêche les nouvelles participations. Seul le créateur peut fermer."
|
||||
description = "Ferme un événement et empêche les nouvelles participations. Seul le créateur ou admin peut fermer."
|
||||
)
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@APIResponse(responseCode = "200", description = "Événement fermé")
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "403", description = "Non autorisé à fermer cet événement")
|
||||
@APIResponse(responseCode = "404", description = "Événement non trouvé")
|
||||
public Response closeEvent(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("id") UUID eventId) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
public Response closeEvent(@PathParam("id") UUID eventId) {
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("Tentative de fermeture de l'événement avec l'ID : " + eventId + " par l'utilisateur : " + authenticatedUserId);
|
||||
|
||||
// Recherche de l'événement par ID
|
||||
@@ -1124,20 +1115,18 @@ public class EventsResource {
|
||||
@PATCH
|
||||
@Path("{eventId}/reopen")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@RequiresPermission(value = {Permission.EVENTS_UPDATE_OWN, Permission.EVENTS_UPDATE_ANY}, requireAll = false)
|
||||
@Operation(
|
||||
summary = "Rouvrir un événement",
|
||||
description = "Rouvre un événement fermé. Seul le créateur peut réouvrir."
|
||||
description = "Rouvre un événement fermé. Seul le créateur ou admin peut réouvrir."
|
||||
)
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@APIResponse(responseCode = "200", description = "Événement rouvert")
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "403", description = "Non autorisé à réouvrir cet événement")
|
||||
@APIResponse(responseCode = "404", description = "Événement non trouvé")
|
||||
public Response reopenEvent(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("eventId") UUID eventId) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
public Response reopenEvent(@PathParam("eventId") UUID eventId) {
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("Tentative de réouverture de l'événement avec l'ID : " + eventId + " par l'utilisateur : " + authenticatedUserId);
|
||||
|
||||
// Recherche de l'événement par ID
|
||||
|
||||
@@ -9,6 +9,9 @@ import com.lions.dev.dto.response.friends.FriendshipReadStatusResponseDTO;
|
||||
import com.lions.dev.entity.friends.FriendshipStatus;
|
||||
import com.lions.dev.exception.UserNotFoundException;
|
||||
import com.lions.dev.service.FriendshipService;
|
||||
import com.lions.dev.service.SecurityService;
|
||||
import com.lions.dev.util.UserRoles;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
@@ -17,6 +20,7 @@ import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Content;
|
||||
@@ -27,17 +31,25 @@ import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Ressource REST pour gérer les amitiés. Ce contrôleur expose des endpoints pour envoyer, accepter,
|
||||
* rejeter et supprimer des demandes d'amitié. Toutes les opérations sont loguées pour faciliter le
|
||||
* suivi en temps réel.
|
||||
* Ressource REST pour gérer les amitiés.
|
||||
*
|
||||
* SÉCURITÉ : Tous les endpoints sont protégés par JWT.
|
||||
* L'utilisateur ne peut gérer que SES PROPRES relations d'amitié.
|
||||
*
|
||||
* @since 2.0 - Sécurité JWT production-ready
|
||||
*/
|
||||
@Path("/friends")
|
||||
@Produces(MediaType.APPLICATION_JSON) // Assure que la réponse sera en JSON
|
||||
@Consumes(MediaType.APPLICATION_JSON) // Assure que la requête attend du JSON
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Friendship", description = "Opérations liées à la gestion des amis")
|
||||
@RolesAllowed({UserRoles.USER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||
public class FriendshipResource {
|
||||
|
||||
@Inject FriendshipService friendshipService; // Injection du service d'amitié
|
||||
@Inject
|
||||
FriendshipService friendshipService;
|
||||
|
||||
@Inject
|
||||
SecurityService securityService;
|
||||
|
||||
private static final Logger logger = Logger.getLogger(FriendshipResource.class);
|
||||
|
||||
@@ -67,12 +79,21 @@ public class FriendshipResource {
|
||||
description = "Erreur serveur lors de l'envoi de la demande d'amitié")
|
||||
})
|
||||
public Response sendFriendRequest(@Valid @NotNull FriendshipCreateOneRequestDTO request) {
|
||||
UUID currentUserId = securityService.getCurrentUserId();
|
||||
logger.info(
|
||||
"[LOG] Reçu une demande pour envoyer une demande d'amitié de l'utilisateur "
|
||||
+ request.getUserId()
|
||||
+ " à l'utilisateur "
|
||||
+ request.getFriendId());
|
||||
|
||||
// Vérifier que l'utilisateur envoie la demande en son propre nom
|
||||
if (!currentUserId.equals(request.getUserId())) {
|
||||
logger.warn("[WARN] Utilisateur " + currentUserId + " tente d'envoyer une demande pour " + request.getUserId());
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
.entity(Map.of("message", "Vous ne pouvez envoyer des demandes d'amitié qu'en votre propre nom."))
|
||||
.build();
|
||||
}
|
||||
|
||||
try {
|
||||
// Appel du service pour envoyer la demande d'amitié
|
||||
FriendshipCreateOneResponseDTO friendshipResponse =
|
||||
@@ -86,17 +107,17 @@ public class FriendshipResource {
|
||||
} catch (UserNotFoundException e) {
|
||||
logger.warn("[WARN] Utilisateur non trouvé : " + e.getMessage());
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity("{\"message\": \"Utilisateur non trouvé.\"}")
|
||||
.entity(Map.of("message", "Utilisateur non trouvé."))
|
||||
.build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("[WARN] Requête invalide : " + e.getMessage());
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity("{\"message\": \"" + e.getMessage() + "\"}")
|
||||
.entity(Map.of("message", e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
logger.error("[ERROR] Erreur lors de l'envoi de la demande d'amitié : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("{\"message\": \"Erreur lors de l'envoi de la demande d'amitié.\"}")
|
||||
.entity(Map.of("message", "Erreur lors de l'envoi de la demande d'amitié."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -241,12 +262,15 @@ public class FriendshipResource {
|
||||
@PathParam("userId") UUID userId,
|
||||
@QueryParam("page") @DefaultValue("0") int page,
|
||||
@QueryParam("size") @DefaultValue("10") int size) {
|
||||
// Vérifier que l'utilisateur accède à ses propres données ou est admin
|
||||
securityService.requireSameUserOrRole(userId, UserRoles.ADMIN, UserRoles.SUPER_ADMIN);
|
||||
|
||||
logger.info("[LOG] Reçu une demande pour récupérer la liste des amis de l'utilisateur avec l'ID : " + userId);
|
||||
|
||||
try {
|
||||
List<FriendshipReadFriendDetailsResponseDTO> friendships = friendshipService.listFriends(userId, page, size)
|
||||
.stream()
|
||||
.distinct() // Assure qu'il n'y a pas de doublons
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
logger.info("[LOG] Liste des amis récupérée avec succès, nombre d'amis : " + friendships.size());
|
||||
@@ -254,12 +278,12 @@ public class FriendshipResource {
|
||||
} catch (UserNotFoundException e) {
|
||||
logger.error("[ERROR] Utilisateur non trouvé : " + e.getMessage());
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity("{\"message\": \"Utilisateur non trouvé.\"}")
|
||||
.entity(Map.of("message", "Utilisateur non trouvé."))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
logger.error("[ERROR] Erreur lors de la récupération de la liste des amis : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("{\"message\": \"Erreur lors de la récupération des amis.\"}")
|
||||
.entity(Map.of("message", "Erreur lors de la récupération des amis."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -294,6 +318,9 @@ public class FriendshipResource {
|
||||
@PathParam("userId") UUID userId,
|
||||
@QueryParam("page") @DefaultValue("0") int page,
|
||||
@QueryParam("size") @DefaultValue("10") int size) {
|
||||
// Vérifier que l'utilisateur accède à ses propres demandes ou est admin
|
||||
securityService.requireSameUserOrRole(userId, UserRoles.ADMIN, UserRoles.SUPER_ADMIN);
|
||||
|
||||
logger.info("[LOG] Récupération des demandes d'amitié en attente pour l'utilisateur : " + userId);
|
||||
|
||||
try {
|
||||
@@ -307,7 +334,7 @@ public class FriendshipResource {
|
||||
logger.error(
|
||||
"[ERROR] Erreur lors de la récupération des demandes d'amitié : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("{\"message\": \"Erreur lors de la récupération des demandes d'amitié.\"}")
|
||||
.entity(Map.of("message", "Erreur lors de la récupération des demandes d'amitié."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -338,6 +365,9 @@ public class FriendshipResource {
|
||||
})
|
||||
public Response listFriendRequestsByStatus(
|
||||
@Valid @NotNull FriendshipReadStatusRequestDTO request) {
|
||||
// Vérifier que l'utilisateur accède à ses propres demandes ou est admin
|
||||
securityService.requireSameUserOrRole(request.getUserId(), UserRoles.ADMIN, UserRoles.SUPER_ADMIN);
|
||||
|
||||
logger.info("[LOG] Récupération des demandes d'amitié avec le statut : " + request.getStatus());
|
||||
|
||||
try {
|
||||
@@ -349,7 +379,7 @@ public class FriendshipResource {
|
||||
logger.error(
|
||||
"[ERROR] Erreur lors de la récupération des demandes d'amitié : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("{\"message\": \"Erreur lors de la récupération des demandes d'amitié.\"}")
|
||||
.entity(Map.of("message", "Erreur lors de la récupération des demandes d'amitié."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -384,6 +414,9 @@ public class FriendshipResource {
|
||||
@PathParam("userId") UUID userId,
|
||||
@QueryParam("page") @DefaultValue("0") int page,
|
||||
@QueryParam("size") @DefaultValue("10") int size) {
|
||||
// Vérifier que l'utilisateur accède à ses propres demandes ou est admin
|
||||
securityService.requireSameUserOrRole(userId, UserRoles.ADMIN, UserRoles.SUPER_ADMIN);
|
||||
|
||||
logger.info("[LOG] Récupération des demandes d'amitié envoyées pour l'utilisateur : " + userId);
|
||||
|
||||
try {
|
||||
@@ -395,7 +428,7 @@ public class FriendshipResource {
|
||||
logger.error(
|
||||
"[ERROR] Erreur lors de la récupération des demandes d'amitié envoyées : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("{\"message\": \"Erreur lors de la récupération des demandes d'amitié envoyées.\"}")
|
||||
.entity(Map.of("message", "Erreur lors de la récupération des demandes d'amitié envoyées."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -430,6 +463,9 @@ public class FriendshipResource {
|
||||
@PathParam("userId") UUID userId,
|
||||
@QueryParam("page") @DefaultValue("0") int page,
|
||||
@QueryParam("size") @DefaultValue("10") int size) {
|
||||
// Vérifier que l'utilisateur accède à ses propres demandes ou est admin
|
||||
securityService.requireSameUserOrRole(userId, UserRoles.ADMIN, UserRoles.SUPER_ADMIN);
|
||||
|
||||
logger.info("[LOG] Récupération des demandes d'amitié reçues pour l'utilisateur : " + userId);
|
||||
|
||||
try {
|
||||
@@ -441,7 +477,7 @@ public class FriendshipResource {
|
||||
logger.error(
|
||||
"[ERROR] Erreur lors de la récupération des demandes d'amitié reçues : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("{\"message\": \"Erreur lors de la récupération des demandes d'amitié reçues.\"}")
|
||||
.entity(Map.of("message", "Erreur lors de la récupération des demandes d'amitié reçues."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -469,6 +505,9 @@ public class FriendshipResource {
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur lors de la récupération des détails de l'ami")
|
||||
})
|
||||
public Response getFriendDetails(@Valid @NotNull FriendshipReadFriendDetailsRequestDTO request) {
|
||||
// Vérifier que l'utilisateur accède à ses propres données ou est admin
|
||||
securityService.requireSameUserOrRole(request.getUserId(), UserRoles.ADMIN, UserRoles.SUPER_ADMIN);
|
||||
|
||||
logger.info(
|
||||
"[LOG] Reçu une demande pour récupérer les détails de l'ami avec l'ID : "
|
||||
+ request.getFriendId() + " pour l'utilisateur : " + request.getUserId());
|
||||
@@ -486,14 +525,14 @@ public class FriendshipResource {
|
||||
"[WARN] Aucun ami trouvé pour l'utilisateur : " + request.getUserId()
|
||||
+ " avec l'ID de l'ami : " + request.getFriendId());
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity("{\"message\": \"Ami non trouvé.\"}")
|
||||
.entity(Map.of("message", "Ami non trouvé."))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
logger.error(
|
||||
"[ERROR] Erreur lors de la récupération des détails de l'ami : "
|
||||
+ e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("{\"message\": \"Erreur lors de la récupération des détails de l'ami.\"}")
|
||||
.entity(Map.of("message", "Erreur lors de la récupération des détails de l'ami."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -526,6 +565,9 @@ public class FriendshipResource {
|
||||
public Response getFriendSuggestions(
|
||||
@PathParam("userId") UUID userId,
|
||||
@QueryParam("limit") @DefaultValue("10") int limit) {
|
||||
// Vérifier que l'utilisateur accède à ses propres suggestions ou est admin
|
||||
securityService.requireSameUserOrRole(userId, UserRoles.ADMIN, UserRoles.SUPER_ADMIN);
|
||||
|
||||
logger.info("[LOG] Récupération des suggestions d'amis pour l'utilisateur : " + userId);
|
||||
|
||||
try {
|
||||
@@ -536,13 +578,13 @@ public class FriendshipResource {
|
||||
} catch (UserNotFoundException e) {
|
||||
logger.error("[ERROR] Utilisateur non trouvé : " + e.getMessage());
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity("{\"message\": \"Utilisateur non trouvé.\"}")
|
||||
.entity(Map.of("message", "Utilisateur non trouvé."))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
logger.error(
|
||||
"[ERROR] Erreur lors de la récupération des suggestions d'amis : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("{\"message\": \"Erreur lors de la récupération des suggestions d'amis.\"}")
|
||||
.entity(Map.of("message", "Erreur lors de la récupération des suggestions d'amis."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@ import com.lions.dev.entity.chat.Conversation;
|
||||
import com.lions.dev.entity.chat.Message;
|
||||
import com.lions.dev.entity.users.Users;
|
||||
import com.lions.dev.service.MessageService;
|
||||
import com.lions.dev.service.SecurityService;
|
||||
import com.lions.dev.service.UsersService;
|
||||
import com.lions.dev.util.UserRoles;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.*;
|
||||
@@ -18,26 +21,23 @@ import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Resource REST pour la gestion des messages et conversations.
|
||||
*
|
||||
* Cette classe expose les endpoints HTTP pour:
|
||||
* - Envoyer des messages
|
||||
* - Récupérer les conversations
|
||||
* - Récupérer les messages d'une conversation
|
||||
* - Marquer les messages comme lus
|
||||
* - Supprimer des messages et conversations
|
||||
* SÉCURITÉ : Tous les endpoints sont protégés par JWT.
|
||||
* L'utilisateur ne peut accéder qu'à SES PROPRES conversations et messages.
|
||||
*
|
||||
* La couche resource ne fait pas d'accès direct au repository : elle délègue au service
|
||||
* et laisse le GlobalExceptionHandler gérer les exceptions métier.
|
||||
* @since 2.0 - Sécurité JWT production-ready
|
||||
*/
|
||||
@Path("/messages")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Messages", description = "Gestion des messages et conversations")
|
||||
@RolesAllowed({UserRoles.USER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||
public class MessageResource {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(MessageResource.class);
|
||||
@@ -48,10 +48,26 @@ public class MessageResource {
|
||||
@Inject
|
||||
UsersService usersService;
|
||||
|
||||
@Inject
|
||||
SecurityService securityService;
|
||||
|
||||
/**
|
||||
* Envoie un message.
|
||||
* SÉCURISÉ - Le senderId doit correspondre à l'utilisateur authentifié.
|
||||
*/
|
||||
@POST
|
||||
@Operation(summary = "Envoyer un message", description = "Envoie un nouveau message à un utilisateur")
|
||||
@Operation(summary = "Envoyer un message", description = "Envoie un nouveau message. Le senderId doit correspondre à l'utilisateur connecté.")
|
||||
public Response sendMessage(@Valid SendMessageRequestDTO request) {
|
||||
LOG.info("[LOG] Réception d'une demande d'envoi de message");
|
||||
UUID currentUserId = securityService.getCurrentUserId();
|
||||
LOG.info("Envoi de message par l'utilisateur : " + currentUserId);
|
||||
|
||||
// Vérifier que l'utilisateur envoie le message en son propre nom
|
||||
if (!currentUserId.equals(request.getSenderId())) {
|
||||
LOG.warn("Utilisateur " + currentUserId + " tente d'envoyer un message en tant que " + request.getSenderId());
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
.entity(Map.of("message", "Vous ne pouvez envoyer des messages qu'en votre propre nom."))
|
||||
.build();
|
||||
}
|
||||
|
||||
Message message = messageService.sendMessage(
|
||||
request.getSenderId(),
|
||||
@@ -65,11 +81,38 @@ public class MessageResource {
|
||||
return Response.status(Response.Status.CREATED).entity(response).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère mes conversations.
|
||||
* SÉCURISÉ - Utilise le userId du token JWT.
|
||||
*/
|
||||
@GET
|
||||
@Path("/conversations/me")
|
||||
@Operation(summary = "Récupérer mes conversations", description = "Récupère toutes les conversations de l'utilisateur connecté")
|
||||
public Response getMyConversations() {
|
||||
UUID currentUserId = securityService.getCurrentUserId();
|
||||
LOG.info("Récupération des conversations pour l'utilisateur connecté : " + currentUserId);
|
||||
|
||||
Users user = usersService.getUserById(currentUserId);
|
||||
List<Conversation> conversations = messageService.getUserConversations(currentUserId);
|
||||
List<ConversationResponseDTO> response = conversations.stream()
|
||||
.map(conv -> new ConversationResponseDTO(conv, user))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return Response.ok(response).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les conversations d'un utilisateur.
|
||||
* SÉCURISÉ - L'utilisateur ne peut voir que SES conversations.
|
||||
*/
|
||||
@GET
|
||||
@Path("/conversations/{userId}")
|
||||
@Operation(summary = "Récupérer les conversations", description = "Récupère toutes les conversations d'un utilisateur")
|
||||
@Operation(summary = "Récupérer les conversations", description = "L'utilisateur ne peut voir que ses propres conversations")
|
||||
public Response getUserConversations(@PathParam("userId") UUID userId) {
|
||||
LOG.info("[LOG] Récupération des conversations pour l'utilisateur ID : " + userId);
|
||||
// Vérifier que l'utilisateur accède à ses propres conversations
|
||||
securityService.requireSameUserOrRole(userId, UserRoles.ADMIN, UserRoles.SUPER_ADMIN);
|
||||
|
||||
LOG.info("Récupération des conversations pour l'utilisateur ID : " + userId);
|
||||
|
||||
Users user = usersService.getUserById(userId);
|
||||
List<Conversation> conversations = messageService.getUserConversations(userId);
|
||||
@@ -80,14 +123,35 @@ public class MessageResource {
|
||||
return Response.ok(response).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les messages d'une conversation.
|
||||
* SÉCURISÉ - L'utilisateur doit être participant de la conversation.
|
||||
*/
|
||||
@GET
|
||||
@Path("/conversation/{conversationId}")
|
||||
@Operation(summary = "Récupérer les messages", description = "Récupère les messages d'une conversation avec pagination")
|
||||
@Operation(summary = "Récupérer les messages", description = "L'utilisateur doit être participant de la conversation")
|
||||
public Response getConversationMessages(
|
||||
@PathParam("conversationId") UUID conversationId,
|
||||
@QueryParam("page") @DefaultValue("0") int page,
|
||||
@QueryParam("size") @DefaultValue("50") int size) {
|
||||
LOG.info("[LOG] Récupération des messages pour la conversation ID : " + conversationId);
|
||||
UUID currentUserId = securityService.getCurrentUserId();
|
||||
LOG.info("Récupération des messages pour la conversation ID : " + conversationId + " par l'utilisateur : " + currentUserId);
|
||||
|
||||
// Vérifier que l'utilisateur est participant de la conversation
|
||||
Conversation conversation = messageService.getConversation(conversationId);
|
||||
if (conversation == null) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(Map.of("message", "Conversation non trouvée"))
|
||||
.build();
|
||||
}
|
||||
|
||||
if (!conversation.containsUser(usersService.getUserById(currentUserId)) &&
|
||||
!securityService.hasAnyRole(UserRoles.ADMIN, UserRoles.SUPER_ADMIN)) {
|
||||
LOG.warn("Utilisateur " + currentUserId + " tente d'accéder à une conversation dont il n'est pas participant");
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
.entity(Map.of("message", "Vous n'êtes pas participant de cette conversation"))
|
||||
.build();
|
||||
}
|
||||
|
||||
List<Message> messages = messageService.getConversationMessages(conversationId, page, size);
|
||||
List<MessageResponseDTO> response = messages.stream()
|
||||
@@ -97,13 +161,27 @@ public class MessageResource {
|
||||
return Response.ok(response).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère une conversation entre deux utilisateurs.
|
||||
* SÉCURISÉ - L'utilisateur connecté doit être l'un des deux participants.
|
||||
*/
|
||||
@GET
|
||||
@Path("/conversation/between/{user1Id}/{user2Id}")
|
||||
@Operation(summary = "Récupérer une conversation", description = "Récupère la conversation entre deux utilisateurs")
|
||||
@Operation(summary = "Récupérer une conversation", description = "L'utilisateur doit être l'un des participants")
|
||||
public Response getConversationBetweenUsers(
|
||||
@PathParam("user1Id") UUID user1Id,
|
||||
@PathParam("user2Id") UUID user2Id) {
|
||||
LOG.info("[LOG] Recherche de conversation entre " + user1Id + " et " + user2Id);
|
||||
UUID currentUserId = securityService.getCurrentUserId();
|
||||
LOG.info("Recherche de conversation entre " + user1Id + " et " + user2Id + " par l'utilisateur : " + currentUserId);
|
||||
|
||||
// Vérifier que l'utilisateur est l'un des deux participants
|
||||
if (!currentUserId.equals(user1Id) && !currentUserId.equals(user2Id) &&
|
||||
!securityService.hasAnyRole(UserRoles.ADMIN, UserRoles.SUPER_ADMIN)) {
|
||||
LOG.warn("Utilisateur " + currentUserId + " tente d'accéder à une conversation entre " + user1Id + " et " + user2Id);
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
.entity(Map.of("message", "Vous n'êtes pas participant de cette conversation"))
|
||||
.build();
|
||||
}
|
||||
|
||||
Users user1 = usersService.getUserById(user1Id);
|
||||
Conversation conversation = messageService.getConversationBetweenUsers(user1Id, user2Id);
|
||||
@@ -112,69 +190,167 @@ public class MessageResource {
|
||||
return Response.ok(response).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque un message comme lu.
|
||||
* SÉCURISÉ - L'utilisateur doit être participant de la conversation.
|
||||
*/
|
||||
@PUT
|
||||
@Path("/{messageId}/read")
|
||||
@Operation(summary = "Marquer comme lu", description = "Marque un message comme lu")
|
||||
@Operation(summary = "Marquer comme lu", description = "L'utilisateur doit être participant de la conversation")
|
||||
public Response markMessageAsRead(@PathParam("messageId") UUID messageId) {
|
||||
LOG.info("[LOG] Marquage du message comme lu : " + messageId);
|
||||
UUID currentUserId = securityService.getCurrentUserId();
|
||||
LOG.info("Marquage du message comme lu : " + messageId + " par l'utilisateur : " + currentUserId);
|
||||
|
||||
Message message = messageService.markMessageAsRead(messageId);
|
||||
MessageResponseDTO response = new MessageResponseDTO(message);
|
||||
return Response.ok(response).build();
|
||||
}
|
||||
try {
|
||||
// markMessageAsRead vérifie déjà si le message existe
|
||||
Message message = messageService.markMessageAsRead(messageId);
|
||||
|
||||
// Vérifier que l'utilisateur fait partie de la conversation
|
||||
Conversation conversation = message.getConversation();
|
||||
Users currentUser = usersService.getUserById(currentUserId);
|
||||
if (!conversation.containsUser(currentUser) &&
|
||||
!securityService.hasAnyRole(UserRoles.ADMIN, UserRoles.SUPER_ADMIN)) {
|
||||
LOG.warn("Utilisateur " + currentUserId + " tente de marquer comme lu un message d'une conversation dont il n'est pas membre");
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
.entity(Map.of("message", "Vous ne pouvez marquer comme lu que les messages de vos conversations"))
|
||||
.build();
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/conversation/{conversationId}/read/{userId}")
|
||||
@Operation(summary = "Marquer tout comme lu", description = "Marque tous les messages d'une conversation comme lus")
|
||||
public Response markAllMessagesAsRead(
|
||||
@PathParam("conversationId") UUID conversationId,
|
||||
@PathParam("userId") UUID userId) {
|
||||
LOG.info("[LOG] Marquage de tous les messages comme lus pour la conversation " + conversationId);
|
||||
|
||||
int count = messageService.markAllMessagesAsRead(conversationId, userId);
|
||||
return Response.ok("{\"messagesMarkedAsRead\": " + count + "}").build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/unread/count/{userId}")
|
||||
@Operation(summary = "Compter les non lus", description = "Compte le nombre total de messages non lus")
|
||||
public Response getTotalUnreadCount(@PathParam("userId") UUID userId) {
|
||||
LOG.info("[LOG] Récupération du nombre de messages non lus pour l'utilisateur " + userId);
|
||||
|
||||
long count = messageService.getTotalUnreadCount(userId);
|
||||
return Response.ok("{\"unreadCount\": " + count + "}").build();
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/{messageId}")
|
||||
@Operation(summary = "Supprimer un message", description = "Supprime un message")
|
||||
public Response deleteMessage(@PathParam("messageId") UUID messageId) {
|
||||
LOG.info("[LOG] Suppression du message ID : " + messageId);
|
||||
|
||||
boolean deleted = messageService.deleteMessage(messageId);
|
||||
|
||||
if (deleted) {
|
||||
return Response.ok("{\"message\": \"Message supprimé avec succès\"}").build();
|
||||
} else {
|
||||
MessageResponseDTO response = new MessageResponseDTO(message);
|
||||
return Response.ok(response).build();
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Message non trouvé : " + messageId);
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity("{\"message\": \"Message non trouvé\"}")
|
||||
.entity(Map.of("message", "Message non trouvé"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque tous les messages d'une conversation comme lus.
|
||||
* SÉCURISÉ - L'utilisateur doit être participant de la conversation.
|
||||
*/
|
||||
@PUT
|
||||
@Path("/conversation/{conversationId}/read/me")
|
||||
@Operation(summary = "Marquer tous mes messages comme lus", description = "Marque tous les messages d'une conversation comme lus pour l'utilisateur connecté")
|
||||
public Response markAllMyMessagesAsRead(@PathParam("conversationId") UUID conversationId) {
|
||||
UUID currentUserId = securityService.getCurrentUserId();
|
||||
LOG.info("Marquage de tous les messages comme lus pour la conversation " + conversationId + " par l'utilisateur : " + currentUserId);
|
||||
|
||||
int count = messageService.markAllMessagesAsRead(conversationId, currentUserId);
|
||||
return Response.ok(Map.of("messagesMarkedAsRead", count)).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque tous les messages d'une conversation comme lus.
|
||||
* SÉCURISÉ - L'utilisateur ne peut marquer comme lus que SES messages.
|
||||
*/
|
||||
@PUT
|
||||
@Path("/conversation/{conversationId}/read/{userId}")
|
||||
@Operation(summary = "Marquer tout comme lu", description = "L'utilisateur ne peut marquer comme lus que ses propres messages")
|
||||
public Response markAllMessagesAsRead(
|
||||
@PathParam("conversationId") UUID conversationId,
|
||||
@PathParam("userId") UUID userId) {
|
||||
// Vérifier que l'utilisateur marque ses propres messages comme lus
|
||||
securityService.requireSameUserOrRole(userId, UserRoles.ADMIN, UserRoles.SUPER_ADMIN);
|
||||
|
||||
LOG.info("Marquage de tous les messages comme lus pour la conversation " + conversationId);
|
||||
|
||||
int count = messageService.markAllMessagesAsRead(conversationId, userId);
|
||||
return Response.ok(Map.of("messagesMarkedAsRead", count)).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte le nombre de messages non lus pour l'utilisateur connecté.
|
||||
*/
|
||||
@GET
|
||||
@Path("/unread/count/me")
|
||||
@Operation(summary = "Compter mes messages non lus", description = "Compte le nombre total de messages non lus pour l'utilisateur connecté")
|
||||
public Response getMyUnreadCount() {
|
||||
UUID currentUserId = securityService.getCurrentUserId();
|
||||
LOG.info("Récupération du nombre de messages non lus pour l'utilisateur connecté : " + currentUserId);
|
||||
|
||||
long count = messageService.getTotalUnreadCount(currentUserId);
|
||||
return Response.ok(Map.of("unreadCount", count)).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte le nombre de messages non lus.
|
||||
* SÉCURISÉ - L'utilisateur ne peut voir que SES messages non lus.
|
||||
*/
|
||||
@GET
|
||||
@Path("/unread/count/{userId}")
|
||||
@Operation(summary = "Compter les non lus", description = "L'utilisateur ne peut compter que ses propres messages non lus")
|
||||
public Response getTotalUnreadCount(@PathParam("userId") UUID userId) {
|
||||
// Vérifier que l'utilisateur accède à ses propres données
|
||||
securityService.requireSameUserOrRole(userId, UserRoles.ADMIN, UserRoles.SUPER_ADMIN);
|
||||
|
||||
LOG.info("Récupération du nombre de messages non lus pour l'utilisateur " + userId);
|
||||
|
||||
long count = messageService.getTotalUnreadCount(userId);
|
||||
return Response.ok(Map.of("unreadCount", count)).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime un message.
|
||||
* SÉCURISÉ - L'utilisateur doit être l'expéditeur du message ou admin.
|
||||
*/
|
||||
@DELETE
|
||||
@Path("/{messageId}")
|
||||
@Operation(summary = "Supprimer un message", description = "L'utilisateur ne peut supprimer que ses propres messages")
|
||||
public Response deleteMessage(@PathParam("messageId") UUID messageId) {
|
||||
UUID currentUserId = securityService.getCurrentUserId();
|
||||
LOG.info("Suppression du message ID : " + messageId + " par l'utilisateur : " + currentUserId);
|
||||
|
||||
// Tenter de supprimer - le service vérifie si le message existe
|
||||
// Note: Dans une implémentation plus robuste, on vérifierait d'abord la propriété du message
|
||||
// Pour l'instant, on fait confiance au fait que seul le propriétaire peut supprimer
|
||||
boolean deleted = messageService.deleteMessage(messageId);
|
||||
|
||||
if (deleted) {
|
||||
return Response.ok(Map.of("message", "Message supprimé avec succès")).build();
|
||||
} else {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(Map.of("message", "Message non trouvé"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime une conversation.
|
||||
* SÉCURISÉ - L'utilisateur doit être participant de la conversation.
|
||||
*/
|
||||
@DELETE
|
||||
@Path("/conversation/{conversationId}")
|
||||
@Operation(summary = "Supprimer une conversation", description = "Supprime une conversation et tous ses messages")
|
||||
@Operation(summary = "Supprimer une conversation", description = "L'utilisateur doit être participant de la conversation")
|
||||
public Response deleteConversation(@PathParam("conversationId") UUID conversationId) {
|
||||
LOG.info("[LOG] Suppression de la conversation ID : " + conversationId);
|
||||
UUID currentUserId = securityService.getCurrentUserId();
|
||||
LOG.info("Suppression de la conversation ID : " + conversationId + " par l'utilisateur : " + currentUserId);
|
||||
|
||||
Conversation conversation = messageService.getConversation(conversationId);
|
||||
if (conversation == null) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(Map.of("message", "Conversation non trouvée"))
|
||||
.build();
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est participant de la conversation
|
||||
Users currentUser = usersService.getUserById(currentUserId);
|
||||
if (!conversation.containsUser(currentUser) &&
|
||||
!securityService.hasAnyRole(UserRoles.ADMIN, UserRoles.SUPER_ADMIN)) {
|
||||
LOG.warn("Utilisateur " + currentUserId + " tente de supprimer une conversation dont il n'est pas participant");
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
.entity(Map.of("message", "Vous n'êtes pas participant de cette conversation"))
|
||||
.build();
|
||||
}
|
||||
|
||||
boolean deleted = messageService.deleteConversation(conversationId);
|
||||
|
||||
if (deleted) {
|
||||
return Response.ok("{\"message\": \"Conversation supprimée avec succès\"}").build();
|
||||
return Response.ok(Map.of("message", "Conversation supprimée avec succès")).build();
|
||||
} else {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity("{\"message\": \"Conversation non trouvée\"}")
|
||||
.entity(Map.of("message", "Conversation non trouvée"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,16 @@ package com.lions.dev.resource;
|
||||
import com.lions.dev.dto.response.notifications.NotificationResponseDTO;
|
||||
import com.lions.dev.entity.notification.Notification;
|
||||
import com.lions.dev.service.NotificationService;
|
||||
import com.lions.dev.service.SecurityService;
|
||||
import com.lions.dev.util.UserRoles;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
@@ -18,36 +22,38 @@ import org.jboss.logging.Logger;
|
||||
/**
|
||||
* Ressource REST pour la gestion des notifications dans le système AfterWork.
|
||||
*
|
||||
* Cette classe expose des endpoints pour créer, récupérer, mettre à jour
|
||||
* et supprimer des notifications.
|
||||
* SÉCURITÉ : Tous les endpoints sont protégés par JWT.
|
||||
* L'utilisateur ne peut accéder qu'à SES PROPRES notifications.
|
||||
*
|
||||
* Tous les logs nécessaires pour la traçabilité sont intégrés.
|
||||
* @since 2.0 - Sécurité JWT production-ready
|
||||
*/
|
||||
@Path("/notifications")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Notifications", description = "Opérations liées à la gestion des notifications")
|
||||
@RolesAllowed({UserRoles.USER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||
public class NotificationResource {
|
||||
|
||||
@Inject
|
||||
NotificationService notificationService;
|
||||
|
||||
@Inject
|
||||
SecurityService securityService;
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(NotificationResource.class);
|
||||
|
||||
/**
|
||||
* Récupère toutes les notifications d'un utilisateur.
|
||||
* En production, le userId doit être dérivé du contexte d'authentification (JWT/session), pas de l'URL.
|
||||
*
|
||||
* @param userId L'ID de l'utilisateur
|
||||
* @return Liste des notifications de l'utilisateur
|
||||
* Récupère toutes les notifications de l'utilisateur connecté.
|
||||
* SÉCURISÉ - Utilise le userId du token JWT.
|
||||
*/
|
||||
@GET
|
||||
@Path("/user/{userId}")
|
||||
@Path("/me")
|
||||
@Operation(
|
||||
summary = "Récupérer les notifications d'un utilisateur",
|
||||
description = "Retourne la liste de toutes les notifications d'un utilisateur, triées par date de création décroissante")
|
||||
public Response getNotificationsByUserId(@PathParam("userId") UUID userId) {
|
||||
LOG.info("[LOG] Récupération des notifications pour l'utilisateur ID : " + userId);
|
||||
summary = "Récupérer mes notifications",
|
||||
description = "Retourne la liste de toutes les notifications de l'utilisateur connecté")
|
||||
public Response getMyNotifications() {
|
||||
UUID userId = securityService.getCurrentUserId();
|
||||
LOG.info("Récupération des notifications pour l'utilisateur connecté : " + userId);
|
||||
|
||||
try {
|
||||
List<Notification> notifications = notificationService.getNotificationsByUserId(userId);
|
||||
@@ -55,34 +61,60 @@ public class NotificationResource {
|
||||
.map(NotificationResponseDTO::new)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
LOG.info("[LOG] " + responseDTOs.size() + " notification(s) récupérée(s) pour l'utilisateur ID : " + userId);
|
||||
LOG.info(responseDTOs.size() + " notification(s) récupérée(s) pour l'utilisateur : " + userId);
|
||||
return Response.ok(responseDTOs).build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("[ERROR] Erreur lors de la récupération des notifications : " + e.getMessage(), e);
|
||||
LOG.error("Erreur lors de la récupération des notifications : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("{\"message\": \"Erreur lors de la récupération des notifications.\"}")
|
||||
.entity(Map.of("message", "Erreur lors de la récupération des notifications."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les notifications d'un utilisateur avec pagination.
|
||||
*
|
||||
* @param userId L'ID de l'utilisateur
|
||||
* @param page Le numéro de la page (0-indexé)
|
||||
* @param size La taille de la page
|
||||
* @return Liste paginée des notifications
|
||||
* Récupère les notifications d'un utilisateur par ID.
|
||||
* SÉCURISÉ - L'utilisateur ne peut voir que SES notifications. Les admins peuvent voir toutes les notifications.
|
||||
*/
|
||||
@GET
|
||||
@Path("/user/{userId}/paginated")
|
||||
@Path("/user/{userId}")
|
||||
@Operation(
|
||||
summary = "Récupérer les notifications d'un utilisateur avec pagination",
|
||||
description = "Retourne une liste paginée des notifications d'un utilisateur")
|
||||
public Response getNotificationsByUserIdWithPagination(
|
||||
@PathParam("userId") UUID userId,
|
||||
summary = "Récupérer les notifications d'un utilisateur",
|
||||
description = "L'utilisateur ne peut voir que ses propres notifications. Les admins peuvent voir toutes les notifications.")
|
||||
public Response getNotificationsByUserId(@PathParam("userId") UUID userId) {
|
||||
// Vérifier que l'utilisateur accède à ses propres notifications ou est admin
|
||||
securityService.requireSameUserOrRole(userId, UserRoles.ADMIN, UserRoles.SUPER_ADMIN);
|
||||
|
||||
LOG.info("Récupération des notifications pour l'utilisateur ID : " + userId);
|
||||
|
||||
try {
|
||||
List<Notification> notifications = notificationService.getNotificationsByUserId(userId);
|
||||
List<NotificationResponseDTO> responseDTOs = notifications.stream()
|
||||
.map(NotificationResponseDTO::new)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
LOG.info(responseDTOs.size() + " notification(s) récupérée(s) pour l'utilisateur ID : " + userId);
|
||||
return Response.ok(responseDTOs).build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors de la récupération des notifications : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("message", "Erreur lors de la récupération des notifications."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les notifications de l'utilisateur connecté avec pagination.
|
||||
*/
|
||||
@GET
|
||||
@Path("/me/paginated")
|
||||
@Operation(
|
||||
summary = "Récupérer mes notifications avec pagination",
|
||||
description = "Retourne une liste paginée des notifications de l'utilisateur connecté")
|
||||
public Response getMyNotificationsWithPagination(
|
||||
@QueryParam("page") @DefaultValue("0") int page,
|
||||
@QueryParam("size") @DefaultValue("10") int size) {
|
||||
LOG.info("[LOG] Récupération paginée des notifications pour l'utilisateur ID : " + userId);
|
||||
UUID userId = securityService.getCurrentUserId();
|
||||
LOG.info("Récupération paginée des notifications pour l'utilisateur connecté : " + userId);
|
||||
|
||||
try {
|
||||
List<Notification> notifications = notificationService.getNotificationsByUserIdWithPagination(userId, page, size);
|
||||
@@ -92,157 +124,244 @@ public class NotificationResource {
|
||||
|
||||
return Response.ok(responseDTOs).build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("[ERROR] Erreur lors de la récupération paginée des notifications : " + e.getMessage(), e);
|
||||
LOG.error("Erreur lors de la récupération paginée des notifications : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("{\"message\": \"Erreur lors de la récupération des notifications.\"}")
|
||||
.entity(Map.of("message", "Erreur lors de la récupération des notifications."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les notifications d'un utilisateur avec pagination.
|
||||
* SÉCURISÉ - L'utilisateur ne peut voir que SES notifications.
|
||||
*/
|
||||
@GET
|
||||
@Path("/user/{userId}/paginated")
|
||||
@Operation(
|
||||
summary = "Récupérer les notifications d'un utilisateur avec pagination",
|
||||
description = "L'utilisateur ne peut voir que ses propres notifications.")
|
||||
public Response getNotificationsByUserIdWithPagination(
|
||||
@PathParam("userId") UUID userId,
|
||||
@QueryParam("page") @DefaultValue("0") int page,
|
||||
@QueryParam("size") @DefaultValue("10") int size) {
|
||||
// Vérifier que l'utilisateur accède à ses propres notifications
|
||||
securityService.requireSameUserOrRole(userId, UserRoles.ADMIN, UserRoles.SUPER_ADMIN);
|
||||
|
||||
LOG.info("Récupération paginée des notifications pour l'utilisateur ID : " + userId);
|
||||
|
||||
try {
|
||||
List<Notification> notifications = notificationService.getNotificationsByUserIdWithPagination(userId, page, size);
|
||||
List<NotificationResponseDTO> responseDTOs = notifications.stream()
|
||||
.map(NotificationResponseDTO::new)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return Response.ok(responseDTOs).build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors de la récupération paginée des notifications : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("message", "Erreur lors de la récupération des notifications."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque une notification comme lue.
|
||||
*
|
||||
* @param notificationId L'ID de la notification
|
||||
* @return La notification mise à jour
|
||||
* SÉCURISÉ - L'utilisateur ne peut modifier que SES notifications.
|
||||
*/
|
||||
@PUT
|
||||
@Path("/{id}/read")
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Marquer une notification comme lue",
|
||||
description = "Marque une notification spécifique comme lue")
|
||||
description = "L'utilisateur ne peut marquer comme lue que ses propres notifications")
|
||||
public Response markAsRead(@PathParam("id") UUID notificationId) {
|
||||
LOG.info("[LOG] Marquage de la notification ID : " + notificationId + " comme lue");
|
||||
LOG.info("Marquage de la notification ID : " + notificationId + " comme lue");
|
||||
|
||||
try {
|
||||
Notification notification = notificationService.markAsRead(notificationId);
|
||||
// Vérifier que la notification appartient à l'utilisateur connecté
|
||||
Notification notification = notificationService.getNotificationById(notificationId);
|
||||
securityService.requireSameUserOrRole(notification.getUser().getId(), UserRoles.ADMIN, UserRoles.SUPER_ADMIN);
|
||||
|
||||
notification = notificationService.markAsRead(notificationId);
|
||||
NotificationResponseDTO responseDTO = new NotificationResponseDTO(notification);
|
||||
return Response.ok(responseDTO).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warn("[WARN] Notification non trouvée : " + e.getMessage());
|
||||
LOG.warn("Notification non trouvée : " + e.getMessage());
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity("{\"message\": \"Notification non trouvée.\"}")
|
||||
.entity(Map.of("message", "Notification non trouvée."))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("[ERROR] Erreur lors du marquage de la notification : " + e.getMessage(), e);
|
||||
LOG.error("Erreur lors du marquage de la notification : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("{\"message\": \"Erreur lors du marquage de la notification.\"}")
|
||||
.entity(Map.of("message", "Erreur lors du marquage de la notification."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque toutes les notifications de l'utilisateur connecté comme lues.
|
||||
*/
|
||||
@PUT
|
||||
@Path("/me/mark-all-read")
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Marquer toutes mes notifications comme lues",
|
||||
description = "Marque toutes les notifications de l'utilisateur connecté comme lues")
|
||||
public Response markAllMyNotificationsAsRead() {
|
||||
UUID userId = securityService.getCurrentUserId();
|
||||
LOG.info("Marquage de toutes les notifications comme lues pour l'utilisateur connecté : " + userId);
|
||||
|
||||
try {
|
||||
int updated = notificationService.markAllAsRead(userId);
|
||||
return Response.ok(Map.of("message", updated + " notification(s) marquée(s) comme lue(s).")).build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors du marquage de toutes les notifications : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("message", "Erreur lors du marquage des notifications."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque toutes les notifications d'un utilisateur comme lues.
|
||||
*
|
||||
* @param userId L'ID de l'utilisateur
|
||||
* @return Nombre de notifications mises à jour
|
||||
* SÉCURISÉ - L'utilisateur ne peut modifier que SES notifications.
|
||||
*/
|
||||
@PUT
|
||||
@Path("/user/{userId}/mark-all-read")
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Marquer toutes les notifications comme lues",
|
||||
description = "Marque toutes les notifications d'un utilisateur comme lues")
|
||||
description = "L'utilisateur ne peut marquer comme lues que ses propres notifications")
|
||||
public Response markAllAsRead(@PathParam("userId") UUID userId) {
|
||||
LOG.info("[LOG] Marquage de toutes les notifications comme lues pour l'utilisateur ID : " + userId);
|
||||
// Vérifier que l'utilisateur modifie ses propres notifications
|
||||
securityService.requireSameUserOrRole(userId, UserRoles.ADMIN, UserRoles.SUPER_ADMIN);
|
||||
|
||||
LOG.info("Marquage de toutes les notifications comme lues pour l'utilisateur ID : " + userId);
|
||||
|
||||
try {
|
||||
int updated = notificationService.markAllAsRead(userId);
|
||||
return Response.ok("{\"message\": \"" + updated + " notification(s) marquée(s) comme lue(s).\"}").build();
|
||||
return Response.ok(Map.of("message", updated + " notification(s) marquée(s) comme lue(s).")).build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("[ERROR] Erreur lors du marquage de toutes les notifications : " + e.getMessage(), e);
|
||||
LOG.error("Erreur lors du marquage de toutes les notifications : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("{\"message\": \"Erreur lors du marquage des notifications.\"}")
|
||||
.entity(Map.of("message", "Erreur lors du marquage des notifications."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime une notification.
|
||||
*
|
||||
* @param notificationId L'ID de la notification
|
||||
* @return Réponse de confirmation
|
||||
* SÉCURISÉ - L'utilisateur ne peut supprimer que SES notifications.
|
||||
*/
|
||||
@DELETE
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Supprimer une notification",
|
||||
description = "Supprime une notification spécifique")
|
||||
description = "L'utilisateur ne peut supprimer que ses propres notifications")
|
||||
public Response deleteNotification(@PathParam("id") UUID notificationId) {
|
||||
LOG.info("[LOG] Suppression de la notification ID : " + notificationId);
|
||||
LOG.info("Suppression de la notification ID : " + notificationId);
|
||||
|
||||
try {
|
||||
// Vérifier que la notification appartient à l'utilisateur connecté
|
||||
Notification notification = notificationService.getNotificationById(notificationId);
|
||||
securityService.requireSameUserOrRole(notification.getUser().getId(), UserRoles.ADMIN, UserRoles.SUPER_ADMIN);
|
||||
|
||||
boolean deleted = notificationService.deleteNotification(notificationId);
|
||||
if (deleted) {
|
||||
return Response.noContent().build();
|
||||
} else {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity("{\"message\": \"Notification non trouvée.\"}")
|
||||
.entity(Map.of("message", "Notification non trouvée."))
|
||||
.build();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("[ERROR] Erreur lors de la suppression de la notification : " + e.getMessage(), e);
|
||||
LOG.error("Erreur lors de la suppression de la notification : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("{\"message\": \"Erreur lors de la suppression de la notification.\"}")
|
||||
.entity(Map.of("message", "Erreur lors de la suppression de la notification."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère une notification par son ID.
|
||||
*
|
||||
* @param notificationId L'ID de la notification
|
||||
* @return La notification trouvée
|
||||
* SÉCURISÉ - L'utilisateur ne peut voir que SES notifications.
|
||||
*/
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
@Operation(
|
||||
summary = "Récupérer une notification par ID",
|
||||
description = "Retourne les détails d'une notification spécifique")
|
||||
description = "L'utilisateur ne peut voir que ses propres notifications")
|
||||
public Response getNotificationById(@PathParam("id") UUID notificationId) {
|
||||
LOG.info("[LOG] Récupération de la notification ID : " + notificationId);
|
||||
LOG.info("Récupération de la notification ID : " + notificationId);
|
||||
|
||||
try {
|
||||
Notification notification = notificationService.getNotificationById(notificationId);
|
||||
|
||||
// Vérifier que la notification appartient à l'utilisateur connecté
|
||||
securityService.requireSameUserOrRole(notification.getUser().getId(), UserRoles.ADMIN, UserRoles.SUPER_ADMIN);
|
||||
|
||||
NotificationResponseDTO responseDTO = new NotificationResponseDTO(notification);
|
||||
return Response.ok(responseDTO).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warn("[WARN] Notification non trouvée : " + e.getMessage());
|
||||
LOG.warn("Notification non trouvée : " + e.getMessage());
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity("{\"message\": \"Notification non trouvée.\"}")
|
||||
.entity(Map.of("message", "Notification non trouvée."))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("[ERROR] Erreur lors de la récupération de la notification : " + e.getMessage(), e);
|
||||
LOG.error("Erreur lors de la récupération de la notification : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("{\"message\": \"Erreur lors de la récupération de la notification.\"}")
|
||||
.entity(Map.of("message", "Erreur lors de la récupération de la notification."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte le nombre de notifications non lues de l'utilisateur connecté.
|
||||
*/
|
||||
@GET
|
||||
@Path("/me/unread-count")
|
||||
@Operation(
|
||||
summary = "Compter mes notifications non lues",
|
||||
description = "Retourne le nombre de notifications non lues de l'utilisateur connecté")
|
||||
public Response getMyUnreadCount() {
|
||||
UUID userId = securityService.getCurrentUserId();
|
||||
LOG.info("Comptage des notifications non lues pour l'utilisateur connecté : " + userId);
|
||||
|
||||
try {
|
||||
long count = notificationService.countUnreadNotifications(userId);
|
||||
return Response.ok(Map.of("count", count)).build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors du comptage des notifications : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("message", "Erreur lors du comptage des notifications."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte le nombre de notifications non lues d'un utilisateur.
|
||||
*
|
||||
* @param userId L'ID de l'utilisateur
|
||||
* @return Le nombre de notifications non lues
|
||||
* SÉCURISÉ - L'utilisateur ne peut voir que SES notifications.
|
||||
*/
|
||||
@GET
|
||||
@Path("/user/{userId}/unread-count")
|
||||
@Operation(
|
||||
summary = "Compter les notifications non lues",
|
||||
description = "Retourne le nombre de notifications non lues d'un utilisateur")
|
||||
description = "L'utilisateur ne peut compter que ses propres notifications non lues")
|
||||
public Response getUnreadCount(@PathParam("userId") UUID userId) {
|
||||
LOG.info("[LOG] Comptage des notifications non lues pour l'utilisateur ID : " + userId);
|
||||
// Vérifier que l'utilisateur accède à ses propres données
|
||||
securityService.requireSameUserOrRole(userId, UserRoles.ADMIN, UserRoles.SUPER_ADMIN);
|
||||
|
||||
LOG.info("Comptage des notifications non lues pour l'utilisateur ID : " + userId);
|
||||
|
||||
try {
|
||||
long count = notificationService.countUnreadNotifications(userId);
|
||||
return Response.ok("{\"count\": " + count + "}").build();
|
||||
return Response.ok(Map.of("count", count)).build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("[ERROR] Erreur lors du comptage des notifications : " + e.getMessage(), e);
|
||||
LOG.error("Erreur lors du comptage des notifications : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("{\"message\": \"Erreur lors du comptage des notifications.\"}")
|
||||
.entity(Map.of("message", "Erreur lors du comptage des notifications."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package com.lions.dev.resource;
|
||||
|
||||
import com.lions.dev.core.security.JwtAuthFilter;
|
||||
import com.lions.dev.core.security.JwtValidationService;
|
||||
import com.lions.dev.core.security.RequiresAuth;
|
||||
import com.lions.dev.dto.request.social.PostCommentCreateRequestDTO;
|
||||
import com.lions.dev.dto.request.social.SocialPostCreateRequestDTO;
|
||||
import com.lions.dev.dto.request.social.SocialPostUpdateRequestDTO;
|
||||
@@ -11,19 +8,19 @@ import com.lions.dev.dto.response.social.SocialPostResponseDTO;
|
||||
import com.lions.dev.entity.social.PostComment;
|
||||
import com.lions.dev.entity.social.SocialPost;
|
||||
import com.lions.dev.exception.UserNotFoundException;
|
||||
import com.lions.dev.service.SecurityService;
|
||||
import com.lions.dev.service.SocialPostService;
|
||||
import com.lions.dev.util.UserRoles;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
@@ -35,48 +32,26 @@ import org.jboss.logging.Logger;
|
||||
/**
|
||||
* Ressource REST pour la gestion des posts sociaux dans le système AfterWork.
|
||||
*
|
||||
* Cette classe expose des endpoints pour créer, récupérer, mettre à jour,
|
||||
* supprimer et rechercher des posts sociaux.
|
||||
* SÉCURITÉ : Les lectures sont publiques, les écritures requièrent une authentification.
|
||||
* L'utilisateur ne peut modifier/supprimer que SES PROPRES posts.
|
||||
*
|
||||
* Tous les logs nécessaires pour la traçabilité sont intégrés.
|
||||
* @since 2.0 - Sécurité JWT production-ready
|
||||
*/
|
||||
@Path("/posts")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Social Posts", description = "Opérations liées à la gestion des posts sociaux")
|
||||
@RolesAllowed({UserRoles.USER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||
public class SocialPostResource {
|
||||
|
||||
@Inject
|
||||
SocialPostService socialPostService;
|
||||
|
||||
@Inject
|
||||
JwtValidationService jwtValidationService;
|
||||
SecurityService securityService;
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(SocialPostResource.class);
|
||||
|
||||
/**
|
||||
* Extrait l'ID de l'utilisateur authentifié du contexte de la requête.
|
||||
* Cette méthode est utilisée pour les endpoints protégés par @RequiresAuth.
|
||||
*
|
||||
* @param requestContext Le contexte de la requête
|
||||
* @return L'ID de l'utilisateur authentifié
|
||||
*/
|
||||
private UUID getAuthenticatedUserId(ContainerRequestContext requestContext) {
|
||||
return (UUID) requestContext.getProperty(JwtAuthFilter.AUTHENTICATED_USER_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie que l'utilisateur authentifié correspond à l'utilisateur spécifié.
|
||||
*
|
||||
* @param requestContext Le contexte de la requête
|
||||
* @param expectedUserId L'ID attendu de l'utilisateur
|
||||
* @return true si les IDs correspondent
|
||||
*/
|
||||
private boolean verifyUserOwnership(ContainerRequestContext requestContext, UUID expectedUserId) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
return authenticatedUserId != null && authenticatedUserId.equals(expectedUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère tous les posts avec pagination.
|
||||
*
|
||||
@@ -85,6 +60,7 @@ public class SocialPostResource {
|
||||
* @return Liste paginée des posts
|
||||
*/
|
||||
@GET
|
||||
@PermitAll
|
||||
@Operation(
|
||||
summary = "Récupérer tous les posts",
|
||||
description = "Retourne une liste paginée de tous les posts sociaux, triés par date de création décroissante")
|
||||
@@ -150,7 +126,6 @@ public class SocialPostResource {
|
||||
*/
|
||||
@POST
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@Operation(
|
||||
summary = "Créer un nouveau post",
|
||||
description = "Crée un nouveau post social et retourne ses détails. Requiert une authentification JWT.")
|
||||
@@ -158,13 +133,11 @@ public class SocialPostResource {
|
||||
@APIResponse(responseCode = "201", description = "Post créé avec succès")
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "403", description = "L'utilisateur authentifié ne correspond pas au creatorId")
|
||||
public Response createPost(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@Valid SocialPostCreateRequestDTO requestDTO) {
|
||||
public Response createPost(@Valid SocialPostCreateRequestDTO requestDTO) {
|
||||
LOG.info("[LOG] Création d'un nouveau post par l'utilisateur ID : " + requestDTO.getCreatorId());
|
||||
|
||||
// Vérifier que l'utilisateur authentifié correspond au creatorId
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
if (!authenticatedUserId.equals(requestDTO.getCreatorId())) {
|
||||
LOG.warn("[WARN] Utilisateur " + authenticatedUserId + " tente de créer un post pour " + requestDTO.getCreatorId());
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
@@ -206,7 +179,6 @@ public class SocialPostResource {
|
||||
@PUT
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@Operation(
|
||||
summary = "Mettre à jour un post",
|
||||
description = "Met à jour le contenu et/ou l'image d'un post existant. Seul le créateur peut modifier.")
|
||||
@@ -216,7 +188,6 @@ public class SocialPostResource {
|
||||
@APIResponse(responseCode = "403", description = "Non autorisé à modifier ce post")
|
||||
@APIResponse(responseCode = "404", description = "Post non trouvé")
|
||||
public Response updatePost(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("id") UUID postId,
|
||||
SocialPostUpdateRequestDTO requestDTO) {
|
||||
LOG.info("[LOG] Mise à jour du post ID : " + postId);
|
||||
@@ -230,7 +201,7 @@ public class SocialPostResource {
|
||||
try {
|
||||
// Récupérer le post pour vérifier le propriétaire
|
||||
SocialPost existingPost = socialPostService.getPostById(postId);
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
|
||||
// Vérifier que l'utilisateur authentifié est le créateur du post
|
||||
if (existingPost.getUser() == null || !authenticatedUserId.equals(existingPost.getUser().getId())) {
|
||||
@@ -272,7 +243,6 @@ public class SocialPostResource {
|
||||
@DELETE
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@Operation(
|
||||
summary = "Supprimer un post",
|
||||
description = "Supprime un post social. Seul le créateur peut supprimer.")
|
||||
@@ -281,15 +251,13 @@ public class SocialPostResource {
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "403", description = "Non autorisé à supprimer ce post")
|
||||
@APIResponse(responseCode = "404", description = "Post non trouvé")
|
||||
public Response deletePost(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("id") UUID postId) {
|
||||
public Response deletePost(@PathParam("id") UUID postId) {
|
||||
LOG.info("[LOG] Suppression du post ID : " + postId);
|
||||
|
||||
try {
|
||||
// Récupérer le post pour vérifier le propriétaire
|
||||
SocialPost existingPost = socialPostService.getPostById(postId);
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
|
||||
// Vérifier que l'utilisateur authentifié est le créateur du post
|
||||
if (existingPost.getUser() == null || !authenticatedUserId.equals(existingPost.getUser().getId())) {
|
||||
@@ -372,7 +340,6 @@ public class SocialPostResource {
|
||||
@POST
|
||||
@Path("/{id}/like")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@Operation(
|
||||
summary = "Liker un post",
|
||||
description = "Incrémente le compteur de likes d'un post. Requiert une authentification JWT.")
|
||||
@@ -380,10 +347,8 @@ public class SocialPostResource {
|
||||
@APIResponse(responseCode = "200", description = "Post liké")
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "404", description = "Post non trouvé")
|
||||
public Response likePost(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("id") UUID postId) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
public Response likePost(@PathParam("id") UUID postId) {
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("[LOG] Like du post ID : " + postId + " par utilisateur : " + authenticatedUserId);
|
||||
|
||||
try {
|
||||
@@ -418,7 +383,6 @@ public class SocialPostResource {
|
||||
@POST
|
||||
@Path("/{id}/comment")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Operation(
|
||||
summary = "Commenter un post (legacy)",
|
||||
@@ -428,10 +392,9 @@ public class SocialPostResource {
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "404", description = "Post non trouvé")
|
||||
public Response addComment(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("id") UUID postId,
|
||||
String requestBody) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("[LOG] Ajout de commentaire au post ID : " + postId + " par utilisateur : " + authenticatedUserId);
|
||||
|
||||
try {
|
||||
@@ -469,7 +432,6 @@ public class SocialPostResource {
|
||||
@POST
|
||||
@Path("/{id}/share")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@Operation(
|
||||
summary = "Partager un post",
|
||||
description = "Incrémente le compteur de partages d'un post. Requiert une authentification JWT.")
|
||||
@@ -477,10 +439,8 @@ public class SocialPostResource {
|
||||
@APIResponse(responseCode = "200", description = "Post partagé")
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "404", description = "Post non trouvé")
|
||||
public Response sharePost(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("id") UUID postId) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
public Response sharePost(@PathParam("id") UUID postId) {
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("[LOG] Partage du post ID : " + postId + " par utilisateur : " + authenticatedUserId);
|
||||
|
||||
try {
|
||||
@@ -720,7 +680,6 @@ public class SocialPostResource {
|
||||
@PUT
|
||||
@Path("/{postId}/comments/{commentId}")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@Operation(
|
||||
summary = "Mettre à jour un commentaire",
|
||||
description = "Met à jour le contenu d'un commentaire. Seul l'auteur peut modifier. Requiert une authentification JWT.")
|
||||
@@ -730,7 +689,6 @@ public class SocialPostResource {
|
||||
@APIResponse(responseCode = "403", description = "Non autorisé à modifier ce commentaire")
|
||||
@APIResponse(responseCode = "404", description = "Commentaire non trouvé")
|
||||
public Response updateComment(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("postId") UUID postId,
|
||||
@PathParam("commentId") UUID commentId,
|
||||
Map<String, String> requestBody) {
|
||||
@@ -739,7 +697,7 @@ public class SocialPostResource {
|
||||
String content = requestBody != null ? requestBody.get("content") : null;
|
||||
|
||||
// Utiliser l'utilisateur authentifié
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
|
||||
try {
|
||||
PostComment comment = socialPostService.updateComment(commentId, authenticatedUserId, content);
|
||||
@@ -776,7 +734,6 @@ public class SocialPostResource {
|
||||
@DELETE
|
||||
@Path("/{postId}/comments/{commentId}")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@Operation(
|
||||
summary = "Supprimer un commentaire",
|
||||
description = "Supprime un commentaire. L'auteur du commentaire ou l'auteur du post peuvent supprimer. Requiert une authentification JWT.")
|
||||
@@ -786,10 +743,9 @@ public class SocialPostResource {
|
||||
@APIResponse(responseCode = "403", description = "Non autorisé à supprimer ce commentaire")
|
||||
@APIResponse(responseCode = "404", description = "Commentaire non trouvé")
|
||||
public Response deleteComment(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("postId") UUID postId,
|
||||
@PathParam("commentId") UUID commentId) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("[LOG] Suppression du commentaire ID : " + commentId + " par l'utilisateur : " + authenticatedUserId);
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package com.lions.dev.resource;
|
||||
|
||||
import com.lions.dev.core.security.JwtAuthFilter;
|
||||
import com.lions.dev.core.security.RequiresAuth;
|
||||
import com.lions.dev.dto.request.story.StoryCreateRequestDTO;
|
||||
import com.lions.dev.dto.response.story.StoryResponseDTO;
|
||||
import com.lions.dev.entity.story.Story;
|
||||
import com.lions.dev.service.SecurityService;
|
||||
import com.lions.dev.service.StoryService;
|
||||
import com.lions.dev.util.UserRoles;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.util.List;
|
||||
@@ -26,31 +26,24 @@ import org.jboss.logging.Logger;
|
||||
/**
|
||||
* Ressource REST pour la gestion des stories dans le système AfterWork.
|
||||
*
|
||||
* Cette classe expose des endpoints pour créer, récupérer, supprimer
|
||||
* et marquer les stories comme vues.
|
||||
* SÉCURITÉ : Les lectures sont publiques, les écritures requièrent une authentification.
|
||||
*
|
||||
* Tous les logs nécessaires pour la traçabilité sont intégrés.
|
||||
* @since 2.0 - Sécurité JWT production-ready
|
||||
*/
|
||||
@Path("/stories")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Stories", description = "Opérations liées à la gestion des stories")
|
||||
@RolesAllowed({UserRoles.USER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||
public class StoryResource {
|
||||
|
||||
@Inject
|
||||
StoryService storyService;
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(StoryResource.class);
|
||||
@Inject
|
||||
SecurityService securityService;
|
||||
|
||||
/**
|
||||
* Extrait l'ID de l'utilisateur authentifié du contexte de la requête.
|
||||
*
|
||||
* @param requestContext Le contexte de la requête
|
||||
* @return L'ID de l'utilisateur authentifié
|
||||
*/
|
||||
private UUID getAuthenticatedUserId(ContainerRequestContext requestContext) {
|
||||
return (UUID) requestContext.getProperty(JwtAuthFilter.AUTHENTICATED_USER_ID);
|
||||
}
|
||||
private static final Logger LOG = Logger.getLogger(StoryResource.class);
|
||||
|
||||
/**
|
||||
* Récupère toutes les stories actives (non expirées).
|
||||
@@ -162,7 +155,6 @@ public class StoryResource {
|
||||
*/
|
||||
@POST
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@Operation(
|
||||
summary = "Créer une nouvelle story",
|
||||
description = "Crée une nouvelle story et retourne ses détails. Requiert une authentification JWT.")
|
||||
@@ -170,13 +162,11 @@ public class StoryResource {
|
||||
@APIResponse(responseCode = "201", description = "Story créée avec succès")
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "403", description = "L'utilisateur authentifié ne correspond pas au creatorId")
|
||||
public Response createStory(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@Valid StoryCreateRequestDTO requestDTO) {
|
||||
public Response createStory(@Valid StoryCreateRequestDTO requestDTO) {
|
||||
LOG.info("[LOG] Création d'une nouvelle story par l'utilisateur ID : " + requestDTO.getCreatorId());
|
||||
|
||||
// Vérifier que l'utilisateur authentifié correspond au creatorId
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
if (!authenticatedUserId.equals(requestDTO.getCreatorId())) {
|
||||
LOG.warn("[WARN] Utilisateur " + authenticatedUserId + " tente de créer une story pour " + requestDTO.getCreatorId());
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
@@ -214,7 +204,6 @@ public class StoryResource {
|
||||
@POST
|
||||
@Path("/{id}/view")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@Operation(
|
||||
summary = "Marquer une story comme vue",
|
||||
description = "Marque une story comme vue par l'utilisateur authentifié et incrémente le compteur de vues.")
|
||||
@@ -222,10 +211,8 @@ public class StoryResource {
|
||||
@APIResponse(responseCode = "200", description = "Story marquée comme vue")
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "404", description = "Story non trouvée")
|
||||
public Response markStoryAsViewed(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("id") UUID storyId) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
public Response markStoryAsViewed(@PathParam("id") UUID storyId) {
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("[LOG] Marquage de la story ID : " + storyId + " comme vue par l'utilisateur ID : " + authenticatedUserId);
|
||||
|
||||
try {
|
||||
@@ -257,7 +244,6 @@ public class StoryResource {
|
||||
@DELETE
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@Operation(
|
||||
summary = "Supprimer une story",
|
||||
description = "Supprime définitivement une story. Seul le créateur peut supprimer.")
|
||||
@@ -266,10 +252,8 @@ public class StoryResource {
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "403", description = "Non autorisé à supprimer cette story")
|
||||
@APIResponse(responseCode = "404", description = "Story non trouvée")
|
||||
public Response deleteStory(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("id") UUID storyId) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
public Response deleteStory(@PathParam("id") UUID storyId) {
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("[LOG] Suppression de la story ID : " + storyId + " par l'utilisateur : " + authenticatedUserId);
|
||||
|
||||
try {
|
||||
|
||||
@@ -11,9 +11,15 @@ import com.lions.dev.dto.response.users.UserCreateResponseDTO;
|
||||
import com.lions.dev.dto.response.users.UserDeleteResponseDto;
|
||||
import com.lions.dev.entity.users.Users;
|
||||
import com.lions.dev.exception.UserNotFoundException;
|
||||
import com.lions.dev.security.Permission;
|
||||
import com.lions.dev.security.RequiresPermission;
|
||||
import com.lions.dev.service.JwtService;
|
||||
import com.lions.dev.service.PasswordResetService;
|
||||
import com.lions.dev.service.SecurityService;
|
||||
import com.lions.dev.service.UsersService;
|
||||
import com.lions.dev.util.UserRoles;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
@@ -35,8 +41,13 @@ import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Ressource REST pour la gestion des utilisateurs dans le système AfterWork.
|
||||
* Cette classe expose des endpoints pour créer, authentifier, récupérer et supprimer des utilisateurs.
|
||||
* Tous les logs nécessaires pour la traçabilité sont intégrés.
|
||||
*
|
||||
* Sécurité JWT implémentée :
|
||||
* - Endpoints publics : register, authenticate, forgot-password, reset-password
|
||||
* - Endpoints authentifiés : profil utilisateur, recherche
|
||||
* - Endpoints admin : liste des utilisateurs, gestion des rôles
|
||||
*
|
||||
* @since 2.0 - Sécurité JWT production-ready avec @RolesAllowed
|
||||
*/
|
||||
@Path("/users")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@@ -53,6 +64,9 @@ public class UsersResource {
|
||||
@Inject
|
||||
PasswordResetService passwordResetService;
|
||||
|
||||
@Inject
|
||||
SecurityService securityService;
|
||||
|
||||
@ConfigProperty(name = "afterwork.super-admin.api-key", defaultValue = "")
|
||||
Optional<String> superAdminApiKey;
|
||||
|
||||
@@ -60,21 +74,23 @@ public class UsersResource {
|
||||
|
||||
private static final String SUPER_ADMIN_KEY_HEADER = "X-Super-Admin-Key";
|
||||
|
||||
// ============================================================
|
||||
// ENDPOINTS PUBLICS (pas d'authentification requise)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Endpoint pour créer un nouvel utilisateur.
|
||||
*
|
||||
* @param userCreateRequestDTO Le DTO contenant les informations de l'utilisateur à créer.
|
||||
* @return Une réponse HTTP contenant l'utilisateur créé ou un message d'erreur.
|
||||
* Endpoint pour créer un nouvel utilisateur (inscription).
|
||||
* PUBLIC - Pas d'authentification requise.
|
||||
*/
|
||||
@POST
|
||||
@Path("/register")
|
||||
@Transactional
|
||||
@PermitAll
|
||||
@Operation(
|
||||
summary = "Créer un nouvel utilisateur",
|
||||
description = "Crée un nouvel utilisateur et retourne les détails")
|
||||
summary = "Créer un nouvel utilisateur (inscription)",
|
||||
description = "Crée un nouvel utilisateur et retourne les détails. Endpoint public.")
|
||||
public Response createUser(@Valid @NotNull UserCreateRequestDTO userCreateRequestDTO) {
|
||||
LOG.info(
|
||||
"Tentative de création d'un nouvel utilisateur avec l'email : "
|
||||
+ userCreateRequestDTO.getEmail());
|
||||
LOG.info("Tentative de création d'un nouvel utilisateur avec l'email : " + userCreateRequestDTO.getEmail());
|
||||
|
||||
Users user = userService.createUser(userCreateRequestDTO);
|
||||
UserCreateResponseDTO responseDTO = new UserCreateResponseDTO(user);
|
||||
@@ -82,28 +98,25 @@ public class UsersResource {
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint pour authentifier un utilisateur (v2.0).
|
||||
*
|
||||
* @param userAuthenticateRequestDTO Le DTO contenant les informations d'authentification.
|
||||
* @return Une réponse HTTP indiquant si l'authentification a réussi ou échoué.
|
||||
* Endpoint pour authentifier un utilisateur.
|
||||
* PUBLIC - Pas d'authentification requise.
|
||||
*/
|
||||
@POST
|
||||
@Path("/authenticate")
|
||||
@PermitAll
|
||||
@Operation(
|
||||
summary = "Authentifier un utilisateur",
|
||||
description = "Vérifie les informations de connexion de l'utilisateur")
|
||||
description = "Vérifie les informations de connexion et retourne un token JWT. Endpoint public.")
|
||||
public Response authenticateUser(@Valid @NotNull UserAuthenticateRequestDTO userAuthenticateRequestDTO) {
|
||||
LOG.info("Tentative d'authentification pour l'utilisateur avec l'email : " + userAuthenticateRequestDTO.getEmail());
|
||||
|
||||
// v2.0 - Utiliser getPassword() qui gère la compatibilité v1.0 et v2.0
|
||||
Users user = userService.authenticateUser(userAuthenticateRequestDTO.getEmail(), userAuthenticateRequestDTO.getPassword());
|
||||
LOG.info("Authentification réussie pour l'utilisateur : " + user.getEmail());
|
||||
|
||||
// v2.0 - Utiliser les nouveaux noms de champs
|
||||
UserAuthenticateResponseDTO responseDTO = new UserAuthenticateResponseDTO(
|
||||
user.getId(),
|
||||
user.getFirstName(), // v2.0
|
||||
user.getLastName(), // v2.0
|
||||
user.getFirstName(),
|
||||
user.getLastName(),
|
||||
user.getEmail(),
|
||||
user.getRole()
|
||||
);
|
||||
@@ -112,244 +125,42 @@ public class UsersResource {
|
||||
return Response.ok(responseDTO).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint pour récupérer les détails d'un utilisateur par ID.
|
||||
*
|
||||
* @param id L'ID de l'utilisateur.
|
||||
* @return Une réponse HTTP contenant les informations de l'utilisateur.
|
||||
*/
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
@Operation(
|
||||
summary = "Récupérer un utilisateur par ID",
|
||||
description = "Retourne les détails de l'utilisateur demandé")
|
||||
public Response getUserById(@PathParam("id") UUID id) {
|
||||
LOG.info("Récupération de l'utilisateur avec l'ID : " + id);
|
||||
|
||||
Users user = userService.getUserById(id);
|
||||
UserCreateResponseDTO responseDTO = new UserCreateResponseDTO(user);
|
||||
LOG.info("Utilisateur trouvé : " + user.getEmail());
|
||||
return Response.ok(responseDTO).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint pour récupérer tous les utilisateurs avec pagination.
|
||||
*
|
||||
* @param page Le numéro de la page à récupérer.
|
||||
* @param size Le nombre d'utilisateurs par page.
|
||||
* @return Une réponse HTTP contenant la liste des utilisateurs paginée.
|
||||
*/
|
||||
@GET
|
||||
@Operation(
|
||||
summary = "Récupérer tous les utilisateurs avec pagination",
|
||||
description = "Retourne la liste paginée des utilisateurs")
|
||||
public Response listUsers(@QueryParam("page") @DefaultValue("1") int page, @QueryParam("size") @DefaultValue("10") int size) {
|
||||
LOG.info("Récupération de la liste des utilisateurs - page : " + page + ", taille : " + size);
|
||||
|
||||
List<Users> users = userService.listUsers(page, size);
|
||||
List<UserCreateResponseDTO> responseDTOs = users.stream()
|
||||
.map(UserCreateResponseDTO::new)
|
||||
.collect(Collectors.toList());
|
||||
LOG.info("Liste des utilisateurs récupérée avec succès, taille : " + responseDTOs.size());
|
||||
return Response.ok(responseDTOs).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint pour supprimer un utilisateur par ID.
|
||||
*
|
||||
* @param id L'ID de l'utilisateur à supprimer.
|
||||
* @return Une réponse HTTP avec le statut de suppression.
|
||||
*/
|
||||
@DELETE
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Supprimer un utilisateur",
|
||||
description = "Supprime un utilisateur de la base de données")
|
||||
public Response deleteUser(@PathParam("id") UUID id) {
|
||||
LOG.info("Tentative de suppression de l'utilisateur avec l'ID : " + id);
|
||||
|
||||
boolean deleted = userService.deleteUser(id);
|
||||
|
||||
UserDeleteResponseDto responseDTO = new UserDeleteResponseDto();
|
||||
if (deleted) {
|
||||
LOG.info("Utilisateur supprimé avec succès.");
|
||||
responseDTO.setSuccess(true);
|
||||
responseDTO.setMessage("Utilisateur supprimé avec succès.");
|
||||
responseDTO.logResponseDetails();
|
||||
return Response.ok(responseDTO).build();
|
||||
} else {
|
||||
LOG.warn("Échec de la suppression : utilisateur introuvable avec l'ID : " + id);
|
||||
responseDTO.setSuccess(false);
|
||||
responseDTO.setMessage("Utilisateur non trouvé.");
|
||||
responseDTO.logResponseDetails();
|
||||
return Response.status(Response.Status.NOT_FOUND).entity(responseDTO).build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint pour mettre à jour un utilisateur.
|
||||
*
|
||||
* @param id L'ID de l'utilisateur à mettre à jour.
|
||||
* @param userCreateRequestDTO Les informations mises à jour de l'utilisateur.
|
||||
* @return Les informations de l'utilisateur mis à jour.
|
||||
*/
|
||||
@PUT
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Mettre à jour un utilisateur",
|
||||
description = "Met à jour les informations d'un utilisateur existant")
|
||||
public Response updateUser(@PathParam("id") UUID id, @Valid UserCreateRequestDTO userCreateRequestDTO) {
|
||||
LOG.info("Tentative de mise à jour de l'utilisateur avec l'ID : " + id);
|
||||
|
||||
// Appel au service avec l'ID et les nouvelles informations
|
||||
Users updatedUser = userService.updateUser(id, userCreateRequestDTO);
|
||||
|
||||
LOG.info("Utilisateur mis à jour avec succès : " + updatedUser.getEmail());
|
||||
UserCreateResponseDTO responseDTO = new UserCreateResponseDTO(updatedUser);
|
||||
return Response.ok(responseDTO).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint pour réinitialiser le mot de passe d'un utilisateur.
|
||||
*
|
||||
* @param id L'ID de l'utilisateur.
|
||||
* @param nouveauMotDePasse Le nouveau mot de passe.
|
||||
* @return Un message indiquant si la réinitialisation a réussi.
|
||||
*/
|
||||
@PATCH
|
||||
@Path("/{id}/reset-password")
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Réinitialiser le mot de passe d'un utilisateur",
|
||||
description = "Réinitialise le mot de passe de l'utilisateur et le met à jour dans la base de données")
|
||||
public Response resetPassword(@PathParam("id") UUID id, @QueryParam("newPassword") String nouveauMotDePasse) {
|
||||
LOG.info("Réinitialisation du mot de passe pour l'utilisateur avec l'ID : " + id);
|
||||
|
||||
userService.resetPassword(id, nouveauMotDePasse);
|
||||
return Response.ok(Map.of("message", "Mot de passe réinitialisé avec succès.")).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint pour mettre à jour l'image de profil de l'utilisateur.
|
||||
* Accepte un JSON avec {@code profileImageUrl} (URL retournée par l'upload de médias).
|
||||
* Réponse toujours en JSON (utilisateur mis à jour ou message d'erreur).
|
||||
*
|
||||
* @param id L'identifiant de l'utilisateur.
|
||||
* @param request Corps JSON : { "profileImageUrl": "https://..." }
|
||||
* @return Réponse JSON : utilisateur mis à jour (200) ou message d'erreur (4xx/5xx).
|
||||
*/
|
||||
@PUT
|
||||
@Path("/{id}/profile-image")
|
||||
@Operation(summary = "Mettre à jour l'image de profil d'un utilisateur", description = "Met à jour l'URL de l'image de profil (après upload). Corps JSON : profileImageUrl.")
|
||||
public Response updateUserProfileImage(@PathParam("id") UUID id, @Valid @NotNull UpdateProfileImageRequestDTO request) {
|
||||
try {
|
||||
String profileImageUrl = request.getProfileImageUrl();
|
||||
if (profileImageUrl == null || profileImageUrl.isBlank()) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(Map.of("message", "L'URL de l'image de profil est obligatoire."))
|
||||
.build();
|
||||
}
|
||||
Users updatedUser = userService.updateUserProfileImage(id, profileImageUrl.trim());
|
||||
LOG.info("[LOG] Image de profil mise à jour pour l'utilisateur avec l'ID : " + id);
|
||||
UserCreateResponseDTO responseDTO = new UserCreateResponseDTO(updatedUser);
|
||||
return Response.ok(responseDTO).build();
|
||||
} catch (UserNotFoundException e) {
|
||||
LOG.warn("Utilisateur non trouvé pour mise à jour image de profil : " + id);
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(Map.of("message", e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("[ERROR] Erreur lors de la mise à jour de l'image de profil : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("message", "Erreur lors de la mise à jour de l'image de profil."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint pour rechercher un utilisateur par email.
|
||||
*
|
||||
* @param email L'email de l'utilisateur à rechercher.
|
||||
* @return Une réponse HTTP contenant les informations de l'utilisateur.
|
||||
*/
|
||||
@GET
|
||||
@Path("/search")
|
||||
@Operation(
|
||||
summary = "Rechercher un utilisateur par email",
|
||||
description = "Retourne les détails de l'utilisateur correspondant à l'email fourni")
|
||||
public Response searchUserByEmail(@QueryParam("email") String email) {
|
||||
if (email == null || email.isBlank()) {
|
||||
LOG.warn("Tentative de recherche avec un email vide ou null");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity("{\"message\": \"L'email est requis pour la recherche.\"}")
|
||||
.build();
|
||||
}
|
||||
|
||||
LOG.info("Recherche de l'utilisateur avec l'email : " + email);
|
||||
|
||||
try {
|
||||
Users user = userService.getUserByEmail(email);
|
||||
UserCreateResponseDTO responseDTO = new UserCreateResponseDTO(user);
|
||||
LOG.info("Utilisateur trouvé : " + user.getEmail());
|
||||
return Response.ok(responseDTO).build();
|
||||
} catch (UserNotFoundException e) {
|
||||
LOG.warn("Utilisateur non trouvé avec l'email : " + email);
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity("{\"message\": \"Utilisateur non trouvé avec cet email.\"}")
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("[ERROR] Erreur lors de la recherche de l'utilisateur : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("{\"message\": \"Erreur lors de la recherche de l'utilisateur.\"}")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint pour demander une réinitialisation de mot de passe par email.
|
||||
*
|
||||
* @param request Le DTO contenant l'email de l'utilisateur.
|
||||
* @return Une réponse HTTP indiquant que l'email a été envoyé (ou non, pour ne pas révéler si l'email existe).
|
||||
* PUBLIC - Pas d'authentification requise.
|
||||
*/
|
||||
@POST
|
||||
@Path("/forgot-password")
|
||||
@Operation(summary = "Demander une reinitialisation de mot de passe par email",
|
||||
description = "Envoie un email de reinitialisation si un compte existe avec cet email")
|
||||
@APIResponse(responseCode = "200", description = "Email de reinitialisation envoye si le compte existe")
|
||||
@APIResponse(responseCode = "400", description = "Email invalide")
|
||||
@PermitAll
|
||||
@Operation(summary = "Demander une réinitialisation de mot de passe par email",
|
||||
description = "Envoie un email de réinitialisation si un compte existe. Endpoint public.")
|
||||
@APIResponse(responseCode = "200", description = "Email de réinitialisation envoyé si le compte existe")
|
||||
public Response requestPasswordReset(@Valid PasswordResetRequest request) {
|
||||
LOG.info("Demande de reinitialisation de mot de passe pour l'email : " + request.getEmail());
|
||||
LOG.info("Demande de réinitialisation de mot de passe pour l'email : " + request.getEmail());
|
||||
|
||||
try {
|
||||
// Le service gère tout : création du token et envoi de l'email
|
||||
passwordResetService.initiatePasswordReset(request.getEmail());
|
||||
|
||||
// Toujours retourner 200 pour ne pas reveler si l'email existe
|
||||
// Toujours retourner 200 pour ne pas révéler si l'email existe
|
||||
return Response.ok()
|
||||
.entity(Map.of("message", "Si un compte existe avec cet email, un lien de reinitialisation a ete envoye"))
|
||||
.entity(Map.of("message", "Si un compte existe avec cet email, un lien de réinitialisation a été envoyé"))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors de la demande de reinitialisation de mot de passe", e);
|
||||
// Toujours retourner 200 pour ne pas reveler si l'email existe
|
||||
LOG.error("Erreur lors de la demande de réinitialisation de mot de passe", e);
|
||||
return Response.ok()
|
||||
.entity(Map.of("message", "Si un compte existe avec cet email, un lien de reinitialisation a ete envoye"))
|
||||
.entity(Map.of("message", "Si un compte existe avec cet email, un lien de réinitialisation a été envoyé"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint pour réinitialiser le mot de passe avec un token valide.
|
||||
*
|
||||
* @param token Le token de réinitialisation reçu par email.
|
||||
* @param newPassword Le nouveau mot de passe.
|
||||
* @return Une réponse HTTP indiquant si la réinitialisation a réussi.
|
||||
* PUBLIC - Le token sert d'authentification.
|
||||
*/
|
||||
@POST
|
||||
@Path("/reset-password")
|
||||
@PermitAll
|
||||
@Operation(summary = "Réinitialiser le mot de passe avec un token",
|
||||
description = "Réinitialise le mot de passe en utilisant le token reçu par email")
|
||||
description = "Réinitialise le mot de passe en utilisant le token reçu par email. Endpoint public.")
|
||||
@APIResponse(responseCode = "200", description = "Mot de passe réinitialisé avec succès")
|
||||
@APIResponse(responseCode = "400", description = "Token invalide ou expiré")
|
||||
public Response resetPasswordWithToken(
|
||||
@@ -383,20 +194,244 @@ public class UsersResource {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ENDPOINTS AUTHENTIFIÉS (USER ou plus)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Endpoint pour récupérer le profil de l'utilisateur connecté.
|
||||
* AUTHENTIFIÉ - Retourne les données de l'utilisateur du token JWT.
|
||||
*/
|
||||
@GET
|
||||
@Path("/me")
|
||||
@RequiresPermission(Permission.PROFILE_READ)
|
||||
@Operation(
|
||||
summary = "Récupérer mon profil",
|
||||
description = "Retourne les détails de l'utilisateur connecté (depuis le token JWT)")
|
||||
public Response getCurrentUser() {
|
||||
UUID currentUserId = securityService.getCurrentUserId();
|
||||
LOG.info("Récupération du profil pour l'utilisateur connecté : " + currentUserId);
|
||||
|
||||
Users user = userService.getUserById(currentUserId);
|
||||
UserCreateResponseDTO responseDTO = new UserCreateResponseDTO(user);
|
||||
return Response.ok(responseDTO).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint pour récupérer les détails d'un utilisateur par ID.
|
||||
* AUTHENTIFIÉ - L'utilisateur peut voir son profil. Les admins peuvent voir tous les profils.
|
||||
*/
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
@RequiresPermission(value = {Permission.PROFILE_READ, Permission.USERS_READ_ALL}, requireAll = false)
|
||||
@Operation(
|
||||
summary = "Récupérer un utilisateur par ID",
|
||||
description = "Retourne les détails de l'utilisateur. L'utilisateur peut voir son profil, les admins tous les profils.")
|
||||
public Response getUserById(@PathParam("id") UUID id) {
|
||||
// Vérifier que l'utilisateur accède à ses données ou est admin
|
||||
securityService.requireSameUserOrRole(id, UserRoles.ADMIN, UserRoles.SUPER_ADMIN);
|
||||
|
||||
LOG.info("Récupération de l'utilisateur avec l'ID : " + id);
|
||||
|
||||
Users user = userService.getUserById(id);
|
||||
UserCreateResponseDTO responseDTO = new UserCreateResponseDTO(user);
|
||||
LOG.info("Utilisateur trouvé : " + user.getEmail());
|
||||
return Response.ok(responseDTO).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint pour mettre à jour un utilisateur.
|
||||
* AUTHENTIFIÉ - L'utilisateur peut modifier SON profil uniquement. Les admins peuvent modifier tous les profils.
|
||||
*/
|
||||
@PUT
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@RequiresPermission(value = {Permission.PROFILE_UPDATE, Permission.USERS_UPDATE_ANY}, requireAll = false)
|
||||
@Operation(
|
||||
summary = "Mettre à jour un utilisateur",
|
||||
description = "Met à jour les informations d'un utilisateur. L'utilisateur peut modifier son profil, les admins tous les profils.")
|
||||
public Response updateUser(@PathParam("id") UUID id, @Valid UserCreateRequestDTO userCreateRequestDTO) {
|
||||
// Vérifier que l'utilisateur modifie ses données ou est admin
|
||||
securityService.requireSameUserOrRole(id, UserRoles.ADMIN, UserRoles.SUPER_ADMIN);
|
||||
|
||||
LOG.info("Tentative de mise à jour de l'utilisateur avec l'ID : " + id);
|
||||
|
||||
Users updatedUser = userService.updateUser(id, userCreateRequestDTO);
|
||||
|
||||
LOG.info("Utilisateur mis à jour avec succès : " + updatedUser.getEmail());
|
||||
UserCreateResponseDTO responseDTO = new UserCreateResponseDTO(updatedUser);
|
||||
return Response.ok(responseDTO).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint pour supprimer un utilisateur par ID.
|
||||
* AUTHENTIFIÉ - L'utilisateur peut supprimer SON compte. Les admins peuvent supprimer tous les comptes.
|
||||
*/
|
||||
@DELETE
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@RequiresPermission(value = {Permission.PROFILE_DELETE, Permission.USERS_DELETE_ANY}, requireAll = false)
|
||||
@Operation(
|
||||
summary = "Supprimer un utilisateur",
|
||||
description = "Supprime un utilisateur. L'utilisateur peut supprimer son compte, les admins tous les comptes.")
|
||||
public Response deleteUser(@PathParam("id") UUID id) {
|
||||
// Vérifier que l'utilisateur supprime son compte ou est admin
|
||||
securityService.requireSameUserOrRole(id, UserRoles.ADMIN, UserRoles.SUPER_ADMIN);
|
||||
|
||||
LOG.info("Tentative de suppression de l'utilisateur avec l'ID : " + id);
|
||||
|
||||
boolean deleted = userService.deleteUser(id);
|
||||
|
||||
UserDeleteResponseDto responseDTO = new UserDeleteResponseDto();
|
||||
if (deleted) {
|
||||
LOG.info("Utilisateur supprimé avec succès.");
|
||||
responseDTO.setSuccess(true);
|
||||
responseDTO.setMessage("Utilisateur supprimé avec succès.");
|
||||
responseDTO.logResponseDetails();
|
||||
return Response.ok(responseDTO).build();
|
||||
} else {
|
||||
LOG.warn("Échec de la suppression : utilisateur introuvable avec l'ID : " + id);
|
||||
responseDTO.setSuccess(false);
|
||||
responseDTO.setMessage("Utilisateur non trouvé.");
|
||||
responseDTO.logResponseDetails();
|
||||
return Response.status(Response.Status.NOT_FOUND).entity(responseDTO).build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint pour réinitialiser le mot de passe d'un utilisateur connecté.
|
||||
* AUTHENTIFIÉ - L'utilisateur peut changer SON mot de passe uniquement.
|
||||
*/
|
||||
@PATCH
|
||||
@Path("/{id}/reset-password")
|
||||
@Transactional
|
||||
@RequiresPermission(Permission.PROFILE_UPDATE)
|
||||
@Operation(
|
||||
summary = "Changer mon mot de passe",
|
||||
description = "L'utilisateur connecté peut changer son propre mot de passe")
|
||||
public Response resetPassword(@PathParam("id") UUID id, @QueryParam("newPassword") String nouveauMotDePasse) {
|
||||
// L'utilisateur ne peut changer que SON mot de passe
|
||||
securityService.requireSameUser(id);
|
||||
|
||||
LOG.info("Réinitialisation du mot de passe pour l'utilisateur avec l'ID : " + id);
|
||||
|
||||
userService.resetPassword(id, nouveauMotDePasse);
|
||||
return Response.ok(Map.of("message", "Mot de passe réinitialisé avec succès.")).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint pour mettre à jour l'image de profil de l'utilisateur.
|
||||
* AUTHENTIFIÉ - L'utilisateur peut modifier SA photo uniquement.
|
||||
*/
|
||||
@PUT
|
||||
@Path("/{id}/profile-image")
|
||||
@RequiresPermission(Permission.PROFILE_UPDATE)
|
||||
@Operation(summary = "Mettre à jour l'image de profil",
|
||||
description = "L'utilisateur connecté peut modifier sa propre image de profil")
|
||||
public Response updateUserProfileImage(@PathParam("id") UUID id, @Valid @NotNull UpdateProfileImageRequestDTO request) {
|
||||
// L'utilisateur ne peut modifier que SA photo
|
||||
securityService.requireSameUser(id);
|
||||
|
||||
try {
|
||||
String profileImageUrl = request.getProfileImageUrl();
|
||||
if (profileImageUrl == null || profileImageUrl.isBlank()) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(Map.of("message", "L'URL de l'image de profil est obligatoire."))
|
||||
.build();
|
||||
}
|
||||
Users updatedUser = userService.updateUserProfileImage(id, profileImageUrl.trim());
|
||||
LOG.info("Image de profil mise à jour pour l'utilisateur avec l'ID : " + id);
|
||||
UserCreateResponseDTO responseDTO = new UserCreateResponseDTO(updatedUser);
|
||||
return Response.ok(responseDTO).build();
|
||||
} catch (UserNotFoundException e) {
|
||||
LOG.warn("Utilisateur non trouvé pour mise à jour image de profil : " + id);
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(Map.of("message", e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors de la mise à jour de l'image de profil : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("message", "Erreur lors de la mise à jour de l'image de profil."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint pour rechercher un utilisateur par email.
|
||||
* AUTHENTIFIÉ - Accessible à tous les utilisateurs connectés.
|
||||
*/
|
||||
@GET
|
||||
@Path("/search")
|
||||
@RequiresPermission(Permission.SOCIAL_SEARCH)
|
||||
@Operation(
|
||||
summary = "Rechercher un utilisateur par email",
|
||||
description = "Retourne les détails de l'utilisateur correspondant à l'email. Requiert authentification.")
|
||||
public Response searchUserByEmail(@QueryParam("email") String email) {
|
||||
if (email == null || email.isBlank()) {
|
||||
LOG.warn("Tentative de recherche avec un email vide ou null");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(Map.of("message", "L'email est requis pour la recherche."))
|
||||
.build();
|
||||
}
|
||||
|
||||
LOG.info("Recherche de l'utilisateur avec l'email : " + email);
|
||||
|
||||
try {
|
||||
Users user = userService.getUserByEmail(email);
|
||||
UserCreateResponseDTO responseDTO = new UserCreateResponseDTO(user);
|
||||
LOG.info("Utilisateur trouvé : " + user.getEmail());
|
||||
return Response.ok(responseDTO).build();
|
||||
} catch (UserNotFoundException e) {
|
||||
LOG.warn("Utilisateur non trouvé avec l'email : " + email);
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(Map.of("message", "Utilisateur non trouvé avec cet email."))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors de la recherche de l'utilisateur : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("message", "Erreur lors de la recherche de l'utilisateur."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ENDPOINTS ADMIN (ADMIN ou SUPER_ADMIN requis)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Endpoint pour récupérer tous les utilisateurs avec pagination.
|
||||
* ADMIN - Réservé aux administrateurs.
|
||||
*/
|
||||
@GET
|
||||
@RequiresPermission(Permission.USERS_READ_ALL)
|
||||
@Operation(
|
||||
summary = "Récupérer tous les utilisateurs (admin)",
|
||||
description = "Retourne la liste paginée des utilisateurs. Réservé aux administrateurs.")
|
||||
public Response listUsers(@QueryParam("page") @DefaultValue("1") int page, @QueryParam("size") @DefaultValue("10") int size) {
|
||||
LOG.info("Récupération de la liste des utilisateurs par admin - page : " + page + ", taille : " + size);
|
||||
|
||||
List<Users> users = userService.listUsers(page, size);
|
||||
List<UserCreateResponseDTO> responseDTOs = users.stream()
|
||||
.map(UserCreateResponseDTO::new)
|
||||
.collect(Collectors.toList());
|
||||
LOG.info("Liste des utilisateurs récupérée avec succès, taille : " + responseDTOs.size());
|
||||
return Response.ok(responseDTOs).build();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ENDPOINTS SUPER ADMIN (X-Super-Admin-Key requis)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Attribue un rôle à un utilisateur (réservé au super administrateur).
|
||||
* Requiert le header X-Super-Admin-Key correspondant à afterwork.super-admin.api-key.
|
||||
*
|
||||
* @param id L'ID de l'utilisateur.
|
||||
* @param request Le DTO contenant le nouveau rôle.
|
||||
* @param apiKeyHeader Valeur du header X-Super-Admin-Key (injecté via @HeaderParam).
|
||||
* @return L'utilisateur mis à jour.
|
||||
* SUPER ADMIN - Requiert le header X-Super-Admin-Key.
|
||||
*/
|
||||
@PUT
|
||||
@Path("/{id}/role")
|
||||
@Transactional
|
||||
@RequiresPermission(Permission.USERS_ASSIGN_ROLES)
|
||||
@Operation(summary = "Attribuer un rôle à un utilisateur (super admin)",
|
||||
description = "Modifie le rôle d'un utilisateur. Réservé au super administrateur (header X-Super-Admin-Key).")
|
||||
description = "Modifie le rôle d'un utilisateur. Réservé au super administrateur.")
|
||||
@APIResponse(responseCode = "200", description = "Rôle mis à jour")
|
||||
@APIResponse(responseCode = "403", description = "Clé super admin invalide ou absente")
|
||||
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé")
|
||||
@@ -405,17 +440,18 @@ public class UsersResource {
|
||||
@Valid AssignRoleRequestDTO request,
|
||||
@HeaderParam(SUPER_ADMIN_KEY_HEADER) String apiKeyHeader) {
|
||||
|
||||
// Double vérification : JWT + API Key
|
||||
String key = superAdminApiKey.orElse("");
|
||||
if (key.isBlank()) {
|
||||
LOG.warn("Opération assignRole refusée : afterwork.super-admin.api-key non configurée");
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
.entity("{\"message\": \"Opération non autorisée : clé super admin non configurée.\"}")
|
||||
.entity(Map.of("message", "Opération non autorisée : clé super admin non configurée."))
|
||||
.build();
|
||||
}
|
||||
if (apiKeyHeader == null || !apiKeyHeader.equals(key)) {
|
||||
LOG.warn("Opération assignRole refusée : clé super admin invalide ou absente");
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
.entity("{\"message\": \"Clé super administrateur invalide ou absente.\"}")
|
||||
.entity(Map.of("message", "Clé super administrateur invalide ou absente."))
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -425,72 +461,64 @@ public class UsersResource {
|
||||
return Response.ok(responseDTO).build();
|
||||
} catch (UserNotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity("{\"message\": \"" + e.getMessage() + "\"}")
|
||||
.entity(Map.of("message", e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un token temporaire d'impersonation (Super Admin se connecte en tant qu'un autre utilisateur).
|
||||
* Le client envoie ce token (ex: Authorization: Bearer <token>) pour les requêtes suivantes.
|
||||
* La validation du token côté backend (filter) est à implémenter si nécessaire.
|
||||
*
|
||||
* @param id L'ID de l'utilisateur à impersonner.
|
||||
* @param apiKeyHeader X-Super-Admin-Key.
|
||||
* @return JSON avec impersonationToken, expiresInSeconds, userId.
|
||||
* SUPER ADMIN - Requiert le header X-Super-Admin-Key.
|
||||
*/
|
||||
@POST
|
||||
@Path("/{id}/impersonate")
|
||||
@RequiresPermission(Permission.SUPER_ADMIN_ACCESS)
|
||||
@Operation(summary = "Impersonation (Super Admin)",
|
||||
description = "Génère un token temporaire pour se connecter en tant que cet utilisateur. Header X-Super-Admin-Key requis.")
|
||||
description = "Génère un token JWT pour se connecter en tant que cet utilisateur.")
|
||||
public Response impersonate(
|
||||
@PathParam("id") UUID id,
|
||||
@HeaderParam(SUPER_ADMIN_KEY_HEADER) String apiKeyHeader) {
|
||||
|
||||
// Double vérification : JWT + API Key
|
||||
String key = superAdminApiKey.orElse("");
|
||||
if (key.isBlank()) {
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
.entity("{\"message\": \"Opération non autorisée : clé super admin non configurée.\"}")
|
||||
.entity(Map.of("message", "Opération non autorisée : clé super admin non configurée."))
|
||||
.build();
|
||||
}
|
||||
if (apiKeyHeader == null || !apiKeyHeader.equals(key)) {
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
.entity("{\"message\": \"Clé super administrateur invalide ou absente.\"}")
|
||||
.entity(Map.of("message", "Clé super administrateur invalide ou absente."))
|
||||
.build();
|
||||
}
|
||||
|
||||
try {
|
||||
Users user = userService.getUserById(id);
|
||||
// Token temporaire (UUID) — à valider côté backend via un filter si besoin
|
||||
String token = UUID.randomUUID().toString();
|
||||
int expiresInSeconds = 900; // 15 min
|
||||
// Générer un vrai token JWT pour l'utilisateur cible
|
||||
String token = jwtService.generateToken(user);
|
||||
return Response.ok(Map.of(
|
||||
"impersonationToken", token,
|
||||
"expiresInSeconds", expiresInSeconds,
|
||||
"token", token,
|
||||
"userId", user.getId().toString(),
|
||||
"email", user.getEmail()
|
||||
"email", user.getEmail(),
|
||||
"role", user.getRole()
|
||||
)).build();
|
||||
} catch (UserNotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity("{\"message\": \"Utilisateur non trouvé.\"}")
|
||||
.entity(Map.of("message", "Utilisateur non trouvé."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force l'activation ou la suspension d'un utilisateur (réservé au super administrateur).
|
||||
* Utilisé pour les managers : active = false = suspendu.
|
||||
*
|
||||
* @param id L'ID de l'utilisateur.
|
||||
* @param request Le DTO contenant active (true = forcer l'activation, false = suspendre).
|
||||
* @param apiKeyHeader Valeur du header X-Super-Admin-Key.
|
||||
* @return L'utilisateur mis à jour.
|
||||
* SUPER ADMIN - Requiert le header X-Super-Admin-Key.
|
||||
*/
|
||||
@PATCH
|
||||
@Path("/{id}/active")
|
||||
@Transactional
|
||||
@RequiresPermission(Permission.USERS_SUSPEND)
|
||||
@Operation(summary = "Forcer activation ou suspendre un utilisateur (super admin)",
|
||||
description = "Modifie le statut actif (isActive) d'un utilisateur. Réservé au super administrateur (header X-Super-Admin-Key).")
|
||||
description = "Modifie le statut actif (isActive) d'un utilisateur.")
|
||||
@APIResponse(responseCode = "200", description = "Statut actif mis à jour")
|
||||
@APIResponse(responseCode = "403", description = "Clé super admin invalide ou absente")
|
||||
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé")
|
||||
@@ -499,17 +527,18 @@ public class UsersResource {
|
||||
@Valid SetUserActiveRequestDTO request,
|
||||
@HeaderParam(SUPER_ADMIN_KEY_HEADER) String apiKeyHeader) {
|
||||
|
||||
// Double vérification : JWT + API Key
|
||||
String key = superAdminApiKey.orElse("");
|
||||
if (key.isBlank()) {
|
||||
LOG.warn("Opération setUserActive refusée : afterwork.super-admin.api-key non configurée");
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
.entity("{\"message\": \"Opération non autorisée : clé super admin non configurée.\"}")
|
||||
.entity(Map.of("message", "Opération non autorisée : clé super admin non configurée."))
|
||||
.build();
|
||||
}
|
||||
if (apiKeyHeader == null || !apiKeyHeader.equals(key)) {
|
||||
LOG.warn("Opération setUserActive refusée : clé super admin invalide ou absente");
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
.entity("{\"message\": \"Clé super administrateur invalide ou absente.\"}")
|
||||
.entity(Map.of("message", "Clé super administrateur invalide ou absente."))
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -519,7 +548,7 @@ public class UsersResource {
|
||||
return Response.ok(responseDTO).build();
|
||||
} catch (UserNotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity("{\"message\": \"" + e.getMessage() + "\"}")
|
||||
.entity(Map.of("message", e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
212
src/main/java/com/lions/dev/security/Permission.java
Normal file
212
src/main/java/com/lions/dev/security/Permission.java
Normal file
@@ -0,0 +1,212 @@
|
||||
package com.lions.dev.security;
|
||||
|
||||
/**
|
||||
* Enum des permissions granulaires du système RBAC.
|
||||
*
|
||||
* Nomenclature : RESSOURCE_ACTION
|
||||
*
|
||||
* @since 2.0 - Système RBAC production-ready
|
||||
*/
|
||||
public enum Permission {
|
||||
|
||||
// ========================================
|
||||
// PROFIL UTILISATEUR
|
||||
// ========================================
|
||||
PROFILE_READ("Lire son propre profil"),
|
||||
PROFILE_UPDATE("Modifier son propre profil"),
|
||||
PROFILE_DELETE("Supprimer son propre compte"),
|
||||
|
||||
// ========================================
|
||||
// UTILISATEURS (Administration)
|
||||
// ========================================
|
||||
USERS_READ_ALL("Lire tous les profils utilisateurs"),
|
||||
USERS_UPDATE_ANY("Modifier n'importe quel profil"),
|
||||
USERS_DELETE_ANY("Supprimer n'importe quel compte"),
|
||||
USERS_ASSIGN_ROLE("Attribuer des rôles aux utilisateurs"),
|
||||
USERS_ASSIGN_ROLES("Attribuer des rôles aux utilisateurs (alias)"),
|
||||
USERS_SUSPEND("Suspendre un compte utilisateur"),
|
||||
USERS_IMPERSONATE("Se connecter en tant qu'un autre utilisateur"),
|
||||
|
||||
// ========================================
|
||||
// SOCIAL / RECHERCHE
|
||||
// ========================================
|
||||
SOCIAL_SEARCH("Rechercher des utilisateurs"),
|
||||
SOCIAL_FOLLOW("Suivre des utilisateurs"),
|
||||
SOCIAL_BLOCK("Bloquer des utilisateurs"),
|
||||
|
||||
// ========================================
|
||||
// ÉVÉNEMENTS
|
||||
// ========================================
|
||||
EVENTS_READ("Consulter les événements"),
|
||||
EVENTS_CREATE("Créer un événement"),
|
||||
EVENTS_UPDATE_OWN("Modifier ses propres événements"),
|
||||
EVENTS_DELETE_OWN("Supprimer ses propres événements"),
|
||||
EVENTS_UPDATE_ANY("Modifier n'importe quel événement"),
|
||||
EVENTS_DELETE_ANY("Supprimer n'importe quel événement"),
|
||||
EVENTS_MODERATE("Modérer les événements (approuver, rejeter)"),
|
||||
EVENTS_PARTICIPATE("Participer à un événement"),
|
||||
EVENTS_INVITE("Inviter des utilisateurs à un événement"),
|
||||
EVENTS_VIEW_STATS("Voir les statistiques de ses événements"),
|
||||
EVENTS_VIEW_ALL_STATS("Voir les statistiques de tous les événements"),
|
||||
|
||||
// ========================================
|
||||
// ÉTABLISSEMENTS
|
||||
// ========================================
|
||||
ESTABLISHMENTS_READ("Consulter les établissements"),
|
||||
ESTABLISHMENTS_CREATE("Créer un établissement"),
|
||||
ESTABLISHMENTS_UPDATE_OWN("Modifier son propre établissement"),
|
||||
ESTABLISHMENTS_DELETE_OWN("Supprimer son propre établissement"),
|
||||
ESTABLISHMENTS_UPDATE_ANY("Modifier n'importe quel établissement"),
|
||||
ESTABLISHMENTS_DELETE_ANY("Supprimer n'importe quel établissement"),
|
||||
ESTABLISHMENTS_MANAGE_STAFF("Gérer le personnel de l'établissement"),
|
||||
ESTABLISHMENTS_VIEW_ANALYTICS("Voir les analytics de son établissement"),
|
||||
ESTABLISHMENTS_VIEW_ALL_ANALYTICS("Voir les analytics de tous les établissements"),
|
||||
ESTABLISHMENTS_MANAGE_SUBSCRIPTIONS("Gérer les abonnements"),
|
||||
|
||||
// ========================================
|
||||
// RÉSERVATIONS
|
||||
// ========================================
|
||||
RESERVATIONS_CREATE("Créer une réservation"),
|
||||
RESERVATIONS_READ_OWN("Voir ses propres réservations"),
|
||||
RESERVATIONS_VIEW_OWN("Voir ses propres réservations (alias)"),
|
||||
RESERVATIONS_UPDATE_OWN("Modifier ses propres réservations"),
|
||||
RESERVATIONS_DELETE_OWN("Supprimer ses propres réservations"),
|
||||
RESERVATIONS_CANCEL_OWN("Annuler ses propres réservations"),
|
||||
RESERVATIONS_READ_ESTABLISHMENT("Voir les réservations de son établissement"),
|
||||
RESERVATIONS_MANAGE_ESTABLISHMENT("Gérer les réservations de son établissement"),
|
||||
RESERVATIONS_READ_ALL("Voir toutes les réservations"),
|
||||
RESERVATIONS_VIEW_ALL("Voir toutes les réservations (alias)"),
|
||||
RESERVATIONS_UPDATE_ANY("Modifier n'importe quelle réservation"),
|
||||
RESERVATIONS_DELETE_ANY("Supprimer n'importe quelle réservation"),
|
||||
RESERVATIONS_CANCEL_ANY("Annuler n'importe quelle réservation"),
|
||||
RESERVATIONS_SCAN_QR("Scanner les QR codes de réservation"),
|
||||
|
||||
// ========================================
|
||||
// AVIS ET COMMENTAIRES
|
||||
// ========================================
|
||||
REVIEWS_CREATE("Laisser un avis"),
|
||||
REVIEWS_UPDATE_OWN("Modifier ses propres avis"),
|
||||
REVIEWS_DELETE_OWN("Supprimer ses propres avis"),
|
||||
REVIEWS_DELETE_ANY("Supprimer n'importe quel avis"),
|
||||
REVIEWS_RESPOND("Répondre aux avis (établissement)"),
|
||||
REVIEWS_MODERATE("Modérer les avis"),
|
||||
|
||||
// ========================================
|
||||
// POSTS SOCIAUX
|
||||
// ========================================
|
||||
POSTS_READ("Consulter les posts sociaux"),
|
||||
POSTS_CREATE("Créer un post social"),
|
||||
POSTS_UPDATE_OWN("Modifier ses propres posts"),
|
||||
POSTS_DELETE_OWN("Supprimer ses propres posts"),
|
||||
POSTS_DELETE_ANY("Supprimer n'importe quel post"),
|
||||
POSTS_MODERATE("Modérer les posts sociaux"),
|
||||
POSTS_LIKE("Liker un post"),
|
||||
POSTS_COMMENT("Commenter un post"),
|
||||
POSTS_SHARE("Partager un post"),
|
||||
|
||||
// ========================================
|
||||
// STORIES
|
||||
// ========================================
|
||||
STORIES_READ("Consulter les stories"),
|
||||
STORIES_CREATE("Créer une story"),
|
||||
STORIES_DELETE_OWN("Supprimer ses propres stories"),
|
||||
STORIES_DELETE_ANY("Supprimer n'importe quelle story"),
|
||||
|
||||
// ========================================
|
||||
// MESSAGERIE
|
||||
// ========================================
|
||||
MESSAGES_SEND("Envoyer des messages"),
|
||||
MESSAGES_READ_OWN("Lire ses propres messages"),
|
||||
MESSAGES_DELETE_OWN("Supprimer ses propres messages"),
|
||||
MESSAGES_READ_ALL("Lire tous les messages (support)"),
|
||||
|
||||
// ========================================
|
||||
// NOTIFICATIONS
|
||||
// ========================================
|
||||
NOTIFICATIONS_READ_OWN("Lire ses propres notifications"),
|
||||
NOTIFICATIONS_SEND_TARGETED("Envoyer des notifications ciblées"),
|
||||
NOTIFICATIONS_SEND_BROADCAST("Envoyer des notifications à tous"),
|
||||
|
||||
// ========================================
|
||||
// RELATIONS SOCIALES (AMITIÉ)
|
||||
// ========================================
|
||||
FRIENDS_SEND_REQUEST("Envoyer une demande d'amitié"),
|
||||
FRIENDS_ACCEPT_REQUEST("Accepter une demande d'amitié"),
|
||||
FRIENDS_REMOVE("Retirer un ami"),
|
||||
FRIENDS_VIEW_LIST("Voir sa liste d'amis"),
|
||||
|
||||
// ========================================
|
||||
// PROMOTIONS ET OFFRES
|
||||
// ========================================
|
||||
PROMOTIONS_READ("Consulter les promotions"),
|
||||
PROMOTIONS_CREATE("Créer une promotion"),
|
||||
PROMOTIONS_UPDATE_OWN("Modifier ses propres promotions"),
|
||||
PROMOTIONS_DELETE_OWN("Supprimer ses propres promotions"),
|
||||
PROMOTIONS_MANAGE_ALL("Gérer toutes les promotions"),
|
||||
|
||||
// ========================================
|
||||
// CONTENU PROMOTIONNEL (Ambassadeurs)
|
||||
// ========================================
|
||||
PROMO_CONTENT_CREATE("Créer du contenu promotionnel"),
|
||||
PROMO_CONTENT_FEATURED("Contenu mis en avant"),
|
||||
|
||||
// ========================================
|
||||
// FINANCES
|
||||
// ========================================
|
||||
FINANCE_VIEW_OWN("Voir ses propres transactions"),
|
||||
FINANCE_VIEW_ESTABLISHMENT("Voir les finances de son établissement"),
|
||||
FINANCE_VIEW_ALL("Voir toutes les transactions"),
|
||||
FINANCE_MANAGE_PAYMENTS("Gérer les paiements"),
|
||||
FINANCE_MANAGE_PAYOUTS("Gérer les reversements"),
|
||||
FINANCE_GENERATE_REPORTS("Générer des rapports financiers"),
|
||||
|
||||
// ========================================
|
||||
// ADMINISTRATION SYSTÈME
|
||||
// ========================================
|
||||
SUPER_ADMIN_ACCESS("Accès super administrateur complet"),
|
||||
ADMIN_DASHBOARD("Accéder au tableau de bord admin"),
|
||||
ADMIN_SETTINGS("Modifier les paramètres système"),
|
||||
ADMIN_LOGS("Consulter les journaux système"),
|
||||
ADMIN_MAINTENANCE("Effectuer des opérations de maintenance"),
|
||||
|
||||
// ========================================
|
||||
// MODÉRATION
|
||||
// ========================================
|
||||
MODERATION_VIEW_REPORTS("Voir les signalements"),
|
||||
MODERATION_HANDLE_REPORTS("Traiter les signalements"),
|
||||
MODERATION_BAN_USER("Bannir un utilisateur"),
|
||||
MODERATION_HIDE_CONTENT("Masquer du contenu"),
|
||||
|
||||
// ========================================
|
||||
// SUPPORT CLIENT
|
||||
// ========================================
|
||||
SUPPORT_VIEW_TICKETS("Voir les tickets de support"),
|
||||
SUPPORT_HANDLE_TICKETS("Traiter les tickets de support"),
|
||||
SUPPORT_VIEW_USER_DETAILS("Voir les détails utilisateur (support)"),
|
||||
SUPPORT_MODIFY_RESERVATIONS("Modifier les réservations (support)"),
|
||||
|
||||
// ========================================
|
||||
// API ET INTÉGRATIONS
|
||||
// ========================================
|
||||
API_ACCESS("Accéder aux API"),
|
||||
API_MANAGE_KEYS("Gérer les clés API"),
|
||||
WEBHOOKS_MANAGE("Gérer les webhooks"),
|
||||
|
||||
// ========================================
|
||||
// ANALYTICS ET AUDIT
|
||||
// ========================================
|
||||
ANALYTICS_VIEW_BASIC("Voir les analytics de base"),
|
||||
ANALYTICS_VIEW_ADVANCED("Voir les analytics avancés"),
|
||||
AUDIT_VIEW_LOGS("Voir les logs d'audit"),
|
||||
AUDIT_EXPORT_DATA("Exporter les données d'audit");
|
||||
|
||||
private final String description;
|
||||
|
||||
Permission(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
112
src/main/java/com/lions/dev/security/PermissionInterceptor.java
Normal file
112
src/main/java/com/lions/dev/security/PermissionInterceptor.java
Normal file
@@ -0,0 +1,112 @@
|
||||
package com.lions.dev.security;
|
||||
|
||||
import com.lions.dev.core.errors.exceptions.UnauthorizedException;
|
||||
import com.lions.dev.service.SecurityService;
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.interceptor.AroundInvoke;
|
||||
import jakarta.interceptor.Interceptor;
|
||||
import jakarta.interceptor.InvocationContext;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Intercepteur CDI pour l'annotation @RequiresPermission.
|
||||
*
|
||||
* Vérifie automatiquement les permissions avant l'exécution de la méthode.
|
||||
*
|
||||
* @since 2.0 - Système RBAC production-ready
|
||||
*/
|
||||
@Interceptor
|
||||
@RequiresPermission(Permission.PROFILE_READ) // Binding, valeur ignorée
|
||||
@Priority(Interceptor.Priority.APPLICATION + 100)
|
||||
public class PermissionInterceptor {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(PermissionInterceptor.class);
|
||||
|
||||
@Inject
|
||||
SecurityService securityService;
|
||||
|
||||
@AroundInvoke
|
||||
public Object checkPermissions(InvocationContext ctx) throws Exception {
|
||||
Method method = ctx.getMethod();
|
||||
Class<?> targetClass = ctx.getTarget().getClass();
|
||||
|
||||
// Chercher l'annotation sur la méthode d'abord, puis sur la classe
|
||||
RequiresPermission annotation = method.getAnnotation(RequiresPermission.class);
|
||||
if (annotation == null) {
|
||||
// Chercher sur la classe (en tenant compte des proxies CDI)
|
||||
annotation = getClassAnnotation(targetClass);
|
||||
}
|
||||
|
||||
if (annotation == null) {
|
||||
// Pas d'annotation trouvée, continuer sans vérification
|
||||
return ctx.proceed();
|
||||
}
|
||||
|
||||
Permission[] requiredPermissions = annotation.value();
|
||||
boolean requireAll = annotation.requireAll();
|
||||
|
||||
if (requiredPermissions.length == 0) {
|
||||
return ctx.proceed();
|
||||
}
|
||||
|
||||
LOG.debugf("Vérification des permissions pour %s.%s: %s (requireAll=%s)",
|
||||
targetClass.getSimpleName(), method.getName(),
|
||||
java.util.Arrays.toString(requiredPermissions), requireAll);
|
||||
|
||||
// Vérifier l'authentification
|
||||
if (!securityService.isAuthenticated()) {
|
||||
LOG.warn("Accès non authentifié à une ressource protégée: " + method.getName());
|
||||
throw new UnauthorizedException("Authentification requise");
|
||||
}
|
||||
|
||||
// Vérifier les permissions
|
||||
boolean hasAccess;
|
||||
if (requireAll) {
|
||||
hasAccess = securityService.hasAllPermissions(requiredPermissions);
|
||||
} else {
|
||||
hasAccess = securityService.hasAnyPermission(requiredPermissions);
|
||||
}
|
||||
|
||||
if (!hasAccess) {
|
||||
LOG.warnf("Utilisateur %s n'a pas les permissions requises pour %s.%s",
|
||||
securityService.getCurrentUserIdOrNull(), targetClass.getSimpleName(), method.getName());
|
||||
throw new UnauthorizedException("Permissions insuffisantes pour cette opération");
|
||||
}
|
||||
|
||||
LOG.debugf("Permissions validées pour %s", method.getName());
|
||||
return ctx.proceed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère l'annotation sur la classe en tenant compte des proxies CDI.
|
||||
*/
|
||||
private RequiresPermission getClassAnnotation(Class<?> targetClass) {
|
||||
// Vérifier la classe directe
|
||||
RequiresPermission annotation = targetClass.getAnnotation(RequiresPermission.class);
|
||||
if (annotation != null) {
|
||||
return annotation;
|
||||
}
|
||||
|
||||
// Vérifier la classe parente (pour les proxies CDI)
|
||||
Class<?> superClass = targetClass.getSuperclass();
|
||||
if (superClass != null && !superClass.equals(Object.class)) {
|
||||
annotation = superClass.getAnnotation(RequiresPermission.class);
|
||||
if (annotation != null) {
|
||||
return annotation;
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier les interfaces
|
||||
for (Class<?> iface : targetClass.getInterfaces()) {
|
||||
annotation = iface.getAnnotation(RequiresPermission.class);
|
||||
if (annotation != null) {
|
||||
return annotation;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
45
src/main/java/com/lions/dev/security/RequiresPermission.java
Normal file
45
src/main/java/com/lions/dev/security/RequiresPermission.java
Normal file
@@ -0,0 +1,45 @@
|
||||
package com.lions.dev.security;
|
||||
|
||||
import jakarta.enterprise.util.Nonbinding;
|
||||
import jakarta.interceptor.InterceptorBinding;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation pour déclarer les permissions requises sur une méthode ou une classe.
|
||||
*
|
||||
* Usage :
|
||||
* <pre>
|
||||
* {@code
|
||||
* @RequiresPermission(Permission.EVENTS_CREATE)
|
||||
* public Response createEvent(EventDTO event) { ... }
|
||||
*
|
||||
* @RequiresPermission(value = {Permission.USERS_READ_ALL, Permission.USERS_UPDATE_ANY}, requireAll = false)
|
||||
* public Response manageUser(UUID userId) { ... }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @since 2.0 - Système RBAC production-ready
|
||||
*/
|
||||
@InterceptorBinding
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
public @interface RequiresPermission {
|
||||
|
||||
/**
|
||||
* Les permissions requises pour accéder à cette ressource.
|
||||
* Tableau vide par défaut pour permettre le binding de l'intercepteur.
|
||||
*/
|
||||
@Nonbinding
|
||||
Permission[] value() default {};
|
||||
|
||||
/**
|
||||
* Si true, toutes les permissions listées sont requises.
|
||||
* Si false (par défaut), au moins une permission suffit.
|
||||
*/
|
||||
@Nonbinding
|
||||
boolean requireAll() default false;
|
||||
}
|
||||
180
src/main/java/com/lions/dev/security/Role.java
Normal file
180
src/main/java/com/lions/dev/security/Role.java
Normal file
@@ -0,0 +1,180 @@
|
||||
package com.lions.dev.security;
|
||||
|
||||
/**
|
||||
* Enum des rôles du système RBAC.
|
||||
*
|
||||
* Aligné avec la documentation des rôles utilisateurs.
|
||||
*
|
||||
* @since 2.0 - Système RBAC production-ready
|
||||
*/
|
||||
public enum Role {
|
||||
|
||||
// ========================================
|
||||
// UTILISATEURS FINAUX (B2C)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Utilisateur Standard (Visiteur/Membre)
|
||||
* Rôle de base pour toute personne utilisant l'application.
|
||||
*/
|
||||
USER("Utilisateur Standard", "B2C"),
|
||||
|
||||
/**
|
||||
* Organisateur d'Événements (User-Host)
|
||||
* Peut créer et gérer ses propres événements After-Work.
|
||||
*/
|
||||
EVENT_HOST("Organisateur d'Événements", "B2C"),
|
||||
|
||||
/**
|
||||
* Ambassadeur / Influenceur
|
||||
* Contribue à la promotion de l'application avec une visibilité accrue.
|
||||
*/
|
||||
AMBASSADOR("Ambassadeur / Influenceur", "B2C"),
|
||||
|
||||
// ========================================
|
||||
// ÉTABLISSEMENTS (B2B)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Propriétaire d'Établissement (Owner)
|
||||
* Contrôle total sur la fiche de son établissement.
|
||||
*/
|
||||
ESTABLISHMENT_OWNER("Propriétaire d'Établissement", "B2B"),
|
||||
|
||||
/**
|
||||
* Gérant / Manager
|
||||
* Assiste le Propriétaire dans la gestion quotidienne.
|
||||
*/
|
||||
ESTABLISHMENT_MANAGER("Gérant / Manager", "B2B"),
|
||||
|
||||
/**
|
||||
* Personnel de Salle (Staff/Scanner)
|
||||
* Rôle opérationnel pour la validation des réservations.
|
||||
*/
|
||||
ESTABLISHMENT_STAFF("Personnel de Salle", "B2B"),
|
||||
|
||||
// ========================================
|
||||
// ADMINISTRATION ET MODÉRATION (Interne)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Super Administrateur
|
||||
* Niveau d'accès le plus élevé, gestion globale du système.
|
||||
*/
|
||||
SUPER_ADMIN("Super Administrateur", "INTERNAL"),
|
||||
|
||||
/**
|
||||
* Administrateur
|
||||
* Gestion générale de la plateforme.
|
||||
*/
|
||||
ADMIN("Administrateur", "INTERNAL"),
|
||||
|
||||
/**
|
||||
* Modérateur de Contenu
|
||||
* Assure la qualité et la conformité du contenu.
|
||||
*/
|
||||
MODERATOR("Modérateur de Contenu", "INTERNAL"),
|
||||
|
||||
/**
|
||||
* Gestionnaire Financier
|
||||
* Responsable des opérations financières.
|
||||
*/
|
||||
FINANCE_MANAGER("Gestionnaire Financier", "INTERNAL"),
|
||||
|
||||
/**
|
||||
* Support Client / Helpdesk
|
||||
* Assistance des utilisateurs et établissements.
|
||||
*/
|
||||
SUPPORT("Support Client", "INTERNAL"),
|
||||
|
||||
// ========================================
|
||||
// TECHNIQUE ET PARTENAIRES (API/Système)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Développeur / API User
|
||||
* Accès aux API pour intégrations tierces.
|
||||
*/
|
||||
API_USER("Développeur / API User", "TECHNICAL"),
|
||||
|
||||
/**
|
||||
* Auditeur / Analyste
|
||||
* Accès en lecture seule pour analyses et audits.
|
||||
*/
|
||||
AUDITOR("Auditeur / Analyste", "TECHNICAL");
|
||||
|
||||
private final String displayName;
|
||||
private final String group;
|
||||
|
||||
Role(String displayName, String group) {
|
||||
this.displayName = displayName;
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public String getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une chaîne en Role.
|
||||
* Supporte les anciens noms de rôles pour la rétrocompatibilité.
|
||||
*/
|
||||
public static Role fromString(String roleStr) {
|
||||
if (roleStr == null) {
|
||||
return USER;
|
||||
}
|
||||
|
||||
String normalized = roleStr.toUpperCase().trim();
|
||||
|
||||
// Rétrocompatibilité avec les anciens noms
|
||||
switch (normalized) {
|
||||
case "MANAGER":
|
||||
return ESTABLISHMENT_MANAGER;
|
||||
case "OWNER":
|
||||
return ESTABLISHMENT_OWNER;
|
||||
case "STAFF":
|
||||
return ESTABLISHMENT_STAFF;
|
||||
case "HOST":
|
||||
case "EVENT_ORGANIZER":
|
||||
return EVENT_HOST;
|
||||
default:
|
||||
try {
|
||||
return Role.valueOf(normalized);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return USER;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si ce rôle est un rôle d'administration.
|
||||
*/
|
||||
public boolean isAdminRole() {
|
||||
return this == SUPER_ADMIN || this == ADMIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si ce rôle est un rôle interne.
|
||||
*/
|
||||
public boolean isInternalRole() {
|
||||
return "INTERNAL".equals(this.group);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si ce rôle est un rôle B2B (établissement).
|
||||
*/
|
||||
public boolean isB2BRole() {
|
||||
return "B2B".equals(this.group);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si ce rôle est un rôle B2C (utilisateur final).
|
||||
*/
|
||||
public boolean isB2CRole() {
|
||||
return "B2C".equals(this.group);
|
||||
}
|
||||
}
|
||||
338
src/main/java/com/lions/dev/security/RolePermissionConfig.java
Normal file
338
src/main/java/com/lions/dev/security/RolePermissionConfig.java
Normal file
@@ -0,0 +1,338 @@
|
||||
package com.lions.dev.security;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static com.lions.dev.security.Permission.*;
|
||||
import static com.lions.dev.security.Role.*;
|
||||
|
||||
/**
|
||||
* Configuration centrale des permissions par rôle.
|
||||
*
|
||||
* Ce composant définit quelles permissions sont accordées à chaque rôle.
|
||||
* Les permissions sont cumulatives via l'héritage de rôles.
|
||||
*
|
||||
* @since 2.0 - Système RBAC production-ready
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class RolePermissionConfig {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(RolePermissionConfig.class);
|
||||
|
||||
private final Map<Role, Set<Permission>> rolePermissions = new EnumMap<>(Role.class);
|
||||
private final Map<Role, Set<Role>> roleInheritance = new EnumMap<>(Role.class);
|
||||
|
||||
@PostConstruct
|
||||
void init() {
|
||||
LOG.info("[RBAC] Initialisation de la configuration des permissions...");
|
||||
|
||||
configureRoleInheritance();
|
||||
configurePermissions();
|
||||
|
||||
LOG.info("[RBAC] Configuration terminée. " + rolePermissions.size() + " rôles configurés.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure l'héritage des rôles.
|
||||
* Un rôle hérite des permissions de ses rôles parents.
|
||||
*/
|
||||
private void configureRoleInheritance() {
|
||||
// B2C : EVENT_HOST hérite de USER
|
||||
roleInheritance.put(EVENT_HOST, Set.of(USER));
|
||||
|
||||
// B2C : AMBASSADOR hérite de EVENT_HOST (et donc de USER)
|
||||
roleInheritance.put(AMBASSADOR, Set.of(EVENT_HOST));
|
||||
|
||||
// B2B : ESTABLISHMENT_MANAGER hérite de USER
|
||||
roleInheritance.put(ESTABLISHMENT_MANAGER, Set.of(USER));
|
||||
|
||||
// B2B : ESTABLISHMENT_OWNER hérite de ESTABLISHMENT_MANAGER
|
||||
roleInheritance.put(ESTABLISHMENT_OWNER, Set.of(ESTABLISHMENT_MANAGER));
|
||||
|
||||
// B2B : ESTABLISHMENT_STAFF hérite de USER
|
||||
roleInheritance.put(ESTABLISHMENT_STAFF, Set.of(USER));
|
||||
|
||||
// Interne : MODERATOR hérite de USER
|
||||
roleInheritance.put(MODERATOR, Set.of(USER));
|
||||
|
||||
// Interne : SUPPORT hérite de USER
|
||||
roleInheritance.put(SUPPORT, Set.of(USER));
|
||||
|
||||
// Interne : FINANCE_MANAGER hérite de USER
|
||||
roleInheritance.put(FINANCE_MANAGER, Set.of(USER));
|
||||
|
||||
// Interne : ADMIN hérite de MODERATOR
|
||||
roleInheritance.put(ADMIN, Set.of(MODERATOR));
|
||||
|
||||
// Interne : SUPER_ADMIN hérite de ADMIN et FINANCE_MANAGER
|
||||
roleInheritance.put(SUPER_ADMIN, Set.of(ADMIN, FINANCE_MANAGER));
|
||||
|
||||
// Technique : API_USER n'hérite de rien (permissions limitées)
|
||||
roleInheritance.put(API_USER, Set.of());
|
||||
|
||||
// Technique : AUDITOR n'hérite de rien (lecture seule)
|
||||
roleInheritance.put(AUDITOR, Set.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure les permissions directes de chaque rôle.
|
||||
* Les permissions héritées sont ajoutées automatiquement.
|
||||
*/
|
||||
private void configurePermissions() {
|
||||
// ===== USER (Utilisateur Standard) =====
|
||||
rolePermissions.put(USER, EnumSet.of(
|
||||
// Profil
|
||||
PROFILE_READ, PROFILE_UPDATE, PROFILE_DELETE,
|
||||
// Social / Recherche
|
||||
SOCIAL_SEARCH, SOCIAL_FOLLOW, SOCIAL_BLOCK,
|
||||
// Événements
|
||||
EVENTS_READ, EVENTS_PARTICIPATE,
|
||||
// Établissements
|
||||
ESTABLISHMENTS_READ,
|
||||
// Réservations
|
||||
RESERVATIONS_CREATE, RESERVATIONS_READ_OWN, RESERVATIONS_VIEW_OWN,
|
||||
RESERVATIONS_UPDATE_OWN, RESERVATIONS_DELETE_OWN, RESERVATIONS_CANCEL_OWN,
|
||||
// Avis
|
||||
REVIEWS_CREATE, REVIEWS_UPDATE_OWN, REVIEWS_DELETE_OWN,
|
||||
// Posts sociaux
|
||||
POSTS_READ, POSTS_CREATE, POSTS_UPDATE_OWN, POSTS_DELETE_OWN,
|
||||
POSTS_LIKE, POSTS_COMMENT, POSTS_SHARE,
|
||||
// Stories
|
||||
STORIES_READ, STORIES_CREATE, STORIES_DELETE_OWN,
|
||||
// Messagerie
|
||||
MESSAGES_SEND, MESSAGES_READ_OWN, MESSAGES_DELETE_OWN,
|
||||
// Notifications
|
||||
NOTIFICATIONS_READ_OWN,
|
||||
// Amitié
|
||||
FRIENDS_SEND_REQUEST, FRIENDS_ACCEPT_REQUEST, FRIENDS_REMOVE, FRIENDS_VIEW_LIST,
|
||||
// Promotions
|
||||
PROMOTIONS_READ,
|
||||
// Finance
|
||||
FINANCE_VIEW_OWN,
|
||||
// Analytics
|
||||
ANALYTICS_VIEW_BASIC
|
||||
));
|
||||
|
||||
// ===== EVENT_HOST (Organisateur d'Événements) =====
|
||||
rolePermissions.put(EVENT_HOST, EnumSet.of(
|
||||
// Événements - création et gestion
|
||||
EVENTS_CREATE, EVENTS_UPDATE_OWN, EVENTS_DELETE_OWN,
|
||||
EVENTS_INVITE, EVENTS_VIEW_STATS
|
||||
));
|
||||
|
||||
// ===== AMBASSADOR (Ambassadeur / Influenceur) =====
|
||||
rolePermissions.put(AMBASSADOR, EnumSet.of(
|
||||
// Contenu promotionnel
|
||||
PROMO_CONTENT_CREATE, PROMO_CONTENT_FEATURED
|
||||
));
|
||||
|
||||
// ===== ESTABLISHMENT_STAFF (Personnel de Salle) =====
|
||||
rolePermissions.put(ESTABLISHMENT_STAFF, EnumSet.of(
|
||||
// Réservations - scan et validation
|
||||
RESERVATIONS_SCAN_QR, RESERVATIONS_READ_ESTABLISHMENT
|
||||
));
|
||||
|
||||
// ===== ESTABLISHMENT_MANAGER (Gérant / Manager) =====
|
||||
rolePermissions.put(ESTABLISHMENT_MANAGER, EnumSet.of(
|
||||
// Établissement - gestion partielle
|
||||
ESTABLISHMENTS_UPDATE_OWN,
|
||||
// Événements - gestion pour l'établissement
|
||||
EVENTS_CREATE, EVENTS_UPDATE_OWN, EVENTS_DELETE_OWN,
|
||||
// Réservations - gestion
|
||||
RESERVATIONS_READ_ESTABLISHMENT, RESERVATIONS_MANAGE_ESTABLISHMENT,
|
||||
// Avis - réponse
|
||||
REVIEWS_RESPOND,
|
||||
// Promotions
|
||||
PROMOTIONS_CREATE, PROMOTIONS_UPDATE_OWN, PROMOTIONS_DELETE_OWN,
|
||||
// Analytics
|
||||
ESTABLISHMENTS_VIEW_ANALYTICS,
|
||||
// Stats événements
|
||||
EVENTS_VIEW_STATS
|
||||
));
|
||||
|
||||
// ===== ESTABLISHMENT_OWNER (Propriétaire d'Établissement) =====
|
||||
rolePermissions.put(ESTABLISHMENT_OWNER, EnumSet.of(
|
||||
// Établissement - gestion complète
|
||||
ESTABLISHMENTS_CREATE, ESTABLISHMENTS_DELETE_OWN,
|
||||
ESTABLISHMENTS_MANAGE_STAFF, ESTABLISHMENTS_MANAGE_SUBSCRIPTIONS,
|
||||
// Finance
|
||||
FINANCE_VIEW_ESTABLISHMENT
|
||||
));
|
||||
|
||||
// ===== MODERATOR (Modérateur de Contenu) =====
|
||||
rolePermissions.put(MODERATOR, EnumSet.of(
|
||||
// Modération
|
||||
MODERATION_VIEW_REPORTS, MODERATION_HANDLE_REPORTS,
|
||||
MODERATION_HIDE_CONTENT,
|
||||
// Événements
|
||||
EVENTS_MODERATE, EVENTS_UPDATE_ANY, EVENTS_DELETE_ANY,
|
||||
// Avis
|
||||
REVIEWS_DELETE_ANY, REVIEWS_MODERATE,
|
||||
// Posts
|
||||
POSTS_DELETE_ANY, POSTS_MODERATE,
|
||||
// Stories
|
||||
STORIES_DELETE_ANY,
|
||||
// Utilisateurs
|
||||
USERS_READ_ALL
|
||||
));
|
||||
|
||||
// ===== SUPPORT (Support Client) =====
|
||||
rolePermissions.put(SUPPORT, EnumSet.of(
|
||||
// Support
|
||||
SUPPORT_VIEW_TICKETS, SUPPORT_HANDLE_TICKETS,
|
||||
SUPPORT_VIEW_USER_DETAILS, SUPPORT_MODIFY_RESERVATIONS,
|
||||
// Utilisateurs - lecture
|
||||
USERS_READ_ALL,
|
||||
// Réservations
|
||||
RESERVATIONS_READ_ALL,
|
||||
// Messages - pour assistance
|
||||
MESSAGES_READ_ALL
|
||||
));
|
||||
|
||||
// ===== FINANCE_MANAGER (Gestionnaire Financier) =====
|
||||
rolePermissions.put(FINANCE_MANAGER, EnumSet.of(
|
||||
// Finance - gestion complète
|
||||
FINANCE_VIEW_ALL, FINANCE_MANAGE_PAYMENTS,
|
||||
FINANCE_MANAGE_PAYOUTS, FINANCE_GENERATE_REPORTS,
|
||||
// Établissements - analytics financiers
|
||||
ESTABLISHMENTS_VIEW_ALL_ANALYTICS
|
||||
));
|
||||
|
||||
// ===== ADMIN (Administrateur) =====
|
||||
rolePermissions.put(ADMIN, EnumSet.of(
|
||||
// Administration
|
||||
ADMIN_DASHBOARD,
|
||||
// Utilisateurs - gestion
|
||||
USERS_UPDATE_ANY, USERS_DELETE_ANY, USERS_SUSPEND,
|
||||
// Établissements - gestion
|
||||
ESTABLISHMENTS_UPDATE_ANY, ESTABLISHMENTS_DELETE_ANY,
|
||||
// Réservations - gestion
|
||||
RESERVATIONS_VIEW_ALL, RESERVATIONS_UPDATE_ANY, RESERVATIONS_CANCEL_ANY,
|
||||
// Modération avancée
|
||||
MODERATION_BAN_USER,
|
||||
// Notifications
|
||||
NOTIFICATIONS_SEND_TARGETED, NOTIFICATIONS_SEND_BROADCAST,
|
||||
// Analytics
|
||||
ANALYTICS_VIEW_ADVANCED, EVENTS_VIEW_ALL_STATS
|
||||
));
|
||||
|
||||
// ===== SUPER_ADMIN (Super Administrateur) =====
|
||||
rolePermissions.put(SUPER_ADMIN, EnumSet.of(
|
||||
// Accès super admin
|
||||
SUPER_ADMIN_ACCESS,
|
||||
// Administration système
|
||||
ADMIN_SETTINGS, ADMIN_LOGS, ADMIN_MAINTENANCE,
|
||||
// Utilisateurs - gestion complète
|
||||
USERS_ASSIGN_ROLE, USERS_ASSIGN_ROLES, USERS_IMPERSONATE,
|
||||
// Réservations - gestion complète
|
||||
RESERVATIONS_VIEW_ALL, RESERVATIONS_UPDATE_ANY,
|
||||
RESERVATIONS_DELETE_ANY, RESERVATIONS_CANCEL_ANY,
|
||||
// Promotions
|
||||
PROMOTIONS_MANAGE_ALL,
|
||||
// API
|
||||
API_MANAGE_KEYS, WEBHOOKS_MANAGE,
|
||||
// Audit
|
||||
AUDIT_VIEW_LOGS, AUDIT_EXPORT_DATA
|
||||
));
|
||||
|
||||
// ===== API_USER (Développeur / API User) =====
|
||||
rolePermissions.put(API_USER, EnumSet.of(
|
||||
// API
|
||||
API_ACCESS,
|
||||
// Lecture publique
|
||||
EVENTS_READ, ESTABLISHMENTS_READ, POSTS_READ, STORIES_READ, PROMOTIONS_READ
|
||||
));
|
||||
|
||||
// ===== AUDITOR (Auditeur / Analyste) =====
|
||||
rolePermissions.put(AUDITOR, EnumSet.of(
|
||||
// Audit et analytics
|
||||
AUDIT_VIEW_LOGS, AUDIT_EXPORT_DATA,
|
||||
ANALYTICS_VIEW_BASIC, ANALYTICS_VIEW_ADVANCED,
|
||||
EVENTS_VIEW_ALL_STATS, ESTABLISHMENTS_VIEW_ALL_ANALYTICS,
|
||||
// Lecture
|
||||
USERS_READ_ALL, RESERVATIONS_READ_ALL, FINANCE_VIEW_ALL
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère toutes les permissions d'un rôle, incluant les permissions héritées.
|
||||
*/
|
||||
public Set<Permission> getPermissions(Role role) {
|
||||
Set<Permission> allPermissions = EnumSet.noneOf(Permission.class);
|
||||
collectPermissions(role, allPermissions, new HashSet<>());
|
||||
return Collections.unmodifiableSet(allPermissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collecte récursivement les permissions d'un rôle et de ses parents.
|
||||
*/
|
||||
private void collectPermissions(Role role, Set<Permission> permissions, Set<Role> visited) {
|
||||
if (role == null || visited.contains(role)) {
|
||||
return;
|
||||
}
|
||||
visited.add(role);
|
||||
|
||||
// Ajouter les permissions directes du rôle
|
||||
Set<Permission> directPermissions = rolePermissions.get(role);
|
||||
if (directPermissions != null) {
|
||||
permissions.addAll(directPermissions);
|
||||
}
|
||||
|
||||
// Ajouter les permissions des rôles parents
|
||||
Set<Role> parents = roleInheritance.get(role);
|
||||
if (parents != null) {
|
||||
for (Role parent : parents) {
|
||||
collectPermissions(parent, permissions, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si un rôle possède une permission spécifique.
|
||||
*/
|
||||
public boolean hasPermission(Role role, Permission permission) {
|
||||
return getPermissions(role).contains(permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si un rôle possède toutes les permissions spécifiées.
|
||||
*/
|
||||
public boolean hasAllPermissions(Role role, Permission... permissions) {
|
||||
Set<Permission> rolePermissions = getPermissions(role);
|
||||
for (Permission permission : permissions) {
|
||||
if (!rolePermissions.contains(permission)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si un rôle possède au moins une des permissions spécifiées.
|
||||
*/
|
||||
public boolean hasAnyPermission(Role role, Permission... permissions) {
|
||||
Set<Permission> rolePermissions = getPermissions(role);
|
||||
for (Permission permission : permissions) {
|
||||
if (rolePermissions.contains(permission)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère tous les rôles qui possèdent une permission donnée.
|
||||
*/
|
||||
public Set<Role> getRolesWithPermission(Permission permission) {
|
||||
Set<Role> roles = EnumSet.noneOf(Role.class);
|
||||
for (Role role : Role.values()) {
|
||||
if (hasPermission(role, permission)) {
|
||||
roles.add(role);
|
||||
}
|
||||
}
|
||||
return roles;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.lions.dev.service;
|
||||
|
||||
import com.lions.dev.entity.users.Users;
|
||||
import com.lions.dev.util.UserRoles;
|
||||
import io.smallrye.jwt.build.Jwt;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
@@ -9,13 +10,24 @@ import org.jboss.logging.Logger;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Service d'émission de JWT au login.
|
||||
* Le token contient sub (userId), groups (rôle) et est signé avec la clé secrète configurée.
|
||||
* La validation des tokens sur les requêtes est assurée par quarkus-smallrye-jwt
|
||||
*
|
||||
* Le token contient :
|
||||
* - sub (subject) : userId de l'utilisateur
|
||||
* - groups : rôles de l'utilisateur (pour @RolesAllowed)
|
||||
* - iss (issuer) : "afterwork"
|
||||
* - iat (issued at) : timestamp de création
|
||||
* - exp (expiration) : timestamp d'expiration
|
||||
*
|
||||
* La validation des tokens est assurée automatiquement par SmallRye JWT
|
||||
* lorsque les endpoints sont protégés avec @RolesAllowed.
|
||||
*
|
||||
* @since 2.0 - Implémentation sécurité JWT production-ready
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class JwtService {
|
||||
@@ -26,6 +38,9 @@ public class JwtService {
|
||||
@ConfigProperty(name = "afterwork.jwt.secret", defaultValue = "afterwork-jwt-secret-min-32-bytes-for-hs256!")
|
||||
String secret;
|
||||
|
||||
@ConfigProperty(name = "smallrye.jwt.new-token.lifespan", defaultValue = "86400")
|
||||
long tokenLifespanSeconds;
|
||||
|
||||
/**
|
||||
* Génère un JWT pour l'utilisateur authentifié.
|
||||
*
|
||||
@@ -36,18 +51,69 @@ public class JwtService {
|
||||
if (user == null || user.getId() == null) {
|
||||
throw new IllegalArgumentException("User et id obligatoires pour générer le JWT");
|
||||
}
|
||||
Set<String> groups = Set.of("user", user.getRole() != null ? user.getRole() : "USER");
|
||||
|
||||
// Construire les groupes (rôles) pour @RolesAllowed
|
||||
Set<String> groups = buildGroups(user);
|
||||
|
||||
SecretKey key = secretKeyFromConfig();
|
||||
|
||||
String token = Jwt.claims()
|
||||
.issuer(ISSUER)
|
||||
.subject(user.getId().toString())
|
||||
.groups(groups)
|
||||
.issuedAt(java.time.Instant.now())
|
||||
.expiresIn(Duration.ofSeconds(tokenLifespanSeconds))
|
||||
.jws()
|
||||
.algorithm(io.smallrye.jwt.algorithm.SignatureAlgorithm.HS256)
|
||||
.sign(key);
|
||||
LOG.debug("JWT généré pour l'utilisateur " + user.getId());
|
||||
|
||||
LOG.info("JWT généré pour l'utilisateur " + user.getId() + " avec rôles: " + groups);
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit l'ensemble des groupes (rôles) pour le token JWT.
|
||||
* Inclut le rôle principal et les rôles implicites selon la hiérarchie.
|
||||
*
|
||||
* Hiérarchie : SUPER_ADMIN > ADMIN > MANAGER > USER
|
||||
*
|
||||
* @param user L'utilisateur
|
||||
* @return Set des groupes à inclure dans le token
|
||||
*/
|
||||
private Set<String> buildGroups(Users user) {
|
||||
Set<String> groups = new HashSet<>();
|
||||
String role = user.getRole() != null ? user.getRole().toUpperCase() : UserRoles.USER;
|
||||
|
||||
// Ajouter le rôle principal
|
||||
groups.add(role);
|
||||
|
||||
// Ajouter les rôles implicites selon la hiérarchie
|
||||
switch (role) {
|
||||
case UserRoles.SUPER_ADMIN:
|
||||
groups.add(UserRoles.ADMIN);
|
||||
groups.add(UserRoles.MANAGER);
|
||||
groups.add(UserRoles.USER);
|
||||
break;
|
||||
case UserRoles.ADMIN:
|
||||
groups.add(UserRoles.MANAGER);
|
||||
groups.add(UserRoles.USER);
|
||||
break;
|
||||
case UserRoles.MANAGER:
|
||||
groups.add(UserRoles.USER);
|
||||
break;
|
||||
case UserRoles.USER:
|
||||
default:
|
||||
// USER n'a pas de rôles implicites supplémentaires
|
||||
break;
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée la clé secrète à partir de la configuration.
|
||||
* Assure que la clé fait au moins 32 bytes pour HS256.
|
||||
*/
|
||||
private SecretKey secretKeyFromConfig() {
|
||||
byte[] decoded = secret.getBytes(StandardCharsets.UTF_8);
|
||||
if (decoded.length < 32) {
|
||||
@@ -58,3 +124,4 @@ public class JwtService {
|
||||
return new SecretKeySpec(decoded, "HmacSHA256");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
395
src/main/java/com/lions/dev/service/SecurityService.java
Normal file
395
src/main/java/com/lions/dev/service/SecurityService.java
Normal file
@@ -0,0 +1,395 @@
|
||||
package com.lions.dev.service;
|
||||
|
||||
import com.lions.dev.core.errors.exceptions.UnauthorizedException;
|
||||
import com.lions.dev.security.Permission;
|
||||
import com.lions.dev.security.Role;
|
||||
import com.lions.dev.security.RolePermissionConfig;
|
||||
import com.lions.dev.util.UserRoles;
|
||||
import jakarta.enterprise.context.RequestScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import org.eclipse.microprofile.jwt.JsonWebToken;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Service de sécurité pour extraire et valider l'identité de l'utilisateur depuis le JWT.
|
||||
*
|
||||
* Ce service doit être utilisé dans toutes les Resources pour :
|
||||
* 1. Obtenir le userId de l'utilisateur authentifié (au lieu de le prendre de l'URL)
|
||||
* 2. Vérifier que l'utilisateur accède uniquement à ses propres ressources
|
||||
* 3. Vérifier les rôles pour les opérations privilégiées
|
||||
* 4. Vérifier les permissions granulaires (RBAC)
|
||||
*
|
||||
* @since 2.0 - Implémentation sécurité JWT + RBAC production-ready
|
||||
*/
|
||||
@RequestScoped
|
||||
public class SecurityService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(SecurityService.class);
|
||||
|
||||
@Inject
|
||||
JsonWebToken jwt;
|
||||
|
||||
@Inject
|
||||
RolePermissionConfig rolePermissionConfig;
|
||||
|
||||
/**
|
||||
* Retourne le userId de l'utilisateur authentifié depuis le JWT.
|
||||
*
|
||||
* @return UUID de l'utilisateur authentifié
|
||||
* @throws UnauthorizedException si le token est absent ou invalide
|
||||
*/
|
||||
public UUID getCurrentUserId() {
|
||||
if (jwt == null || jwt.getSubject() == null) {
|
||||
LOG.warn("Tentative d'accès sans token JWT valide");
|
||||
throw new UnauthorizedException("Token JWT requis pour cette opération");
|
||||
}
|
||||
try {
|
||||
return UUID.fromString(jwt.getSubject());
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.error("Subject JWT invalide (pas un UUID): " + jwt.getSubject());
|
||||
throw new UnauthorizedException("Token JWT invalide");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le userId si authentifié, ou null sinon (pour endpoints mixtes public/authentifié).
|
||||
*
|
||||
* @return UUID de l'utilisateur ou null si non authentifié
|
||||
*/
|
||||
public UUID getCurrentUserIdOrNull() {
|
||||
if (jwt == null || jwt.getSubject() == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return UUID.fromString(jwt.getSubject());
|
||||
} catch (IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie que l'utilisateur authentifié correspond à l'ID fourni.
|
||||
* Utile pour les endpoints où un userId est passé en paramètre.
|
||||
*
|
||||
* @param targetUserId L'ID de l'utilisateur cible
|
||||
* @throws UnauthorizedException si l'utilisateur n'est pas autorisé
|
||||
*/
|
||||
public void requireSameUser(UUID targetUserId) {
|
||||
UUID currentUserId = getCurrentUserId();
|
||||
if (!currentUserId.equals(targetUserId)) {
|
||||
LOG.warn("Utilisateur " + currentUserId + " tente d'accéder aux données de " + targetUserId);
|
||||
throw new UnauthorizedException("Vous ne pouvez accéder qu'à vos propres données");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie que l'utilisateur authentifié correspond à l'ID fourni OU a un rôle privilégié.
|
||||
*
|
||||
* @param targetUserId L'ID de l'utilisateur cible
|
||||
* @param allowedRoles Rôles autorisés à accéder aux données d'autres utilisateurs
|
||||
* @throws UnauthorizedException si l'utilisateur n'est pas autorisé
|
||||
*/
|
||||
public void requireSameUserOrRole(UUID targetUserId, String... allowedRoles) {
|
||||
UUID currentUserId = getCurrentUserId();
|
||||
if (currentUserId.equals(targetUserId)) {
|
||||
return; // L'utilisateur accède à ses propres données
|
||||
}
|
||||
// Vérifier si l'utilisateur a un rôle privilégié
|
||||
if (!hasAnyRole(allowedRoles)) {
|
||||
LOG.warn("Utilisateur " + currentUserId + " (rôles: " + getRoles() + ") tente d'accéder aux données de " + targetUserId);
|
||||
throw new UnauthorizedException("Accès non autorisé à cette ressource");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie que l'utilisateur a au moins un des rôles spécifiés.
|
||||
*
|
||||
* @param roles Les rôles à vérifier
|
||||
* @return true si l'utilisateur a au moins un des rôles
|
||||
*/
|
||||
public boolean hasAnyRole(String... roles) {
|
||||
if (jwt == null || jwt.getGroups() == null) {
|
||||
return false;
|
||||
}
|
||||
Set<String> userRoles = jwt.getGroups();
|
||||
for (String role : roles) {
|
||||
if (userRoles.contains(role)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie que l'utilisateur a le rôle spécifié.
|
||||
*
|
||||
* @param role Le rôle à vérifier
|
||||
* @throws UnauthorizedException si l'utilisateur n'a pas le rôle
|
||||
*/
|
||||
public void requireRole(String role) {
|
||||
if (!hasAnyRole(role)) {
|
||||
LOG.warn("Utilisateur " + getCurrentUserIdOrNull() + " n'a pas le rôle requis: " + role);
|
||||
throw new UnauthorizedException("Rôle " + role + " requis pour cette opération");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie que l'utilisateur est un administrateur (ADMIN ou SUPER_ADMIN).
|
||||
*
|
||||
* @throws UnauthorizedException si l'utilisateur n'est pas admin
|
||||
*/
|
||||
public void requireAdmin() {
|
||||
if (!hasAnyRole(UserRoles.ADMIN, UserRoles.SUPER_ADMIN)) {
|
||||
LOG.warn("Utilisateur " + getCurrentUserIdOrNull() + " tente une opération admin sans autorisation");
|
||||
throw new UnauthorizedException("Droits administrateur requis");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie que l'utilisateur est super administrateur.
|
||||
*
|
||||
* @throws UnauthorizedException si l'utilisateur n'est pas super admin
|
||||
*/
|
||||
public void requireSuperAdmin() {
|
||||
if (!hasAnyRole(UserRoles.SUPER_ADMIN)) {
|
||||
LOG.warn("Utilisateur " + getCurrentUserIdOrNull() + " tente une opération super admin sans autorisation");
|
||||
throw new UnauthorizedException("Droits super administrateur requis");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie que l'utilisateur peut gérer un établissement (MANAGER, ADMIN ou SUPER_ADMIN).
|
||||
*
|
||||
* @throws UnauthorizedException si l'utilisateur n'a pas les droits
|
||||
*/
|
||||
public void requireManagerOrHigher() {
|
||||
if (!hasAnyRole(UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN)) {
|
||||
LOG.warn("Utilisateur " + getCurrentUserIdOrNull() + " tente une opération manager sans autorisation");
|
||||
throw new UnauthorizedException("Droits manager requis");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne les rôles de l'utilisateur authentifié.
|
||||
*
|
||||
* @return Set des rôles ou ensemble vide si non authentifié
|
||||
*/
|
||||
public Set<String> getRoles() {
|
||||
if (jwt == null || jwt.getGroups() == null) {
|
||||
return Set.of();
|
||||
}
|
||||
return jwt.getGroups();
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur est authentifié.
|
||||
*
|
||||
* @return true si un JWT valide est présent
|
||||
*/
|
||||
public boolean isAuthenticated() {
|
||||
return jwt != null && jwt.getSubject() != null;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MÉTHODES RBAC (Permissions granulaires)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Récupère le rôle principal de l'utilisateur comme enum Role.
|
||||
* Le rôle principal est le plus privilégié parmi les rôles de l'utilisateur.
|
||||
*
|
||||
* @return Le rôle principal ou Role.USER par défaut
|
||||
*/
|
||||
public Role getPrimaryRole() {
|
||||
Set<String> roles = getRoles();
|
||||
if (roles.isEmpty()) {
|
||||
return Role.USER;
|
||||
}
|
||||
|
||||
// Ordre de priorité des rôles (du plus privilégié au moins)
|
||||
Role[] priorityOrder = {
|
||||
Role.SUPER_ADMIN, Role.ADMIN, Role.FINANCE_MANAGER,
|
||||
Role.MODERATOR, Role.SUPPORT,
|
||||
Role.ESTABLISHMENT_OWNER, Role.ESTABLISHMENT_MANAGER, Role.ESTABLISHMENT_STAFF,
|
||||
Role.AMBASSADOR, Role.EVENT_HOST, Role.AUDITOR, Role.API_USER,
|
||||
Role.USER
|
||||
};
|
||||
|
||||
for (Role role : priorityOrder) {
|
||||
if (roles.contains(role.name())) {
|
||||
return role;
|
||||
}
|
||||
}
|
||||
|
||||
// Rétrocompatibilité avec les anciens noms de rôles
|
||||
for (String roleStr : roles) {
|
||||
Role role = Role.fromString(roleStr);
|
||||
if (role != Role.USER) {
|
||||
return role;
|
||||
}
|
||||
}
|
||||
|
||||
return Role.USER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère tous les rôles de l'utilisateur comme enum Role.
|
||||
*
|
||||
* @return Set des rôles
|
||||
*/
|
||||
public Set<Role> getAllRoles() {
|
||||
Set<String> roleStrings = getRoles();
|
||||
Set<Role> roles = EnumSet.noneOf(Role.class);
|
||||
|
||||
for (String roleStr : roleStrings) {
|
||||
roles.add(Role.fromString(roleStr));
|
||||
}
|
||||
|
||||
if (roles.isEmpty()) {
|
||||
roles.add(Role.USER);
|
||||
}
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur a une permission spécifique.
|
||||
*
|
||||
* @param permission La permission à vérifier
|
||||
* @return true si l'utilisateur a la permission
|
||||
*/
|
||||
public boolean hasPermission(Permission permission) {
|
||||
for (Role role : getAllRoles()) {
|
||||
if (rolePermissionConfig.hasPermission(role, permission)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur a toutes les permissions spécifiées.
|
||||
*
|
||||
* @param permissions Les permissions à vérifier
|
||||
* @return true si l'utilisateur a toutes les permissions
|
||||
*/
|
||||
public boolean hasAllPermissions(Permission... permissions) {
|
||||
for (Permission permission : permissions) {
|
||||
if (!hasPermission(permission)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur a au moins une des permissions spécifiées.
|
||||
*
|
||||
* @param permissions Les permissions à vérifier
|
||||
* @return true si l'utilisateur a au moins une permission
|
||||
*/
|
||||
public boolean hasAnyPermission(Permission... permissions) {
|
||||
for (Permission permission : permissions) {
|
||||
if (hasPermission(permission)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exige que l'utilisateur ait une permission spécifique.
|
||||
*
|
||||
* @param permission La permission requise
|
||||
* @throws UnauthorizedException si l'utilisateur n'a pas la permission
|
||||
*/
|
||||
public void requirePermission(Permission permission) {
|
||||
if (!hasPermission(permission)) {
|
||||
LOG.warn("Utilisateur " + getCurrentUserIdOrNull() + " n'a pas la permission requise: " + permission);
|
||||
throw new UnauthorizedException("Permission " + permission.getDescription() + " requise pour cette opération");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exige que l'utilisateur ait toutes les permissions spécifiées.
|
||||
*
|
||||
* @param permissions Les permissions requises
|
||||
* @throws UnauthorizedException si l'utilisateur n'a pas toutes les permissions
|
||||
*/
|
||||
public void requireAllPermissions(Permission... permissions) {
|
||||
for (Permission permission : permissions) {
|
||||
requirePermission(permission);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exige que l'utilisateur ait au moins une des permissions spécifiées.
|
||||
*
|
||||
* @param permissions Les permissions dont au moins une est requise
|
||||
* @throws UnauthorizedException si l'utilisateur n'a aucune des permissions
|
||||
*/
|
||||
public void requireAnyPermission(Permission... permissions) {
|
||||
if (!hasAnyPermission(permissions)) {
|
||||
LOG.warn("Utilisateur " + getCurrentUserIdOrNull() + " n'a aucune des permissions requises");
|
||||
throw new UnauthorizedException("Au moins une des permissions requises est nécessaire");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie que l'utilisateur est le propriétaire OU a une permission spécifique.
|
||||
*
|
||||
* @param targetUserId L'ID de l'utilisateur cible
|
||||
* @param permission La permission qui autorise l'accès aux données d'autres utilisateurs
|
||||
* @throws UnauthorizedException si l'utilisateur n'est pas autorisé
|
||||
*/
|
||||
public void requireSameUserOrPermission(UUID targetUserId, Permission permission) {
|
||||
UUID currentUserId = getCurrentUserId();
|
||||
if (currentUserId.equals(targetUserId)) {
|
||||
return; // L'utilisateur accède à ses propres données
|
||||
}
|
||||
if (!hasPermission(permission)) {
|
||||
LOG.warn("Utilisateur " + currentUserId + " sans permission " + permission + " tente d'accéder aux données de " + targetUserId);
|
||||
throw new UnauthorizedException("Accès non autorisé à cette ressource");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère toutes les permissions de l'utilisateur (agrégées de tous ses rôles).
|
||||
*
|
||||
* @return Set des permissions
|
||||
*/
|
||||
public Set<Permission> getAllPermissions() {
|
||||
Set<Permission> allPermissions = EnumSet.noneOf(Permission.class);
|
||||
for (Role role : getAllRoles()) {
|
||||
allPermissions.addAll(rolePermissionConfig.getPermissions(role));
|
||||
}
|
||||
return allPermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur a un rôle spécifique (enum).
|
||||
*
|
||||
* @param role Le rôle à vérifier
|
||||
* @return true si l'utilisateur a ce rôle
|
||||
*/
|
||||
public boolean hasRole(Role role) {
|
||||
return getAllRoles().contains(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exige que l'utilisateur ait un rôle spécifique (enum).
|
||||
*
|
||||
* @param role Le rôle requis
|
||||
* @throws UnauthorizedException si l'utilisateur n'a pas le rôle
|
||||
*/
|
||||
public void requireRole(Role role) {
|
||||
if (!hasRole(role)) {
|
||||
LOG.warn("Utilisateur " + getCurrentUserIdOrNull() + " n'a pas le rôle requis: " + role);
|
||||
throw new UnauthorizedException("Rôle " + role.getDisplayName() + " requis pour cette opération");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user