mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-05-09 04:07:31 +08:00
Add rate limit for posts and comments
This commit is contained in:
@@ -5,6 +5,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
|
|||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
import com.openisle.exception.FieldException;
|
import com.openisle.exception.FieldException;
|
||||||
import com.openisle.exception.NotFoundException;
|
import com.openisle.exception.NotFoundException;
|
||||||
|
import com.openisle.exception.RateLimitException;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -22,6 +23,11 @@ public class GlobalExceptionHandler {
|
|||||||
return ResponseEntity.status(404).body(Map.of("error", ex.getMessage()));
|
return ResponseEntity.status(404).body(Map.of("error", ex.getMessage()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(RateLimitException.class)
|
||||||
|
public ResponseEntity<?> handleRateLimitException(RateLimitException ex) {
|
||||||
|
return ResponseEntity.status(429).body(Map.of("error", ex.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
public ResponseEntity<?> handleException(Exception ex) {
|
public ResponseEntity<?> handleException(Exception ex) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));
|
return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));
|
||||||
|
|||||||
10
src/main/java/com/openisle/exception/RateLimitException.java
Normal file
10
src/main/java/com/openisle/exception/RateLimitException.java
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package com.openisle.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when a user exceeds allowed action rate.
|
||||||
|
*/
|
||||||
|
public class RateLimitException extends RuntimeException {
|
||||||
|
public RateLimitException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import com.openisle.repository.NotificationRepository;
|
|||||||
import com.openisle.service.NotificationService;
|
import com.openisle.service.NotificationService;
|
||||||
import com.openisle.service.SubscriptionService;
|
import com.openisle.service.SubscriptionService;
|
||||||
import com.openisle.model.Role;
|
import com.openisle.model.Role;
|
||||||
|
import com.openisle.exception.RateLimitException;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -34,6 +35,11 @@ public class CommentService {
|
|||||||
private final ImageUploader imageUploader;
|
private final ImageUploader imageUploader;
|
||||||
|
|
||||||
public Comment addComment(String username, Long postId, String content) {
|
public Comment addComment(String username, Long postId, String content) {
|
||||||
|
long recent = commentRepository.countByAuthorAfter(username,
|
||||||
|
java.time.LocalDateTime.now().minusMinutes(1));
|
||||||
|
if (recent >= 3) {
|
||||||
|
throw new RateLimitException("Too many comments");
|
||||||
|
}
|
||||||
User author = userRepository.findByUsername(username)
|
User author = userRepository.findByUsername(username)
|
||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||||
Post post = postRepository.findById(postId)
|
Post post = postRepository.findById(postId)
|
||||||
@@ -61,6 +67,11 @@ public class CommentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Comment addReply(String username, Long parentId, String content) {
|
public Comment addReply(String username, Long parentId, String content) {
|
||||||
|
long recent = commentRepository.countByAuthorAfter(username,
|
||||||
|
java.time.LocalDateTime.now().minusMinutes(1));
|
||||||
|
if (recent >= 3) {
|
||||||
|
throw new RateLimitException("Too many comments");
|
||||||
|
}
|
||||||
User author = userRepository.findByUsername(username)
|
User author = userRepository.findByUsername(username)
|
||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||||
Comment parent = commentRepository.findById(parentId)
|
Comment parent = commentRepository.findById(parentId)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import com.openisle.repository.ReactionRepository;
|
|||||||
import com.openisle.repository.PostSubscriptionRepository;
|
import com.openisle.repository.PostSubscriptionRepository;
|
||||||
import com.openisle.repository.NotificationRepository;
|
import com.openisle.repository.NotificationRepository;
|
||||||
import com.openisle.model.Role;
|
import com.openisle.model.Role;
|
||||||
|
import com.openisle.exception.RateLimitException;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -87,6 +88,11 @@ public class PostService {
|
|||||||
String title,
|
String title,
|
||||||
String content,
|
String content,
|
||||||
java.util.List<Long> tagIds) {
|
java.util.List<Long> tagIds) {
|
||||||
|
long recent = postRepository.countByAuthorAfter(username,
|
||||||
|
java.time.LocalDateTime.now().minusMinutes(5));
|
||||||
|
if (recent >= 1) {
|
||||||
|
throw new RateLimitException("Too many posts");
|
||||||
|
}
|
||||||
if (tagIds == null || tagIds.isEmpty()) {
|
if (tagIds == null || tagIds.isEmpty()) {
|
||||||
throw new IllegalArgumentException("At least one tag required");
|
throw new IllegalArgumentException("At least one tag required");
|
||||||
}
|
}
|
||||||
|
|||||||
37
src/test/java/com/openisle/service/CommentServiceTest.java
Normal file
37
src/test/java/com/openisle/service/CommentServiceTest.java
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package com.openisle.service;
|
||||||
|
|
||||||
|
import com.openisle.repository.CommentRepository;
|
||||||
|
import com.openisle.repository.PostRepository;
|
||||||
|
import com.openisle.repository.UserRepository;
|
||||||
|
import com.openisle.repository.ReactionRepository;
|
||||||
|
import com.openisle.repository.CommentSubscriptionRepository;
|
||||||
|
import com.openisle.repository.NotificationRepository;
|
||||||
|
import com.openisle.exception.RateLimitException;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
class CommentServiceTest {
|
||||||
|
@Test
|
||||||
|
void addCommentRespectsRateLimit() {
|
||||||
|
CommentRepository commentRepo = mock(CommentRepository.class);
|
||||||
|
PostRepository postRepo = mock(PostRepository.class);
|
||||||
|
UserRepository userRepo = mock(UserRepository.class);
|
||||||
|
NotificationService notifService = mock(NotificationService.class);
|
||||||
|
SubscriptionService subService = mock(SubscriptionService.class);
|
||||||
|
ReactionRepository reactionRepo = mock(ReactionRepository.class);
|
||||||
|
CommentSubscriptionRepository subRepo = mock(CommentSubscriptionRepository.class);
|
||||||
|
NotificationRepository nRepo = mock(NotificationRepository.class);
|
||||||
|
ImageUploader imageUploader = mock(ImageUploader.class);
|
||||||
|
|
||||||
|
CommentService service = new CommentService(commentRepo, postRepo, userRepo,
|
||||||
|
notifService, subService, reactionRepo, subRepo, nRepo, imageUploader);
|
||||||
|
|
||||||
|
when(commentRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(3L);
|
||||||
|
|
||||||
|
assertThrows(RateLimitException.class,
|
||||||
|
() -> service.addComment("alice", 1L, "hi"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.openisle.service;
|
|||||||
|
|
||||||
import com.openisle.model.*;
|
import com.openisle.model.*;
|
||||||
import com.openisle.repository.*;
|
import com.openisle.repository.*;
|
||||||
|
import com.openisle.exception.RateLimitException;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -50,4 +51,31 @@ class PostServiceTest {
|
|||||||
verify(postReadService).deleteByPost(post);
|
verify(postReadService).deleteByPost(post);
|
||||||
verify(postRepo).delete(post);
|
verify(postRepo).delete(post);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createPostRespectsRateLimit() {
|
||||||
|
PostRepository postRepo = mock(PostRepository.class);
|
||||||
|
UserRepository userRepo = mock(UserRepository.class);
|
||||||
|
CategoryRepository catRepo = mock(CategoryRepository.class);
|
||||||
|
TagRepository tagRepo = mock(TagRepository.class);
|
||||||
|
NotificationService notifService = mock(NotificationService.class);
|
||||||
|
SubscriptionService subService = mock(SubscriptionService.class);
|
||||||
|
CommentService commentService = mock(CommentService.class);
|
||||||
|
CommentRepository commentRepo = mock(CommentRepository.class);
|
||||||
|
ReactionRepository reactionRepo = mock(ReactionRepository.class);
|
||||||
|
PostSubscriptionRepository subRepo = mock(PostSubscriptionRepository.class);
|
||||||
|
NotificationRepository notificationRepo = mock(NotificationRepository.class);
|
||||||
|
PostReadService postReadService = mock(PostReadService.class);
|
||||||
|
ImageUploader imageUploader = mock(ImageUploader.class);
|
||||||
|
|
||||||
|
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo,
|
||||||
|
notifService, subService, commentService, commentRepo,
|
||||||
|
reactionRepo, subRepo, notificationRepo, postReadService,
|
||||||
|
imageUploader, PublishMode.DIRECT);
|
||||||
|
|
||||||
|
when(postRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(1L);
|
||||||
|
|
||||||
|
assertThrows(RateLimitException.class,
|
||||||
|
() -> service.createPost("alice", 1L, "t", "c", List.of(1L)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user