Refactoring
This commit is contained in:
110
src/main/java/com/lions/dev/exception/GlobalExceptionMapper.java
Normal file
110
src/main/java/com/lions/dev/exception/GlobalExceptionMapper.java
Normal file
@@ -0,0 +1,110 @@
|
||||
package com.lions.dev.exception;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import jakarta.ws.rs.ForbiddenException;
|
||||
import jakarta.ws.rs.NotAuthorizedException;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.ext.ExceptionMapper;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Mapper d'exceptions global pour standardiser les réponses d'erreur de l'API.
|
||||
*
|
||||
* Format de réponse d'erreur standard:
|
||||
* {
|
||||
* "timestamp": "2026-02-05T12:00:00Z",
|
||||
* "status": 400,
|
||||
* "error": "Bad Request",
|
||||
* "message": "Description de l'erreur",
|
||||
* "path": "/api/endpoint"
|
||||
* }
|
||||
*
|
||||
* @since 2.0 - Production-ready
|
||||
*/
|
||||
@Provider
|
||||
public class GlobalExceptionMapper implements ExceptionMapper<Throwable> {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(GlobalExceptionMapper.class);
|
||||
|
||||
@Override
|
||||
public Response toResponse(Throwable exception) {
|
||||
// Déterminer le statut et le message en fonction du type d'exception
|
||||
int status;
|
||||
String error;
|
||||
String message;
|
||||
|
||||
if (exception instanceof ConstraintViolationException cve) {
|
||||
status = Response.Status.BAD_REQUEST.getStatusCode();
|
||||
error = "Validation Error";
|
||||
message = cve.getConstraintViolations().stream()
|
||||
.map(cv -> cv.getPropertyPath() + ": " + cv.getMessage())
|
||||
.collect(Collectors.joining("; "));
|
||||
LOG.warnf("Validation error: %s", message);
|
||||
} else if (exception instanceof IllegalArgumentException) {
|
||||
status = Response.Status.BAD_REQUEST.getStatusCode();
|
||||
error = "Bad Request";
|
||||
message = exception.getMessage();
|
||||
LOG.warnf("Bad request: %s", message);
|
||||
} else if (exception instanceof EntityNotFoundException || exception instanceof NotFoundException) {
|
||||
status = Response.Status.NOT_FOUND.getStatusCode();
|
||||
error = "Not Found";
|
||||
message = exception.getMessage() != null ? exception.getMessage() : "Resource not found";
|
||||
LOG.warnf("Not found: %s", message);
|
||||
} else if (exception instanceof UserNotFoundException) {
|
||||
status = Response.Status.NOT_FOUND.getStatusCode();
|
||||
error = "User Not Found";
|
||||
message = exception.getMessage();
|
||||
LOG.warnf("User not found: %s", message);
|
||||
} else if (exception instanceof NotAuthorizedException) {
|
||||
status = Response.Status.UNAUTHORIZED.getStatusCode();
|
||||
error = "Unauthorized";
|
||||
message = "Authentication required";
|
||||
LOG.warnf("Unauthorized access attempt");
|
||||
} else if (exception instanceof ForbiddenException || exception instanceof UnauthorizedException) {
|
||||
status = Response.Status.FORBIDDEN.getStatusCode();
|
||||
error = "Forbidden";
|
||||
message = exception.getMessage() != null ? exception.getMessage() : "Access denied";
|
||||
LOG.warnf("Forbidden: %s", message);
|
||||
} else if (exception instanceof SecurityException) {
|
||||
status = Response.Status.FORBIDDEN.getStatusCode();
|
||||
error = "Security Error";
|
||||
message = exception.getMessage();
|
||||
LOG.warnf("Security error: %s", message);
|
||||
} else if (exception instanceof WebApplicationException wae) {
|
||||
Response response = wae.getResponse();
|
||||
status = response.getStatus();
|
||||
error = Response.Status.fromStatusCode(status).getReasonPhrase();
|
||||
message = exception.getMessage();
|
||||
LOG.warnf("Web application exception (%d): %s", status, message);
|
||||
} else {
|
||||
// Erreur interne non gérée
|
||||
status = Response.Status.INTERNAL_SERVER_ERROR.getStatusCode();
|
||||
error = "Internal Server Error";
|
||||
message = "An unexpected error occurred. Please try again later.";
|
||||
LOG.errorf(exception, "Unhandled exception: %s", exception.getMessage());
|
||||
}
|
||||
|
||||
// Construire la réponse d'erreur standardisée
|
||||
Map<String, Object> errorResponse = new HashMap<>();
|
||||
errorResponse.put("timestamp", Instant.now().toString());
|
||||
errorResponse.put("status", status);
|
||||
errorResponse.put("error", error);
|
||||
errorResponse.put("message", message);
|
||||
|
||||
return Response.status(status)
|
||||
.type(MediaType.APPLICATION_JSON)
|
||||
.entity(errorResponse)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
159
src/main/java/com/lions/dev/filter/RateLimitFilter.java
Normal file
159
src/main/java/com/lions/dev/filter/RateLimitFilter.java
Normal file
@@ -0,0 +1,159 @@
|
||||
package com.lions.dev.filter;
|
||||
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.ws.rs.Priorities;
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
import jakarta.ws.rs.container.ContainerRequestFilter;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Filtre de limitation de débit (rate limiting) pour protéger les endpoints sensibles
|
||||
* contre les attaques par force brute.
|
||||
*
|
||||
* Endpoints protégés:
|
||||
* - /authenticate (login)
|
||||
* - /forgot-password (reset password)
|
||||
* - /register (création de compte)
|
||||
*
|
||||
* Configuration:
|
||||
* - afterwork.ratelimit.max-requests: Nombre max de requêtes par fenêtre (défaut: 10)
|
||||
* - afterwork.ratelimit.window-seconds: Durée de la fenêtre en secondes (défaut: 60)
|
||||
*
|
||||
* @since 2.0 - Production-ready security
|
||||
*/
|
||||
@Provider
|
||||
@Priority(Priorities.AUTHENTICATION - 1)
|
||||
@ApplicationScoped
|
||||
public class RateLimitFilter implements ContainerRequestFilter {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(RateLimitFilter.class);
|
||||
|
||||
@ConfigProperty(name = "afterwork.ratelimit.max-requests", defaultValue = "10")
|
||||
int maxRequests;
|
||||
|
||||
@ConfigProperty(name = "afterwork.ratelimit.window-seconds", defaultValue = "60")
|
||||
int windowSeconds;
|
||||
|
||||
// Stockage des compteurs par IP et endpoint
|
||||
private final ConcurrentHashMap<String, RateLimitEntry> rateLimitCache = new ConcurrentHashMap<>();
|
||||
|
||||
// Endpoints à protéger
|
||||
private static final String[] PROTECTED_ENDPOINTS = {
|
||||
"/authenticate",
|
||||
"/forgot-password",
|
||||
"/register",
|
||||
"/users/authenticate",
|
||||
"/users/register",
|
||||
"/users/forgot-password"
|
||||
};
|
||||
|
||||
@Override
|
||||
public void filter(ContainerRequestContext requestContext) throws IOException {
|
||||
String path = requestContext.getUriInfo().getPath();
|
||||
|
||||
// Vérifier si l'endpoint est protégé
|
||||
boolean isProtected = false;
|
||||
for (String endpoint : PROTECTED_ENDPOINTS) {
|
||||
if (path.contains(endpoint)) {
|
||||
isProtected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isProtected) {
|
||||
return; // Pas de rate limiting pour cet endpoint
|
||||
}
|
||||
|
||||
String clientIp = getClientIp(requestContext);
|
||||
String key = clientIp + ":" + path;
|
||||
|
||||
RateLimitEntry entry = rateLimitCache.compute(key, (k, existing) -> {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
|
||||
if (existing == null || (now - existing.windowStart) >= windowSeconds) {
|
||||
// Nouvelle fenêtre
|
||||
return new RateLimitEntry(now, 1);
|
||||
} else {
|
||||
// Même fenêtre, incrémenter
|
||||
existing.incrementCount();
|
||||
return existing;
|
||||
}
|
||||
});
|
||||
|
||||
if (entry.getCount() > maxRequests) {
|
||||
LOG.warnf("Rate limit exceeded for IP: %s on endpoint: %s (count: %d)",
|
||||
clientIp, path, entry.getCount());
|
||||
|
||||
requestContext.abortWith(
|
||||
Response.status(429)
|
||||
.entity("{\"message\": \"Trop de requêtes. Veuillez réessayer dans " + windowSeconds + " secondes.\"}")
|
||||
.header("Retry-After", String.valueOf(windowSeconds))
|
||||
.header("X-RateLimit-Limit", String.valueOf(maxRequests))
|
||||
.header("X-RateLimit-Remaining", "0")
|
||||
.header("X-RateLimit-Reset", String.valueOf(entry.windowStart + windowSeconds))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrait l'IP du client en tenant compte des proxies.
|
||||
*/
|
||||
private String getClientIp(ContainerRequestContext context) {
|
||||
// Vérifier X-Forwarded-For pour les proxies/load balancers
|
||||
String xff = context.getHeaderString("X-Forwarded-For");
|
||||
if (xff != null && !xff.isBlank()) {
|
||||
// Prendre la première IP (client original)
|
||||
return xff.split(",")[0].trim();
|
||||
}
|
||||
|
||||
// Vérifier X-Real-IP (nginx)
|
||||
String realIp = context.getHeaderString("X-Real-IP");
|
||||
if (realIp != null && !realIp.isBlank()) {
|
||||
return realIp.trim();
|
||||
}
|
||||
|
||||
// Fallback: adresse inconnue (ne devrait pas arriver en production)
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* Nettoie les entrées expirées du cache (à appeler périodiquement).
|
||||
*/
|
||||
public void cleanupExpiredEntries() {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
rateLimitCache.entrySet().removeIf(entry ->
|
||||
(now - entry.getValue().windowStart) >= windowSeconds * 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Classe interne pour stocker les informations de rate limiting.
|
||||
*/
|
||||
private static class RateLimitEntry {
|
||||
final long windowStart;
|
||||
private final AtomicInteger count;
|
||||
|
||||
RateLimitEntry(long windowStart, int initialCount) {
|
||||
this.windowStart = windowStart;
|
||||
this.count = new AtomicInteger(initialCount);
|
||||
}
|
||||
|
||||
void incrementCount() {
|
||||
count.incrementAndGet();
|
||||
}
|
||||
|
||||
int getCount() {
|
||||
return count.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/main/java/com/lions/dev/health/KafkaHealthCheck.java
Normal file
54
src/main/java/com/lions/dev/health/KafkaHealthCheck.java
Normal file
@@ -0,0 +1,54 @@
|
||||
package com.lions.dev.health;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.eclipse.microprofile.health.HealthCheck;
|
||||
import org.eclipse.microprofile.health.HealthCheckResponse;
|
||||
import org.eclipse.microprofile.health.HealthCheckResponseBuilder;
|
||||
import org.eclipse.microprofile.health.Readiness;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Health check pour la connexion Kafka.
|
||||
* Vérifie que le broker Kafka est accessible.
|
||||
*
|
||||
* @since 2.0 - Production-ready
|
||||
*/
|
||||
@Readiness
|
||||
@ApplicationScoped
|
||||
public class KafkaHealthCheck implements HealthCheck {
|
||||
|
||||
@ConfigProperty(name = "kafka.bootstrap.servers", defaultValue = "localhost:9092")
|
||||
String bootstrapServers;
|
||||
|
||||
@Override
|
||||
public HealthCheckResponse call() {
|
||||
HealthCheckResponseBuilder responseBuilder = HealthCheckResponse.named("Kafka Connection");
|
||||
|
||||
try {
|
||||
// Parse the first bootstrap server
|
||||
String[] servers = bootstrapServers.split(",");
|
||||
String[] hostPort = servers[0].trim().split(":");
|
||||
String host = hostPort[0];
|
||||
int port = hostPort.length > 1 ? Integer.parseInt(hostPort[1]) : 9092;
|
||||
|
||||
// Try to connect
|
||||
try (Socket socket = new Socket()) {
|
||||
socket.connect(new InetSocketAddress(host, port), 2000);
|
||||
responseBuilder.up()
|
||||
.withData("bootstrapServers", bootstrapServers)
|
||||
.withData("status", "Kafka broker is reachable");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
responseBuilder.down()
|
||||
.withData("bootstrapServers", bootstrapServers)
|
||||
.withData("error", e.getMessage())
|
||||
.withData("status", "Kafka broker is NOT reachable");
|
||||
}
|
||||
|
||||
return responseBuilder.build();
|
||||
}
|
||||
}
|
||||
28
src/main/java/com/lions/dev/health/LivenessCheck.java
Normal file
28
src/main/java/com/lions/dev/health/LivenessCheck.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package com.lions.dev.health;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import org.eclipse.microprofile.health.HealthCheck;
|
||||
import org.eclipse.microprofile.health.HealthCheckResponse;
|
||||
import org.eclipse.microprofile.health.Liveness;
|
||||
|
||||
/**
|
||||
* Health check de vivacité (liveness) pour Kubernetes.
|
||||
* Vérifie que l'application est vivante et répond.
|
||||
*
|
||||
* Endpoint: GET /q/health/live
|
||||
*
|
||||
* @since 2.0 - Production-ready
|
||||
*/
|
||||
@Liveness
|
||||
@ApplicationScoped
|
||||
public class LivenessCheck implements HealthCheck {
|
||||
|
||||
@Override
|
||||
public HealthCheckResponse call() {
|
||||
return HealthCheckResponse.named("AfterWork API Liveness")
|
||||
.up()
|
||||
.withData("version", "2.0")
|
||||
.withData("status", "Application is running")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
46
src/main/java/com/lions/dev/health/ReadinessCheck.java
Normal file
46
src/main/java/com/lions/dev/health/ReadinessCheck.java
Normal file
@@ -0,0 +1,46 @@
|
||||
package com.lions.dev.health;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import org.eclipse.microprofile.health.HealthCheck;
|
||||
import org.eclipse.microprofile.health.HealthCheckResponse;
|
||||
import org.eclipse.microprofile.health.HealthCheckResponseBuilder;
|
||||
import org.eclipse.microprofile.health.Readiness;
|
||||
|
||||
/**
|
||||
* Health check de disponibilité (readiness) pour Kubernetes.
|
||||
* Vérifie que l'application est prête à recevoir du trafic.
|
||||
* Contrôle notamment la connexion à la base de données.
|
||||
*
|
||||
* Endpoint: GET /q/health/ready
|
||||
*
|
||||
* @since 2.0 - Production-ready
|
||||
*/
|
||||
@Readiness
|
||||
@ApplicationScoped
|
||||
public class ReadinessCheck implements HealthCheck {
|
||||
|
||||
@Inject
|
||||
EntityManager entityManager;
|
||||
|
||||
@Override
|
||||
public HealthCheckResponse call() {
|
||||
HealthCheckResponseBuilder responseBuilder = HealthCheckResponse.named("AfterWork API Readiness");
|
||||
|
||||
try {
|
||||
// Test de la connexion à la base de données
|
||||
entityManager.createNativeQuery("SELECT 1").getSingleResult();
|
||||
responseBuilder.up()
|
||||
.withData("database", "connected")
|
||||
.withData("status", "Application is ready to accept traffic");
|
||||
} catch (Exception e) {
|
||||
responseBuilder.down()
|
||||
.withData("database", "disconnected")
|
||||
.withData("error", e.getMessage())
|
||||
.withData("status", "Application is NOT ready - database connection failed");
|
||||
}
|
||||
|
||||
return responseBuilder.build();
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,16 @@ import com.lions.dev.dto.response.establishment.EstablishmentMediaResponseDTO;
|
||||
import com.lions.dev.entity.establishment.EstablishmentMedia;
|
||||
import com.lions.dev.entity.establishment.MediaType;
|
||||
import com.lions.dev.service.EstablishmentMediaService;
|
||||
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.core.Response;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
@@ -20,7 +24,10 @@ import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Ressource REST pour la gestion des médias d'établissements.
|
||||
* Cette classe expose des endpoints pour uploader, récupérer et supprimer des médias.
|
||||
*
|
||||
* SÉCURITÉ : Les lectures sont publiques, les écritures requièrent une authentification.
|
||||
*
|
||||
* @since 2.0 - Sécurité JWT + RBAC production-ready
|
||||
*/
|
||||
@Path("/establishments/{establishmentId}/media")
|
||||
@Produces(jakarta.ws.rs.core.MediaType.APPLICATION_JSON)
|
||||
@@ -35,8 +42,10 @@ public class EstablishmentMediaResource {
|
||||
|
||||
/**
|
||||
* Récupère tous les médias d'un établissement.
|
||||
* Endpoint public.
|
||||
*/
|
||||
@GET
|
||||
@PermitAll
|
||||
@Operation(summary = "Récupérer tous les médias d'un établissement",
|
||||
description = "Retourne la liste de tous les médias (photos et vidéos) d'un établissement")
|
||||
public Response getEstablishmentMedia(@PathParam("establishmentId") String establishmentId) {
|
||||
@@ -64,12 +73,14 @@ public class EstablishmentMediaResource {
|
||||
|
||||
/**
|
||||
* Upload un nouveau média pour un établissement.
|
||||
* Accepte un body JSON avec les informations du média.
|
||||
* Requiert une authentification (propriétaire ou manager).
|
||||
*/
|
||||
@POST
|
||||
@Transactional
|
||||
@RolesAllowed({UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||
@Operation(summary = "Uploader un média pour un établissement",
|
||||
description = "Upload un nouveau média (photo ou vidéo) pour un établissement")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
public Response uploadMedia(
|
||||
@PathParam("establishmentId") String establishmentId,
|
||||
@Valid EstablishmentMediaRequestDTO requestDTO,
|
||||
@@ -135,12 +146,15 @@ public class EstablishmentMediaResource {
|
||||
|
||||
/**
|
||||
* Supprime un média d'un établissement.
|
||||
* Requiert une authentification (propriétaire ou manager).
|
||||
*/
|
||||
@DELETE
|
||||
@Path("/{mediaId}")
|
||||
@Transactional
|
||||
@RolesAllowed({UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||
@Operation(summary = "Supprimer un média d'un établissement",
|
||||
description = "Supprime un média spécifique d'un établissement")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
public Response deleteMedia(
|
||||
@PathParam("establishmentId") String establishmentId,
|
||||
@PathParam("mediaId") String mediaId) {
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
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.establishment.EstablishmentRatingRequestDTO;
|
||||
import com.lions.dev.dto.response.establishment.EstablishmentRatingResponseDTO;
|
||||
import com.lions.dev.dto.response.establishment.EstablishmentRatingStatsResponseDTO;
|
||||
import com.lions.dev.entity.establishment.EstablishmentRating;
|
||||
import com.lions.dev.security.Permission;
|
||||
import com.lions.dev.security.RequiresPermission;
|
||||
import com.lions.dev.service.EstablishmentRatingService;
|
||||
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.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 org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
@@ -26,7 +28,10 @@ import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Ressource REST pour la gestion des notations d'établissements.
|
||||
* Cette classe expose des endpoints pour soumettre, modifier et récupérer les notes.
|
||||
*
|
||||
* SÉCURITÉ : Les lectures sont publiques, les écritures requièrent une authentification.
|
||||
*
|
||||
* @since 2.0 - Sécurité JWT + RBAC production-ready
|
||||
*/
|
||||
@Path("/establishments/{establishmentId}/ratings")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@@ -37,14 +42,10 @@ public class EstablishmentRatingResource {
|
||||
@Inject
|
||||
EstablishmentRatingService ratingService;
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(EstablishmentRatingResource.class);
|
||||
@Inject
|
||||
SecurityService securityService;
|
||||
|
||||
/**
|
||||
* Extrait l'ID de l'utilisateur authentifié du contexte de la requête.
|
||||
*/
|
||||
private UUID getAuthenticatedUserId(ContainerRequestContext requestContext) {
|
||||
return (UUID) requestContext.getProperty(JwtAuthFilter.AUTHENTICATED_USER_ID);
|
||||
}
|
||||
private static final Logger LOG = Logger.getLogger(EstablishmentRatingResource.class);
|
||||
|
||||
/**
|
||||
* Soumet une nouvelle note pour un établissement.
|
||||
@@ -52,7 +53,8 @@ public class EstablishmentRatingResource {
|
||||
*/
|
||||
@POST
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@RolesAllowed({UserRoles.USER, UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||
@RequiresPermission(Permission.REVIEWS_CREATE)
|
||||
@Operation(summary = "Soumettre une note pour un établissement",
|
||||
description = "Soumet une nouvelle note (1 à 5 étoiles) pour un établissement. Requiert une authentification JWT.")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@@ -60,10 +62,9 @@ public class EstablishmentRatingResource {
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "400", description = "Données invalides")
|
||||
public Response submitRating(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("establishmentId") String establishmentId,
|
||||
@Valid EstablishmentRatingRequestDTO requestDTO) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("Soumission d'une note pour l'établissement " + establishmentId + " par l'utilisateur " + authenticatedUserId);
|
||||
|
||||
try {
|
||||
@@ -96,7 +97,8 @@ public class EstablishmentRatingResource {
|
||||
*/
|
||||
@PUT
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@RolesAllowed({UserRoles.USER, UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||
@RequiresPermission(Permission.REVIEWS_UPDATE_OWN)
|
||||
@Operation(summary = "Modifier une note existante",
|
||||
description = "Met à jour une note existante pour un établissement. Requiert une authentification JWT.")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@@ -104,10 +106,9 @@ public class EstablishmentRatingResource {
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "404", description = "Note non trouvée")
|
||||
public Response updateRating(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("establishmentId") String establishmentId,
|
||||
@Valid EstablishmentRatingRequestDTO requestDTO) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("Mise à jour de la note pour l'établissement " + establishmentId + " par l'utilisateur " + authenticatedUserId);
|
||||
|
||||
try {
|
||||
@@ -136,10 +137,11 @@ public class EstablishmentRatingResource {
|
||||
|
||||
/**
|
||||
* Récupère les statistiques de notation d'un établissement.
|
||||
* Doit être déclaré avant les endpoints génériques GET pour la résolution correcte par JAX-RS.
|
||||
* Endpoint public.
|
||||
*/
|
||||
@GET
|
||||
@Path("/stats")
|
||||
@PermitAll
|
||||
@Operation(summary = "Récupérer les statistiques de notation",
|
||||
description = "Récupère les statistiques de notation d'un établissement (moyenne, total, distribution)")
|
||||
public Response getRatingStats(@PathParam("establishmentId") String establishmentId) {
|
||||
@@ -170,10 +172,11 @@ public class EstablishmentRatingResource {
|
||||
|
||||
/**
|
||||
* Récupère la note d'un utilisateur pour un établissement (via path parameter).
|
||||
* Endpoint alternatif pour compatibilité.
|
||||
* Endpoint public.
|
||||
*/
|
||||
@GET
|
||||
@Path("/users/{userId}")
|
||||
@PermitAll
|
||||
@Operation(summary = "Récupérer la note d'un utilisateur (path parameter)",
|
||||
description = "Récupère la note donnée par un utilisateur spécifique pour un établissement (via path parameter)")
|
||||
public Response getUserRatingByPath(
|
||||
@@ -209,10 +212,10 @@ public class EstablishmentRatingResource {
|
||||
|
||||
/**
|
||||
* Récupère la note d'un utilisateur pour un établissement (via query parameter).
|
||||
* Endpoint utilisé par le frontend Flutter.
|
||||
* Doit être déclaré en dernier car c'est l'endpoint le plus générique.
|
||||
* Endpoint public.
|
||||
*/
|
||||
@GET
|
||||
@PermitAll
|
||||
@Operation(summary = "Récupérer la note d'un utilisateur",
|
||||
description = "Récupère la note donnée par un utilisateur spécifique pour un établissement (via query parameter userId)")
|
||||
public Response getUserRatingByQuery(
|
||||
|
||||
@@ -3,12 +3,16 @@ package com.lions.dev.resource;
|
||||
import com.lions.dev.dto.request.establishment.InitiateSubscriptionRequestDTO;
|
||||
import com.lions.dev.dto.response.establishment.InitiateSubscriptionResponseDTO;
|
||||
import com.lions.dev.service.WavePaymentService;
|
||||
import com.lions.dev.util.UserRoles;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
@@ -17,6 +21,10 @@ import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Ressource pour les abonnements / droits d'accès des établissements (paiement Wave).
|
||||
*
|
||||
* SÉCURITÉ : L'initiation du paiement requiert une authentification, le statut est public.
|
||||
*
|
||||
* @since 2.0 - Sécurité JWT + RBAC production-ready
|
||||
*/
|
||||
@Path("/establishments/{establishmentId}/subscriptions")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@@ -32,11 +40,14 @@ public class EstablishmentSubscriptionResource {
|
||||
/**
|
||||
* Initie un paiement Wave pour les droits d'accès d'un établissement.
|
||||
* Retourne l'URL de redirection vers la page de paiement Wave.
|
||||
* Requiert une authentification (propriétaire ou manager de l'établissement).
|
||||
*/
|
||||
@POST
|
||||
@Path("/initiate")
|
||||
@RolesAllowed({UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||
@Operation(summary = "Initier un paiement Wave (droits d'accès)",
|
||||
description = "Crée une session Wave et retourne l'URL de paiement. Le client redirige l'utilisateur vers payment_url.")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
public Response initiatePayment(
|
||||
@PathParam("establishmentId") UUID establishmentId,
|
||||
@Valid InitiateSubscriptionRequestDTO request) {
|
||||
@@ -55,9 +66,11 @@ public class EstablishmentSubscriptionResource {
|
||||
|
||||
/**
|
||||
* Vérifie si l'établissement a un abonnement actif.
|
||||
* Endpoint public.
|
||||
*/
|
||||
@GET
|
||||
@Path("/status")
|
||||
@PermitAll
|
||||
@Operation(summary = "Statut d'abonnement", description = "Indique si l'établissement a des droits d'accès actifs.")
|
||||
public Response getSubscriptionStatus(@PathParam("establishmentId") UUID establishmentId) {
|
||||
boolean active = wavePaymentService.hasActiveSubscription(establishmentId);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.lions.dev.resource;
|
||||
|
||||
import com.lions.dev.service.FileService;
|
||||
import com.lions.dev.util.UserRoles;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
@@ -11,6 +14,9 @@ import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.reactive.RestForm;
|
||||
import org.jboss.resteasy.reactive.multipart.FileUpload;
|
||||
@@ -22,7 +28,15 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Ressource REST pour la gestion des uploads de fichiers (médias).
|
||||
*
|
||||
* SÉCURITÉ : L'upload requiert une authentification, la lecture est publique.
|
||||
*
|
||||
* @since 2.0 - Sécurité JWT + RBAC production-ready
|
||||
*/
|
||||
@Path("/media")
|
||||
@Tag(name = "Media Upload", description = "Gestion des uploads de fichiers médias")
|
||||
public class FileUploadResource {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(FileUploadResource.class);
|
||||
@@ -37,6 +51,9 @@ public class FileUploadResource {
|
||||
@Path("/upload")
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@RolesAllowed({UserRoles.USER, UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||
@Operation(summary = "Uploader un fichier média", description = "Requiert une authentification JWT")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
public Response uploadFile(
|
||||
@RestForm("file") FileUpload file,
|
||||
@RestForm("type") String type,
|
||||
@@ -194,6 +211,8 @@ public class FileUploadResource {
|
||||
@GET
|
||||
@Path("/files/{fileName}")
|
||||
@Produces(MediaType.APPLICATION_OCTET_STREAM)
|
||||
@PermitAll
|
||||
@Operation(summary = "Récupérer un fichier média", description = "Endpoint public pour servir les fichiers uploadés")
|
||||
public Response getFile(@PathParam("fileName") String fileName) {
|
||||
try {
|
||||
java.nio.file.Path filePath = java.nio.file.Paths.get("/tmp/uploads/", fileName);
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
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.promotion.PromotionCreateRequestDTO;
|
||||
import com.lions.dev.dto.request.promotion.PromotionUpdateRequestDTO;
|
||||
import com.lions.dev.dto.response.promotion.PromotionResponseDTO;
|
||||
import com.lions.dev.entity.promotion.Promotion;
|
||||
import com.lions.dev.security.Permission;
|
||||
import com.lions.dev.security.RequiresPermission;
|
||||
import com.lions.dev.service.PromotionService;
|
||||
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.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 org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
@@ -29,8 +31,10 @@ import java.util.stream.Collectors;
|
||||
/**
|
||||
* Ressource REST pour la gestion des promotions dans le système AfterWork.
|
||||
*
|
||||
* Cette classe expose des endpoints pour créer, récupérer, mettre à jour
|
||||
* et supprimer des promotions d'établissements.
|
||||
* SÉCURITÉ : Les lectures sont publiques, les écritures requièrent une authentification.
|
||||
* Seul le responsable de l'établissement peut créer/modifier/supprimer des promotions.
|
||||
*
|
||||
* @since 2.0 - Sécurité JWT + RBAC production-ready
|
||||
*/
|
||||
@Path("/promotions")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@@ -41,14 +45,10 @@ public class PromotionResource {
|
||||
@Inject
|
||||
PromotionService promotionService;
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(PromotionResource.class);
|
||||
@Inject
|
||||
SecurityService securityService;
|
||||
|
||||
/**
|
||||
* Extrait l'ID de l'utilisateur authentifié du contexte de la requête.
|
||||
*/
|
||||
private UUID getAuthenticatedUserId(ContainerRequestContext requestContext) {
|
||||
return (UUID) requestContext.getProperty(JwtAuthFilter.AUTHENTICATED_USER_ID);
|
||||
}
|
||||
private static final Logger LOG = Logger.getLogger(PromotionResource.class);
|
||||
|
||||
// =====================================================================
|
||||
// ENDPOINTS PUBLICS (LECTURE)
|
||||
@@ -62,6 +62,7 @@ public class PromotionResource {
|
||||
* @return Liste paginée des promotions actives
|
||||
*/
|
||||
@GET
|
||||
@PermitAll
|
||||
@Operation(
|
||||
summary = "Récupérer toutes les promotions actives",
|
||||
description = "Retourne une liste paginée de toutes les promotions actives et valides")
|
||||
@@ -94,6 +95,7 @@ public class PromotionResource {
|
||||
*/
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
@PermitAll
|
||||
@Operation(
|
||||
summary = "Récupérer une promotion par ID",
|
||||
description = "Retourne les détails d'une promotion spécifique")
|
||||
@@ -127,6 +129,7 @@ public class PromotionResource {
|
||||
*/
|
||||
@GET
|
||||
@Path("/code/{code}")
|
||||
@PermitAll
|
||||
@Operation(
|
||||
summary = "Rechercher une promotion par code promo",
|
||||
description = "Retourne la promotion correspondant au code promo")
|
||||
@@ -164,6 +167,7 @@ public class PromotionResource {
|
||||
*/
|
||||
@GET
|
||||
@Path("/establishment/{establishmentId}")
|
||||
@PermitAll
|
||||
@Operation(
|
||||
summary = "Récupérer les promotions d'un établissement",
|
||||
description = "Retourne les promotions d'un établissement spécifique")
|
||||
@@ -211,7 +215,8 @@ public class PromotionResource {
|
||||
*/
|
||||
@POST
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@RolesAllowed({UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||
@RequiresPermission(Permission.PROMOTIONS_CREATE)
|
||||
@Operation(
|
||||
summary = "Créer une promotion",
|
||||
description = "Crée une nouvelle promotion pour un établissement. Seul le responsable peut créer.")
|
||||
@@ -220,10 +225,8 @@ public class PromotionResource {
|
||||
@APIResponse(responseCode = "400", description = "Données invalides")
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "403", description = "Non autorisé à créer des promotions pour cet établissement")
|
||||
public Response createPromotion(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@Valid PromotionCreateRequestDTO requestDTO) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
public Response createPromotion(@Valid PromotionCreateRequestDTO requestDTO) {
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("[LOG] Création d'une promotion pour l'établissement : " + requestDTO.getEstablishmentId() +
|
||||
" par l'utilisateur : " + authenticatedUserId);
|
||||
|
||||
@@ -267,7 +270,8 @@ public class PromotionResource {
|
||||
@PUT
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@RolesAllowed({UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||
@RequiresPermission(Permission.PROMOTIONS_UPDATE_OWN)
|
||||
@Operation(
|
||||
summary = "Mettre à jour une promotion",
|
||||
description = "Met à jour une promotion existante. Seul le responsable peut modifier.")
|
||||
@@ -277,10 +281,9 @@ public class PromotionResource {
|
||||
@APIResponse(responseCode = "403", description = "Non autorisé à modifier cette promotion")
|
||||
@APIResponse(responseCode = "404", description = "Promotion non trouvée")
|
||||
public Response updatePromotion(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("id") UUID promotionId,
|
||||
@Valid PromotionUpdateRequestDTO requestDTO) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("[LOG] Mise à jour de la promotion : " + promotionId + " par l'utilisateur : " + authenticatedUserId);
|
||||
|
||||
try {
|
||||
@@ -321,7 +324,8 @@ public class PromotionResource {
|
||||
@DELETE
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@RolesAllowed({UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||
@RequiresPermission(Permission.PROMOTIONS_DELETE_OWN)
|
||||
@Operation(
|
||||
summary = "Supprimer une promotion",
|
||||
description = "Supprime une promotion. Seul le responsable peut supprimer.")
|
||||
@@ -330,10 +334,8 @@ public class PromotionResource {
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "403", description = "Non autorisé à supprimer cette promotion")
|
||||
@APIResponse(responseCode = "404", description = "Promotion non trouvée")
|
||||
public Response deletePromotion(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("id") UUID promotionId) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
public Response deletePromotion(@PathParam("id") UUID promotionId) {
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("[LOG] Suppression de la promotion : " + promotionId + " par l'utilisateur : " + authenticatedUserId);
|
||||
|
||||
try {
|
||||
@@ -379,7 +381,8 @@ public class PromotionResource {
|
||||
@PATCH
|
||||
@Path("/{id}/active")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@RolesAllowed({UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||
@RequiresPermission(Permission.PROMOTIONS_UPDATE_OWN)
|
||||
@Operation(
|
||||
summary = "Activer/Désactiver une promotion",
|
||||
description = "Change l'état actif d'une promotion. Seul le responsable peut modifier.")
|
||||
@@ -389,10 +392,9 @@ public class PromotionResource {
|
||||
@APIResponse(responseCode = "403", description = "Non autorisé")
|
||||
@APIResponse(responseCode = "404", description = "Promotion non trouvée")
|
||||
public Response setPromotionActive(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("id") UUID promotionId,
|
||||
@QueryParam("active") @DefaultValue("true") boolean isActive) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("[LOG] Changement d'état de la promotion " + promotionId + " à " + isActive);
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
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.review.ReviewCreateRequestDTO;
|
||||
import com.lions.dev.dto.request.review.ReviewUpdateRequestDTO;
|
||||
import com.lions.dev.dto.response.review.ReviewResponseDTO;
|
||||
import com.lions.dev.entity.establishment.Review;
|
||||
import com.lions.dev.security.Permission;
|
||||
import com.lions.dev.security.RequiresPermission;
|
||||
import com.lions.dev.service.ReviewService;
|
||||
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.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 org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
@@ -30,8 +32,10 @@ import java.util.stream.Collectors;
|
||||
/**
|
||||
* Ressource REST pour la gestion des avis d'établissements.
|
||||
*
|
||||
* Cette classe expose des endpoints pour créer, récupérer, mettre à jour
|
||||
* et supprimer des avis sur les établissements.
|
||||
* SÉCURITÉ : Les lectures sont publiques, les écritures requièrent une authentification.
|
||||
* L'utilisateur ne peut modifier/supprimer que SES PROPRES avis.
|
||||
*
|
||||
* @since 2.0 - Sécurité JWT + RBAC production-ready
|
||||
*/
|
||||
@Path("/reviews")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@@ -42,14 +46,10 @@ public class ReviewResource {
|
||||
@Inject
|
||||
ReviewService reviewService;
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(ReviewResource.class);
|
||||
@Inject
|
||||
SecurityService securityService;
|
||||
|
||||
/**
|
||||
* Extrait l'ID de l'utilisateur authentifié du contexte de la requête.
|
||||
*/
|
||||
private UUID getAuthenticatedUserId(ContainerRequestContext requestContext) {
|
||||
return (UUID) requestContext.getProperty(JwtAuthFilter.AUTHENTICATED_USER_ID);
|
||||
}
|
||||
private static final Logger LOG = Logger.getLogger(ReviewResource.class);
|
||||
|
||||
// =====================================================================
|
||||
// ENDPOINTS PUBLICS (LECTURE)
|
||||
@@ -57,9 +57,11 @@ public class ReviewResource {
|
||||
|
||||
/**
|
||||
* Récupère un avis par son ID.
|
||||
* Endpoint public.
|
||||
*/
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
@PermitAll
|
||||
@Operation(
|
||||
summary = "Récupérer un avis par ID",
|
||||
description = "Retourne les détails d'un avis spécifique")
|
||||
@@ -87,9 +89,11 @@ public class ReviewResource {
|
||||
|
||||
/**
|
||||
* Récupère les avis d'un établissement.
|
||||
* Endpoint public.
|
||||
*/
|
||||
@GET
|
||||
@Path("/establishment/{establishmentId}")
|
||||
@PermitAll
|
||||
@Operation(
|
||||
summary = "Récupérer les avis d'un établissement",
|
||||
description = "Retourne la liste paginée des avis pour un établissement")
|
||||
@@ -124,9 +128,11 @@ public class ReviewResource {
|
||||
|
||||
/**
|
||||
* Récupère les statistiques des avis pour un établissement.
|
||||
* Endpoint public.
|
||||
*/
|
||||
@GET
|
||||
@Path("/establishment/{establishmentId}/stats")
|
||||
@PermitAll
|
||||
@Operation(
|
||||
summary = "Récupérer les statistiques des avis",
|
||||
description = "Retourne les statistiques (moyenne, distribution, etc.) des avis pour un établissement")
|
||||
@@ -147,9 +153,11 @@ public class ReviewResource {
|
||||
|
||||
/**
|
||||
* Récupère les avis d'un utilisateur.
|
||||
* Endpoint public.
|
||||
*/
|
||||
@GET
|
||||
@Path("/user/{userId}")
|
||||
@PermitAll
|
||||
@Operation(
|
||||
summary = "Récupérer les avis d'un utilisateur",
|
||||
description = "Retourne la liste paginée des avis écrits par un utilisateur")
|
||||
@@ -177,9 +185,11 @@ public class ReviewResource {
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur a déjà écrit un avis pour un établissement.
|
||||
* Endpoint public.
|
||||
*/
|
||||
@GET
|
||||
@Path("/establishment/{establishmentId}/user/{userId}")
|
||||
@PermitAll
|
||||
@Operation(
|
||||
summary = "Récupérer l'avis d'un utilisateur pour un établissement",
|
||||
description = "Retourne l'avis si l'utilisateur en a écrit un, 404 sinon")
|
||||
@@ -218,7 +228,8 @@ public class ReviewResource {
|
||||
*/
|
||||
@POST
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@RolesAllowed({UserRoles.USER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||
@RequiresPermission(Permission.REVIEWS_CREATE)
|
||||
@Operation(
|
||||
summary = "Créer un avis",
|
||||
description = "Crée un nouvel avis pour un établissement. Un seul avis par utilisateur et établissement.")
|
||||
@@ -226,10 +237,8 @@ public class ReviewResource {
|
||||
@APIResponse(responseCode = "201", description = "Avis créé avec succès")
|
||||
@APIResponse(responseCode = "400", description = "Données invalides ou avis déjà existant")
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
public Response createReview(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@Valid ReviewCreateRequestDTO requestDTO) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
public Response createReview(@Valid ReviewCreateRequestDTO requestDTO) {
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("[LOG] Création d'un avis pour l'établissement : " + requestDTO.getEstablishmentId() +
|
||||
" par l'utilisateur : " + authenticatedUserId);
|
||||
|
||||
@@ -258,7 +267,8 @@ public class ReviewResource {
|
||||
@PUT
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@RolesAllowed({UserRoles.USER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||
@RequiresPermission(Permission.REVIEWS_UPDATE_OWN)
|
||||
@Operation(
|
||||
summary = "Mettre à jour un avis",
|
||||
description = "Met à jour un avis existant. Seul l'auteur peut modifier.")
|
||||
@@ -268,10 +278,9 @@ public class ReviewResource {
|
||||
@APIResponse(responseCode = "403", description = "Non autorisé à modifier cet avis")
|
||||
@APIResponse(responseCode = "404", description = "Avis non trouvé")
|
||||
public Response updateReview(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("id") UUID reviewId,
|
||||
@Valid ReviewUpdateRequestDTO requestDTO) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("[LOG] Mise à jour de l'avis : " + reviewId + " par l'utilisateur : " + authenticatedUserId);
|
||||
|
||||
try {
|
||||
@@ -304,7 +313,8 @@ public class ReviewResource {
|
||||
@DELETE
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@RequiresAuth
|
||||
@RolesAllowed({UserRoles.USER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||
@RequiresPermission(Permission.REVIEWS_DELETE_OWN)
|
||||
@Operation(
|
||||
summary = "Supprimer un avis",
|
||||
description = "Supprime un avis. Seul l'auteur peut supprimer.")
|
||||
@@ -313,10 +323,8 @@ public class ReviewResource {
|
||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||
@APIResponse(responseCode = "403", description = "Non autorisé à supprimer cet avis")
|
||||
@APIResponse(responseCode = "404", description = "Avis non trouvé")
|
||||
public Response deleteReview(
|
||||
@Context ContainerRequestContext requestContext,
|
||||
@PathParam("id") UUID reviewId) {
|
||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
||||
public Response deleteReview(@PathParam("id") UUID reviewId) {
|
||||
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||
LOG.info("[LOG] Suppression de l'avis : " + reviewId + " par l'utilisateur : " + authenticatedUserId);
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user