Refactoring - Bonne version améliorée

This commit is contained in:
dahoud
2026-02-05 16:30:20 +00:00
parent dd4dbe111e
commit 2a794523b6
20 changed files with 2327 additions and 620 deletions

2
.gitignore vendored
View File

@@ -75,6 +75,8 @@ application-local.properties
*.key
*.p12
*.jks
# JWT secret key (ne pas committer en prod!)
src/main/resources/META-INF/jwt-secret.key
# ====================
# Quarkus

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 &lt;token&gt;) 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();
}
}

View 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;
}
}

View 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;
}
}

View 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;
}

View 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);
}
}

View 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;
}
}

View File

@@ -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");
}
}

View 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");
}
}
}

View File

@@ -42,7 +42,7 @@ quarkus.flyway.clean-at-start=false
# ATTENTION: Ne JAMAIS utiliser ce mode en production !
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true
quarkus.hibernate-orm.format_sql=true
quarkus.hibernate-orm.log.format-sql=true
quarkus.hibernate-orm.packages=com.lions.dev.entity
# Script d'import exécuté après création du schéma (données de test)
quarkus.hibernate-orm.sql-load-script=import.sql

View File

@@ -35,15 +35,34 @@ smallrye.jwt.new-token.lifespan=${JWT_LIFESPAN:86400}
smallrye.jwt.new-token.issuer=afterwork
# ====================================================================
# JWT Validation (SmallRye JWT)
# JWT Configuration (SmallRye JWT)
# ====================================================================
# Clé secrète pour vérifier les tokens (doit correspondre à afterwork.jwt.secret)
mp.jwt.verify.publickey.algorithm=HS256
# Algorithme de signature/vérification (symétrique HS256)
smallrye.jwt.verify.algorithm=HS256
smallrye.jwt.sign.key.location=
mp.jwt.verify.issuer=afterwork
# Clé secrète inline (Base64 encodée) - générée dynamiquement via JwtService
smallrye.jwt.verify.key.location=
# Clé secrète pour vérifier les tokens HS256 (même valeur que afterwork.jwt.secret)
# SmallRye JWT supporte les clés symétriques via cette propriété
smallrye.jwt.verify.key.location=META-INF/jwt-secret.key
# Activer la propagation du token pour @RolesAllowed
quarkus.smallrye-jwt.blocking-authentication=true
# ====================================================================
# Sécurité HTTP - Permissions par chemin
# ====================================================================
# Endpoints publics (sans authentification requise)
quarkus.http.auth.permission.public.paths=/afterwork/users/register,/afterwork/users/authenticate,/afterwork/users/forgot-password,/afterwork/users/reset-password,/afterwork/q/*,/afterwork/openapi,/afterwork/webhooks/*
quarkus.http.auth.permission.public.policy=permit
# Endpoints admin (SUPER_ADMIN ou ADMIN requis)
quarkus.http.auth.permission.admin.paths=/afterwork/admin/*
quarkus.http.auth.permission.admin.policy=authenticated
quarkus.http.auth.permission.admin.roles=SUPER_ADMIN,ADMIN
# Tous les autres endpoints requièrent une authentification
quarkus.http.auth.permission.authenticated.paths=/afterwork/*
quarkus.http.auth.permission.authenticated.policy=authenticated
# ====================================================================
# Wave API (paiement droits d'accès établissements)