Refactoring - Bonne version améliorée

This commit is contained in:
dahoud
2026-02-05 14:14:45 +00:00
parent a515963a4a
commit dd4dbe111e
56 changed files with 4274 additions and 2142 deletions

View File

@@ -1,7 +1,9 @@
package com.lions.dev.config;
import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition;
import org.eclipse.microprofile.openapi.annotations.enums.SecuritySchemeType;
import org.eclipse.microprofile.openapi.annotations.info.Info;
import org.eclipse.microprofile.openapi.annotations.security.SecurityScheme;
import org.eclipse.microprofile.openapi.annotations.servers.Server;
import jakarta.ws.rs.core.Application;
@@ -9,8 +11,9 @@ import jakarta.ws.rs.core.Application;
/**
* Configuration OpenAPI pour l'API AfterWork.
*
* Cette classe configure les métadonnées OpenAPI et le serveur de base
* pour que Swagger UI génère correctement les URLs avec le root-path.
* Cette classe configure les métadonnées OpenAPI, le serveur de base
* et les schémas de sécurité (JWT Bearer) pour que Swagger UI génère
* correctement les URLs avec le root-path et permette l'authentification.
*/
@OpenAPIDefinition(
info = @Info(
@@ -22,9 +25,20 @@ import jakarta.ws.rs.core.Application;
@Server(
url = "https://api.lions.dev/afterwork",
description = "Serveur de production"
),
@Server(
url = "http://localhost:8080",
description = "Serveur de développement local"
)
}
)
@SecurityScheme(
securitySchemeName = "bearerAuth",
type = SecuritySchemeType.HTTP,
scheme = "bearer",
bearerFormat = "JWT",
description = "Authentification JWT. Utilisez le token obtenu via /auth/login"
)
public class OpenAPIConfig extends Application {
// Classe de configuration OpenAPI
}

View File

@@ -1,17 +0,0 @@
package com.lions.dev.core.errors;
/**
* Classe de base pour les exceptions personnalisées dans l'application AfterWork.
* Toutes les exceptions spécifiques peuvent étendre cette classe pour centraliser la gestion des erreurs.
*/
public abstract class Exceptions extends Exception {
/**
* Constructeur de base pour les exceptions personnalisées.
*
* @param message Le message d'erreur associé à l'exception.
*/
public Exceptions(String message) {
super(message);
}
}

View File

@@ -1,31 +1,35 @@
package com.lions.dev.core.errors;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lions.dev.core.errors.exceptions.BadRequestException;
import com.lions.dev.core.errors.exceptions.EventNotFoundException;
import com.lions.dev.core.errors.exceptions.NotFoundException;
import com.lions.dev.core.errors.exceptions.ServerException;
import com.lions.dev.core.errors.exceptions.UnauthorizedException;
import com.lions.dev.exception.EstablishmentHasDependenciesException;
import com.lions.dev.exception.FriendshipNotFoundException;
import com.lions.dev.exception.UserNotFoundException;
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.util.Collections;
import java.util.Map;
/**
* Gestionnaire global des exceptions pour l'API.
* Ce gestionnaire intercepte les exceptions spécifiques et renvoie des réponses appropriées.
* Les réponses d'erreur sont sérialisées en JSON de façon sûre (pas de concaténation de chaînes).
*/
@Provider
public class GlobalExceptionHandler implements ExceptionMapper<Throwable> {
private static final Logger logger = Logger.getLogger(GlobalExceptionHandler.class);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
/**
* Gère les exceptions non traitées et retourne une réponse appropriée.
*
* @param exception L'exception interceptée.
* @return Une réponse HTTP avec un message d'erreur et le code de statut approprié.
*/
@Override
public Response toResponse(Throwable exception) {
if (exception instanceof BadRequestException) {
@@ -34,6 +38,12 @@ public class GlobalExceptionHandler implements ExceptionMapper<Throwable> {
} else if (exception instanceof UserNotFoundException) {
logger.warn("UserNotFoundException (404): " + exception.getMessage());
return buildResponse(Response.Status.NOT_FOUND, exception.getMessage());
} else if (exception instanceof FriendshipNotFoundException) {
logger.warn("FriendshipNotFoundException (404): " + exception.getMessage());
return buildResponse(Response.Status.NOT_FOUND, exception.getMessage());
} else if (exception instanceof EstablishmentHasDependenciesException) {
logger.warn("EstablishmentHasDependenciesException (409): " + exception.getMessage());
return buildResponse(Response.Status.CONFLICT, exception.getMessage());
} else if (exception instanceof EventNotFoundException || exception instanceof NotFoundException) {
logger.warn("NotFoundException intercepted: " + exception.getMessage());
return buildResponse(Response.Status.NOT_FOUND, exception.getMessage());
@@ -54,14 +64,18 @@ public class GlobalExceptionHandler implements ExceptionMapper<Throwable> {
/**
* Crée une réponse HTTP avec un code de statut et un message d'erreur.
*
* @param status Le code de statut HTTP.
* @param message Le message d'erreur.
* @return La réponse HTTP formée.
* Le message est sérialisé en JSON de façon sûre (échappement automatique).
*/
private Response buildResponse(Response.Status status, String message) {
return Response.status(status)
.entity("{\"error\":\"" + message + "\"}")
.build();
Map<String, String> body = Collections.singletonMap("error", message != null ? message : "");
try {
return Response.status(status)
.type(MediaType.APPLICATION_JSON)
.entity(OBJECT_MAPPER.writeValueAsString(body))
.build();
} catch (JsonProcessingException e) {
logger.error("Impossible de sérialiser la réponse d'erreur", e);
return Response.status(status).type(MediaType.APPLICATION_JSON).entity("{\"error\":\"Erreur serveur\"}").build();
}
}
}

View File

@@ -1,4 +0,0 @@
package com.lions.dev.core.errors;
public class ServerException {
}

View File

@@ -0,0 +1,83 @@
package com.lions.dev.core.security;
import jakarta.annotation.Priority;
import jakarta.inject.Inject;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Provider;
import org.jboss.logging.Logger;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
/**
* Filtre JAX-RS pour l'authentification JWT.
*
* Ce filtre intercepte les requêtes vers les endpoints marqués avec @RequiresAuth
* et vérifie la validité du token JWT.
*
* Le filtre stocke l'ID de l'utilisateur authentifié dans le contexte de la requête
* sous la clé "authenticatedUserId" pour utilisation ultérieure.
*/
@Provider
@RequiresAuth
@Priority(Priorities.AUTHENTICATION)
public class JwtAuthFilter implements ContainerRequestFilter {
private static final Logger LOG = Logger.getLogger(JwtAuthFilter.class);
/**
* Clé utilisée pour stocker l'ID de l'utilisateur authentifié dans le contexte.
*/
public static final String AUTHENTICATED_USER_ID = "authenticatedUserId";
@Inject
JwtValidationService jwtValidationService;
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
String path = requestContext.getUriInfo().getPath();
String method = requestContext.getMethod();
LOG.debug("[JwtAuthFilter] Vérification de l'authentification pour: " + method + " " + path);
// Récupérer le header Authorization
String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
if (authHeader == null || authHeader.isBlank()) {
LOG.warn("[JwtAuthFilter] Token manquant pour: " + method + " " + path);
abortWithUnauthorized(requestContext, "Token d'authentification manquant");
return;
}
// Valider le token et extraire l'userId
Optional<UUID> userIdOpt = jwtValidationService.validateTokenAndGetUserId(authHeader);
if (userIdOpt.isEmpty()) {
LOG.warn("[JwtAuthFilter] Token invalide pour: " + method + " " + path);
abortWithUnauthorized(requestContext, "Token d'authentification invalide ou expiré");
return;
}
// Stocker l'userId dans le contexte pour utilisation ultérieure
UUID authenticatedUserId = userIdOpt.get();
requestContext.setProperty(AUTHENTICATED_USER_ID, authenticatedUserId);
LOG.debug("[JwtAuthFilter] Authentification réussie pour l'utilisateur: " + authenticatedUserId);
}
/**
* Interrompt la requête avec une réponse 401 Unauthorized.
*/
private void abortWithUnauthorized(ContainerRequestContext requestContext, String message) {
requestContext.abortWith(
Response.status(Response.Status.UNAUTHORIZED)
.entity("{\"message\": \"" + message + "\"}")
.type("application/json")
.build()
);
}
}

View File

@@ -0,0 +1,187 @@
package com.lions.dev.core.security;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Optional;
import java.util.UUID;
/**
* Service de validation des tokens JWT.
*
* Ce service valide les tokens JWT HMAC-SHA256 envoyés par les clients et extrait
* l'identifiant de l'utilisateur authentifié.
*
* Utilise une validation manuelle pour supporter HMAC-SHA256 sans dépendance
* sur la configuration complexe de SmallRye JWT.
*/
@ApplicationScoped
public class JwtValidationService {
private static final Logger LOG = Logger.getLogger(JwtValidationService.class);
private static final String ISSUER = "afterwork";
private static final String BEARER_PREFIX = "Bearer ";
private static final ObjectMapper MAPPER = new ObjectMapper();
@ConfigProperty(name = "afterwork.jwt.secret", defaultValue = "afterwork-jwt-secret-min-32-bytes-for-hs256!")
String secret;
/**
* Valide un token JWT et retourne l'ID de l'utilisateur.
*
* @param authorizationHeader Le header Authorization (avec ou sans préfixe "Bearer ")
* @return L'ID de l'utilisateur si le token est valide, Optional.empty() sinon
*/
public Optional<UUID> validateTokenAndGetUserId(String authorizationHeader) {
if (authorizationHeader == null || authorizationHeader.isBlank()) {
LOG.debug("[JwtValidation] Authorization header absent");
return Optional.empty();
}
String token = extractToken(authorizationHeader);
if (token == null || token.isBlank()) {
LOG.debug("[JwtValidation] Token non trouvé dans le header");
return Optional.empty();
}
try {
// Séparer les parties du token
String[] parts = token.split("\\.");
if (parts.length != 3) {
LOG.warn("[JwtValidation] Format de token invalide (attendu: 3 parties)");
return Optional.empty();
}
String headerPart = parts[0];
String payloadPart = parts[1];
String signaturePart = parts[2];
// Vérifier la signature HMAC-SHA256
if (!verifySignature(headerPart, payloadPart, signaturePart)) {
LOG.warn("[JwtValidation] Signature invalide");
return Optional.empty();
}
// Décoder et parser le payload
String payloadJson = new String(Base64.getUrlDecoder().decode(payloadPart), StandardCharsets.UTF_8);
JsonNode payload = MAPPER.readTree(payloadJson);
// Vérifier l'issuer
JsonNode issNode = payload.get("iss");
if (issNode == null || !ISSUER.equals(issNode.asText())) {
LOG.warn("[JwtValidation] Issuer invalide: " + (issNode != null ? issNode.asText() : "null"));
return Optional.empty();
}
// Vérifier l'expiration
JsonNode expNode = payload.get("exp");
if (expNode != null) {
long expiration = expNode.asLong();
long now = System.currentTimeMillis() / 1000;
if (expiration < now) {
LOG.warn("[JwtValidation] Token expiré (exp: " + expiration + ", now: " + now + ")");
return Optional.empty();
}
}
// Extraire le subject (userId)
JsonNode subNode = payload.get("sub");
if (subNode == null || subNode.asText().isBlank()) {
LOG.warn("[JwtValidation] Subject (userId) absent du token");
return Optional.empty();
}
UUID userId = UUID.fromString(subNode.asText());
LOG.debug("[JwtValidation] Token valide pour l'utilisateur: " + userId);
return Optional.of(userId);
} catch (IllegalArgumentException e) {
LOG.warn("[JwtValidation] Subject invalide (pas un UUID): " + e.getMessage());
return Optional.empty();
} catch (Exception e) {
LOG.error("[JwtValidation] Erreur lors de la validation du token: " + e.getMessage(), e);
return Optional.empty();
}
}
/**
* Vérifie la signature HMAC-SHA256 du token.
*/
private boolean verifySignature(String header, String payload, String signature) {
try {
SecretKey key = getSecretKey();
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(key);
String dataToSign = header + "." + payload;
byte[] expectedSignature = mac.doFinal(dataToSign.getBytes(StandardCharsets.UTF_8));
String expectedSignatureBase64 = Base64.getUrlEncoder().withoutPadding().encodeToString(expectedSignature);
return expectedSignatureBase64.equals(signature);
} catch (Exception e) {
LOG.error("[JwtValidation] Erreur lors de la vérification de la signature: " + e.getMessage());
return false;
}
}
/**
* Vérifie si le token appartient à l'utilisateur spécifié.
*
* @param authorizationHeader Le header Authorization
* @param expectedUserId L'ID de l'utilisateur attendu
* @return true si le token appartient à cet utilisateur
*/
public boolean isTokenOwner(String authorizationHeader, UUID expectedUserId) {
if (expectedUserId == null) {
return false;
}
Optional<UUID> tokenUserId = validateTokenAndGetUserId(authorizationHeader);
return tokenUserId.isPresent() && tokenUserId.get().equals(expectedUserId);
}
/**
* Vérifie si le token est valide sans retourner l'utilisateur.
*
* @param authorizationHeader Le header Authorization
* @return true si le token est valide
*/
public boolean isValidToken(String authorizationHeader) {
return validateTokenAndGetUserId(authorizationHeader).isPresent();
}
/**
* Extrait le token du header Authorization.
*
* @param authorizationHeader Le header complet
* @return Le token sans le préfixe "Bearer ", ou null si invalide
*/
private String extractToken(String authorizationHeader) {
if (authorizationHeader.startsWith(BEARER_PREFIX)) {
return authorizationHeader.substring(BEARER_PREFIX.length()).trim();
}
// Si pas de préfixe, retourner tel quel (pour compatibilité)
return authorizationHeader.trim();
}
/**
* Génère la clé secrète à partir de la configuration.
*/
private SecretKey getSecretKey() {
byte[] decoded = secret.getBytes(StandardCharsets.UTF_8);
if (decoded.length < 32) {
byte[] padded = new byte[32];
System.arraycopy(decoded, 0, padded, 0, decoded.length);
decoded = padded;
}
return new SecretKeySpec(decoded, "HmacSHA256");
}
}

View File

@@ -0,0 +1,33 @@
package com.lions.dev.core.security;
import jakarta.ws.rs.NameBinding;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation pour marquer les endpoints qui nécessitent une authentification JWT.
*
* Lorsque cette annotation est présente sur une méthode ou une classe,
* le filtre {@link JwtAuthFilter} vérifiera la présence et la validité
* du token JWT dans le header Authorization.
*
* Usage:
* <pre>
* @RequiresAuth
* @POST
* public Response createPost(...) { ... }
* </pre>
*/
@NameBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface RequiresAuth {
/**
* Si true, vérifie que l'utilisateur du token correspond au userId de la requête.
* Par défaut, seule la validité du token est vérifiée.
*/
boolean verifyOwnership() default false;
}

View File

@@ -1,5 +1,7 @@
package com.lions.dev.dto.request.chat;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@@ -8,27 +10,22 @@ import java.util.UUID;
/**
* DTO pour l'envoi d'un message.
* Validation déclarative via Bean Validation (Hibernate Validator).
*/
@Getter
@Setter
@NoArgsConstructor
public class SendMessageRequestDTO {
private UUID senderId; // L'ID de l'expéditeur
private UUID recipientId; // L'ID du destinataire
private String content; // Le contenu du message
private String messageType; // Le type de message (text, image, video, file)
private String mediaUrl; // L'URL du média (optionnel)
@NotNull(message = "L'ID de l'expéditeur est obligatoire")
private UUID senderId;
/**
* Valide les données du DTO.
*
* @return true si les données sont valides, false sinon
*/
public boolean isValid() {
return senderId != null
&& recipientId != null
&& content != null
&& !content.trim().isEmpty();
}
@NotNull(message = "L'ID du destinataire est obligatoire")
private UUID recipientId;
@NotBlank(message = "Le contenu du message est obligatoire")
private String content;
private String messageType; // text, image, video, file (optionnel, défaut text)
private String mediaUrl; // optionnel
}

View File

@@ -0,0 +1,49 @@
package com.lions.dev.dto.request.promotion;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* DTO pour la création d'une promotion.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class PromotionCreateRequestDTO {
@NotNull(message = "L'ID de l'établissement est obligatoire")
private UUID establishmentId;
@NotBlank(message = "Le titre est obligatoire")
@Size(max = 200, message = "Le titre ne peut pas dépasser 200 caractères")
private String title;
private String description;
@Size(max = 50, message = "Le code promo ne peut pas dépasser 50 caractères")
private String promoCode;
@NotBlank(message = "Le type de réduction est obligatoire")
private String discountType; // PERCENTAGE, FIXED_AMOUNT, FREE_ITEM
@NotNull(message = "La valeur de réduction est obligatoire")
@Positive(message = "La valeur de réduction doit être positive")
private BigDecimal discountValue;
@NotNull(message = "La date de début est obligatoire")
private LocalDateTime validFrom;
@NotNull(message = "La date de fin est obligatoire")
private LocalDateTime validUntil;
}

View File

@@ -0,0 +1,41 @@
package com.lions.dev.dto.request.promotion;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* DTO pour la mise à jour d'une promotion.
* Tous les champs sont optionnels.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class PromotionUpdateRequestDTO {
@Size(max = 200, message = "Le titre ne peut pas dépasser 200 caractères")
private String title;
private String description;
@Size(max = 50, message = "Le code promo ne peut pas dépasser 50 caractères")
private String promoCode;
private String discountType; // PERCENTAGE, FIXED_AMOUNT, FREE_ITEM
@Positive(message = "La valeur de réduction doit être positive")
private BigDecimal discountValue;
private LocalDateTime validFrom;
private LocalDateTime validUntil;
private Boolean isActive;
}

View File

@@ -0,0 +1,37 @@
package com.lions.dev.dto.request.review;
import jakarta.validation.constraints.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.Map;
import java.util.UUID;
/**
* DTO pour la création d'un avis sur un établissement.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ReviewCreateRequestDTO {
@NotNull(message = "L'ID de l'établissement est obligatoire")
private UUID establishmentId;
@NotNull(message = "La note globale est obligatoire")
@Min(value = 1, message = "La note doit être au minimum 1")
@Max(value = 5, message = "La note doit être au maximum 5")
private Integer overallRating;
@Size(max = 2000, message = "Le commentaire ne peut pas dépasser 2000 caractères")
private String comment;
/**
* Notes par critères (optionnel).
* Clés possibles: "ambiance", "service", "qualite", "rapport_qualite_prix", "proprete"
*/
private Map<String, Integer> criteriaRatings;
}

View File

@@ -0,0 +1,34 @@
package com.lions.dev.dto.request.review;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.Map;
/**
* DTO pour la mise à jour d'un avis.
* Tous les champs sont optionnels.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ReviewUpdateRequestDTO {
@Min(value = 1, message = "La note doit être au minimum 1")
@Max(value = 5, message = "La note doit être au maximum 5")
private Integer overallRating;
@Size(max = 2000, message = "Le commentaire ne peut pas dépasser 2000 caractères")
private String comment;
/**
* Notes par critères (optionnel).
*/
private Map<String, Integer> criteriaRatings;
}

View File

@@ -0,0 +1,30 @@
package com.lions.dev.dto.request.social;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.UUID;
/**
* DTO (Data Transfer Object) pour la création d'un commentaire sur un post social.
*
* Valide que le contenu n'est pas vide et ne dépasse pas 1000 caractères.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class PostCommentCreateRequestDTO {
@NotBlank(message = "Le contenu du commentaire est obligatoire")
@Size(max = 1000, message = "Le commentaire ne peut pas dépasser 1000 caractères")
private String content;
@NotNull(message = "L'identifiant de l'utilisateur est obligatoire")
private UUID userId;
}

View File

@@ -0,0 +1,24 @@
package com.lions.dev.dto.request.users;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
/**
* DTO pour la mise à jour de l'image de profil (URL après upload).
* Le client envoie l'URL retournée par l'endpoint d'upload de médias.
* Accepte profile_image_url (snake_case) ou profileImageUrl (camelCase).
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UpdateProfileImageRequestDTO {
@NotBlank(message = "L'URL de l'image de profil est obligatoire.")
@JsonProperty("profile_image_url")
private String profileImageUrl;
}

View File

@@ -0,0 +1,79 @@
package com.lions.dev.dto.response.promotion;
import com.lions.dev.entity.promotion.Promotion;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* DTO pour la réponse d'une promotion.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class PromotionResponseDTO {
private UUID id;
private UUID establishmentId;
private String establishmentName;
private String title;
private String description;
private String promoCode;
private String discountType;
private BigDecimal discountValue;
private LocalDateTime validFrom;
private LocalDateTime validUntil;
private Boolean isActive;
private boolean isValid;
private boolean isExpired;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
/**
* Constructeur à partir d'une entité Promotion.
*
* @param promotion L'entité Promotion
*/
public PromotionResponseDTO(Promotion promotion) {
if (promotion != null) {
this.id = promotion.getId();
this.establishmentId = promotion.getEstablishment() != null ? promotion.getEstablishment().getId() : null;
this.establishmentName = promotion.getEstablishment() != null ? promotion.getEstablishment().getName() : null;
this.title = promotion.getTitle();
this.description = promotion.getDescription();
this.promoCode = promotion.getPromoCode();
this.discountType = promotion.getDiscountType();
this.discountValue = promotion.getDiscountValue();
this.validFrom = promotion.getValidFrom();
this.validUntil = promotion.getValidUntil();
this.isActive = promotion.getIsActive();
this.isValid = promotion.isValid();
this.isExpired = promotion.isExpired();
this.createdAt = promotion.getCreatedAt();
this.updatedAt = promotion.getUpdatedAt();
}
}
/**
* Formate la réduction pour l'affichage.
*
* @return La réduction formatée (ex: "20%", "10€", "1 article offert")
*/
public String getFormattedDiscount() {
if (discountValue == null || discountType == null) {
return "";
}
return switch (discountType.toUpperCase()) {
case "PERCENTAGE" -> discountValue.stripTrailingZeros().toPlainString() + "%";
case "FIXED_AMOUNT" -> discountValue.stripTrailingZeros().toPlainString() + "";
case "FREE_ITEM" -> discountValue.intValue() + " article(s) offert(s)";
default -> discountValue.toString();
};
}
}

View File

@@ -0,0 +1,73 @@
package com.lions.dev.dto.response.review;
import com.lions.dev.entity.establishment.Review;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.UUID;
/**
* DTO pour la réponse d'un avis.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ReviewResponseDTO {
private UUID id;
private UUID userId;
private String userFirstName;
private String userLastName;
private String userProfileImageUrl;
private UUID establishmentId;
private String establishmentName;
private Integer overallRating;
private String comment;
private Map<String, Integer> criteriaRatings;
private Boolean isVerifiedVisit;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
/**
* Constructeur à partir d'une entité Review.
*
* @param review L'entité Review
*/
public ReviewResponseDTO(Review review) {
if (review != null) {
this.id = review.getId();
this.userId = review.getUser() != null ? review.getUser().getId() : null;
this.userFirstName = review.getUser() != null ? review.getUser().getFirstName() : null;
this.userLastName = review.getUser() != null ? review.getUser().getLastName() : null;
this.userProfileImageUrl = review.getUser() != null ? review.getUser().getProfileImageUrl() : null;
this.establishmentId = review.getEstablishment() != null ? review.getEstablishment().getId() : null;
this.establishmentName = review.getEstablishment() != null ? review.getEstablishment().getName() : null;
this.overallRating = review.getOverallRating();
this.comment = review.getComment();
this.criteriaRatings = review.getCriteriaRatings();
this.isVerifiedVisit = review.getIsVerifiedVisit();
this.createdAt = review.getCreatedAt();
this.updatedAt = review.getUpdatedAt();
}
}
/**
* Retourne le nom complet de l'auteur de l'avis.
*/
public String getUserFullName() {
StringBuilder sb = new StringBuilder();
if (userFirstName != null) {
sb.append(userFirstName);
}
if (userLastName != null) {
if (sb.length() > 0) sb.append(" ");
sb.append(userLastName);
}
return sb.toString();
}
}

View File

@@ -0,0 +1,69 @@
package com.lions.dev.dto.response.social;
import com.lions.dev.entity.social.PostComment;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* DTO (Data Transfer Object) pour la réponse d'un commentaire de post social.
*
* Cette classe représente un commentaire avec les informations de l'auteur
* pour l'affichage dans l'interface utilisateur.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class PostCommentResponseDTO {
private UUID id;
private String content;
private UUID postId;
private UUID userId;
private String userFirstName;
private String userLastName;
private String userProfileImageUrl;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
/**
* Constructeur à partir d'une entité PostComment.
*
* @param comment L'entité PostComment
*/
public PostCommentResponseDTO(PostComment comment) {
if (comment != null) {
this.id = comment.getId();
this.content = comment.getContent();
this.postId = comment.getPost() != null ? comment.getPost().getId() : null;
this.userId = comment.getUser() != null ? comment.getUser().getId() : null;
this.userFirstName = comment.getUser() != null ? comment.getUser().getFirstName() : null;
this.userLastName = comment.getUser() != null ? comment.getUser().getLastName() : null;
this.userProfileImageUrl = comment.getUser() != null ? comment.getUser().getProfileImageUrl() : null;
this.createdAt = comment.getCreatedAt();
this.updatedAt = comment.getUpdatedAt();
}
}
/**
* Retourne le nom complet de l'auteur du commentaire.
*
* @return Le nom complet (prénom + nom)
*/
public String getUserFullName() {
StringBuilder sb = new StringBuilder();
if (userFirstName != null) {
sb.append(userFirstName);
}
if (userLastName != null) {
if (sb.length() > 0) sb.append(" ");
sb.append(userLastName);
}
return sb.toString();
}
}

View File

@@ -48,6 +48,11 @@ public class UserAuthenticateResponseDTO {
*/
private String role;
/**
* Token JWT à envoyer dans l'en-tête Authorization: Bearer &lt;token&gt; pour les requêtes protégées.
*/
private String token;
// Champs de compatibilité v1.0 (dépréciés)
/**
* @deprecated Utiliser {@link #id} à la place.

View File

@@ -0,0 +1,69 @@
package com.lions.dev.entity.social;
import com.lions.dev.entity.BaseEntity;
import com.lions.dev.entity.users.Users;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
/**
* Entité représentant un commentaire sur un post social dans le système AfterWork.
*
* Chaque commentaire est lié à un utilisateur (auteur) et à un post social.
* Les commentaires sont limités à 1000 caractères.
*/
@Entity
@Table(name = "post_comments")
@Getter
@Setter
@NoArgsConstructor
@ToString(exclude = {"post", "user"})
public class PostComment extends BaseEntity {
@Column(name = "content", nullable = false, length = 1000)
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private SocialPost post;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id", nullable = false)
private Users user;
/**
* Constructeur pour créer un nouveau commentaire.
*
* @param content Le contenu du commentaire
* @param post Le post social commenté
* @param user L'utilisateur auteur du commentaire
*/
public PostComment(String content, SocialPost post, Users user) {
this.content = content;
this.post = post;
this.user = user;
}
/**
* Met à jour le contenu du commentaire.
*
* @param newContent Le nouveau contenu
*/
public void updateContent(String newContent) {
if (newContent != null && !newContent.isBlank() && newContent.length() <= 1000) {
this.content = newContent;
}
}
/**
* Vérifie si l'utilisateur donné est l'auteur du commentaire.
*
* @param userId L'ID de l'utilisateur à vérifier
* @return true si l'utilisateur est l'auteur, false sinon
*/
public boolean isAuthor(java.util.UUID userId) {
return this.user != null && this.user.getId() != null && this.user.getId().equals(userId);
}
}

View File

@@ -1,21 +0,0 @@
package com.lions.dev.exception;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Response;
import java.util.UUID;
/**
* Exception levée lorsque l'événement demandé n'est pas trouvé dans la base de données.
* Cette exception renvoie une réponse HTTP 404 (NOT FOUND).
*/
public class EventNotFoundException extends WebApplicationException {
/**
* Constructeur qui prend un UUID et convertit l'UUID en message détaillant l'erreur.
*
* @param eventId L'UUID de l'événement qui n'a pas été trouvé.
*/
public EventNotFoundException(UUID eventId) {
super("Événement non trouvé avec l'ID : " + eventId.toString(), Response.Status.NOT_FOUND);
}
}

View File

@@ -0,0 +1,96 @@
package com.lions.dev.repository;
import com.lions.dev.entity.social.PostComment;
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import jakarta.enterprise.context.ApplicationScoped;
import org.jboss.logging.Logger;
import java.util.List;
import java.util.UUID;
/**
* Repository pour l'entité PostComment.
*
* Ce repository gère les opérations CRUD sur les commentaires de posts sociaux
* ainsi que des méthodes personnalisées pour la pagination et la recherche.
*/
@ApplicationScoped
public class PostCommentRepository implements PanacheRepositoryBase<PostComment, UUID> {
private static final Logger LOG = Logger.getLogger(PostCommentRepository.class);
/**
* Récupère tous les commentaires d'un post avec pagination.
*
* @param postId L'ID du post
* @param page Le numéro de la page (0-indexé)
* @param size La taille de la page
* @return Liste paginée des commentaires, triés par date de création croissante
*/
public List<PostComment> findByPostId(UUID postId, int page, int size) {
LOG.debug("[PostCommentRepository] Recherche des commentaires pour le post: " + postId);
return find("post.id", Sort.by("createdAt", Sort.Direction.Ascending), postId)
.page(Page.of(page, size))
.list();
}
/**
* Récupère tous les commentaires d'un post sans pagination.
*
* @param postId L'ID du post
* @return Liste de tous les commentaires du post
*/
public List<PostComment> findAllByPostId(UUID postId) {
LOG.debug("[PostCommentRepository] Recherche de tous les commentaires pour le post: " + postId);
return find("post.id", Sort.by("createdAt", Sort.Direction.Ascending), postId).list();
}
/**
* Compte le nombre de commentaires pour un post.
*
* @param postId L'ID du post
* @return Le nombre de commentaires
*/
public long countByPostId(UUID postId) {
return count("post.id", postId);
}
/**
* Récupère tous les commentaires d'un utilisateur.
*
* @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 commentaires de l'utilisateur
*/
public List<PostComment> findByUserId(UUID userId, int page, int size) {
LOG.debug("[PostCommentRepository] Recherche des commentaires de l'utilisateur: " + userId);
return find("user.id", Sort.by("createdAt", Sort.Direction.Descending), userId)
.page(Page.of(page, size))
.list();
}
/**
* Supprime tous les commentaires d'un post.
*
* @param postId L'ID du post
* @return Le nombre de commentaires supprimés
*/
public long deleteByPostId(UUID postId) {
LOG.info("[PostCommentRepository] Suppression de tous les commentaires du post: " + postId);
return delete("post.id", postId);
}
/**
* Vérifie si un utilisateur a déjà commenté un post.
*
* @param postId L'ID du post
* @param userId L'ID de l'utilisateur
* @return true si l'utilisateur a commenté, false sinon
*/
public boolean hasUserCommented(UUID postId, UUID userId) {
return count("post.id = ?1 AND user.id = ?2", postId, userId) > 0;
}
}

View File

@@ -0,0 +1,140 @@
package com.lions.dev.repository;
import com.lions.dev.entity.promotion.Promotion;
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import jakarta.enterprise.context.ApplicationScoped;
import org.jboss.logging.Logger;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* Repository pour l'entité Promotion.
*
* Ce repository gère les opérations CRUD sur les promotions
* ainsi que des méthodes personnalisées pour la recherche et la pagination.
*/
@ApplicationScoped
public class PromotionRepository implements PanacheRepositoryBase<Promotion, UUID> {
private static final Logger LOG = Logger.getLogger(PromotionRepository.class);
/**
* Récupère toutes les promotions d'un établissement avec pagination.
*
* @param establishmentId L'ID de l'établissement
* @param page Le numéro de la page (0-indexé)
* @param size La taille de la page
* @return Liste paginée des promotions
*/
public List<Promotion> findByEstablishmentId(UUID establishmentId, int page, int size) {
LOG.debug("[PromotionRepository] Recherche des promotions pour l'établissement: " + establishmentId);
return find("establishment.id", Sort.by("validFrom", Sort.Direction.Descending), establishmentId)
.page(Page.of(page, size))
.list();
}
/**
* Récupère toutes les promotions actives et valides d'un établissement.
*
* @param establishmentId L'ID de l'établissement
* @return Liste des promotions actives et valides
*/
public List<Promotion> findActiveByEstablishmentId(UUID establishmentId) {
LOG.debug("[PromotionRepository] Recherche des promotions actives pour l'établissement: " + establishmentId);
LocalDateTime now = LocalDateTime.now();
return find("establishment.id = ?1 AND isActive = true AND validFrom <= ?2 AND validUntil >= ?2",
Sort.by("validFrom", Sort.Direction.Ascending), establishmentId, now)
.list();
}
/**
* Récupère toutes les promotions actives et valides (tous établissements).
*
* @param page Le numéro de la page (0-indexé)
* @param size La taille de la page
* @return Liste paginée des promotions actives
*/
public List<Promotion> findAllActive(int page, int size) {
LOG.debug("[PromotionRepository] Recherche de toutes les promotions actives");
LocalDateTime now = LocalDateTime.now();
return find("isActive = true AND validFrom <= ?1 AND validUntil >= ?1",
Sort.by("validFrom", Sort.Direction.Ascending), now)
.page(Page.of(page, size))
.list();
}
/**
* Recherche une promotion par son code promo.
*
* @param promoCode Le code promo
* @return La promotion si trouvée
*/
public Optional<Promotion> findByPromoCode(String promoCode) {
LOG.debug("[PromotionRepository] Recherche de la promotion avec le code: " + promoCode);
return find("promoCode", promoCode).firstResultOptional();
}
/**
* Vérifie si un code promo existe déjà.
*
* @param promoCode Le code promo à vérifier
* @return true si le code existe déjà
*/
public boolean promoCodeExists(String promoCode) {
return count("promoCode", promoCode) > 0;
}
/**
* Recherche les promotions par type de réduction.
*
* @param discountType Le type de réduction (PERCENTAGE, FIXED_AMOUNT, FREE_ITEM)
* @param page Le numéro de la page
* @param size La taille de la page
* @return Liste des promotions du type spécifié
*/
public List<Promotion> findByDiscountType(String discountType, int page, int size) {
LOG.debug("[PromotionRepository] Recherche des promotions de type: " + discountType);
return find("discountType", Sort.by("createdAt", Sort.Direction.Descending), discountType)
.page(Page.of(page, size))
.list();
}
/**
* Recherche les promotions expirées.
*
* @return Liste des promotions expirées
*/
public List<Promotion> findExpired() {
LOG.debug("[PromotionRepository] Recherche des promotions expirées");
LocalDateTime now = LocalDateTime.now();
return find("validUntil < ?1", now).list();
}
/**
* Désactive les promotions expirées.
*
* @return Nombre de promotions désactivées
*/
public long deactivateExpired() {
LOG.info("[PromotionRepository] Désactivation des promotions expirées");
LocalDateTime now = LocalDateTime.now();
return update("isActive = false WHERE validUntil < ?1 AND isActive = true", now);
}
/**
* Compte les promotions actives d'un établissement.
*
* @param establishmentId L'ID de l'établissement
* @return Le nombre de promotions actives
*/
public long countActiveByEstablishmentId(UUID establishmentId) {
LocalDateTime now = LocalDateTime.now();
return count("establishment.id = ?1 AND isActive = true AND validFrom <= ?2 AND validUntil >= ?2",
establishmentId, now);
}
}

View File

@@ -0,0 +1,130 @@
package com.lions.dev.repository;
import com.lions.dev.entity.establishment.Review;
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import jakarta.enterprise.context.ApplicationScoped;
import org.jboss.logging.Logger;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* Repository pour l'entité Review.
*
* Ce repository gère les opérations CRUD sur les avis d'établissements
* ainsi que des méthodes personnalisées pour la recherche et la pagination.
*/
@ApplicationScoped
public class ReviewRepository implements PanacheRepositoryBase<Review, UUID> {
private static final Logger LOG = Logger.getLogger(ReviewRepository.class);
/**
* Récupère tous les avis d'un établissement avec pagination.
*
* @param establishmentId L'ID de l'établissement
* @param page Le numéro de la page (0-indexé)
* @param size La taille de la page
* @return Liste paginée des avis
*/
public List<Review> findByEstablishmentId(UUID establishmentId, int page, int size) {
LOG.debug("[ReviewRepository] Recherche des avis pour l'établissement: " + establishmentId);
return find("establishment.id", Sort.by("createdAt", Sort.Direction.Descending), establishmentId)
.page(Page.of(page, size))
.list();
}
/**
* Récupère tous les avis d'un utilisateur avec pagination.
*
* @param userId L'ID de l'utilisateur
* @param page Le numéro de la page
* @param size La taille de la page
* @return Liste paginée des avis
*/
public List<Review> findByUserId(UUID userId, int page, int size) {
LOG.debug("[ReviewRepository] Recherche des avis de l'utilisateur: " + userId);
return find("user.id", Sort.by("createdAt", Sort.Direction.Descending), userId)
.page(Page.of(page, size))
.list();
}
/**
* Récupère l'avis d'un utilisateur pour un établissement spécifique.
*
* @param establishmentId L'ID de l'établissement
* @param userId L'ID de l'utilisateur
* @return L'avis si trouvé
*/
public Optional<Review> findByEstablishmentAndUser(UUID establishmentId, UUID userId) {
LOG.debug("[ReviewRepository] Recherche de l'avis pour établissement " + establishmentId + " et utilisateur " + userId);
return find("establishment.id = ?1 AND user.id = ?2", establishmentId, userId).firstResultOptional();
}
/**
* Vérifie si un utilisateur a déjà écrit un avis pour un établissement.
*
* @param establishmentId L'ID de l'établissement
* @param userId L'ID de l'utilisateur
* @return true si l'utilisateur a déjà un avis
*/
public boolean hasUserReviewed(UUID establishmentId, UUID userId) {
return count("establishment.id = ?1 AND user.id = ?2", establishmentId, userId) > 0;
}
/**
* Compte le nombre d'avis pour un établissement.
*
* @param establishmentId L'ID de l'établissement
* @return Le nombre d'avis
*/
public long countByEstablishmentId(UUID establishmentId) {
return count("establishment.id", establishmentId);
}
/**
* Récupère uniquement les avis vérifiés d'un établissement.
*
* @param establishmentId L'ID de l'établissement
* @param page Le numéro de la page
* @param size La taille de la page
* @return Liste des avis vérifiés
*/
public List<Review> findVerifiedByEstablishmentId(UUID establishmentId, int page, int size) {
LOG.debug("[ReviewRepository] Recherche des avis vérifiés pour l'établissement: " + establishmentId);
return find("establishment.id = ?1 AND isVerifiedVisit = true",
Sort.by("createdAt", Sort.Direction.Descending), establishmentId)
.page(Page.of(page, size))
.list();
}
/**
* Calcule la note moyenne d'un établissement.
*
* @param establishmentId L'ID de l'établissement
* @return La note moyenne, ou null si aucun avis
*/
public Double getAverageRating(UUID establishmentId) {
return find("establishment.id", establishmentId)
.project(Double.class)
.stream()
.mapToInt(r -> 0) // Placeholder - needs aggregate query
.average()
.orElse(0.0);
}
/**
* Supprime l'avis d'un utilisateur pour un établissement.
*
* @param establishmentId L'ID de l'établissement
* @param userId L'ID de l'utilisateur
* @return Le nombre d'avis supprimés (0 ou 1)
*/
public long deleteByEstablishmentAndUser(UUID establishmentId, UUID userId) {
LOG.info("[ReviewRepository] Suppression de l'avis pour établissement " + establishmentId + " et utilisateur " + userId);
return delete("establishment.id = ?1 AND user.id = ?2", establishmentId, userId);
}
}

View File

@@ -45,6 +45,21 @@ public class SocialPostRepository implements PanacheRepositoryBase<SocialPost, U
return posts;
}
/**
* Récupère tous les posts 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 posts de l'utilisateur
*/
public List<SocialPost> findByUserIdWithPagination(UUID userId, int page, int size) {
List<SocialPost> posts = find("user.id", Sort.by("createdAt", Sort.Direction.Descending), userId)
.page(Page.of(page, size))
.list();
return posts;
}
/**
* Recherche des posts par contenu (recherche textuelle).
*
@@ -57,6 +72,22 @@ public class SocialPostRepository implements PanacheRepositoryBase<SocialPost, U
return posts;
}
/**
* Recherche des posts par contenu avec pagination.
*
* @param query Le terme de recherche
* @param page Le numéro de la page (0-indexé)
* @param size La taille de la page
* @return Liste paginée des posts correspondant à la recherche
*/
public List<SocialPost> searchByContentWithPagination(String query, int page, int size) {
String searchPattern = "%" + query.toLowerCase() + "%";
List<SocialPost> posts = find("LOWER(content) LIKE ?1", Sort.by("createdAt", Sort.Direction.Descending), searchPattern)
.page(Page.of(page, size))
.list();
return posts;
}
/**
* Récupère les posts les plus populaires (par nombre de likes).
*

View File

@@ -1,5 +1,7 @@
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;
@@ -9,9 +11,13 @@ 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;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.logging.Logger;
@@ -33,30 +39,37 @@ public class EstablishmentRatingResource {
private static final Logger LOG = Logger.getLogger(EstablishmentRatingResource.class);
/**
* 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);
}
/**
* Soumet une nouvelle note pour un établissement.
* Requiert une authentification JWT.
*/
@POST
@Transactional
@RequiresAuth
@Operation(summary = "Soumettre une note pour un établissement",
description = "Soumet une nouvelle note (1 à 5 étoiles) pour un établissement")
description = "Soumet une nouvelle note (1 à 5 étoiles) pour un établissement. Requiert une authentification JWT.")
@SecurityRequirement(name = "bearerAuth")
@APIResponse(responseCode = "201", description = "Note soumise avec succès")
@APIResponse(responseCode = "401", description = "Non authentifié")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response submitRating(
@Context ContainerRequestContext requestContext,
@PathParam("establishmentId") String establishmentId,
@QueryParam("userId") String userIdStr,
@Valid EstablishmentRatingRequestDTO requestDTO) {
if (userIdStr == null || userIdStr.isBlank()) {
LOG.warn("Soumission de note sans userId pour l'établissement " + establishmentId);
return Response.status(Response.Status.BAD_REQUEST)
.entity("Le paramètre userId est requis")
.build();
}
LOG.info("Soumission d'une note pour l'établissement " + establishmentId + " par l'utilisateur " + userIdStr);
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
LOG.info("Soumission d'une note pour l'établissement " + establishmentId + " par l'utilisateur " + authenticatedUserId);
try {
UUID id = UUID.fromString(establishmentId);
UUID userId = UUID.fromString(userIdStr);
EstablishmentRating rating = ratingService.submitRating(id, userId, requestDTO);
EstablishmentRating rating = ratingService.submitRating(id, authenticatedUserId, requestDTO);
EstablishmentRatingResponseDTO responseDTO = new EstablishmentRatingResponseDTO(rating);
return Response.status(Response.Status.CREATED).entity(responseDTO).build();
} catch (IllegalArgumentException e) {
@@ -79,28 +92,28 @@ public class EstablishmentRatingResource {
/**
* Met à jour une note existante.
* Requiert une authentification JWT.
*/
@PUT
@Transactional
@RequiresAuth
@Operation(summary = "Modifier une note existante",
description = "Met à jour une note existante pour un établissement")
description = "Met à jour une note existante pour un établissement. Requiert une authentification JWT.")
@SecurityRequirement(name = "bearerAuth")
@APIResponse(responseCode = "200", description = "Note mise à jour")
@APIResponse(responseCode = "401", description = "Non authentifié")
@APIResponse(responseCode = "404", description = "Note non trouvée")
public Response updateRating(
@Context ContainerRequestContext requestContext,
@PathParam("establishmentId") String establishmentId,
@QueryParam("userId") String userIdStr,
@Valid EstablishmentRatingRequestDTO requestDTO) {
if (userIdStr == null || userIdStr.isBlank()) {
LOG.warn("Mise à jour de note sans userId pour l'établissement " + establishmentId);
return Response.status(Response.Status.BAD_REQUEST)
.entity("Le paramètre userId est requis")
.build();
}
LOG.info("Mise à jour de la note pour l'établissement " + establishmentId + " par l'utilisateur " + userIdStr);
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
LOG.info("Mise à jour de la note pour l'établissement " + establishmentId + " par l'utilisateur " + authenticatedUserId);
try {
UUID id = UUID.fromString(establishmentId);
UUID userId = UUID.fromString(userIdStr);
EstablishmentRating rating = ratingService.updateRating(id, userId, requestDTO);
EstablishmentRating rating = ratingService.updateRating(id, authenticatedUserId, requestDTO);
EstablishmentRatingResponseDTO responseDTO = new EstablishmentRatingResponseDTO(rating);
return Response.ok(responseDTO).build();
} catch (IllegalArgumentException e) {

View File

@@ -1,6 +1,8 @@
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;
@@ -22,6 +24,8 @@ import com.lions.dev.service.FriendshipService;
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;
@@ -32,6 +36,8 @@ import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.logging.Logger;
@@ -64,14 +70,33 @@ public class EventsResource {
private static final Logger LOG = Logger.getLogger(EventsResource.class);
/**
* 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);
}
// *********** Création d'un événement ***********
@POST
@Transactional
@Operation(summary = "Créer un nouvel événement", description = "Crée un nouvel événement et retourne ses détails")
public Response createEvent(EventCreateRequestDTO eventCreateRequestDTO) {
@RequiresAuth
@Operation(summary = "Créer un nouvel événement", description = "Crée un nouvel événement. Requiert une authentification JWT.")
@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) {
LOG.info("[LOG] Tentative de création d'un nouvel événement : " + eventCreateRequestDTO.getTitle());
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
// Valider que creatorId est fourni
if (eventCreateRequestDTO.getCreatorId() == null) {
LOG.error("[ERROR] creatorId est obligatoire pour créer un événement");
@@ -80,6 +105,14 @@ public class EventsResource {
.build();
}
// Vérifier que l'utilisateur authentifié correspond au creatorId
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.\"}")
.build();
}
// Récupérer le créateur par son ID
Users creator = usersRepository.findById(eventCreateRequestDTO.getCreatorId());
if (creator == null) {
@@ -118,17 +151,21 @@ public class EventsResource {
@DELETE
@Path("/{id}")
@Transactional
@Operation(summary = "Supprimer un événement", description = "Supprime un événement de la base de données")
public Response deleteEvent(@PathParam("id") UUID id, @QueryParam("userId") UUID userId) {
LOG.info("Tentative de suppression de l'événement avec l'ID : " + id + " par l'utilisateur : " + userId);
if (userId == null) {
LOG.error("[ERROR] userId est obligatoire pour supprimer un événement");
return Response.status(Response.Status.BAD_REQUEST).entity("L'identifiant de l'utilisateur (userId) est obligatoire").build();
}
@RequiresAuth
@Operation(summary = "Supprimer un événement", description = "Supprime un événement. Seul le créateur 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);
LOG.info("Tentative de suppression de l'événement avec l'ID : " + id + " par l'utilisateur : " + authenticatedUserId);
try {
boolean deleted = eventService.deleteEvent(id, userId);
boolean deleted = eventService.deleteEvent(id, authenticatedUserId);
if (deleted) {
LOG.info("Événement supprimé avec succès.");
return Response.noContent().build();
@@ -193,14 +230,19 @@ public class EventsResource {
@PUT
@Path("/{id}")
@Transactional
@Operation(summary = "Mettre à jour un événement", description = "Modifie un événement existant")
public Response updateEvent(@PathParam("id") UUID id, @QueryParam("userId") UUID userId, EventUpdateRequestDTO eventUpdateRequestDTO) {
LOG.info("[LOG] Tentative de mise à jour de l'événement avec l'ID : " + id + " par l'utilisateur : " + userId);
if (userId == null) {
LOG.error("[ERROR] userId est obligatoire pour mettre à jour un événement");
return Response.status(Response.Status.BAD_REQUEST).entity("L'identifiant de l'utilisateur (userId) est obligatoire").build();
}
@RequiresAuth
@Operation(summary = "Mettre à jour un événement", description = "Modifie un événement. Seul le créateur 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);
LOG.info("[LOG] Tentative de mise à jour de l'événement avec l'ID : " + id + " par l'utilisateur : " + authenticatedUserId);
Events event = eventsRepository.findById(id);
if (event == null) {
@@ -209,8 +251,8 @@ public class EventsResource {
}
// Vérifier que l'utilisateur est le créateur
if (!eventService.canModifyEvent(event, userId)) {
LOG.error("[ERROR] L'utilisateur " + userId + " n'a pas les permissions pour modifier l'événement " + id);
if (!eventService.canModifyEvent(event, authenticatedUserId)) {
LOG.error("[ERROR] L'utilisateur " + authenticatedUserId + " n'a pas les permissions pour modifier l'événement " + id);
return Response.status(Response.Status.FORBIDDEN).entity("Vous n'avez pas les permissions pour modifier cet événement").build();
}
@@ -274,19 +316,24 @@ public class EventsResource {
// *********** Récupérer les événements par catégorie ***********
/**
* Endpoint pour récupérer les événements par catégorie.
* Endpoint pour récupérer les événements par catégorie avec pagination.
*
* @param category La catégorie d'événement à filtrer.
* @return Une réponse HTTP contenant la liste des événements dans cette catégorie.
* @param page Le numéro de la page (0-indexé)
* @param size La taille de la page
* @return Une réponse HTTP contenant la liste paginée des événements dans cette catégorie.
*/
@GET
@Path("/category/{category}")
@Operation(
summary = "Récupérer les événements par catégorie",
description = "Retourne la liste des événements correspondant à une catégorie donnée")
public Response getEventsByCategory(@PathParam("category") String category) {
LOG.info("[LOG] Récupération des événements dans la catégorie : " + category);
List<Events> events = eventService.findEventsByCategory(category);
description = "Retourne la liste paginée des événements correspondant à une catégorie donnée")
public Response getEventsByCategory(
@PathParam("category") String category,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("10") int size) {
LOG.info("[LOG] Récupération des événements dans la catégorie : " + category + " (page: " + page + ", size: " + size + ")");
List<Events> events = eventService.findEventsByCategory(category, page, size);
if (events.isEmpty()) {
LOG.warn("[LOG] Aucun événement trouvé pour la catégorie : " + category);
return Response.status(Response.Status.NOT_FOUND)
@@ -371,19 +418,24 @@ public class EventsResource {
// *********** Rechercher des événements par mots-clés ***********
/**
* Endpoint pour rechercher des événements par mots-clés.
* Endpoint pour rechercher des événements par mots-clés avec pagination.
*
* @param keyword Le mot-clé à rechercher.
* @return Une réponse HTTP contenant la liste des événements correspondant au mot-clé.
* @param page Le numéro de la page (0-indexé)
* @param size La taille de la page
* @return Une réponse HTTP contenant la liste paginée des événements correspondant au mot-clé.
*/
@GET
@Path("/search")
@Operation(
summary = "Rechercher des événements par mots-clés",
description = "Retourne la liste des événements dont le titre ou la description contient les mots-clés spécifiés")
public Response searchEvents(@QueryParam("keyword") String keyword) {
LOG.info("[LOG] Recherche d'événements avec le mot-clé : " + keyword);
List<Events> events = eventService.searchEvents(keyword);
description = "Retourne la liste paginée des événements dont le titre ou la description contient les mots-clés spécifiés")
public Response searchEvents(
@QueryParam("keyword") String keyword,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("10") int size) {
LOG.info("[LOG] Recherche d'événements avec le mot-clé : " + keyword + " (page: " + page + ", size: " + size + ")");
List<Events> events = eventService.searchEvents(keyword, page, size);
if (events.isEmpty()) {
LOG.warn("[LOG] Aucun événement trouvé avec le mot-clé : " + keyword);
return Response.status(Response.Status.NOT_FOUND)
@@ -401,7 +453,9 @@ public class EventsResource {
/**
* Endpoint pour mettre à jour le statut d'un événement.
* Requiert une authentification JWT.
*
* @param requestContext Le contexte de la requête (injecté)
* @param id L'ID de l'événement.
* @param status Le nouveau statut de l'événement.
* @return Une réponse HTTP indiquant la mise à jour du statut.
@@ -409,16 +463,20 @@ public class EventsResource {
@PUT
@Path("/{id}/status")
@Transactional
@RequiresAuth
@Operation(
summary = "Mettre à jour le statut d'un événement",
description = "Modifie le statut d'un événement (ouvert, fermé, annulé, etc.)")
public Response updateEventStatus(@PathParam("id") UUID id, @QueryParam("status") String status, @QueryParam("userId") UUID userId) {
LOG.info("[LOG] Mise à jour du statut de l'événement avec l'ID : " + id + " par l'utilisateur : " + userId);
if (userId == null) {
LOG.error("[ERROR] userId est obligatoire pour mettre à jour le statut d'un événement");
return Response.status(Response.Status.BAD_REQUEST).entity("L'identifiant de l'utilisateur (userId) est obligatoire").build();
}
description = "Modifie le statut d'un événement. Seul le créateur 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);
LOG.info("[LOG] Mise à jour du statut de l'événement avec l'ID : " + id + " par l'utilisateur : " + authenticatedUserId);
Events event = eventsRepository.findById(id);
if (event == null) {
@@ -427,8 +485,8 @@ public class EventsResource {
}
// Vérifier que l'utilisateur est le créateur
if (!eventService.canModifyEvent(event, userId)) {
LOG.error("[ERROR] L'utilisateur " + userId + " n'a pas les permissions pour modifier le statut de l'événement " + id);
if (!eventService.canModifyEvent(event, authenticatedUserId)) {
LOG.error("[ERROR] L'utilisateur " + authenticatedUserId + " n'a pas les permissions pour modifier le statut de l'événement " + id);
return Response.status(Response.Status.FORBIDDEN).entity("Vous n'avez pas les permissions pour modifier cet événement").build();
}
@@ -478,20 +536,27 @@ public class EventsResource {
/**
* Endpoint pour mettre à jour l'image d'un événement.
* Requiert une authentification JWT.
*
* @param requestContext Le contexte de la requête (injecté)
* @param id L'identifiant de l'événement.
* @param imageFilePath Le chemin vers l'image de l'événement.
* @return Un message indiquant si la mise à jour a réussi ou non.
*/
@PUT
@Path("/{id}/image")
public Response updateEventImage(@PathParam("id") UUID id, @QueryParam("userId") UUID userId, String imageFilePath) {
LOG.info("[LOG] Tentative de mise à jour de l'image pour l'événement avec l'ID : " + id + " par l'utilisateur : " + userId);
if (userId == null) {
LOG.error("[ERROR] userId est obligatoire pour mettre à jour l'image d'un événement");
return Response.status(Response.Status.BAD_REQUEST).entity("L'identifiant de l'utilisateur (userId) est obligatoire").build();
}
@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.")
@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);
LOG.info("[LOG] Tentative de mise à jour de l'image pour l'événement avec l'ID : " + id + " par l'utilisateur : " + authenticatedUserId);
try {
if (imageFilePath == null || imageFilePath.isEmpty()) {
@@ -512,14 +577,14 @@ public class EventsResource {
}
// Vérifier que l'utilisateur est le créateur
if (!eventService.canModifyEvent(event, userId)) {
LOG.error("[ERROR] L'utilisateur " + userId + " n'a pas les permissions pour modifier l'image de l'événement " + id);
if (!eventService.canModifyEvent(event, authenticatedUserId)) {
LOG.error("[ERROR] L'utilisateur " + authenticatedUserId + " n'a pas les permissions pour modifier l'image de l'événement " + id);
return Response.status(Response.Status.FORBIDDEN).entity("Vous n'avez pas les permissions pour modifier cet événement").build();
}
String imageUrl = file.getAbsolutePath();
event.setImageUrl(imageUrl);
eventService.updateEvent(event, userId);
eventService.updateEvent(event, authenticatedUserId);
LOG.info("[LOG] Image de l'événement mise à jour avec succès pour : " + event.getTitle());
return Response.ok("Image de l'événement mise à jour avec succès.").build();
@@ -533,14 +598,19 @@ public class EventsResource {
@PATCH
@Path("/{id}/partial-update")
@Transactional
@Operation(summary = "Mettre à jour partiellement un événement", description = "Mise à jour partielle des informations d'un événement")
public Response partialUpdateEvent(@PathParam("id") UUID id, @QueryParam("userId") UUID userId, Map<String, Object> updates) {
LOG.info("[LOG] Tentative de mise à jour partielle de l'événement avec l'ID : " + id + " par l'utilisateur : " + userId);
if (userId == null) {
LOG.error("[ERROR] userId est obligatoire pour mettre à jour un événement");
return Response.status(Response.Status.BAD_REQUEST).entity("L'identifiant de l'utilisateur (userId) est obligatoire").build();
}
@RequiresAuth
@Operation(summary = "Mettre à jour partiellement un événement", description = "Mise à jour partielle. Seul le créateur 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);
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);
if (event == null) {
@@ -549,36 +619,38 @@ public class EventsResource {
}
// Vérifier que l'utilisateur est le créateur
if (!eventService.canModifyEvent(event, userId)) {
LOG.error("[ERROR] L'utilisateur " + userId + " n'a pas les permissions pour modifier l'événement " + id);
if (!eventService.canModifyEvent(event, authenticatedUserId)) {
LOG.error("[ERROR] L'utilisateur " + authenticatedUserId + " n'a pas les permissions pour modifier l'événement " + id);
return Response.status(Response.Status.FORBIDDEN).entity("Vous n'avez pas les permissions pour modifier cet événement").build();
}
// Mise à jour des champs dynamiquement
updates.forEach((field, value) -> {
switch (field) {
case "title":
event.setTitle(value != null ? value.toString() : null);
break;
case "description":
event.setDescription(value != null ? value.toString() : null);
break;
case "category":
event.setCategory(value != null ? value.toString() : null);
break;
case "link":
event.setLink(value != null ? value.toString() : null);
break;
case "imageUrl":
event.setImageUrl(value != null ? value.toString() : null);
break;
case "status":
event.setStatus(value != null ? value.toString() : null);
break;
default:
LOG.warn("[LOG] Champ inconnu ignoré lors de la mise à jour partielle : " + field);
}
});
if (updates != null) {
updates.forEach((field, value) -> {
switch (field) {
case "title":
event.setTitle(value != null ? value.toString() : null);
break;
case "description":
event.setDescription(value != null ? value.toString() : null);
break;
case "category":
event.setCategory(value != null ? value.toString() : null);
break;
case "link":
event.setLink(value != null ? value.toString() : null);
break;
case "imageUrl":
event.setImageUrl(value != null ? value.toString() : null);
break;
case "status":
event.setStatus(value != null ? value.toString() : null);
break;
default:
LOG.warn("[LOG] Champ inconnu ignoré lors de la mise à jour partielle : " + field);
}
});
}
eventsRepository.persist(event);
LOG.info("[LOG] Événement mis à jour partiellement avec succès : " + event.getTitle());
@@ -588,11 +660,13 @@ public class EventsResource {
// *********** Récupérer les événements à venir ***********
@GET
@Path("/upcoming")
@Operation(summary = "Récupérer les événements à venir", description = "Retourne les événements futurs.")
public Response getUpcomingEvents() {
LOG.info("[LOG] Récupération des événements à venir.");
@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,
@QueryParam("size") @DefaultValue("10") int size) {
LOG.info("[LOG] Récupération des événements à venir (page: " + page + ", size: " + size + ")");
List<Events> events = eventService.findUpcomingEvents();
List<Events> events = eventService.findUpcomingEvents(page, size);
if (events.isEmpty()) {
LOG.warn("[LOG] Aucun événement futur trouvé.");
return Response.status(Response.Status.NOT_FOUND).entity("Aucun événement futur trouvé.").build();
@@ -605,11 +679,13 @@ public class EventsResource {
// *********** Récupérer les événements passés ***********
@GET
@Path("/past")
@Operation(summary = "Récupérer les événements passés", description = "Retourne les événements déjà terminés.")
public Response getPastEvents() {
LOG.info("[LOG] Récupération des événements passés.");
@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,
@QueryParam("size") @DefaultValue("10") int size) {
LOG.info("[LOG] Récupération des événements passés (page: " + page + ", size: " + size + ")");
List<Events> events = eventService.findPastEvents();
List<Events> events = eventService.findPastEvents(page, size);
if (events.isEmpty()) {
LOG.warn("[LOG] Aucun événement passé trouvé.");
return Response.status(Response.Status.NOT_FOUND).entity("Aucun événement passé trouvé.").build();
@@ -623,14 +699,18 @@ public class EventsResource {
@POST
@Path("/{id}/cancel")
@Transactional
@Operation(summary = "Annuler un événement", description = "Annule un événement sans le supprimer.")
public Response cancelEvent(@PathParam("id") UUID id, @QueryParam("userId") UUID userId) {
LOG.info("[LOG] Annulation de l'événement avec l'ID : " + id + " par l'utilisateur : " + userId);
if (userId == null) {
LOG.error("[ERROR] userId est obligatoire pour annuler un événement");
return Response.status(Response.Status.BAD_REQUEST).entity("L'identifiant de l'utilisateur (userId) est obligatoire").build();
}
@RequiresAuth
@Operation(summary = "Annuler un événement", description = "Annule un événement sans le supprimer. Seul le créateur 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);
LOG.info("[LOG] Annulation de l'événement avec l'ID : " + id + " par l'utilisateur : " + authenticatedUserId);
Events event = eventsRepository.findById(id);
if (event == null) {
@@ -639,8 +719,8 @@ public class EventsResource {
}
// Vérifier que l'utilisateur est le créateur
if (!eventService.canModifyEvent(event, userId)) {
LOG.error("[ERROR] L'utilisateur " + userId + " n'a pas les permissions pour annuler l'événement " + id);
if (!eventService.canModifyEvent(event, authenticatedUserId)) {
LOG.error("[ERROR] L'utilisateur " + authenticatedUserId + " n'a pas les permissions pour annuler l'événement " + id);
return Response.status(Response.Status.FORBIDDEN).entity("Vous n'avez pas les permissions pour annuler cet événement").build();
}
@@ -770,18 +850,30 @@ public class EventsResource {
@POST
@Path("/{id}/favorite")
@Transactional
@Operation(summary = "Toggle favori d'un événement", description = "Permet à un utilisateur d'ajouter ou retirer un événement de ses favoris (toggle).")
public Response favoriteEvent(@PathParam("id") UUID eventId, @QueryParam("userId") UUID userId) {
LOG.info("[LOG] Toggle favori de l'événement " + eventId + " pour l'utilisateur ID : " + userId);
@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);
LOG.info("[LOG] Toggle favori de l'événement " + eventId + " pour l'utilisateur ID : " + authenticatedUserId);
Events event = eventsRepository.findById(eventId);
Users user = usersRepository.findById(userId);
if (event == null || user == null) {
LOG.warn("[LOG] Événement ou utilisateur non trouvé.");
return Response.status(Response.Status.NOT_FOUND).entity("Événement ou utilisateur non trouvé.").build();
Users user = usersRepository.findById(authenticatedUserId);
if (event == null) {
LOG.warn("[LOG] Événement non trouvé.");
return Response.status(Response.Status.NOT_FOUND).entity("Événement non trouvé.").build();
}
if (user == null) {
LOG.warn("[LOG] Utilisateur non trouvé.");
return Response.status(Response.Status.NOT_FOUND).entity("Utilisateur non trouvé.").build();
}
// Toggle : ajouter si pas favori, retirer si déjà favori
// Toggle : ajouter si pas favori, retirer si déjà favori
boolean wasFavorite = user.hasFavoriteEvent(event);
if (wasFavorite) {
user.removeFavoriteEvent(event);
@@ -848,12 +940,18 @@ public class EventsResource {
@POST
@Path("/{id}/comments")
@Transactional
@Operation(summary = "Ajouter un commentaire à un événement", description = "Crée un nouveau commentaire pour un événement.")
@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,
@QueryParam("userId") UUID userId,
Map<String, String> requestBody) {
LOG.info("[LOG] Ajout d'un commentaire à l'événement ID : " + eventId + " par l'utilisateur ID : " + userId);
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
LOG.info("[LOG] Ajout d'un commentaire à l'événement ID : " + eventId + " par l'utilisateur ID : " + authenticatedUserId);
Events event = eventsRepository.findById(eventId);
if (event == null) {
@@ -861,13 +959,13 @@ public class EventsResource {
return Response.status(Response.Status.NOT_FOUND).entity("Événement non trouvé.").build();
}
Users user = usersRepository.findById(userId);
Users user = usersRepository.findById(authenticatedUserId);
if (user == null) {
LOG.warn("[LOG] Utilisateur non trouvé avec l'ID : " + userId);
LOG.warn("[LOG] Utilisateur non trouvé avec l'ID : " + authenticatedUserId);
return Response.status(Response.Status.NOT_FOUND).entity("Utilisateur non trouvé.").build();
}
String text = requestBody.get("text");
String text = requestBody != null ? requestBody.get("text") : null;
if (text == null || text.trim().isEmpty()) {
LOG.warn("[LOG] Le texte du commentaire est vide");
return Response.status(Response.Status.BAD_REQUEST).entity("Le texte du commentaire est requis.").build();
@@ -926,9 +1024,17 @@ public class EventsResource {
@POST
@Path("/{id}/share")
@Transactional
@Operation(summary = "Enregistrer un partage d'événement", description = "Enregistre qu'un utilisateur a partagé l'événement (incrémente le compteur).")
public Response shareEvent(@PathParam("id") UUID eventId, @QueryParam("userId") UUID userId) {
LOG.info("[LOG] Partage de l'événement ID : " + eventId + " par l'utilisateur ID : " + userId);
@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);
LOG.info("[LOG] Partage de l'événement ID : " + eventId + " par l'utilisateur ID : " + authenticatedUserId);
Events event = eventsRepository.findById(eventId);
if (event == null) {
@@ -936,9 +1042,9 @@ public class EventsResource {
return Response.status(Response.Status.NOT_FOUND).entity("Événement non trouvé.").build();
}
Users user = usersRepository.findById(userId);
Users user = usersRepository.findById(authenticatedUserId);
if (user == null) {
LOG.warn("[LOG] Utilisateur non trouvé avec l'ID : " + userId);
LOG.warn("[LOG] Utilisateur non trouvé avec l'ID : " + authenticatedUserId);
return Response.status(Response.Status.NOT_FOUND).entity("Utilisateur non trouvé.").build();
}
@@ -956,19 +1062,31 @@ public class EventsResource {
/**
* Endpoint pour fermer un événement.
* Requiert une authentification JWT.
* Seul le créateur peut fermer l'événement.
*
* @param requestContext Le contexte de la requête (injecté)
* @param eventId L'ID de l'événement.
* @return Une réponse HTTP indiquant le succès de la fermeture.
*/
@PATCH
@Path("/{id}/close")
@Transactional
@RequiresAuth
@Operation(
summary = "Fermer un événement",
description = "Ferme un événement et empêche les nouvelles participations"
description = "Ferme un événement et empêche les nouvelles participations. Seul le créateur peut fermer."
)
public Response closeEvent(@PathParam("id") UUID eventId) {
LOG.info("Tentative de fermeture de l'événement avec l'ID : " + eventId);
@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);
LOG.info("Tentative de fermeture de l'événement avec l'ID : " + eventId + " par l'utilisateur : " + authenticatedUserId);
// Recherche de l'événement par ID
Events event = eventsRepository.findById(eventId);
@@ -979,6 +1097,12 @@ public class EventsResource {
.build();
}
// Vérifier que l'utilisateur est le créateur
if (!eventService.canModifyEvent(event, authenticatedUserId)) {
LOG.error("[ERROR] L'utilisateur " + authenticatedUserId + " n'a pas les permissions pour fermer l'événement " + eventId);
return Response.status(Response.Status.FORBIDDEN).entity("Vous n'avez pas les permissions pour fermer cet événement").build();
}
// Marquer l'événement comme fermé
event.setStatus("fermé"); // Modification du statut de l'événement
eventsRepository.persist(event); // Persister les modifications dans la base
@@ -990,19 +1114,31 @@ public class EventsResource {
/**
* Endpoint pour réouvrir un événement.
* Requiert une authentification JWT.
* Seul le créateur peut réouvrir l'événement.
*
* @param requestContext Le contexte de la requête (injecté)
* @param eventId L'ID de l'événement à rouvrir.
* @return Une réponse HTTP indiquant le succès ou l'échec de la réouverture.
*/
@PATCH
@Path("{eventId}/reopen")
@Transactional
@RequiresAuth
@Operation(
summary = "Rouvrir un événement",
description = "Rouvre un événement existant qui est actuellement fermé"
description = "Rouvre un événement fermé. Seul le créateur peut réouvrir."
)
public Response reopenEvent(@PathParam("eventId") UUID eventId) {
LOG.info("Tentative de réouverture de l'événement avec l'ID : " + eventId);
@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);
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
Events event = eventsRepository.findById(eventId);
@@ -1013,6 +1149,12 @@ public class EventsResource {
.build();
}
// Vérifier que l'utilisateur est le créateur
if (!eventService.canModifyEvent(event, authenticatedUserId)) {
LOG.error("[ERROR] L'utilisateur " + authenticatedUserId + " n'a pas les permissions pour réouvrir l'événement " + eventId);
return Response.status(Response.Status.FORBIDDEN).entity("Vous n'avez pas les permissions pour réouvrir cet événement").build();
}
// Vérifier si l'événement est déjà ouvert
if ("ouvert".equals(event.getStatus())) {
LOG.warn("L'événement est déjà ouvert : " + eventId);

View File

@@ -6,9 +6,10 @@ import com.lions.dev.dto.response.chat.MessageResponseDTO;
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.repository.UsersRepository;
import com.lions.dev.service.MessageService;
import com.lions.dev.service.UsersService;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
@@ -29,6 +30,9 @@ import java.util.stream.Collectors;
* - Récupérer les messages d'une conversation
* - Marquer les messages comme lus
* - Supprimer des messages et conversations
*
* 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.
*/
@Path("/messages")
@Produces(MediaType.APPLICATION_JSON)
@@ -42,90 +46,40 @@ public class MessageResource {
MessageService messageService;
@Inject
UsersRepository usersRepository;
UsersService usersService;
/**
* Envoie un nouveau message.
*
* @param request Le DTO contenant les informations du message
* @return Le message créé
*/
@POST
@Operation(summary = "Envoyer un message", description = "Envoie un nouveau message à un utilisateur")
public Response sendMessage(SendMessageRequestDTO request) {
public Response sendMessage(@Valid SendMessageRequestDTO request) {
LOG.info("[LOG] Réception d'une demande d'envoi de message");
try {
// Validation
if (!request.isValid()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"message\": \"Données invalides\"}")
.build();
}
Message message = messageService.sendMessage(
request.getSenderId(),
request.getRecipientId(),
request.getContent(),
request.getMessageType(),
request.getMediaUrl()
);
// Envoyer le message
Message message = messageService.sendMessage(
request.getSenderId(),
request.getRecipientId(),
request.getContent(),
request.getMessageType(),
request.getMediaUrl()
);
MessageResponseDTO response = new MessageResponseDTO(message);
return Response.status(Response.Status.CREATED).entity(response).build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de l'envoi du message : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de l'envoi du message\"}")
.build();
}
MessageResponseDTO response = new MessageResponseDTO(message);
return Response.status(Response.Status.CREATED).entity(response).build();
}
/**
* Récupère toutes les conversations d'un utilisateur.
*
* @param userId L'ID de l'utilisateur
* @return Liste des conversations
*/
@GET
@Path("/conversations/{userId}")
@Operation(summary = "Récupérer les conversations", description = "Récupère toutes les conversations d'un utilisateur")
public Response getUserConversations(@PathParam("userId") UUID userId) {
LOG.info("[LOG] Récupération des conversations pour l'utilisateur ID : " + userId);
try {
Users user = usersRepository.findById(userId);
if (user == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"Utilisateur non trouvé\"}")
.build();
}
Users user = usersService.getUserById(userId);
List<Conversation> conversations = messageService.getUserConversations(userId);
List<ConversationResponseDTO> response = conversations.stream()
.map(conv -> new ConversationResponseDTO(conv, user))
.collect(Collectors.toList());
List<Conversation> conversations = messageService.getUserConversations(userId);
List<ConversationResponseDTO> response = conversations.stream()
.map(conv -> new ConversationResponseDTO(conv, user))
.collect(Collectors.toList());
return Response.ok(response).build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la récupération des conversations : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la récupération des conversations\"}")
.build();
}
return Response.ok(response).build();
}
/**
* Récupère les messages d'une conversation.
*
* @param conversationId L'ID de la conversation
* @param page Le numéro de la page (défaut: 0)
* @param size La taille de la page (défaut: 50)
* @return Liste des messages
*/
@GET
@Path("/conversation/{conversationId}")
@Operation(summary = "Récupérer les messages", description = "Récupère les messages d'une conversation avec pagination")
@@ -135,29 +89,14 @@ public class MessageResource {
@QueryParam("size") @DefaultValue("50") int size) {
LOG.info("[LOG] Récupération des messages pour la conversation ID : " + conversationId);
try {
List<Message> messages = messageService.getConversationMessages(conversationId, page, size);
List<MessageResponseDTO> response = messages.stream()
.map(MessageResponseDTO::new)
.collect(Collectors.toList());
List<Message> messages = messageService.getConversationMessages(conversationId, page, size);
List<MessageResponseDTO> response = messages.stream()
.map(MessageResponseDTO::new)
.collect(Collectors.toList());
return Response.ok(response).build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la récupération des messages : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la récupération des messages\"}")
.build();
}
return Response.ok(response).build();
}
/**
* Récupère une conversation entre deux utilisateurs.
*
* @param user1Id L'ID du premier utilisateur
* @param user2Id L'ID du deuxième utilisateur
* @return La conversation
*/
@GET
@Path("/conversation/between/{user1Id}/{user2Id}")
@Operation(summary = "Récupérer une conversation", description = "Récupère la conversation entre deux utilisateurs")
@@ -166,65 +105,24 @@ public class MessageResource {
@PathParam("user2Id") UUID user2Id) {
LOG.info("[LOG] Recherche de conversation entre " + user1Id + " et " + user2Id);
try {
Users user1 = usersRepository.findById(user1Id);
if (user1 == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"Utilisateur non trouvé\"}")
.build();
}
Users user1 = usersService.getUserById(user1Id);
Conversation conversation = messageService.getConversationBetweenUsers(user1Id, user2Id);
ConversationResponseDTO response = new ConversationResponseDTO(conversation, user1);
Conversation conversation = messageService.getConversationBetweenUsers(user1Id, user2Id);
if (conversation == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"Conversation non trouvée\"}")
.build();
}
ConversationResponseDTO response = new ConversationResponseDTO(conversation, user1);
return Response.ok(response).build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la récupération de la conversation : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la récupération de la conversation\"}")
.build();
}
return Response.ok(response).build();
}
/**
* Marque un message comme lu.
*
* @param messageId L'ID du message
* @return Le message mis à jour
*/
@PUT
@Path("/{messageId}/read")
@Operation(summary = "Marquer comme lu", description = "Marque un message comme lu")
public Response markMessageAsRead(@PathParam("messageId") UUID messageId) {
LOG.info("[LOG] Marquage du message comme lu : " + messageId);
try {
Message message = messageService.markMessageAsRead(messageId);
MessageResponseDTO response = new MessageResponseDTO(message);
return Response.ok(response).build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors du marquage du message : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors du marquage du message\"}")
.build();
}
Message message = messageService.markMessageAsRead(messageId);
MessageResponseDTO response = new MessageResponseDTO(message);
return Response.ok(response).build();
}
/**
* Marque tous les messages d'une conversation comme lus.
*
* @param conversationId L'ID de la conversation
* @param userId L'ID de l'utilisateur
* @return Le nombre de messages marqués comme lus
*/
@PUT
@Path("/conversation/{conversationId}/read/{userId}")
@Operation(summary = "Marquer tout comme lu", description = "Marque tous les messages d'une conversation comme lus")
@@ -233,100 +131,50 @@ public class MessageResource {
@PathParam("userId") UUID userId) {
LOG.info("[LOG] Marquage de tous les messages comme lus pour la conversation " + conversationId);
try {
int count = messageService.markAllMessagesAsRead(conversationId, userId);
return Response.ok("{\"messagesMarkedAsRead\": " + count + "}").build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors du marquage des messages : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors du marquage des messages\"}")
.build();
}
int count = messageService.markAllMessagesAsRead(conversationId, userId);
return Response.ok("{\"messagesMarkedAsRead\": " + count + "}").build();
}
/**
* Récupère le nombre total de messages non lus pour un utilisateur.
*
* @param userId L'ID de l'utilisateur
* @return Le nombre de messages non lus
*/
@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);
try {
long count = messageService.getTotalUnreadCount(userId);
return Response.ok("{\"unreadCount\": " + count + "}").build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors du comptage des messages non lus : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors du comptage\"}")
.build();
}
long count = messageService.getTotalUnreadCount(userId);
return Response.ok("{\"unreadCount\": " + count + "}").build();
}
/**
* Supprime un message.
*
* @param messageId L'ID du message
* @return Confirmation de suppression
*/
@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);
try {
boolean deleted = messageService.deleteMessage(messageId);
boolean deleted = messageService.deleteMessage(messageId);
if (deleted) {
return Response.ok("{\"message\": \"Message supprimé avec succès\"}").build();
} else {
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"Message non trouvé\"}")
.build();
}
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la suppression du message : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la suppression\"}")
if (deleted) {
return Response.ok("{\"message\": \"Message supprimé avec succès\"}").build();
} else {
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"Message non trouvé\"}")
.build();
}
}
/**
* Supprime une conversation.
*
* @param conversationId L'ID de la conversation
* @return Confirmation de suppression
*/
@DELETE
@Path("/conversation/{conversationId}")
@Operation(summary = "Supprimer une conversation", description = "Supprime une conversation et tous ses messages")
public Response deleteConversation(@PathParam("conversationId") UUID conversationId) {
LOG.info("[LOG] Suppression de la conversation ID : " + conversationId);
try {
boolean deleted = messageService.deleteConversation(conversationId);
boolean deleted = messageService.deleteConversation(conversationId);
if (deleted) {
return Response.ok("{\"message\": \"Conversation supprimée avec succès\"}").build();
} else {
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"Conversation non trouvée\"}")
.build();
}
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la suppression de la conversation : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la suppression\"}")
if (deleted) {
return Response.ok("{\"message\": \"Conversation supprimée avec succès\"}").build();
} else {
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"Conversation non trouvée\"}")
.build();
}
}

View File

@@ -0,0 +1,423 @@
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.service.PromotionService;
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;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.logging.Logger;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
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.
*/
@Path("/promotions")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Promotions", description = "Opérations liées à la gestion des promotions")
public class PromotionResource {
@Inject
PromotionService promotionService;
private static final Logger LOG = Logger.getLogger(PromotionResource.class);
/**
* 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);
}
// =====================================================================
// ENDPOINTS PUBLICS (LECTURE)
// =====================================================================
/**
* Récupère toutes les promotions actives et valides.
*
* @param page Le numéro de la page (0-indexé)
* @param size La taille de la page
* @return Liste paginée des promotions actives
*/
@GET
@Operation(
summary = "Récupérer toutes les promotions actives",
description = "Retourne une liste paginée de toutes les promotions actives et valides")
@APIResponse(responseCode = "200", description = "Liste des promotions")
public Response getAllActivePromotions(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("10") int size) {
LOG.info("[LOG] Récupération de toutes les promotions actives (page: " + page + ", size: " + size + ")");
try {
List<Promotion> promotions = promotionService.getAllActivePromotions(page, size);
List<PromotionResponseDTO> responseDTOs = promotions.stream()
.map(PromotionResponseDTO::new)
.collect(Collectors.toList());
return Response.ok(responseDTOs).build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la récupération des promotions : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la récupération des promotions.\"}")
.build();
}
}
/**
* Récupère une promotion par son ID.
*
* @param promotionId L'ID de la promotion
* @return La promotion trouvée
*/
@GET
@Path("/{id}")
@Operation(
summary = "Récupérer une promotion par ID",
description = "Retourne les détails d'une promotion spécifique")
@APIResponse(responseCode = "200", description = "Promotion trouvée")
@APIResponse(responseCode = "404", description = "Promotion non trouvée")
public Response getPromotionById(@PathParam("id") UUID promotionId) {
LOG.info("[LOG] Récupération de la promotion ID : " + promotionId);
try {
Promotion promotion = promotionService.getPromotionById(promotionId);
PromotionResponseDTO responseDTO = new PromotionResponseDTO(promotion);
return Response.ok(responseDTO).build();
} catch (IllegalArgumentException e) {
LOG.warn("[WARN] Promotion non trouvée : " + e.getMessage());
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"Promotion non trouvée.\"}")
.build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la récupération de la promotion : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la récupération de la promotion.\"}")
.build();
}
}
/**
* Recherche une promotion par code promo.
*
* @param code Le code promo à rechercher
* @return La promotion trouvée
*/
@GET
@Path("/code/{code}")
@Operation(
summary = "Rechercher une promotion par code promo",
description = "Retourne la promotion correspondant au code promo")
@APIResponse(responseCode = "200", description = "Promotion trouvée")
@APIResponse(responseCode = "404", description = "Code promo non trouvé")
public Response getPromotionByCode(@PathParam("code") String code) {
LOG.info("[LOG] Recherche de la promotion avec le code : " + code);
try {
Optional<Promotion> promotionOpt = promotionService.getPromotionByCode(code);
if (promotionOpt.isEmpty()) {
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"Code promo non trouvé.\"}")
.build();
}
PromotionResponseDTO responseDTO = new PromotionResponseDTO(promotionOpt.get());
return Response.ok(responseDTO).build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la recherche du code promo : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la recherche du code promo.\"}")
.build();
}
}
/**
* Récupère les promotions d'un établissement.
*
* @param establishmentId L'ID de l'établissement
* @param activeOnly Si true, retourne uniquement les promotions actives et valides
* @param page Le numéro de la page
* @param size La taille de la page
* @return Liste des promotions
*/
@GET
@Path("/establishment/{establishmentId}")
@Operation(
summary = "Récupérer les promotions d'un établissement",
description = "Retourne les promotions d'un établissement spécifique")
@APIResponse(responseCode = "200", description = "Liste des promotions")
public Response getPromotionsByEstablishment(
@PathParam("establishmentId") UUID establishmentId,
@QueryParam("activeOnly") @DefaultValue("false") boolean activeOnly,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("10") int size) {
LOG.info("[LOG] Récupération des promotions pour l'établissement : " + establishmentId);
try {
List<Promotion> promotions;
if (activeOnly) {
promotions = promotionService.getActivePromotionsByEstablishment(establishmentId);
} else {
promotions = promotionService.getPromotionsByEstablishment(establishmentId, page, size);
}
List<PromotionResponseDTO> responseDTOs = promotions.stream()
.map(PromotionResponseDTO::new)
.collect(Collectors.toList());
return Response.ok(responseDTOs).build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la récupération des promotions : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la récupération des promotions.\"}")
.build();
}
}
// =====================================================================
// ENDPOINTS PROTÉGÉS (CRÉATION, MODIFICATION, SUPPRESSION)
// =====================================================================
/**
* Crée une nouvelle promotion.
* Requiert une authentification JWT.
* Seul le responsable de l'établissement peut créer des promotions.
*
* @param requestContext Le contexte de la requête (injecté)
* @param requestDTO Le DTO de création
* @return La promotion créée
*/
@POST
@Transactional
@RequiresAuth
@Operation(
summary = "Créer une promotion",
description = "Crée une nouvelle promotion pour un établissement. Seul le responsable peut créer.")
@SecurityRequirement(name = "bearerAuth")
@APIResponse(responseCode = "201", description = "Promotion créée avec succès")
@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);
LOG.info("[LOG] Création d'une promotion pour l'établissement : " + requestDTO.getEstablishmentId() +
" par l'utilisateur : " + authenticatedUserId);
try {
Promotion promotion = promotionService.createPromotion(requestDTO);
// Vérifier que l'utilisateur est bien le responsable
if (!promotionService.canModifyPromotion(promotion, authenticatedUserId)) {
promotionService.deletePromotion(promotion.getId()); // Rollback
LOG.warn("[WARN] Utilisateur " + authenticatedUserId + " non autorisé à créer des promotions pour cet établissement");
return Response.status(Response.Status.FORBIDDEN)
.entity("{\"message\": \"Vous n'êtes pas autorisé à créer des promotions pour cet établissement.\"}")
.build();
}
PromotionResponseDTO responseDTO = new PromotionResponseDTO(promotion);
return Response.status(Response.Status.CREATED).entity(responseDTO).build();
} catch (IllegalArgumentException e) {
LOG.warn("[WARN] " + e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"message\": \"" + e.getMessage() + "\"}")
.build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la création de la promotion : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la création de la promotion.\"}")
.build();
}
}
/**
* Met à jour une promotion.
* Requiert une authentification JWT.
* Seul le responsable de l'établissement peut modifier.
*
* @param requestContext Le contexte de la requête (injecté)
* @param promotionId L'ID de la promotion
* @param requestDTO Le DTO de mise à jour
* @return La promotion mise à jour
*/
@PUT
@Path("/{id}")
@Transactional
@RequiresAuth
@Operation(
summary = "Mettre à jour une promotion",
description = "Met à jour une promotion existante. Seul le responsable peut modifier.")
@SecurityRequirement(name = "bearerAuth")
@APIResponse(responseCode = "200", description = "Promotion mise à jour")
@APIResponse(responseCode = "401", description = "Non authentifié")
@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);
LOG.info("[LOG] Mise à jour de la promotion : " + promotionId + " par l'utilisateur : " + authenticatedUserId);
try {
// Vérifier les permissions
Promotion existingPromotion = promotionService.getPromotionById(promotionId);
if (!promotionService.canModifyPromotion(existingPromotion, authenticatedUserId)) {
LOG.warn("[WARN] Utilisateur " + authenticatedUserId + " non autorisé à modifier la promotion " + promotionId);
return Response.status(Response.Status.FORBIDDEN)
.entity("{\"message\": \"Vous n'êtes pas autorisé à modifier cette promotion.\"}")
.build();
}
Promotion promotion = promotionService.updatePromotion(promotionId, requestDTO);
PromotionResponseDTO responseDTO = new PromotionResponseDTO(promotion);
return Response.ok(responseDTO).build();
} catch (IllegalArgumentException e) {
LOG.warn("[WARN] " + e.getMessage());
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"" + e.getMessage() + "\"}")
.build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la mise à jour de la promotion : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la mise à jour de la promotion.\"}")
.build();
}
}
/**
* Supprime une promotion.
* Requiert une authentification JWT.
* Seul le responsable de l'établissement peut supprimer.
*
* @param requestContext Le contexte de la requête (injecté)
* @param promotionId L'ID de la promotion
* @return Confirmation de suppression
*/
@DELETE
@Path("/{id}")
@Transactional
@RequiresAuth
@Operation(
summary = "Supprimer une promotion",
description = "Supprime une promotion. Seul le responsable peut supprimer.")
@SecurityRequirement(name = "bearerAuth")
@APIResponse(responseCode = "204", description = "Promotion supprimée")
@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);
LOG.info("[LOG] Suppression de la promotion : " + promotionId + " par l'utilisateur : " + authenticatedUserId);
try {
// Vérifier les permissions
Promotion existingPromotion = promotionService.getPromotionById(promotionId);
if (!promotionService.canModifyPromotion(existingPromotion, authenticatedUserId)) {
LOG.warn("[WARN] Utilisateur " + authenticatedUserId + " non autorisé à supprimer la promotion " + promotionId);
return Response.status(Response.Status.FORBIDDEN)
.entity("{\"message\": \"Vous n'êtes pas autorisé à supprimer cette promotion.\"}")
.build();
}
boolean deleted = promotionService.deletePromotion(promotionId);
if (deleted) {
return Response.noContent().build();
} else {
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"Promotion non trouvée.\"}")
.build();
}
} catch (IllegalArgumentException e) {
LOG.warn("[WARN] " + e.getMessage());
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"Promotion non trouvée.\"}")
.build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la suppression de la promotion : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la suppression de la promotion.\"}")
.build();
}
}
/**
* Active ou désactive une promotion.
* Requiert une authentification JWT.
*
* @param requestContext Le contexte de la requête (injecté)
* @param promotionId L'ID de la promotion
* @param isActive L'état à appliquer
* @return La promotion mise à jour
*/
@PATCH
@Path("/{id}/active")
@Transactional
@RequiresAuth
@Operation(
summary = "Activer/Désactiver une promotion",
description = "Change l'état actif d'une promotion. Seul le responsable peut modifier.")
@SecurityRequirement(name = "bearerAuth")
@APIResponse(responseCode = "200", description = "État mis à jour")
@APIResponse(responseCode = "401", description = "Non authentifié")
@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);
LOG.info("[LOG] Changement d'état de la promotion " + promotionId + " à " + isActive);
try {
// Vérifier les permissions
Promotion existingPromotion = promotionService.getPromotionById(promotionId);
if (!promotionService.canModifyPromotion(existingPromotion, authenticatedUserId)) {
LOG.warn("[WARN] Utilisateur " + authenticatedUserId + " non autorisé");
return Response.status(Response.Status.FORBIDDEN)
.entity("{\"message\": \"Vous n'êtes pas autorisé à modifier cette promotion.\"}")
.build();
}
Promotion promotion = promotionService.setPromotionActive(promotionId, isActive);
PromotionResponseDTO responseDTO = new PromotionResponseDTO(promotion);
return Response.ok(responseDTO).build();
} catch (IllegalArgumentException e) {
LOG.warn("[WARN] " + e.getMessage());
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"Promotion non trouvée.\"}")
.build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la mise à jour.\"}")
.build();
}
}
}

View File

@@ -0,0 +1,343 @@
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.service.ReviewService;
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;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.logging.Logger;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
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.
*/
@Path("/reviews")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Reviews", description = "Opérations liées aux avis sur les établissements")
public class ReviewResource {
@Inject
ReviewService reviewService;
private static final Logger LOG = Logger.getLogger(ReviewResource.class);
/**
* 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);
}
// =====================================================================
// ENDPOINTS PUBLICS (LECTURE)
// =====================================================================
/**
* Récupère un avis par son ID.
*/
@GET
@Path("/{id}")
@Operation(
summary = "Récupérer un avis par ID",
description = "Retourne les détails d'un avis spécifique")
@APIResponse(responseCode = "200", description = "Avis trouvé")
@APIResponse(responseCode = "404", description = "Avis non trouvé")
public Response getReviewById(@PathParam("id") UUID reviewId) {
LOG.info("[LOG] Récupération de l'avis ID : " + reviewId);
try {
Review review = reviewService.getReviewById(reviewId);
ReviewResponseDTO responseDTO = new ReviewResponseDTO(review);
return Response.ok(responseDTO).build();
} catch (IllegalArgumentException e) {
LOG.warn("[WARN] Avis non trouvé : " + e.getMessage());
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"Avis non trouvé.\"}")
.build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la récupération de l'avis : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la récupération de l'avis.\"}")
.build();
}
}
/**
* Récupère les avis d'un établissement.
*/
@GET
@Path("/establishment/{establishmentId}")
@Operation(
summary = "Récupérer les avis d'un établissement",
description = "Retourne la liste paginée des avis pour un établissement")
@APIResponse(responseCode = "200", description = "Liste des avis")
public Response getReviewsByEstablishment(
@PathParam("establishmentId") UUID establishmentId,
@QueryParam("verifiedOnly") @DefaultValue("false") boolean verifiedOnly,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("10") int size) {
LOG.info("[LOG] Récupération des avis pour l'établissement : " + establishmentId);
try {
List<Review> reviews;
if (verifiedOnly) {
reviews = reviewService.getVerifiedReviewsByEstablishment(establishmentId, page, size);
} else {
reviews = reviewService.getReviewsByEstablishment(establishmentId, page, size);
}
List<ReviewResponseDTO> responseDTOs = reviews.stream()
.map(ReviewResponseDTO::new)
.collect(Collectors.toList());
return Response.ok(responseDTOs).build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la récupération des avis : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la récupération des avis.\"}")
.build();
}
}
/**
* Récupère les statistiques des avis pour un établissement.
*/
@GET
@Path("/establishment/{establishmentId}/stats")
@Operation(
summary = "Récupérer les statistiques des avis",
description = "Retourne les statistiques (moyenne, distribution, etc.) des avis pour un établissement")
@APIResponse(responseCode = "200", description = "Statistiques des avis")
public Response getReviewStats(@PathParam("establishmentId") UUID establishmentId) {
LOG.info("[LOG] Récupération des statistiques pour l'établissement : " + establishmentId);
try {
Map<String, Object> stats = reviewService.getReviewStats(establishmentId);
return Response.ok(stats).build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la récupération des statistiques : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la récupération des statistiques.\"}")
.build();
}
}
/**
* Récupère les avis d'un utilisateur.
*/
@GET
@Path("/user/{userId}")
@Operation(
summary = "Récupérer les avis d'un utilisateur",
description = "Retourne la liste paginée des avis écrits par un utilisateur")
@APIResponse(responseCode = "200", description = "Liste des avis")
public Response getReviewsByUser(
@PathParam("userId") UUID userId,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("10") int size) {
LOG.info("[LOG] Récupération des avis de l'utilisateur : " + userId);
try {
List<Review> reviews = reviewService.getReviewsByUser(userId, page, size);
List<ReviewResponseDTO> responseDTOs = reviews.stream()
.map(ReviewResponseDTO::new)
.collect(Collectors.toList());
return Response.ok(responseDTOs).build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la récupération des avis : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la récupération des avis.\"}")
.build();
}
}
/**
* Vérifie si l'utilisateur a déjà écrit un avis pour un établissement.
*/
@GET
@Path("/establishment/{establishmentId}/user/{userId}")
@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")
@APIResponse(responseCode = "200", description = "Avis trouvé")
@APIResponse(responseCode = "404", description = "Aucun avis trouvé")
public Response getUserReviewForEstablishment(
@PathParam("establishmentId") UUID establishmentId,
@PathParam("userId") UUID userId) {
LOG.info("[LOG] Recherche de l'avis de l'utilisateur " + userId + " pour l'établissement " + establishmentId);
try {
Optional<Review> reviewOpt = reviewService.getUserReviewForEstablishment(establishmentId, userId);
if (reviewOpt.isEmpty()) {
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"Aucun avis trouvé.\"}")
.build();
}
ReviewResponseDTO responseDTO = new ReviewResponseDTO(reviewOpt.get());
return Response.ok(responseDTO).build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la recherche de l'avis : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la recherche de l'avis.\"}")
.build();
}
}
// =====================================================================
// ENDPOINTS PROTÉGÉS (CRÉATION, MODIFICATION, SUPPRESSION)
// =====================================================================
/**
* Crée un nouvel avis.
* Requiert une authentification JWT.
*/
@POST
@Transactional
@RequiresAuth
@Operation(
summary = "Créer un avis",
description = "Crée un nouvel avis pour un établissement. Un seul avis par utilisateur et établissement.")
@SecurityRequirement(name = "bearerAuth")
@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);
LOG.info("[LOG] Création d'un avis pour l'établissement : " + requestDTO.getEstablishmentId() +
" par l'utilisateur : " + authenticatedUserId);
try {
Review review = reviewService.createReview(authenticatedUserId, requestDTO);
ReviewResponseDTO responseDTO = new ReviewResponseDTO(review);
return Response.status(Response.Status.CREATED).entity(responseDTO).build();
} catch (IllegalArgumentException e) {
LOG.warn("[WARN] " + e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"message\": \"" + e.getMessage() + "\"}")
.build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la création de l'avis : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la création de l'avis.\"}")
.build();
}
}
/**
* Met à jour un avis.
* Requiert une authentification JWT.
* Seul l'auteur peut modifier.
*/
@PUT
@Path("/{id}")
@Transactional
@RequiresAuth
@Operation(
summary = "Mettre à jour un avis",
description = "Met à jour un avis existant. Seul l'auteur peut modifier.")
@SecurityRequirement(name = "bearerAuth")
@APIResponse(responseCode = "200", description = "Avis mis à jour")
@APIResponse(responseCode = "401", description = "Non authentifié")
@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);
LOG.info("[LOG] Mise à jour de l'avis : " + reviewId + " par l'utilisateur : " + authenticatedUserId);
try {
Review review = reviewService.updateReview(reviewId, authenticatedUserId, requestDTO);
ReviewResponseDTO responseDTO = new ReviewResponseDTO(review);
return Response.ok(responseDTO).build();
} catch (IllegalArgumentException e) {
LOG.warn("[WARN] " + e.getMessage());
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"" + e.getMessage() + "\"}")
.build();
} catch (SecurityException e) {
LOG.warn("[WARN] " + e.getMessage());
return Response.status(Response.Status.FORBIDDEN)
.entity("{\"message\": \"" + e.getMessage() + "\"}")
.build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la mise à jour de l'avis : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la mise à jour de l'avis.\"}")
.build();
}
}
/**
* Supprime un avis.
* Requiert une authentification JWT.
* Seul l'auteur peut supprimer.
*/
@DELETE
@Path("/{id}")
@Transactional
@RequiresAuth
@Operation(
summary = "Supprimer un avis",
description = "Supprime un avis. Seul l'auteur peut supprimer.")
@SecurityRequirement(name = "bearerAuth")
@APIResponse(responseCode = "204", description = "Avis supprimé")
@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);
LOG.info("[LOG] Suppression de l'avis : " + reviewId + " par l'utilisateur : " + authenticatedUserId);
try {
boolean deleted = reviewService.deleteReview(reviewId, authenticatedUserId);
if (deleted) {
return Response.noContent().build();
} else {
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"Avis non trouvé.\"}")
.build();
}
} catch (SecurityException e) {
LOG.warn("[WARN] " + e.getMessage());
return Response.status(Response.Status.FORBIDDEN)
.entity("{\"message\": \"" + e.getMessage() + "\"}")
.build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la suppression de l'avis : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la suppression de l'avis.\"}")
.build();
}
}
}

View File

@@ -1,8 +1,14 @@
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;
import com.lions.dev.dto.response.social.PostCommentResponseDTO;
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.SocialPostService;
@@ -10,13 +16,19 @@ 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;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.logging.Logger;
@@ -37,8 +49,34 @@ public class SocialPostResource {
@Inject
SocialPostService socialPostService;
@Inject
JwtValidationService jwtValidationService;
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.
*
@@ -103,18 +141,37 @@ public class SocialPostResource {
/**
* Crée un nouveau post social.
* Requiert une authentification JWT.
* L'utilisateur authentifié doit correspondre au creatorId.
*
* @param requestContext Le contexte de la requête (injecté)
* @param requestDTO Le DTO contenant les informations du post à créer
* @return Le post créé
*/
@POST
@Transactional
@RequiresAuth
@Operation(
summary = "Créer un nouveau post",
description = "Crée un nouveau post social et retourne ses détails")
public Response createPost(@Valid SocialPostCreateRequestDTO requestDTO) {
description = "Crée un nouveau post social et retourne ses détails. Requiert une authentification JWT.")
@SecurityRequirement(name = "bearerAuth")
@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) {
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);
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)
.entity("{\"message\": \"Vous ne pouvez créer un post que pour votre propre compte.\"}")
.build();
}
try {
SocialPost post = socialPostService.createPost(
requestDTO.getContent(),
@@ -138,7 +195,10 @@ public class SocialPostResource {
/**
* Met à jour un post.
* Requiert une authentification JWT.
* Seul le créateur du post peut le modifier.
*
* @param requestContext Le contexte de la requête (injecté)
* @param postId L'ID du post
* @param requestDTO Body JSON contenant content et/ou imageUrl (compatible client Flutter)
* @return Le post mis à jour
@@ -146,10 +206,17 @@ 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 (body JSON)")
description = "Met à jour le contenu et/ou l'image d'un post existant. Seul le créateur peut modifier.")
@SecurityRequirement(name = "bearerAuth")
@APIResponse(responseCode = "200", description = "Post mis à jour")
@APIResponse(responseCode = "401", description = "Non authentifié")
@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);
@@ -161,6 +228,18 @@ public class SocialPostResource {
}
try {
// Récupérer le post pour vérifier le propriétaire
SocialPost existingPost = socialPostService.getPostById(postId);
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
// Vérifier que l'utilisateur authentifié est le créateur du post
if (existingPost.getUser() == null || !authenticatedUserId.equals(existingPost.getUser().getId())) {
LOG.warn("[WARN] Utilisateur " + authenticatedUserId + " tente de modifier le post " + postId + " d'un autre utilisateur");
return Response.status(Response.Status.FORBIDDEN)
.entity("{\"message\": \"Vous ne pouvez modifier que vos propres posts.\"}")
.build();
}
SocialPost post = socialPostService.updatePost(
postId,
requestDTO.getContent(),
@@ -183,20 +262,43 @@ public class SocialPostResource {
/**
* Supprime un post.
* Requiert une authentification JWT.
* Seul le créateur du post peut le supprimer.
*
* @param requestContext Le contexte de la requête (injecté)
* @param postId L'ID du post
* @return Réponse de confirmation
*/
@DELETE
@Path("/{id}")
@Transactional
@RequiresAuth
@Operation(
summary = "Supprimer un post",
description = "Supprime un post social spécifique")
public Response deletePost(@PathParam("id") UUID postId) {
description = "Supprime un post social. Seul le créateur peut supprimer.")
@SecurityRequirement(name = "bearerAuth")
@APIResponse(responseCode = "204", description = "Post supprimé")
@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) {
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);
// Vérifier que l'utilisateur authentifié est le créateur du post
if (existingPost.getUser() == null || !authenticatedUserId.equals(existingPost.getUser().getId())) {
LOG.warn("[WARN] Utilisateur " + authenticatedUserId + " tente de supprimer le post " + postId + " d'un autre utilisateur");
return Response.status(Response.Status.FORBIDDEN)
.entity("{\"message\": \"Vous ne pouvez supprimer que vos propres posts.\"}")
.build();
}
boolean deleted = socialPostService.deletePost(postId);
if (deleted) {
return Response.noContent().build();
@@ -205,6 +307,11 @@ public class SocialPostResource {
.entity("{\"message\": \"Post non trouvé.\"}")
.build();
}
} catch (IllegalArgumentException e) {
LOG.warn("[WARN] Post non trouvé : " + e.getMessage());
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"Post non trouvé.\"}")
.build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la suppression du post : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
@@ -214,18 +321,23 @@ public class SocialPostResource {
}
/**
* Recherche des posts par contenu.
* Recherche des posts par contenu avec pagination.
*
* @param query Le terme de recherche
* @return Liste des posts correspondant à la recherche
* @param page Le numéro de la page (0-indexé)
* @param size La taille de la page
* @return Liste paginée des posts correspondant à la recherche
*/
@GET
@Path("/search")
@Operation(
summary = "Rechercher des posts",
description = "Recherche des posts sociaux par contenu textuel")
public Response searchPosts(@QueryParam("q") String query) {
LOG.info("[LOG] Recherche de posts avec la requête : " + query);
description = "Recherche des posts sociaux par contenu textuel avec pagination")
public Response searchPosts(
@QueryParam("q") String query,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("10") int size) {
LOG.info("[LOG] Recherche de posts avec la requête : " + query + " (page: " + page + ", size: " + size + ")");
if (query == null || query.trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
@@ -234,7 +346,7 @@ public class SocialPostResource {
}
try {
List<SocialPost> posts = socialPostService.searchPosts(query);
List<SocialPost> posts = socialPostService.searchPosts(query, page, size);
List<SocialPostResponseDTO> responseDTOs = posts.stream()
.map(SocialPostResponseDTO::new)
.collect(Collectors.toList());
@@ -250,29 +362,32 @@ public class SocialPostResource {
/**
* Like un post.
* Requiert une authentification JWT.
* L'utilisateur authentifié est automatiquement utilisé comme likeur.
*
* @param requestContext Le contexte de la requête (injecté)
* @param postId L'ID du post
* @return Le post mis à jour
*/
@POST
@Path("/{id}/like")
@Transactional
@RequiresAuth
@Operation(
summary = "Liker un post",
description = "Incrémente le compteur de likes d'un post")
description = "Incrémente le compteur de likes d'un post. Requiert une authentification JWT.")
@SecurityRequirement(name = "bearerAuth")
@APIResponse(responseCode = "200", description = "Post liké")
@APIResponse(responseCode = "401", description = "Non authentifié")
@APIResponse(responseCode = "404", description = "Post non trouvé")
public Response likePost(
@PathParam("id") UUID postId,
@QueryParam("userId") UUID userId) {
LOG.info("[LOG] Like du post ID : " + postId + " par utilisateur : " + userId);
if (userId == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"message\": \"userId est requis.\"}")
.build();
}
@Context ContainerRequestContext requestContext,
@PathParam("id") UUID postId) {
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
LOG.info("[LOG] Like du post ID : " + postId + " par utilisateur : " + authenticatedUserId);
try {
SocialPost post = socialPostService.likePost(postId, userId);
SocialPost post = socialPostService.likePost(postId, authenticatedUserId);
SocialPostResponseDTO responseDTO = new SocialPostResponseDTO(post);
return Response.ok(responseDTO).build();
} catch (IllegalArgumentException e) {
@@ -289,29 +404,35 @@ public class SocialPostResource {
}
/**
* Ajoute un commentaire à un post.
* Ajoute un commentaire à un post (version simplifiée - incrémente le compteur).
* Requiert une authentification JWT.
* L'utilisateur authentifié est automatiquement utilisé comme auteur.
*
* Note: Préférer l'endpoint POST /{id}/comments qui crée un vrai commentaire persisté.
*
* @param requestContext Le contexte de la requête (injecté)
* @param postId L'ID du post
* @param requestBody Le body JSON avec le contenu du commentaire
* @return Le post mis à jour
*/
@POST
@Path("/{id}/comment")
@Transactional
@RequiresAuth
@Consumes(MediaType.APPLICATION_JSON)
@Operation(
summary = "Commenter un post",
description = "Ajoute un commentaire à un post")
summary = "Commenter un post (legacy)",
description = "Ajoute un commentaire à un post. Requiert une authentification JWT.")
@SecurityRequirement(name = "bearerAuth")
@APIResponse(responseCode = "200", description = "Commentaire ajouté")
@APIResponse(responseCode = "401", description = "Non authentifié")
@APIResponse(responseCode = "404", description = "Post non trouvé")
public Response addComment(
@Context ContainerRequestContext requestContext,
@PathParam("id") UUID postId,
@QueryParam("userId") UUID userId,
String requestBody) {
LOG.info("[LOG] Ajout de commentaire au post ID : " + postId + " par utilisateur : " + userId);
if (userId == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"message\": \"userId est requis.\"}")
.build();
}
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
LOG.info("[LOG] Ajout de commentaire au post ID : " + postId + " par utilisateur : " + authenticatedUserId);
try {
// Parser le body pour obtenir le contenu du commentaire
@@ -320,7 +441,7 @@ public class SocialPostResource {
Map<String, Object> body = mapper.readValue(requestBody, Map.class);
String commentContent = (String) body.getOrDefault("content", "");
SocialPost post = socialPostService.addComment(postId, userId, commentContent);
SocialPost post = socialPostService.addComment(postId, authenticatedUserId, commentContent);
SocialPostResponseDTO responseDTO = new SocialPostResponseDTO(post);
return Response.ok(responseDTO).build();
} catch (IllegalArgumentException e) {
@@ -338,29 +459,32 @@ public class SocialPostResource {
/**
* Partage un post.
* Requiert une authentification JWT.
* L'utilisateur authentifié est automatiquement utilisé comme partageur.
*
* @param requestContext Le contexte de la requête (injecté)
* @param postId L'ID du post
* @return Le post mis à jour
*/
@POST
@Path("/{id}/share")
@Transactional
@RequiresAuth
@Operation(
summary = "Partager un post",
description = "Incrémente le compteur de partages d'un post")
description = "Incrémente le compteur de partages d'un post. Requiert une authentification JWT.")
@SecurityRequirement(name = "bearerAuth")
@APIResponse(responseCode = "200", description = "Post partagé")
@APIResponse(responseCode = "401", description = "Non authentifié")
@APIResponse(responseCode = "404", description = "Post non trouvé")
public Response sharePost(
@PathParam("id") UUID postId,
@QueryParam("userId") UUID userId) {
LOG.info("[LOG] Partage du post ID : " + postId + " par utilisateur : " + userId);
if (userId == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"message\": \"userId est requis.\"}")
.build();
}
@Context ContainerRequestContext requestContext,
@PathParam("id") UUID postId) {
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
LOG.info("[LOG] Partage du post ID : " + postId + " par utilisateur : " + authenticatedUserId);
try {
SocialPost post = socialPostService.sharePost(postId, userId);
SocialPost post = socialPostService.sharePost(postId, authenticatedUserId);
SocialPostResponseDTO responseDTO = new SocialPostResponseDTO(post);
return Response.ok(responseDTO).build();
} catch (IllegalArgumentException e) {
@@ -377,21 +501,26 @@ public class SocialPostResource {
}
/**
* Récupère tous les posts d'un utilisateur.
* Récupère tous les posts d'un utilisateur avec pagination.
*
* @param userId L'ID de l'utilisateur
* @return Liste des posts 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 posts de l'utilisateur
*/
@GET
@Path("/user/{userId}")
@Operation(
summary = "Récupérer les posts d'un utilisateur",
description = "Retourne tous les posts créés par un utilisateur spécifique")
public Response getPostsByUserId(@PathParam("userId") UUID userId) {
LOG.info("[LOG] Récupération des posts pour l'utilisateur ID : " + userId);
description = "Retourne une liste paginée des posts créés par un utilisateur spécifique")
public Response getPostsByUserId(
@PathParam("userId") UUID userId,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("10") int size) {
LOG.info("[LOG] Récupération des posts pour l'utilisateur ID : " + userId + " (page: " + page + ", size: " + size + ")");
try {
List<SocialPost> posts = socialPostService.getPostsByUserId(userId);
List<SocialPost> posts = socialPostService.getPostsByUserId(userId, page, size);
List<SocialPostResponseDTO> responseDTOs = posts.stream()
.map(SocialPostResponseDTO::new)
.collect(Collectors.toList());
@@ -448,5 +577,241 @@ public class SocialPostResource {
.build();
}
}
// =====================================================================
// ENDPOINTS POUR LES COMMENTAIRES
// =====================================================================
/**
* Récupère tous les commentaires d'un post avec pagination.
*
* @param postId L'ID du post
* @param page Le numéro de la page (0-indexé)
* @param size La taille de la page
* @return Liste paginée des commentaires
*/
@GET
@Path("/{id}/comments")
@Operation(
summary = "Récupérer les commentaires d'un post",
description = "Retourne une liste paginée des commentaires d'un post, triés par date de création")
@APIResponse(responseCode = "200", description = "Liste des commentaires")
@APIResponse(responseCode = "404", description = "Post non trouvé")
public Response getComments(
@PathParam("id") UUID postId,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size) {
LOG.info("[LOG] Récupération des commentaires pour le post ID : " + postId);
try {
List<PostComment> comments = socialPostService.getCommentsByPostId(postId, page, size);
List<PostCommentResponseDTO> responseDTOs = comments.stream()
.map(PostCommentResponseDTO::new)
.collect(Collectors.toList());
return Response.ok(responseDTOs).build();
} catch (IllegalArgumentException e) {
LOG.warn("[WARN] Post non trouvé : " + e.getMessage());
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"Post non trouvé.\"}")
.build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la récupération des commentaires : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la récupération des commentaires.\"}")
.build();
}
}
/**
* Crée un nouveau commentaire sur un post.
*
* @param postId L'ID du post
* @param requestDTO Le DTO contenant le contenu et l'ID de l'utilisateur
* @return Le commentaire créé
*/
@POST
@Path("/{id}/comments")
@Transactional
@Operation(
summary = "Créer un commentaire sur un post",
description = "Crée un nouveau commentaire sur le post spécifié")
@APIResponse(responseCode = "201", description = "Commentaire créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
@APIResponse(responseCode = "404", description = "Post ou utilisateur non trouvé")
public Response createComment(
@PathParam("id") UUID postId,
@Valid PostCommentCreateRequestDTO requestDTO) {
LOG.info("[LOG] Création d'un commentaire sur le post ID : " + postId + " par l'utilisateur : " + requestDTO.getUserId());
try {
PostComment comment = socialPostService.createComment(
postId,
requestDTO.getUserId(),
requestDTO.getContent()
);
PostCommentResponseDTO responseDTO = new PostCommentResponseDTO(comment);
return Response.status(Response.Status.CREATED).entity(responseDTO).build();
} catch (IllegalArgumentException e) {
LOG.warn("[WARN] " + e.getMessage());
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"" + e.getMessage() + "\"}")
.build();
} catch (UserNotFoundException e) {
LOG.warn("[WARN] Utilisateur non trouvé : " + e.getMessage());
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"Utilisateur non trouvé.\"}")
.build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la création du commentaire : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la création du commentaire.\"}")
.build();
}
}
/**
* Récupère un commentaire par son ID.
*
* @param postId L'ID du post (pour cohérence de l'URL)
* @param commentId L'ID du commentaire
* @return Le commentaire
*/
@GET
@Path("/{postId}/comments/{commentId}")
@Operation(
summary = "Récupérer un commentaire par ID",
description = "Retourne les détails d'un commentaire spécifique")
@APIResponse(responseCode = "200", description = "Commentaire trouvé")
@APIResponse(responseCode = "404", description = "Commentaire non trouvé")
public Response getCommentById(
@PathParam("postId") UUID postId,
@PathParam("commentId") UUID commentId) {
LOG.info("[LOG] Récupération du commentaire ID : " + commentId);
try {
PostComment comment = socialPostService.getCommentById(commentId);
PostCommentResponseDTO responseDTO = new PostCommentResponseDTO(comment);
return Response.ok(responseDTO).build();
} catch (IllegalArgumentException e) {
LOG.warn("[WARN] Commentaire non trouvé : " + e.getMessage());
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"Commentaire non trouvé.\"}")
.build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la récupération du commentaire : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la récupération du commentaire.\"}")
.build();
}
}
/**
* Met à jour un commentaire.
* Requiert une authentification JWT.
* Seul l'auteur du commentaire peut le modifier.
*
* @param requestContext Le contexte de la requête (injecté)
* @param postId L'ID du post
* @param commentId L'ID du commentaire
* @param requestBody Map contenant "content"
* @return Le commentaire mis à jour
*/
@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.")
@SecurityRequirement(name = "bearerAuth")
@APIResponse(responseCode = "200", description = "Commentaire mis à jour")
@APIResponse(responseCode = "401", description = "Non authentifié")
@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) {
LOG.info("[LOG] Mise à jour du commentaire ID : " + commentId);
String content = requestBody != null ? requestBody.get("content") : null;
// Utiliser l'utilisateur authentifié
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
try {
PostComment comment = socialPostService.updateComment(commentId, authenticatedUserId, content);
PostCommentResponseDTO responseDTO = new PostCommentResponseDTO(comment);
return Response.ok(responseDTO).build();
} catch (IllegalArgumentException e) {
LOG.warn("[WARN] " + e.getMessage());
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"" + e.getMessage() + "\"}")
.build();
} catch (SecurityException e) {
LOG.warn("[WARN] " + e.getMessage());
return Response.status(Response.Status.FORBIDDEN)
.entity("{\"message\": \"" + e.getMessage() + "\"}")
.build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la mise à jour du commentaire : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la mise à jour du commentaire.\"}")
.build();
}
}
/**
* Supprime un commentaire.
* Requiert une authentification JWT.
* L'auteur du commentaire ou l'auteur du post peuvent supprimer.
*
* @param requestContext Le contexte de la requête (injecté)
* @param postId L'ID du post
* @param commentId L'ID du commentaire
* @return Confirmation de suppression
*/
@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.")
@SecurityRequirement(name = "bearerAuth")
@APIResponse(responseCode = "204", description = "Commentaire supprimé")
@APIResponse(responseCode = "401", description = "Non authentifié")
@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);
LOG.info("[LOG] Suppression du commentaire ID : " + commentId + " par l'utilisateur : " + authenticatedUserId);
try {
boolean deleted = socialPostService.deleteComment(commentId, authenticatedUserId);
if (deleted) {
return Response.noContent().build();
} else {
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"Commentaire non trouvé.\"}")
.build();
}
} catch (SecurityException e) {
LOG.warn("[WARN] " + e.getMessage());
return Response.status(Response.Status.FORBIDDEN)
.entity("{\"message\": \"" + e.getMessage() + "\"}")
.build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la suppression du commentaire : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"message\": \"Erreur lors de la suppression du commentaire.\"}")
.build();
}
}
}

View File

@@ -1,5 +1,7 @@
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;
@@ -8,12 +10,16 @@ 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;
import java.util.UUID;
import java.util.stream.Collectors;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.logging.Logger;
@@ -36,6 +42,16 @@ public class StoryResource {
private static final Logger LOG = Logger.getLogger(StoryResource.class);
/**
* 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);
}
/**
* Récupère toutes les stories actives (non expirées).
*
@@ -137,18 +153,37 @@ public class StoryResource {
/**
* Crée une nouvelle story.
* Requiert une authentification JWT.
* L'utilisateur authentifié doit correspondre au creatorId.
*
* @param requestContext Le contexte de la requête (injecté)
* @param requestDTO Le DTO contenant les informations de la story à créer
* @return La story créée
*/
@POST
@Transactional
@RequiresAuth
@Operation(
summary = "Créer une nouvelle story",
description = "Crée une nouvelle story et retourne ses détails")
public Response createStory(@Valid StoryCreateRequestDTO requestDTO) {
description = "Crée une nouvelle story et retourne ses détails. Requiert une authentification JWT.")
@SecurityRequirement(name = "bearerAuth")
@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) {
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);
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)
.entity("{\"message\": \"Vous ne pouvez créer une story que pour votre propre compte.\"}")
.build();
}
try {
Story story = storyService.createStory(
requestDTO.getCreatorId(),
@@ -169,29 +204,33 @@ public class StoryResource {
/**
* Marque une story comme vue par un utilisateur.
* Requiert une authentification JWT.
* L'utilisateur authentifié est automatiquement utilisé comme viewer.
*
* @param requestContext Le contexte de la requête (injecté)
* @param storyId L'ID de la story
* @param viewerId L'ID de l'utilisateur qui voit la story
* @return La story mise à jour
*/
@POST
@Path("/{id}/view")
@Transactional
@RequiresAuth
@Operation(
summary = "Marquer une story comme vue",
description = "Marque une story comme vue par un utilisateur et incrémente le compteur de vues")
public Response markStoryAsViewed(@PathParam("id") UUID storyId, @QueryParam("userId") UUID viewerId) {
LOG.info("[LOG] Marquage de la story ID : " + storyId + " comme vue par l'utilisateur ID : " + viewerId);
if (viewerId == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"message\": \"L'ID de l'utilisateur est obligatoire.\"}")
.build();
}
description = "Marque une story comme vue par l'utilisateur authentifié et incrémente le compteur de vues.")
@SecurityRequirement(name = "bearerAuth")
@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);
LOG.info("[LOG] Marquage de la story ID : " + storyId + " comme vue par l'utilisateur ID : " + authenticatedUserId);
try {
Story story = storyService.markStoryAsViewed(storyId, viewerId);
StoryResponseDTO responseDTO = new StoryResponseDTO(story, viewerId);
Story story = storyService.markStoryAsViewed(storyId, authenticatedUserId);
StoryResponseDTO responseDTO = new StoryResponseDTO(story, authenticatedUserId);
return Response.ok(responseDTO).build();
} catch (IllegalArgumentException e) {
LOG.warn("[WARN] Story non trouvée : " + e.getMessage());
@@ -208,20 +247,43 @@ public class StoryResource {
/**
* Supprime une story.
* Requiert une authentification JWT.
* Seul le créateur de la story peut la supprimer.
*
* @param requestContext Le contexte de la requête (injecté)
* @param storyId L'ID de la story
* @return Confirmation de suppression
*/
@DELETE
@Path("/{id}")
@Transactional
@RequiresAuth
@Operation(
summary = "Supprimer une story",
description = "Supprime définitivement une story")
public Response deleteStory(@PathParam("id") UUID storyId) {
LOG.info("[LOG] Suppression de la story ID : " + storyId);
description = "Supprime définitivement une story. Seul le créateur peut supprimer.")
@SecurityRequirement(name = "bearerAuth")
@APIResponse(responseCode = "200", description = "Story supprimée")
@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);
LOG.info("[LOG] Suppression de la story ID : " + storyId + " par l'utilisateur : " + authenticatedUserId);
try {
// Récupérer la story pour vérifier le propriétaire
Story existingStory = storyService.getStoryById(storyId);
// Vérifier que l'utilisateur authentifié est le créateur de la story
if (existingStory.getUser() == null || !authenticatedUserId.equals(existingStory.getUser().getId())) {
LOG.warn("[WARN] Utilisateur " + authenticatedUserId + " tente de supprimer la story " + storyId + " d'un autre utilisateur");
return Response.status(Response.Status.FORBIDDEN)
.entity("{\"message\": \"Vous ne pouvez supprimer que vos propres stories.\"}")
.build();
}
boolean deleted = storyService.deleteStory(storyId);
if (deleted) {
return Response.ok("{\"message\": \"Story supprimée avec succès.\"}").build();
@@ -230,6 +292,11 @@ public class StoryResource {
.entity("{\"message\": \"Story non trouvée.\"}")
.build();
}
} catch (IllegalArgumentException e) {
LOG.warn("[WARN] Story non trouvée : " + e.getMessage());
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"message\": \"Story non trouvée.\"}")
.build();
} catch (Exception e) {
LOG.error("[ERROR] Erreur lors de la suppression de la story : " + e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)

View File

@@ -3,6 +3,7 @@ package com.lions.dev.resource;
import com.lions.dev.dto.PasswordResetRequest;
import com.lions.dev.dto.request.users.AssignRoleRequestDTO;
import com.lions.dev.dto.request.users.SetUserActiveRequestDTO;
import com.lions.dev.dto.request.users.UpdateProfileImageRequestDTO;
import com.lions.dev.dto.request.users.UserAuthenticateRequestDTO;
import com.lions.dev.dto.request.users.UserCreateRequestDTO;
import com.lions.dev.dto.response.users.UserAuthenticateResponseDTO;
@@ -10,6 +11,7 @@ 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.service.JwtService;
import com.lions.dev.service.PasswordResetService;
import com.lions.dev.service.UsersService;
import jakarta.inject.Inject;
@@ -20,7 +22,6 @@ import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -31,6 +32,7 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
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.
@@ -45,6 +47,9 @@ public class UsersResource {
@Inject
UsersService userService;
@Inject
JwtService jwtService;
@Inject
PasswordResetService passwordResetService;
@@ -102,6 +107,7 @@ public class UsersResource {
user.getEmail(),
user.getRole()
);
responseDTO.setToken(jwtService.generateToken(user));
responseDTO.logResponseDetails();
return Response.ok(responseDTO).build();
}
@@ -222,35 +228,43 @@ public class UsersResource {
LOG.info("Réinitialisation du mot de passe pour l'utilisateur avec l'ID : " + id);
userService.resetPassword(id, nouveauMotDePasse);
return Response.ok("{\"message\": \"Mot de passe réinitialisé avec succès.\"}").build();
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 imageFilePath Le chemin vers l'image de profil.
* @return Un message indiquant si la mise à jour a réussi.
* @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'image de profil d'un utilisateur.")
public String updateUserProfileImage(@PathParam("id") UUID id, String imageFilePath) {
@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 {
File file = new File(imageFilePath);
if (!file.exists()) {
LOG.error("[ERROR] Le fichier spécifié n'existe pas : " + imageFilePath);
return "Le fichier spécifié n'existe pas.";
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();
}
String profileImageUrl = file.getAbsolutePath();
userService.updateUserProfileImage(id, profileImageUrl); // Appel à la méthode correcte
Users updatedUser = userService.updateUserProfileImage(id, profileImageUrl.trim());
LOG.info("[LOG] Image de profil mise à jour pour l'utilisateur avec l'ID : " + id);
return "Image de profil mise à jour avec succès.";
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());
return "Erreur lors de la mise à jour de l'image de profil.";
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();
}
}

View File

@@ -7,7 +7,7 @@ import com.lions.dev.dto.request.events.EventCreateRequestDTO;
import com.lions.dev.entity.events.Events;
import com.lions.dev.entity.friends.Friendship;
import com.lions.dev.entity.users.Users;
import com.lions.dev.exception.EventNotFoundException;
import com.lions.dev.core.errors.exceptions.EventNotFoundException;
import com.lions.dev.exception.UserNotFoundException;
import com.lions.dev.repository.EventsRepository;
import com.lions.dev.repository.FriendshipRepository;
@@ -322,6 +322,23 @@ public class EventService {
return events;
}
/**
* Récupère les événements par catégorie avec pagination.
*
* @param category La catégorie des événements.
* @param page Le numéro de la page (0-indexé)
* @param size La taille de la page
* @return La liste paginée des événements dans cette catégorie.
*/
public List<Events> findEventsByCategory(String category, int page, int size) {
logger.info("[logger] Récupération des événements dans la catégorie : {} (page: {}, size: {})", category, page, size);
List<Events> events = eventsRepository.find("category", category)
.page(page, size)
.list();
logger.info("[logger] Nombre d'événements trouvés dans la catégorie '{}' : {}", category, events.size());
return events;
}
/**
* Recherche des événements par mot-clé dans le titre ou la description.
*
@@ -335,6 +352,23 @@ public class EventService {
return events;
}
/**
* Recherche des événements par mot-clé avec pagination.
*
* @param keyword Le mot-clé à rechercher.
* @param page Le numéro de la page (0-indexé)
* @param size La taille de la page
* @return La liste paginée des événements correspondant au mot-clé.
*/
public List<Events> searchEvents(String keyword, int page, int size) {
logger.info("[logger] Recherche d'événements avec le mot-clé : {} (page: {}, size: {})", keyword, page, size);
List<Events> events = eventsRepository.find("title like ?1 or description like ?1", "%" + keyword + "%")
.page(page, size)
.list();
logger.info("[logger] Nombre d'événements trouvés pour le mot-clé '{}' : {}", keyword, events.size());
return events;
}
/**
* Récupère les événements auxquels un utilisateur participe.
*
@@ -410,6 +444,23 @@ public class EventService {
return events;
}
/**
* Récupère les événements futurs avec pagination.
*
* @param page Le numéro de la page (0-indexé)
* @param size La taille de la page
* @return Une liste paginée d'événements à venir.
*/
public List<Events> findUpcomingEvents(int page, int size) {
logger.info("[logger] Récupération des événements futurs (page: {}, size: {})", page, size);
LocalDateTime now = LocalDateTime.now();
List<Events> events = eventsRepository.find("startDate > ?1 ORDER BY startDate ASC", now)
.page(page, size)
.list();
logger.info("[logger] Nombre d'événements futurs trouvés : " + events.size());
return events;
}
/**
* Récupère les événements passés.
*
@@ -423,6 +474,23 @@ public class EventService {
return events;
}
/**
* Récupère les événements passés avec pagination.
*
* @param page Le numéro de la page (0-indexé)
* @param size La taille de la page
* @return Une liste paginée d'événements passés.
*/
public List<Events> findPastEvents(int page, int size) {
logger.info("[logger] Récupération des événements passés (page: {}, size: {})", page, size);
LocalDateTime now = LocalDateTime.now();
List<Events> events = eventsRepository.find("endDate < ?1 ORDER BY endDate DESC", now)
.page(page, size)
.list();
logger.info("[logger] Nombre d'événements passés trouvés : " + events.size());
return events;
}
/**
* Récupère les événements par localisation (ville ou adresse de l'établissement).
* v2.0 : plus de colonne location ; recherche sur establishment.address et establishment.city.
@@ -483,8 +551,8 @@ public class EventService {
Users user = usersRepository.findById(userId);
if (user == null) {
logger.error("[ERROR] Utilisateur non trouvé avec l'ID : " + userId);
throw new UserNotFoundException("Utilisateur non trouvé avec l'ID : " + userId);
logger.warn("[WARN] Utilisateur non trouvé avec l'ID : " + userId + " — retour liste vide");
return List.of();
}
// Récupérer toutes les relations d'amitié acceptées

View File

@@ -420,8 +420,8 @@ public class FriendshipService {
public List<FriendshipReadStatusResponseDTO> listSentFriendRequests(UUID userId, int page, int size) {
Users user = usersRepository.findById(userId);
if (user == null) {
logger.error("[ERROR] Utilisateur non trouvé.");
throw new UserNotFoundException("Utilisateur introuvable.");
logger.warn("[WARN] Utilisateur non trouvé pour demandes envoyées — retour liste vide.");
return List.of();
}
List<Friendship> friendships = friendshipRepository.findSentRequestsByUser(user, FriendshipStatus.PENDING, page - 1, size);
@@ -441,8 +441,8 @@ public class FriendshipService {
public List<FriendshipReadStatusResponseDTO> listReceivedFriendRequests(UUID userId, int page, int size) {
Users user = usersRepository.findById(userId);
if (user == null) {
logger.error("[ERROR] Utilisateur non trouvé.");
throw new UserNotFoundException("Utilisateur introuvable.");
logger.warn("[WARN] Utilisateur non trouvé pour demandes reçues — retour liste vide.");
return List.of();
}
List<Friendship> friendships = friendshipRepository.findReceivedRequestsByUser(user, FriendshipStatus.PENDING, page - 1, size);

View File

@@ -0,0 +1,60 @@
package com.lions.dev.service;
import com.lions.dev.entity.users.Users;
import io.smallrye.jwt.build.Jwt;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
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
* lorsque les endpoints sont protégés avec @RolesAllowed.
*/
@ApplicationScoped
public class JwtService {
private static final Logger LOG = Logger.getLogger(JwtService.class);
private static final String ISSUER = "afterwork";
@ConfigProperty(name = "afterwork.jwt.secret", defaultValue = "afterwork-jwt-secret-min-32-bytes-for-hs256!")
String secret;
/**
* Génère un JWT pour l'utilisateur authentifié.
*
* @param user L'utilisateur après login réussi
* @return Le token JWT signé (à envoyer au client dans la réponse d'authentification)
*/
public String generateToken(Users user) {
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");
SecretKey key = secretKeyFromConfig();
String token = Jwt.claims()
.issuer(ISSUER)
.subject(user.getId().toString())
.groups(groups)
.jws()
.sign(key);
LOG.debug("JWT généré pour l'utilisateur " + user.getId());
return token;
}
private SecretKey secretKeyFromConfig() {
byte[] decoded = secret.getBytes(StandardCharsets.UTF_8);
if (decoded.length < 32) {
byte[] padded = new byte[32];
System.arraycopy(decoded, 0, padded, 0, decoded.length);
decoded = padded;
}
return new SecretKeySpec(decoded, "HmacSHA256");
}
}

View File

@@ -1,5 +1,6 @@
package com.lions.dev.service;
import com.lions.dev.core.errors.exceptions.NotFoundException;
import com.lions.dev.entity.chat.Conversation;
import com.lions.dev.entity.chat.Message;
import com.lions.dev.entity.users.Users;
@@ -242,8 +243,9 @@ public class MessageService {
*
* @param user1Id L'ID du premier utilisateur
* @param user2Id L'ID du deuxième utilisateur
* @return La conversation ou null si elle n'existe pas
* @return La conversation
* @throws UserNotFoundException Si l'un des utilisateurs n'existe pas
* @throws NotFoundException Si la conversation n'existe pas
*/
public Conversation getConversationBetweenUsers(UUID user1Id, UUID user2Id) {
logger.info("[MessageService] Recherche de conversation entre " + user1Id + " et " + user2Id);
@@ -255,7 +257,11 @@ public class MessageService {
throw new UserNotFoundException("Un ou plusieurs utilisateurs non trouvés");
}
return conversationRepository.findBetweenUsers(user1, user2);
Conversation conversation = conversationRepository.findBetweenUsers(user1, user2);
if (conversation == null) {
throw new NotFoundException("Conversation non trouvée");
}
return conversation;
}
/**
@@ -270,7 +276,7 @@ public class MessageService {
Message message = messageRepository.findById(messageId);
if (message == null) {
throw new IllegalArgumentException("Message non trouvé avec l'ID : " + messageId);
throw new NotFoundException("Message non trouvé avec l'ID : " + messageId);
}
message.markAsRead();

View File

@@ -88,8 +88,8 @@ public class NotificationService {
Users user = usersRepository.findById(userId);
if (user == null) {
logger.error("[NotificationService] Utilisateur non trouvé avec l'ID : " + userId);
throw new UserNotFoundException("Utilisateur non trouvé avec l'ID : " + userId);
logger.warn("[NotificationService] Utilisateur non trouvé avec l'ID : " + userId + " — retour liste vide.");
return List.of();
}
List<Notification> notifications = notificationRepository.findByUserId(userId);
@@ -111,8 +111,8 @@ public class NotificationService {
Users user = usersRepository.findById(userId);
if (user == null) {
logger.error("[NotificationService] Utilisateur non trouvé avec l'ID : " + userId);
throw new UserNotFoundException("Utilisateur non trouvé avec l'ID : " + userId);
logger.warn("[NotificationService] Utilisateur non trouvé avec l'ID : " + userId + " — retour liste vide.");
return List.of();
}
return notificationRepository.findByUserIdWithPagination(userId, page, size);

View File

@@ -0,0 +1,268 @@
package com.lions.dev.service;
import com.lions.dev.dto.request.promotion.PromotionCreateRequestDTO;
import com.lions.dev.dto.request.promotion.PromotionUpdateRequestDTO;
import com.lions.dev.entity.establishment.Establishment;
import com.lions.dev.entity.promotion.Promotion;
import com.lions.dev.repository.EstablishmentRepository;
import com.lions.dev.repository.PromotionRepository;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.jboss.logging.Logger;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* Service de gestion des promotions.
*
* Ce service contient la logique métier pour la création, récupération,
* mise à jour et suppression des promotions.
*/
@ApplicationScoped
public class PromotionService {
private static final Logger LOG = Logger.getLogger(PromotionService.class);
@Inject
PromotionRepository promotionRepository;
@Inject
EstablishmentRepository establishmentRepository;
/**
* Crée une nouvelle promotion.
*
* @param dto Le DTO de création
* @return La promotion créée
*/
@Transactional
public Promotion createPromotion(PromotionCreateRequestDTO dto) {
LOG.info("[PromotionService] Création d'une promotion pour l'établissement: " + dto.getEstablishmentId());
// Vérifier que l'établissement existe
Establishment establishment = establishmentRepository.findById(dto.getEstablishmentId());
if (establishment == null) {
throw new IllegalArgumentException("Établissement non trouvé avec l'ID: " + dto.getEstablishmentId());
}
// Vérifier l'unicité du code promo s'il est fourni
if (dto.getPromoCode() != null && !dto.getPromoCode().isBlank()) {
if (promotionRepository.promoCodeExists(dto.getPromoCode())) {
throw new IllegalArgumentException("Le code promo '" + dto.getPromoCode() + "' est déjà utilisé");
}
}
// Valider les dates
if (dto.getValidUntil().isBefore(dto.getValidFrom())) {
throw new IllegalArgumentException("La date de fin doit être après la date de début");
}
// Valider le type de réduction
if (!isValidDiscountType(dto.getDiscountType())) {
throw new IllegalArgumentException("Type de réduction invalide. Valeurs acceptées: PERCENTAGE, FIXED_AMOUNT, FREE_ITEM");
}
// Créer la promotion
Promotion promotion = new Promotion(
establishment,
dto.getTitle(),
dto.getDescription(),
dto.getDiscountType().toUpperCase(),
dto.getDiscountValue(),
dto.getValidFrom(),
dto.getValidUntil()
);
promotion.setPromoCode(dto.getPromoCode());
promotionRepository.persist(promotion);
LOG.info("[PromotionService] Promotion créée avec succès: " + promotion.getId());
return promotion;
}
/**
* Récupère une promotion par son ID.
*
* @param promotionId L'ID de la promotion
* @return La promotion
*/
public Promotion getPromotionById(UUID promotionId) {
LOG.debug("[PromotionService] Récupération de la promotion: " + promotionId);
Promotion promotion = promotionRepository.findById(promotionId);
if (promotion == null) {
throw new IllegalArgumentException("Promotion non trouvée avec l'ID: " + promotionId);
}
return promotion;
}
/**
* Récupère toutes les promotions d'un établissement.
*
* @param establishmentId L'ID de l'établissement
* @param page Le numéro de la page
* @param size La taille de la page
* @return Liste des promotions
*/
public List<Promotion> getPromotionsByEstablishment(UUID establishmentId, int page, int size) {
LOG.debug("[PromotionService] Récupération des promotions pour l'établissement: " + establishmentId);
return promotionRepository.findByEstablishmentId(establishmentId, page, size);
}
/**
* Récupère les promotions actives et valides d'un établissement.
*
* @param establishmentId L'ID de l'établissement
* @return Liste des promotions actives
*/
public List<Promotion> getActivePromotionsByEstablishment(UUID establishmentId) {
LOG.debug("[PromotionService] Récupération des promotions actives pour l'établissement: " + establishmentId);
return promotionRepository.findActiveByEstablishmentId(establishmentId);
}
/**
* Récupère toutes les promotions actives et valides.
*
* @param page Le numéro de la page
* @param size La taille de la page
* @return Liste des promotions actives
*/
public List<Promotion> getAllActivePromotions(int page, int size) {
LOG.debug("[PromotionService] Récupération de toutes les promotions actives");
return promotionRepository.findAllActive(page, size);
}
/**
* Recherche une promotion par son code promo.
*
* @param promoCode Le code promo
* @return La promotion si trouvée
*/
public Optional<Promotion> getPromotionByCode(String promoCode) {
LOG.debug("[PromotionService] Recherche de la promotion avec le code: " + promoCode);
return promotionRepository.findByPromoCode(promoCode);
}
/**
* Met à jour une promotion.
*
* @param promotionId L'ID de la promotion
* @param dto Le DTO de mise à jour
* @return La promotion mise à jour
*/
@Transactional
public Promotion updatePromotion(UUID promotionId, PromotionUpdateRequestDTO dto) {
LOG.info("[PromotionService] Mise à jour de la promotion: " + promotionId);
Promotion promotion = getPromotionById(promotionId);
// Mettre à jour les champs non-nuls
if (dto.getTitle() != null && !dto.getTitle().isBlank()) {
promotion.setTitle(dto.getTitle());
}
if (dto.getDescription() != null) {
promotion.setDescription(dto.getDescription());
}
if (dto.getPromoCode() != null) {
// Vérifier l'unicité du nouveau code promo s'il change
if (!dto.getPromoCode().equals(promotion.getPromoCode())) {
if (!dto.getPromoCode().isBlank() && promotionRepository.promoCodeExists(dto.getPromoCode())) {
throw new IllegalArgumentException("Le code promo '" + dto.getPromoCode() + "' est déjà utilisé");
}
}
promotion.setPromoCode(dto.getPromoCode().isBlank() ? null : dto.getPromoCode());
}
if (dto.getDiscountType() != null && !dto.getDiscountType().isBlank()) {
if (!isValidDiscountType(dto.getDiscountType())) {
throw new IllegalArgumentException("Type de réduction invalide");
}
promotion.setDiscountType(dto.getDiscountType().toUpperCase());
}
if (dto.getDiscountValue() != null) {
promotion.setDiscountValue(dto.getDiscountValue());
}
if (dto.getValidFrom() != null) {
promotion.setValidFrom(dto.getValidFrom());
}
if (dto.getValidUntil() != null) {
promotion.setValidUntil(dto.getValidUntil());
}
if (dto.getIsActive() != null) {
promotion.setIsActive(dto.getIsActive());
}
// Valider les dates après mise à jour
if (promotion.getValidUntil().isBefore(promotion.getValidFrom())) {
throw new IllegalArgumentException("La date de fin doit être après la date de début");
}
promotionRepository.persist(promotion);
LOG.info("[PromotionService] Promotion mise à jour avec succès: " + promotionId);
return promotion;
}
/**
* Supprime une promotion.
*
* @param promotionId L'ID de la promotion
* @return true si supprimée, false sinon
*/
@Transactional
public boolean deletePromotion(UUID promotionId) {
LOG.info("[PromotionService] Suppression de la promotion: " + promotionId);
Promotion promotion = promotionRepository.findById(promotionId);
if (promotion == null) {
return false;
}
promotionRepository.delete(promotion);
LOG.info("[PromotionService] Promotion supprimée avec succès: " + promotionId);
return true;
}
/**
* Active ou désactive une promotion.
*
* @param promotionId L'ID de la promotion
* @param isActive L'état à appliquer
* @return La promotion mise à jour
*/
@Transactional
public Promotion setPromotionActive(UUID promotionId, boolean isActive) {
LOG.info("[PromotionService] Changement d'état de la promotion " + promotionId + " à " + isActive);
Promotion promotion = getPromotionById(promotionId);
promotion.setIsActive(isActive);
promotionRepository.persist(promotion);
return promotion;
}
/**
* Vérifie si le responsable de l'établissement peut modifier cette promotion.
*
* @param promotion La promotion
* @param userId L'ID de l'utilisateur
* @return true si l'utilisateur peut modifier, false sinon
*/
public boolean canModifyPromotion(Promotion promotion, UUID userId) {
if (promotion == null || userId == null) {
return false;
}
Establishment establishment = promotion.getEstablishment();
if (establishment == null || establishment.getManager() == null) {
return false;
}
return userId.equals(establishment.getManager().getId());
}
/**
* Valide le type de réduction.
*/
private boolean isValidDiscountType(String discountType) {
if (discountType == null) return false;
return discountType.equalsIgnoreCase("PERCENTAGE") ||
discountType.equalsIgnoreCase("FIXED_AMOUNT") ||
discountType.equalsIgnoreCase("FREE_ITEM");
}
}

View File

@@ -0,0 +1,276 @@
package com.lions.dev.service;
import com.lions.dev.dto.request.review.ReviewCreateRequestDTO;
import com.lions.dev.dto.request.review.ReviewUpdateRequestDTO;
import com.lions.dev.entity.establishment.Establishment;
import com.lions.dev.entity.establishment.Review;
import com.lions.dev.entity.users.Users;
import com.lions.dev.repository.EstablishmentRepository;
import com.lions.dev.repository.ReviewRepository;
import com.lions.dev.repository.UsersRepository;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.jboss.logging.Logger;
import java.util.*;
/**
* Service de gestion des avis d'établissements.
*
* Ce service contient la logique métier pour la création, récupération,
* mise à jour et suppression des avis.
*/
@ApplicationScoped
public class ReviewService {
private static final Logger LOG = Logger.getLogger(ReviewService.class);
@Inject
ReviewRepository reviewRepository;
@Inject
EstablishmentRepository establishmentRepository;
@Inject
UsersRepository usersRepository;
/**
* Crée un nouvel avis.
*
* @param userId L'ID de l'utilisateur qui crée l'avis
* @param dto Le DTO de création
* @return L'avis créé
*/
@Transactional
public Review createReview(UUID userId, ReviewCreateRequestDTO dto) {
LOG.info("[ReviewService] Création d'un avis pour l'établissement: " + dto.getEstablishmentId() + " par l'utilisateur: " + userId);
// Vérifier que l'utilisateur existe
Users user = usersRepository.findById(userId);
if (user == null) {
throw new IllegalArgumentException("Utilisateur non trouvé avec l'ID: " + userId);
}
// Vérifier que l'établissement existe
Establishment establishment = establishmentRepository.findById(dto.getEstablishmentId());
if (establishment == null) {
throw new IllegalArgumentException("Établissement non trouvé avec l'ID: " + dto.getEstablishmentId());
}
// Vérifier que l'utilisateur n'a pas déjà écrit un avis
if (reviewRepository.hasUserReviewed(dto.getEstablishmentId(), userId)) {
throw new IllegalArgumentException("Vous avez déjà écrit un avis pour cet établissement");
}
// Créer l'avis
Review review = new Review(user, establishment, dto.getOverallRating(), dto.getComment());
// Ajouter les notes par critères si fournies
if (dto.getCriteriaRatings() != null) {
for (Map.Entry<String, Integer> entry : dto.getCriteriaRatings().entrySet()) {
if (entry.getValue() >= 1 && entry.getValue() <= 5) {
review.addCriteriaRating(entry.getKey(), entry.getValue());
}
}
}
reviewRepository.persist(review);
LOG.info("[ReviewService] Avis créé avec succès: " + review.getId());
return review;
}
/**
* Récupère un avis par son ID.
*
* @param reviewId L'ID de l'avis
* @return L'avis
*/
public Review getReviewById(UUID reviewId) {
LOG.debug("[ReviewService] Récupération de l'avis: " + reviewId);
Review review = reviewRepository.findById(reviewId);
if (review == null) {
throw new IllegalArgumentException("Avis non trouvé avec l'ID: " + reviewId);
}
return review;
}
/**
* Récupère tous les avis d'un établissement.
*
* @param establishmentId L'ID de l'établissement
* @param page Le numéro de la page
* @param size La taille de la page
* @return Liste des avis
*/
public List<Review> getReviewsByEstablishment(UUID establishmentId, int page, int size) {
LOG.debug("[ReviewService] Récupération des avis pour l'établissement: " + establishmentId);
return reviewRepository.findByEstablishmentId(establishmentId, page, size);
}
/**
* Récupère les avis vérifiés d'un établissement.
*
* @param establishmentId L'ID de l'établissement
* @param page Le numéro de la page
* @param size La taille de la page
* @return Liste des avis vérifiés
*/
public List<Review> getVerifiedReviewsByEstablishment(UUID establishmentId, int page, int size) {
LOG.debug("[ReviewService] Récupération des avis vérifiés pour l'établissement: " + establishmentId);
return reviewRepository.findVerifiedByEstablishmentId(establishmentId, page, size);
}
/**
* Récupère tous les avis d'un utilisateur.
*
* @param userId L'ID de l'utilisateur
* @param page Le numéro de la page
* @param size La taille de la page
* @return Liste des avis
*/
public List<Review> getReviewsByUser(UUID userId, int page, int size) {
LOG.debug("[ReviewService] Récupération des avis de l'utilisateur: " + userId);
return reviewRepository.findByUserId(userId, page, size);
}
/**
* Récupère l'avis d'un utilisateur pour un établissement spécifique.
*
* @param establishmentId L'ID de l'établissement
* @param userId L'ID de l'utilisateur
* @return L'avis si trouvé
*/
public Optional<Review> getUserReviewForEstablishment(UUID establishmentId, UUID userId) {
LOG.debug("[ReviewService] Recherche de l'avis pour établissement " + establishmentId + " et utilisateur " + userId);
return reviewRepository.findByEstablishmentAndUser(establishmentId, userId);
}
/**
* Met à jour un avis.
*
* @param reviewId L'ID de l'avis
* @param userId L'ID de l'utilisateur (pour vérifier les permissions)
* @param dto Le DTO de mise à jour
* @return L'avis mis à jour
*/
@Transactional
public Review updateReview(UUID reviewId, UUID userId, ReviewUpdateRequestDTO dto) {
LOG.info("[ReviewService] Mise à jour de l'avis: " + reviewId + " par l'utilisateur: " + userId);
Review review = getReviewById(reviewId);
// Vérifier que l'utilisateur est l'auteur de l'avis
if (!review.getUser().getId().equals(userId)) {
throw new SecurityException("Vous n'êtes pas autorisé à modifier cet avis");
}
// Mettre à jour les champs non-nuls
if (dto.getOverallRating() != null) {
review.setOverallRating(dto.getOverallRating());
}
if (dto.getComment() != null) {
review.setComment(dto.getComment());
}
if (dto.getCriteriaRatings() != null) {
// Remplacer les notes par critères
review.getCriteriaRatings().clear();
for (Map.Entry<String, Integer> entry : dto.getCriteriaRatings().entrySet()) {
if (entry.getValue() >= 1 && entry.getValue() <= 5) {
review.addCriteriaRating(entry.getKey(), entry.getValue());
}
}
}
reviewRepository.persist(review);
LOG.info("[ReviewService] Avis mis à jour avec succès: " + reviewId);
return review;
}
/**
* Supprime un avis.
*
* @param reviewId L'ID de l'avis
* @param userId L'ID de l'utilisateur (pour vérifier les permissions)
* @return true si supprimé, false sinon
*/
@Transactional
public boolean deleteReview(UUID reviewId, UUID userId) {
LOG.info("[ReviewService] Suppression de l'avis: " + reviewId + " par l'utilisateur: " + userId);
Review review = reviewRepository.findById(reviewId);
if (review == null) {
return false;
}
// Vérifier que l'utilisateur est l'auteur de l'avis
if (!review.getUser().getId().equals(userId)) {
throw new SecurityException("Vous n'êtes pas autorisé à supprimer cet avis");
}
reviewRepository.delete(review);
LOG.info("[ReviewService] Avis supprimé avec succès: " + reviewId);
return true;
}
/**
* Calcule les statistiques des avis pour un établissement.
*
* @param establishmentId L'ID de l'établissement
* @return Map contenant les statistiques
*/
public Map<String, Object> getReviewStats(UUID establishmentId) {
LOG.debug("[ReviewService] Calcul des statistiques pour l'établissement: " + establishmentId);
List<Review> allReviews = reviewRepository.find("establishment.id", establishmentId).list();
Map<String, Object> stats = new HashMap<>();
if (allReviews.isEmpty()) {
stats.put("averageRating", 0.0);
stats.put("totalReviews", 0);
stats.put("verifiedReviews", 0);
stats.put("distribution", new HashMap<Integer, Integer>());
return stats;
}
// Calcul de la moyenne
double average = allReviews.stream()
.mapToInt(Review::getOverallRating)
.average()
.orElse(0.0);
// Distribution par note
Map<Integer, Integer> distribution = new HashMap<>();
for (int i = 1; i <= 5; i++) {
final int rating = i;
distribution.put(i, (int) allReviews.stream()
.filter(r -> r.getOverallRating() == rating)
.count());
}
// Nombre d'avis vérifiés
long verifiedCount = allReviews.stream()
.filter(r -> Boolean.TRUE.equals(r.getIsVerifiedVisit()))
.count();
stats.put("averageRating", Math.round(average * 10.0) / 10.0); // Arrondi à 1 décimale
stats.put("totalReviews", allReviews.size());
stats.put("verifiedReviews", (int) verifiedCount);
stats.put("distribution", distribution);
return stats;
}
/**
* Compte le nombre d'avis pour un établissement.
*
* @param establishmentId L'ID de l'établissement
* @return Le nombre d'avis
*/
public long countReviewsByEstablishment(UUID establishmentId) {
return reviewRepository.countByEstablishmentId(establishmentId);
}
}

View File

@@ -1,10 +1,12 @@
package com.lions.dev.service;
import com.lions.dev.entity.friends.Friendship;
import com.lions.dev.entity.social.PostComment;
import com.lions.dev.entity.social.SocialPost;
import com.lions.dev.entity.users.Users;
import com.lions.dev.exception.UserNotFoundException;
import com.lions.dev.repository.FriendshipRepository;
import com.lions.dev.repository.PostCommentRepository;
import com.lions.dev.repository.SocialPostRepository;
import com.lions.dev.repository.UsersRepository;
import com.lions.dev.dto.events.ReactionEvent;
@@ -34,6 +36,9 @@ public class SocialPostService {
@Inject
SocialPostRepository socialPostRepository;
@Inject
PostCommentRepository postCommentRepository;
@Inject
UsersRepository usersRepository;
@@ -78,6 +83,27 @@ public class SocialPostService {
return socialPostRepository.findByUserId(userId);
}
/**
* Récupère tous les posts 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 posts de l'utilisateur
* @throws UserNotFoundException Si l'utilisateur n'existe pas
*/
public List<SocialPost> getPostsByUserId(UUID userId, int page, int size) {
logger.info("[SocialPostService] Récupération des posts pour l'utilisateur ID : " + userId + " (page: " + page + ", size: " + size + ")");
Users user = usersRepository.findById(userId);
if (user == null) {
logger.error("[SocialPostService] Utilisateur non trouvé avec l'ID : " + userId);
throw new UserNotFoundException("Utilisateur non trouvé avec l'ID : " + userId);
}
return socialPostRepository.findByUserIdWithPagination(userId, page, size);
}
/**
* Crée un nouveau post social.
*
@@ -217,6 +243,19 @@ public class SocialPostService {
return socialPostRepository.searchByContent(query);
}
/**
* Recherche des posts par contenu avec pagination.
*
* @param query Le terme de recherche
* @param page Le numéro de la page (0-indexé)
* @param size La taille de la page
* @return Liste paginée des posts correspondant à la recherche
*/
public List<SocialPost> searchPosts(String query, int page, int size) {
logger.info("[SocialPostService] Recherche de posts avec la requête : " + query + " (page: " + page + ", size: " + size + ")");
return socialPostRepository.searchByContentWithPagination(query, page, size);
}
/**
* Like un post (incrémente le compteur de likes).
*
@@ -436,5 +475,245 @@ public class SocialPostService {
// Récupérer les posts de l'utilisateur et de ses amis
return socialPostRepository.findPostsByFriends(userId, friendIds, page, size);
}
// =====================================================================
// GESTION DES COMMENTAIRES
// =====================================================================
/**
* Récupère tous les commentaires d'un post avec pagination.
*
* @param postId L'ID du post
* @param page Le numéro de la page (0-indexé)
* @param size La taille de la page
* @return Liste paginée des commentaires
*/
public List<PostComment> getCommentsByPostId(UUID postId, int page, int size) {
logger.info("[SocialPostService] Récupération des commentaires pour le post ID : " + postId);
// Vérifier que le post existe
SocialPost post = socialPostRepository.findById(postId);
if (post == null) {
logger.error("[SocialPostService] Post non trouvé avec l'ID : " + postId);
throw new IllegalArgumentException("Post non trouvé avec l'ID : " + postId);
}
return postCommentRepository.findByPostId(postId, page, size);
}
/**
* Récupère tous les commentaires d'un post sans pagination.
*
* @param postId L'ID du post
* @return Liste de tous les commentaires
*/
public List<PostComment> getAllCommentsByPostId(UUID postId) {
logger.info("[SocialPostService] Récupération de tous les commentaires pour le post ID : " + postId);
SocialPost post = socialPostRepository.findById(postId);
if (post == null) {
logger.error("[SocialPostService] Post non trouvé avec l'ID : " + postId);
throw new IllegalArgumentException("Post non trouvé avec l'ID : " + postId);
}
return postCommentRepository.findAllByPostId(postId);
}
/**
* Crée un nouveau commentaire sur un post.
*
* @param postId L'ID du post à commenter
* @param userId L'ID de l'utilisateur qui commente
* @param content Le contenu du commentaire
* @return Le commentaire créé
*/
@Transactional
public PostComment createComment(UUID postId, UUID userId, String content) {
logger.info("[SocialPostService] Création d'un commentaire sur le post ID : " + postId + " par l'utilisateur : " + userId);
// Valider le contenu
if (content == null || content.isBlank()) {
throw new IllegalArgumentException("Le contenu du commentaire ne peut pas être vide");
}
if (content.length() > 1000) {
throw new IllegalArgumentException("Le commentaire ne peut pas dépasser 1000 caractères");
}
// Vérifier que le post existe
SocialPost post = socialPostRepository.findById(postId);
if (post == null) {
logger.error("[SocialPostService] Post non trouvé avec l'ID : " + postId);
throw new IllegalArgumentException("Post non trouvé avec l'ID : " + postId);
}
// Vérifier que l'utilisateur existe
Users user = usersRepository.findById(userId);
if (user == null) {
logger.error("[SocialPostService] Utilisateur non trouvé avec l'ID : " + userId);
throw new UserNotFoundException("Utilisateur non trouvé avec l'ID : " + userId);
}
// Créer et persister le commentaire
PostComment comment = new PostComment(content, post, user);
postCommentRepository.persist(comment);
// Incrémenter le compteur de commentaires du post
post.incrementComments();
socialPostRepository.persist(post);
logger.info("[SocialPostService] Commentaire créé avec succès : " + comment.getId());
// Notification pour l'auteur du post (sauf auto-commentaire)
try {
Users author = post.getUser();
if (author != null && !author.getId().equals(userId)) {
String commenterName = user.getFirstName() + " " + user.getLastName();
String preview = content.length() > 60 ? content.substring(0, 60) + "..." : content;
notificationService.createNotification(
"Nouveau commentaire",
commenterName + " a commenté votre post : " + preview,
"post",
author.getId(),
null
);
}
} catch (Exception e) {
logger.error("[SocialPostService] Erreur création notification commentaire : " + e.getMessage());
}
// Publier dans Kafka pour le temps réel
try {
Map<String, Object> reactionData = new HashMap<>();
reactionData.put("ownerId", post.getUser().getId().toString());
reactionData.put("commentsCount", post.getCommentsCount());
reactionData.put("commentContent", content.length() > 100 ? content.substring(0, 100) + "..." : content);
reactionData.put("commenterName", user.getFirstName() + " " + user.getLastName());
reactionData.put("commentId", comment.getId().toString());
ReactionEvent event = new ReactionEvent(
postId.toString(),
"post",
userId.toString(),
"comment",
reactionData
);
reactionEmitter.send(event);
logger.info("[SocialPostService] Événement commentaire publié dans Kafka pour post: " + postId);
} catch (Exception e) {
logger.error("[SocialPostService] Erreur publication Kafka: " + e.getMessage());
}
return comment;
}
/**
* Récupère un commentaire par son ID.
*
* @param commentId L'ID du commentaire
* @return Le commentaire trouvé
*/
public PostComment getCommentById(UUID commentId) {
logger.info("[SocialPostService] Récupération du commentaire ID : " + commentId);
PostComment comment = postCommentRepository.findById(commentId);
if (comment == null) {
logger.error("[SocialPostService] Commentaire non trouvé avec l'ID : " + commentId);
throw new IllegalArgumentException("Commentaire non trouvé avec l'ID : " + commentId);
}
return comment;
}
/**
* Met à jour le contenu d'un commentaire.
* Seul l'auteur du commentaire peut le modifier.
*
* @param commentId L'ID du commentaire
* @param userId L'ID de l'utilisateur qui modifie
* @param newContent Le nouveau contenu
* @return Le commentaire mis à jour
*/
@Transactional
public PostComment updateComment(UUID commentId, UUID userId, String newContent) {
logger.info("[SocialPostService] Mise à jour du commentaire ID : " + commentId + " par l'utilisateur : " + userId);
PostComment comment = postCommentRepository.findById(commentId);
if (comment == null) {
logger.error("[SocialPostService] Commentaire non trouvé avec l'ID : " + commentId);
throw new IllegalArgumentException("Commentaire non trouvé avec l'ID : " + commentId);
}
// Vérifier que l'utilisateur est l'auteur
if (!comment.isAuthor(userId)) {
logger.error("[SocialPostService] L'utilisateur " + userId + " n'est pas l'auteur du commentaire " + commentId);
throw new SecurityException("Vous n'êtes pas autorisé à modifier ce commentaire");
}
// Valider le nouveau contenu
if (newContent == null || newContent.isBlank()) {
throw new IllegalArgumentException("Le contenu du commentaire ne peut pas être vide");
}
if (newContent.length() > 1000) {
throw new IllegalArgumentException("Le commentaire ne peut pas dépasser 1000 caractères");
}
comment.updateContent(newContent);
postCommentRepository.persist(comment);
logger.info("[SocialPostService] Commentaire mis à jour avec succès");
return comment;
}
/**
* Supprime un commentaire.
* Seul l'auteur du commentaire ou l'auteur du post peut le supprimer.
*
* @param commentId L'ID du commentaire
* @param userId L'ID de l'utilisateur qui supprime
* @return true si le commentaire a été supprimé
*/
@Transactional
public boolean deleteComment(UUID commentId, UUID userId) {
logger.info("[SocialPostService] Suppression du commentaire ID : " + commentId + " par l'utilisateur : " + userId);
PostComment comment = postCommentRepository.findById(commentId);
if (comment == null) {
logger.error("[SocialPostService] Commentaire non trouvé avec l'ID : " + commentId);
return false;
}
// Vérifier que l'utilisateur est l'auteur du commentaire OU l'auteur du post
boolean isCommentAuthor = comment.isAuthor(userId);
boolean isPostAuthor = comment.getPost() != null
&& comment.getPost().getUser() != null
&& comment.getPost().getUser().getId().equals(userId);
if (!isCommentAuthor && !isPostAuthor) {
logger.error("[SocialPostService] L'utilisateur " + userId + " n'est pas autorisé à supprimer ce commentaire");
throw new SecurityException("Vous n'êtes pas autorisé à supprimer ce commentaire");
}
// Décrémenter le compteur de commentaires du post
SocialPost post = comment.getPost();
if (post != null && post.getCommentsCount() > 0) {
post.setCommentsCount(post.getCommentsCount() - 1);
socialPostRepository.persist(post);
}
postCommentRepository.delete(comment);
logger.info("[SocialPostService] Commentaire supprimé avec succès");
return true;
}
/**
* Compte le nombre de commentaires pour un post.
*
* @param postId L'ID du post
* @return Le nombre de commentaires
*/
public long countCommentsByPostId(UUID postId) {
return postCommentRepository.countByPostId(postId);
}
}