diff --git a/src/main/java/com/guams/review/configuration/JwtAuthenticationFilter.java b/src/main/java/com/guams/review/configuration/JwtAuthenticationFilter.java index fcca4b0..4850def 100644 --- a/src/main/java/com/guams/review/configuration/JwtAuthenticationFilter.java +++ b/src/main/java/com/guams/review/configuration/JwtAuthenticationFilter.java @@ -1,4 +1,5 @@ package com.guams.review.configuration; + import com.guams.review.service.AuthorService; import io.jsonwebtoken.JwtException; import jakarta.servlet.FilterChain; @@ -11,7 +12,11 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; + import java.io.IOException; +import java.time.Instant; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @@ -19,6 +24,13 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenUtil jwtTokenUtil; private final AuthorService authorService; + // Map pour traquer les requêtes : UUID -> { dernière réinitialisation, compteur de requêtes } + private final ConcurrentHashMap rateLimiters = new ConcurrentHashMap<>(); + + // Configuration du rate limiting + private static final int MAX_REQUESTS = 5; + private static final long TIME_WINDOW_MS = 60 * 1000; // 1 minute + public JwtAuthenticationFilter(JwtTokenUtil jwtTokenUtil, AuthorService authorService) { this.jwtTokenUtil = jwtTokenUtil; this.authorService = authorService; @@ -35,9 +47,16 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { String authorUuid = jwtTokenUtil.extractAuthorId(token); // Récupère l'UUID du token if (authorUuid != null && SecurityContextHolder.getContext().getAuthentication() == null) { - UserDetails userDetails = authorService.loadUserByUsername(authorUuid); // Utilise le service pour charger l'auteur par UUID + UserDetails userDetails = authorService.loadUserByUsername(authorUuid); // Charge l'auteur if (jwtTokenUtil.validateToken(token, userDetails, authorUuid)) { + // Applique le rate limiting + if (isRateLimited(authorUuid, request)) { + response.setStatus(429); // TOO MANY REQUESTS + response.getWriter().write("Rate limit exceeded. Please try again later."); + return; + } + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); @@ -50,7 +69,42 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { } } - filterChain.doFilter(request, response); // Continue le traitement } + + private boolean isRateLimited(String authorUuid, HttpServletRequest request) { + // Ne limite que les requêtes POST + if (!"POST".equalsIgnoreCase(request.getMethod())) { + return false; + } + + // Récupère ou crée un RateLimiter pour l'utilisateur + RateLimiter limiter = rateLimiters.computeIfAbsent(authorUuid, k -> new RateLimiter()); + + synchronized (limiter) { + long now = Instant.now().toEpochMilli(); + if (now - limiter.lastReset > TIME_WINDOW_MS) { + // Réinitialise la fenêtre de temps et le compteur + limiter.lastReset = now; + limiter.requestCount.set(0); + } + + if (limiter.requestCount.incrementAndGet() > MAX_REQUESTS) { + return true; // Limite atteinte + } + } + + return false; + } + + // Classe interne pour suivre les requêtes + private static class RateLimiter { + private long lastReset; + private AtomicInteger requestCount; + + public RateLimiter() { + this.lastReset = Instant.now().toEpochMilli(); + this.requestCount = new AtomicInteger(0); + } + } } diff --git a/src/main/java/com/guams/review/controller/AuthorController.java b/src/main/java/com/guams/review/controller/AuthorController.java index 4a61f5a..4203302 100644 --- a/src/main/java/com/guams/review/controller/AuthorController.java +++ b/src/main/java/com/guams/review/controller/AuthorController.java @@ -64,6 +64,12 @@ public class AuthorController { } } +// @PutMapping("/{id}/password") +// public void changePassword(@PathVariable UUID id, @RequestBody Author updatedAuthor, Authentication authentication) { +// Author authorToUpdate = authorService.verifyIfUserIsAuthorized(authentication, id); +// if (passwordEncoder.matches(updatedAuthor.getPassword(), authorToUpdate.getPassword())) {} +// } + @PutMapping(value = "{id}/avatar", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}) public Author updateUserAvatar(@PathVariable UUID id, @RequestPart MultipartFile avatar, Authentication authentication) throws IOException { Author authorToUpdate = authorService.verifyIfUserIsAuthorized(authentication, id); diff --git a/src/main/java/com/guams/review/controller/CommentController.java b/src/main/java/com/guams/review/controller/CommentController.java index 8e38333..f03e5f6 100644 --- a/src/main/java/com/guams/review/controller/CommentController.java +++ b/src/main/java/com/guams/review/controller/CommentController.java @@ -85,7 +85,7 @@ public class CommentController { throw new UnauthorizedExecption("You are not authorized to access this resource"); } Comment commentToDelete = commentService.findById(id).orElseThrow(() -> new NotFoundException("Comment not found")); - commentService.delete(commentToDelete); commentService.deleteAssociationByCommentId(commentToDelete.getId()); + commentService.delete(commentToDelete); } } diff --git a/src/main/java/com/guams/review/controller/PostController.java b/src/main/java/com/guams/review/controller/PostController.java index 574ffa1..cb12e72 100644 --- a/src/main/java/com/guams/review/controller/PostController.java +++ b/src/main/java/com/guams/review/controller/PostController.java @@ -107,7 +107,14 @@ public class PostController { Author authenticatedAuthor = authorService.findByName(authentication.getName()).orElseThrow(() -> new NotFoundException("Author not found")); if (authorService.listPublicationOfAuthor(authenticatedAuthor.getId()).stream().map(Post::getId).toList().contains(id)) { Post postToDelete = postService.findById(id).orElseThrow(() -> new NotFoundException("Post not found")); - commentService.getCommentsByPostId(id).stream().map(mapper::mapCommentWithAuthor).forEach(commentService::delete); + commentService.getCommentsByPostId(id) + .stream() + .map(mapper::mapCommentWithAuthor) + .forEach(comment -> commentService.deleteAssociationByCommentId(comment.getId())); + commentService.getCommentsByPostId(id) + .stream() + .map(mapper::mapCommentWithAuthor) + .forEach(commentService::delete); postService.delete(authenticatedAuthor.getId(), postToDelete.getId()); } else { throw new UnauthorizedExecption("You do not have permission to delete this post");