From 02c7e8887e62cf37b395632e4651ca39507b95d0 Mon Sep 17 00:00:00 2001 From: Jesse Brault Date: Wed, 4 Feb 2026 17:58:08 -0600 Subject: [PATCH] MME-7 Add resource exists endpoint for images. --- .../api/image/ImageControllerTests.java | 2 +- .../api/image/ImageController.java | 17 ++++++++++++++--- .../image/ImageEndpointAuthConfigurator.java | 1 + .../mealsmadeeasy/api/image/ImageSecurity.java | 1 + .../api/image/ImageSecurityImpl.java | 5 +++++ .../mealsmadeeasy/api/image/ImageService.java | 2 ++ .../mealsmadeeasy/api/image/S3ImageService.java | 6 ++++++ .../api/util/ResourceExistsView.java | 3 +++ 8 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 src/main/java/app/mealsmadeeasy/api/util/ResourceExistsView.java diff --git a/src/integrationTest/java/app/mealsmadeeasy/api/image/ImageControllerTests.java b/src/integrationTest/java/app/mealsmadeeasy/api/image/ImageControllerTests.java index 4dbbc7c..b491e82 100644 --- a/src/integrationTest/java/app/mealsmadeeasy/api/image/ImageControllerTests.java +++ b/src/integrationTest/java/app/mealsmadeeasy/api/image/ImageControllerTests.java @@ -188,7 +188,7 @@ public class ImageControllerTests { } @Test - public void putImage() throws Exception { + public void uploadImage() throws Exception { final User owner = this.seedUser(); final String accessToken = this.getAccessToken(owner); try (final InputStream hal9000 = getHal9000InputStream()) { diff --git a/src/main/java/app/mealsmadeeasy/api/image/ImageController.java b/src/main/java/app/mealsmadeeasy/api/image/ImageController.java index d0e15c7..05a0ef8 100644 --- a/src/main/java/app/mealsmadeeasy/api/image/ImageController.java +++ b/src/main/java/app/mealsmadeeasy/api/image/ImageController.java @@ -8,6 +8,7 @@ import app.mealsmadeeasy.api.image.view.ImageView; import app.mealsmadeeasy.api.user.User; import app.mealsmadeeasy.api.user.UserService; import app.mealsmadeeasy.api.util.AccessDeniedView; +import app.mealsmadeeasy.api.util.ResourceExistsView; import lombok.RequiredArgsConstructor; import org.springframework.core.io.InputStreamResource; import org.springframework.http.HttpStatus; @@ -61,8 +62,8 @@ public class ImageController { .body(new InputStreamResource(imageInputStream)); } - @PutMapping - public ResponseEntity putImage( + @PostMapping + public ResponseEntity uploadImage( @RequestParam MultipartFile image, @RequestParam String filename, @RequestParam(required = false) String alt, @@ -89,7 +90,7 @@ public class ImageController { return ResponseEntity.status(201).body(this.imageToViewConverter.convert(saved, principal, true)); } - @PostMapping("/{username}/{filename}") + @PutMapping("/{username}/{filename}") public ResponseEntity updateInfo( @AuthenticationPrincipal User principal, @PathVariable String username, @@ -106,6 +107,16 @@ public class ImageController { return ResponseEntity.ok(this.imageToViewConverter.convert(updated, principal, true)); } + @GetMapping("/{username}/{filename}/exists") + public ResponseEntity resourceExists( + @AuthenticationPrincipal User principal, + @PathVariable String username, + @PathVariable String filename + ) { + final boolean exists = this.imageService.exists(principal, username, filename); + return ResponseEntity.ok(new ResourceExistsView(exists)); + } + @DeleteMapping("/{username}/{filename}") public ResponseEntity deleteImage( @AuthenticationPrincipal User principal, diff --git a/src/main/java/app/mealsmadeeasy/api/image/ImageEndpointAuthConfigurator.java b/src/main/java/app/mealsmadeeasy/api/image/ImageEndpointAuthConfigurator.java index 620895f..9676596 100644 --- a/src/main/java/app/mealsmadeeasy/api/image/ImageEndpointAuthConfigurator.java +++ b/src/main/java/app/mealsmadeeasy/api/image/ImageEndpointAuthConfigurator.java @@ -12,6 +12,7 @@ public class ImageEndpointAuthConfigurator implements EndpointAuthConfigurator { @Override public void configure(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { registry.requestMatchers(HttpMethod.GET, "/images/**").permitAll(); + registry.requestMatchers(HttpMethod.GET, "/images/*/*/exists").authenticated(); registry.requestMatchers(HttpMethod.POST, "/images/**").authenticated(); registry.requestMatchers(HttpMethod.PUT, "/images/**").authenticated(); registry.requestMatchers(HttpMethod.DELETE, "/images/**").authenticated(); diff --git a/src/main/java/app/mealsmadeeasy/api/image/ImageSecurity.java b/src/main/java/app/mealsmadeeasy/api/image/ImageSecurity.java index 8790a8a..e7ac215 100644 --- a/src/main/java/app/mealsmadeeasy/api/image/ImageSecurity.java +++ b/src/main/java/app/mealsmadeeasy/api/image/ImageSecurity.java @@ -6,4 +6,5 @@ import org.jetbrains.annotations.Nullable; public interface ImageSecurity { boolean isViewableBy(Image image, @Nullable User viewer); boolean isOwner(Image image, @Nullable User user); + boolean canSeeExists(User viewer, String username, String filename); } diff --git a/src/main/java/app/mealsmadeeasy/api/image/ImageSecurityImpl.java b/src/main/java/app/mealsmadeeasy/api/image/ImageSecurityImpl.java index 4630fbe..7765b25 100644 --- a/src/main/java/app/mealsmadeeasy/api/image/ImageSecurityImpl.java +++ b/src/main/java/app/mealsmadeeasy/api/image/ImageSecurityImpl.java @@ -45,4 +45,9 @@ public class ImageSecurityImpl implements ImageSecurity { return image.getOwner() != null && user != null && Objects.equals(image.getOwner().getId(), user.getId()); } + @Override + public boolean canSeeExists(User viewer, String username, String filename) { + return viewer.getUsername().equals(username); + } + } diff --git a/src/main/java/app/mealsmadeeasy/api/image/ImageService.java b/src/main/java/app/mealsmadeeasy/api/image/ImageService.java index bc7112d..f96579b 100644 --- a/src/main/java/app/mealsmadeeasy/api/image/ImageService.java +++ b/src/main/java/app/mealsmadeeasy/api/image/ImageService.java @@ -21,6 +21,8 @@ public interface ImageService { InputStream getImageContent(Image image, @Nullable User viewer) throws IOException; List getImagesOwnedBy(User user); + boolean exists(User viewer, String username, String filename); + Image update(Image image, User modifier, ImageUpdateSpec spec); void deleteImage(Image image, User modifier) throws IOException; diff --git a/src/main/java/app/mealsmadeeasy/api/image/S3ImageService.java b/src/main/java/app/mealsmadeeasy/api/image/S3ImageService.java index aecf79a..9609384 100644 --- a/src/main/java/app/mealsmadeeasy/api/image/S3ImageService.java +++ b/src/main/java/app/mealsmadeeasy/api/image/S3ImageService.java @@ -196,6 +196,12 @@ public class S3ImageService implements ImageService { return new ArrayList<>(this.imageRepository.findAllByOwner(user)); } + @Override + @PreAuthorize("@imageSecurity.canSeeExists(#viewer, #username, #filename)") + public boolean exists(User viewer, String username, String filename) { + return this.imageRepository.findByOwnerUsernameAndFilename(username, filename).isPresent(); + } + @Override @PreAuthorize("@imageSecurity.isOwner(#image, #modifier)") public Image update(final Image image, User modifier, ImageUpdateSpec updateSpec) { diff --git a/src/main/java/app/mealsmadeeasy/api/util/ResourceExistsView.java b/src/main/java/app/mealsmadeeasy/api/util/ResourceExistsView.java new file mode 100644 index 0000000..2b28f56 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/util/ResourceExistsView.java @@ -0,0 +1,3 @@ +package app.mealsmadeeasy.api.util; + +public record ResourceExistsView(boolean exists) {}