From 19d3b3446a3a9803742be8330b36530dd3c912bc Mon Sep 17 00:00:00 2001 From: Lucas Aguiar Date: Tue, 16 Dec 2025 20:53:58 -0300 Subject: [PATCH 1/2] fix(validation): prevent non-sponsors from using GIF URLs - Add validateGif() method to enforce GIF restrictions server-side - Prevent users from bypassing frontend validation by copying sponsor URLs - GIFs remain blocked for banners regardless of sponsor status --- .../implementation/user/UserServiceImpl.java | 24 ++++--- .../infra/exception/ControllerAdvice.java | 69 ++++++++++--------- .../infra/exception/CustomExceptions.java | 13 ++++ 3 files changed, 67 insertions(+), 39 deletions(-) diff --git a/src/main/java/br/com/notehub/application/implementation/user/UserServiceImpl.java b/src/main/java/br/com/notehub/application/implementation/user/UserServiceImpl.java index 9b44b73..92f68b6 100644 --- a/src/main/java/br/com/notehub/application/implementation/user/UserServiceImpl.java +++ b/src/main/java/br/com/notehub/application/implementation/user/UserServiceImpl.java @@ -10,7 +10,6 @@ import br.com.notehub.domain.user.User; import br.com.notehub.domain.user.UserRepository; import br.com.notehub.domain.user.UserService; -import br.com.notehub.infra.exception.CustomExceptions; import jakarta.annotation.Nullable; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; @@ -34,6 +33,8 @@ import java.util.function.Function; import java.util.stream.Collectors; +import static br.com.notehub.infra.exception.CustomExceptions.*; + @Component @RequiredArgsConstructor public class UserServiceImpl implements UserService { @@ -46,6 +47,12 @@ public class UserServiceImpl implements UserService { private final PasswordEncoder encoder; private final Counter counter; + private void validateGif(User user, String img, String field) { + if (img == null || !img.endsWith(".gif")) return; + if (field.equals("banner")) throw new GifNotAllowedException("banner", "GIFs são proibidos como banner."); + if (!user.isSponsor()) throw new GifNotAllowedException("avatar", "GIFs apenas para patrocinadores."); + } + @SneakyThrows private void changeField(UUID idFromToken, String field, Function getter, Consumer setter) { User user = repository.findById(idFromToken).orElseThrow(EntityNotFoundException::new); @@ -53,19 +60,20 @@ private void changeField(UUID idFromToken, String field, Function g setter.accept(user); T newValue = getter.apply(user); if (Objects.equals(oldValue, newValue)) return; - if (user.isBlocked() && (field.equals("avatar") || field.equals("banner"))) throw new CustomExceptions.UserBlockedException(field); + if (user.isBlocked() && (field.equals("avatar") || field.equals("banner"))) throw new UserBlockedException(field); + if (field.equals("avatar") || field.equals("banner")) validateGif(user, (String) newValue, field); repository.save(user); historian.setHistory(user, field, String.valueOf(oldValue), String.valueOf(newValue)); } private String validatePassword(String oldPassword, String newPassword) { - if (encoder.matches(newPassword, oldPassword)) throw new CustomExceptions.SamePasswordException(); + if (encoder.matches(newPassword, oldPassword)) throw new SamePasswordException(); return encoder.encode(newPassword); } private void validateEmail(String oldEmail, String newEmail) { repository.findByEmail(newEmail).ifPresent(user -> { - if (Objects.equals(oldEmail, newEmail)) throw new CustomExceptions.SameEmailExpection(); + if (Objects.equals(oldEmail, newEmail)) throw new SameEmailExpection(); throw new DataIntegrityViolationException("email"); }); } @@ -106,7 +114,7 @@ private Page getUserConnections(Pageable pageable, String q, UUID userRequ private boolean isFollowing(User follower, User following) { if (Objects.equals(follower.getId(), following.getId())) { - throw new CustomExceptions.SelfFollowException(); + throw new SelfFollowException(); } return following.getFollowers().contains(follower); } @@ -115,7 +123,7 @@ private Subscription validateSubscription(String subscriptionStr) { try { return Subscription.from(subscriptionStr); } catch (IllegalArgumentException e) { - throw new CustomExceptions.SubscriptionException("Inscrição inválida."); + throw new SubscriptionException("Inscrição inválida."); } } @@ -237,7 +245,7 @@ public void disallowSubscription(UUID idFromToken, String subscriptionStr) { public void follow(UUID idFromToken, String username) { User follower = repository.findByIdWithFollowersAndFollowing(idFromToken).orElseThrow(EntityNotFoundException::new); User following = repository.findByUsernameWithFollowersAndFollowing(username).orElseThrow(EntityNotFoundException::new); - if (isFollowing(follower, following)) throw new CustomExceptions.AlreadyFollowingException(); + if (isFollowing(follower, following)) throw new AlreadyFollowingException(); counter.updateFollowersAndFollowingCount(follower, following, true); notifier.notify(follower, following, follower, MessageNotification.of(follower)); } @@ -247,7 +255,7 @@ public void follow(UUID idFromToken, String username) { public void unfollow(UUID idFromToken, String username) { User follower = repository.findByIdWithFollowersAndFollowing(idFromToken).orElseThrow(EntityNotFoundException::new); User following = repository.findByUsernameWithFollowersAndFollowing(username).orElseThrow(EntityNotFoundException::new); - if (!isFollowing(follower, following)) throw new CustomExceptions.NotFollowingException(); + if (!isFollowing(follower, following)) throw new NotFollowingException(); counter.updateFollowersAndFollowingCount(follower, following, false); } diff --git a/src/main/java/br/com/notehub/infra/exception/ControllerAdvice.java b/src/main/java/br/com/notehub/infra/exception/ControllerAdvice.java index bcffe29..578f263 100644 --- a/src/main/java/br/com/notehub/infra/exception/ControllerAdvice.java +++ b/src/main/java/br/com/notehub/infra/exception/ControllerAdvice.java @@ -3,7 +3,6 @@ import com.auth0.jwt.exceptions.JWTCreationException; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.exceptions.TokenExpiredException; -import com.stripe.exception.StripeException; import jakarta.persistence.EntityExistsException; import jakarta.persistence.EntityNotFoundException; import org.springframework.dao.DataIntegrityViolationException; @@ -24,6 +23,8 @@ import java.util.ArrayList; import java.util.List; +import static br.com.notehub.infra.exception.CustomExceptions.*; + @RestControllerAdvice public class ControllerAdvice { @@ -40,43 +41,43 @@ private ResponseEntity> handleMethdArgumentNotValidExceptio return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); } - @ExceptionHandler(CustomExceptions.CustomStripeException.class) - private ResponseEntity> handleStripeException(CustomExceptions.CustomStripeException ex) { + @ExceptionHandler(CustomStripeException.class) + private ResponseEntity> handleStripeException(CustomStripeException ex) { List errors = new ArrayList<>(); errors.add(new FieldError("Payment", "Payment", ex.getMessage())); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors.stream().map(CustomResponse::new).toList()); } - @ExceptionHandler(CustomExceptions.InvalidSecretException.class) - private ResponseEntity> handleInvalidSecretException(CustomExceptions.InvalidSecretException ex) { + @ExceptionHandler(InvalidSecretException.class) + private ResponseEntity> handleInvalidSecretException(InvalidSecretException ex) { List errors = new ArrayList<>(); errors.add(new FieldError("Headers", "Headers", ex.getMessage())); return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errors.stream().map(CustomResponse::new).toList()); } - @ExceptionHandler(CustomExceptions.MissingDeviceException.class) - private ResponseEntity> handleMissingDeviceException(CustomExceptions.MissingDeviceException ex) { + @ExceptionHandler(MissingDeviceException.class) + private ResponseEntity> handleMissingDeviceException(MissingDeviceException ex) { List errors = new ArrayList<>(); errors.add(new FieldError("Headers", "Headers", ex.getMessage())); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors.stream().map(CustomResponse::new).toList()); } - @ExceptionHandler(CustomExceptions.InvalidDeviceException.class) - private ResponseEntity> handleInvalidDeviceException(CustomExceptions.InvalidDeviceException ex) { + @ExceptionHandler(InvalidDeviceException.class) + private ResponseEntity> handleInvalidDeviceException(InvalidDeviceException ex) { List errors = new ArrayList<>(); errors.add(new FieldError("Headers", "Headers", ex.getMessage())); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors.stream().map(CustomResponse::new).toList()); } - @ExceptionHandler(CustomExceptions.MissingRefreshToken.class) - private ResponseEntity> handleMissingRefreshTokenException(CustomExceptions.MissingRefreshToken ex) { + @ExceptionHandler(MissingRefreshToken.class) + private ResponseEntity> handleMissingRefreshTokenException(MissingRefreshToken ex) { List errors = new ArrayList<>(); errors.add(new FieldError("Headers", "Headers", ex.getMessage())); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors.stream().map(CustomResponse::new).toList()); } - @ExceptionHandler(CustomExceptions.InvalidRefreshTokenException.class) - private ResponseEntity> handleInvalidRefreshTokenException(CustomExceptions.InvalidRefreshTokenException ex) { + @ExceptionHandler(InvalidRefreshTokenException.class) + private ResponseEntity> handleInvalidRefreshTokenException(InvalidRefreshTokenException ex) { List errors = new ArrayList<>(); errors.add(new FieldError("Headers", "Headers", ex.getMessage())); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors.stream().map(CustomResponse::new).toList()); @@ -176,8 +177,8 @@ private ResponseEntity> handleIllegalStateException(Illegal return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errors.stream().map(CustomResponse::new).toList()); } - @ExceptionHandler(CustomExceptions.UserBlockedException.class) - private ResponseEntity> handleUserBlockedException(CustomExceptions.UserBlockedException ex) { + @ExceptionHandler(UserBlockedException.class) + private ResponseEntity> handleUserBlockedException(UserBlockedException ex) { List errors = new ArrayList<>(); return switch (ex.getMessage()) { case "avatar" -> { @@ -213,60 +214,66 @@ private ResponseEntity> handleTokenExpiredException(TokenEx return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errors.stream().map(CustomResponse::new).toList()); } - @ExceptionHandler(CustomExceptions.ScopeNotAllowedException.class) - private ResponseEntity> handleScopeNotAllowedException(CustomExceptions.ScopeNotAllowedException ex) { + @ExceptionHandler(ScopeNotAllowedException.class) + private ResponseEntity> handleScopeNotAllowedException(ScopeNotAllowedException ex) { List errors = new ArrayList<>(); errors.add(new FieldError("token", "scope", ex.getMessage())); return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errors.stream().map(CustomResponse::new).toList()); } - @ExceptionHandler(CustomExceptions.SamePasswordException.class) - private ResponseEntity> handleSamePasswordException(CustomExceptions.SamePasswordException ex) { + @ExceptionHandler(SamePasswordException.class) + private ResponseEntity> handleSamePasswordException(SamePasswordException ex) { List errors = new ArrayList<>(); errors.add(new FieldError("user", "password", ex.getMessage())); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors.stream().map(CustomResponse::new).toList()); } - @ExceptionHandler(CustomExceptions.SameEmailExpection.class) - private ResponseEntity> handleSameEmailExpection(CustomExceptions.SameEmailExpection ex) { + @ExceptionHandler(SameEmailExpection.class) + private ResponseEntity> handleSameEmailExpection(SameEmailExpection ex) { List errors = new ArrayList<>(); errors.add(new FieldError("user", "email", ex.getMessage())); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors.stream().map(CustomResponse::new).toList()); } - @ExceptionHandler(CustomExceptions.SelfFollowException.class) - private ResponseEntity> handleSelfFollowException(CustomExceptions.SelfFollowException ex) { + @ExceptionHandler(SelfFollowException.class) + private ResponseEntity> handleSelfFollowException(SelfFollowException ex) { List errors = new ArrayList<>(); errors.add(new FieldError("user", "user", ex.getMessage())); return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errors.stream().map(CustomResponse::new).toList()); } - @ExceptionHandler(CustomExceptions.AlreadyFollowingException.class) - private ResponseEntity> handleAlreadyFollowingException(CustomExceptions.AlreadyFollowingException ex) { + @ExceptionHandler(AlreadyFollowingException.class) + private ResponseEntity> handleAlreadyFollowingException(AlreadyFollowingException ex) { List errors = new ArrayList<>(); errors.add(new FieldError("user", "user", ex.getMessage())); return ResponseEntity.status(HttpStatus.CONFLICT).body(errors.stream().map(CustomResponse::new).toList()); } - @ExceptionHandler(CustomExceptions.NotFollowingException.class) - private ResponseEntity> handleNotFollowingException(CustomExceptions.NotFollowingException ex) { + @ExceptionHandler(NotFollowingException.class) + private ResponseEntity> handleNotFollowingException(NotFollowingException ex) { List errors = new ArrayList<>(); errors.add(new FieldError("user", "user", ex.getMessage())); return ResponseEntity.status(HttpStatus.CONFLICT).body(errors.stream().map(CustomResponse::new).toList()); } - @ExceptionHandler(CustomExceptions.HostNotAllowedException.class) - private ResponseEntity> handleHostNotAllowedException(CustomExceptions.HostNotAllowedException ex) { + @ExceptionHandler(HostNotAllowedException.class) + private ResponseEntity> handleHostNotAllowedException(HostNotAllowedException ex) { List errors = new ArrayList<>(); errors.add(new FieldError("user", "host", ex.getMessage())); return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errors.stream().map(CustomResponse::new).toList()); } - @ExceptionHandler(CustomExceptions.SubscriptionException.class) - private ResponseEntity> handleSubscriptionException(CustomExceptions.SubscriptionException ex) { + @ExceptionHandler(SubscriptionException.class) + private ResponseEntity> handleSubscriptionException(SubscriptionException ex) { List errors = new ArrayList<>(); errors.add(new FieldError("parameter", "subscription", ex.getMessage())); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors.stream().map(CustomResponse::new).toList()); } + @ExceptionHandler(GifNotAllowedException.class) + private ResponseEntity> handleForbiddenImageFormatException(GifNotAllowedException ex) { + FieldError error = new FieldError("user", ex.getField(), ex.getMessage()); + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(List.of(new CustomResponse(error))); + } + } \ No newline at end of file diff --git a/src/main/java/br/com/notehub/infra/exception/CustomExceptions.java b/src/main/java/br/com/notehub/infra/exception/CustomExceptions.java index ea79282..1951d6e 100644 --- a/src/main/java/br/com/notehub/infra/exception/CustomExceptions.java +++ b/src/main/java/br/com/notehub/infra/exception/CustomExceptions.java @@ -1,6 +1,7 @@ package br.com.notehub.infra.exception; import com.stripe.exception.StripeException; +import lombok.Getter; import java.util.UUID; @@ -112,4 +113,16 @@ public SubscriptionException(String message) { } } + @Getter + public static class GifNotAllowedException extends BusinessException { + + private final String field; + + public GifNotAllowedException(String field, String message) { + super(message); + this.field = field; + } + + } + } \ No newline at end of file From c46175a336d9a41b7f61573123a1cb02b2e81e76 Mon Sep 17 00:00:00 2001 From: Lucas Aguiar Date: Wed, 17 Dec 2025 10:44:43 -0300 Subject: [PATCH 2/2] chore(release): update version to 2.0.1 --- Dockerfile | 2 +- README.md | 4 ++-- pom.xml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3fd9477..9ba087d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,6 @@ RUN ./mvnw clean install FROM eclipse-temurin:21-jdk-alpine -COPY --from=build ./target/NoteHub-2.0.0.jar app.jar +COPY --from=build ./target/NoteHub-2.0.1.jar app.jar ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/README.md b/README.md index 0c2dddf..8ebb401 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@
diff --git a/pom.xml b/pom.xml index ab8e8e0..24d0eec 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ br.com.notehub NoteHub - 2.0.0 + 2.0.1 NoteHub https://notehub.com.br