diff --git a/src/integrationTest/java/app/mealsmadeeasy/api/recipe/RecipeControllerTests.java b/src/integrationTest/java/app/mealsmadeeasy/api/recipe/RecipeControllerTests.java index 214502f..d1d44ab 100644 --- a/src/integrationTest/java/app/mealsmadeeasy/api/recipe/RecipeControllerTests.java +++ b/src/integrationTest/java/app/mealsmadeeasy/api/recipe/RecipeControllerTests.java @@ -35,7 +35,7 @@ public class RecipeControllerTests { } private Recipe createTestRecipe(User owner) { - return this.recipeService.create(owner, "Test Recipe", "Hello, World!"); + return this.recipeService.create(owner, "Test Recipe", "# Hello, World!"); } @Test @@ -46,7 +46,12 @@ public class RecipeControllerTests { this.mockMvc.perform(get("/recipe/{id}", recipe.getId())) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(1)) - .andExpect(jsonPath("$.title").value("Test Recipe")); + .andExpect(jsonPath("$.title").value("Test Recipe")) + .andExpect(jsonPath("$.text").value("

Hello, World!

")) + .andExpect(jsonPath("$.ownerId").value(owner.getId())) + .andExpect(jsonPath("$.ownerUsername").value(owner.getUsername())) + .andExpect(jsonPath("$.starCount").value(0)) + .andExpect(jsonPath("$.viewerCount").value(0)); } } diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeController.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeController.java index b70800a..8b4a9ea 100644 --- a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeController.java +++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeController.java @@ -1,7 +1,7 @@ package app.mealsmadeeasy.api.recipe; import app.mealsmadeeasy.api.recipe.view.RecipeExceptionView; -import app.mealsmadeeasy.api.recipe.view.RecipeGetView; +import app.mealsmadeeasy.api.recipe.view.RecipePageView; import app.mealsmadeeasy.api.user.User; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -26,15 +26,9 @@ public class RecipeController { } @GetMapping("/{id}") - public ResponseEntity getById(@PathVariable long id, @AuthenticationPrincipal User user) + public ResponseEntity getById(@PathVariable long id, @AuthenticationPrincipal User user) throws RecipeException { - final Recipe recipe; - if (user != null) { - recipe = this.recipeService.getById(id, user); - } else { - recipe = this.recipeService.getById(id); - } - return ResponseEntity.ok(new RecipeGetView(recipe.getId(), recipe.getTitle())); + return ResponseEntity.ok(this.recipeService.getPageViewById(id, user)); } } diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeRepository.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeRepository.java index dcfbc8f..b3f2643 100644 --- a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeRepository.java +++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeRepository.java @@ -28,4 +28,10 @@ public interface RecipeRepository extends JpaRepository { @EntityGraph(attributePaths = { "stars" }) Optional findByIdWithStars(long id); + @Query("SELECT size(r.stars) FROM Recipe r WHERE r.id = ?1") + int getStarCount(long recipeId); + + @Query("SELECT size(r.viewers) FROM Recipe r WHERE r.id = ?1") + int getViewerCount(long recipeId); + } diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeSecurity.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeSecurity.java index 52eedc8..13eb23d 100644 --- a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeSecurity.java +++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeSecurity.java @@ -1,9 +1,11 @@ package app.mealsmadeeasy.api.recipe; import app.mealsmadeeasy.api.user.User; +import org.jetbrains.annotations.Nullable; public interface RecipeSecurity { boolean isOwner(Recipe recipe, User user); boolean isOwner(long recipeId, User user) throws RecipeException; - boolean isViewableBy(Recipe recipe, User user); + boolean isViewableBy(Recipe recipe, @Nullable User user); + boolean isViewableBy(long recipeId, @Nullable User user) throws RecipeException; } diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeSecurityImpl.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeSecurityImpl.java index ea9a9c3..6c2bcbd 100644 --- a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeSecurityImpl.java +++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeSecurityImpl.java @@ -1,6 +1,7 @@ package app.mealsmadeeasy.api.recipe; import app.mealsmadeeasy.api.user.User; +import org.jetbrains.annotations.Nullable; import org.springframework.stereotype.Component; import java.util.Objects; @@ -29,10 +30,18 @@ public class RecipeSecurityImpl implements RecipeSecurity { } @Override - public boolean isViewableBy(Recipe recipe, User user) { - if (Objects.equals(recipe.getOwner().getId(), user.getId())) { + public boolean isViewableBy(Recipe recipe, @Nullable User user) { + if (recipe.isPublic()) { + // public recipe + return true; + } else if (user == null) { + // a non-public recipe with no principal + return false; + } else if (Objects.equals(recipe.getOwner().getId(), user.getId())) { + // is owner return true; } else { + // check if viewer final RecipeEntity withViewers = this.recipeRepository.getByIdWithViewers(recipe.getId()); for (final User viewer : withViewers.getViewers()) { if (viewer.getId() != null && viewer.getId().equals(user.getId())) { @@ -40,7 +49,17 @@ public class RecipeSecurityImpl implements RecipeSecurity { } } } + // non-public recipe and not viewer return false; } + @Override + public boolean isViewableBy(long recipeId, @Nullable User user) throws RecipeException { + final Recipe recipe = this.recipeRepository.findById(recipeId).orElseThrow(() -> new RecipeException( + RecipeException.Type.INVALID_ID, + "No such Recipe with id " + recipeId + )); + return this.isViewableBy(recipe, user); + } + } diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeService.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeService.java index fbedba8..6ecd181 100644 --- a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeService.java +++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeService.java @@ -2,7 +2,9 @@ package app.mealsmadeeasy.api.recipe; import app.mealsmadeeasy.api.recipe.comment.RecipeComment; import app.mealsmadeeasy.api.recipe.star.RecipeStar; +import app.mealsmadeeasy.api.recipe.view.RecipePageView; import app.mealsmadeeasy.api.user.User; +import org.jetbrains.annotations.Nullable; import java.util.List; @@ -17,6 +19,8 @@ public interface RecipeService { Recipe getByIdWithStars(long id) throws RecipeException; Recipe getByIdWithStars(long id, User viewer) throws RecipeException; + RecipePageView getPageViewById(long id, @Nullable User viewer) throws RecipeException; + List getByMinimumStars(long minimumStars); List getByMinimumStars(long minimumStars, User viewer); @@ -33,12 +37,14 @@ public interface RecipeService { RecipeStar addStar(Recipe recipe, User giver) throws RecipeException; void deleteStarByUser(Recipe recipe, User giver) throws RecipeException; void deleteStar(RecipeStar recipeStar); + int getStarCount(Recipe recipe, @Nullable User viewer); Recipe setPublic(Recipe recipe, User owner, boolean isPublic); Recipe addViewer(Recipe recipe, User user); Recipe removeViewer(Recipe recipe, User user); Recipe clearViewers(Recipe recipe); + int getViewerCount(Recipe recipe, @Nullable User viewer); RecipeComment getCommentById(long id) throws RecipeException; RecipeComment addComment(Recipe recipe, String rawCommentText, User commenter); diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeServiceImpl.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeServiceImpl.java index e9aa255..457d7c4 100644 --- a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeServiceImpl.java +++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeServiceImpl.java @@ -6,11 +6,13 @@ import app.mealsmadeeasy.api.recipe.comment.RecipeCommentRepository; import app.mealsmadeeasy.api.recipe.star.RecipeStar; import app.mealsmadeeasy.api.recipe.star.RecipeStarEntity; import app.mealsmadeeasy.api.recipe.star.RecipeStarRepository; +import app.mealsmadeeasy.api.recipe.view.RecipePageView; import app.mealsmadeeasy.api.user.User; import app.mealsmadeeasy.api.user.UserEntity; import app.mealsmadeeasy.api.user.UserRepository; import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; +import org.jetbrains.annotations.Nullable; import org.jsoup.Jsoup; import org.jsoup.safety.Safelist; import org.springframework.security.access.prepost.PostAuthorize; @@ -82,7 +84,7 @@ public class RecipeServiceImpl implements RecipeService { } @Override - @PostAuthorize("returnObject.isPublic || @recipeSecurity.isViewableBy(returnObject, #viewer)") + @PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)") public Recipe getById(long id, User viewer) throws RecipeException { return this.recipeRepository.findById(id).orElseThrow(() -> new RecipeException( RecipeException.Type.INVALID_ID, @@ -100,7 +102,7 @@ public class RecipeServiceImpl implements RecipeService { } @Override - @PostAuthorize("returnObject.isPublic || @recipeSecurity.isViewableBy(returnObject, #viewer)") + @PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)") public Recipe getByIdWithStars(long id, User viewer) throws RecipeException { return this.recipeRepository.findByIdWithStars(id).orElseThrow(() -> new RecipeException( RecipeException.Type.INVALID_ID, @@ -108,6 +110,23 @@ public class RecipeServiceImpl implements RecipeService { )); } + @Override + @PostAuthorize("@recipeSecurity.isViewableBy(#id, #viewer)") + public RecipePageView getPageViewById(long id, @Nullable User viewer) throws RecipeException { + final Recipe recipe = this.recipeRepository.getReferenceById(id); + final RecipePageView view = new RecipePageView(); + view.setId(recipe.getId()); + view.setCreated(recipe.getCreated()); + view.setModified(recipe.getModified()); + view.setTitle(recipe.getTitle()); + view.setText(this.getRenderedMarkdown(recipe, viewer)); + view.setOwnerId(recipe.getOwner().getId()); + view.setOwnerUsername(recipe.getOwner().getUsername()); + view.setStarCount(this.getStarCount(recipe, viewer)); + view.setViewerCount(this.getViewerCount(recipe, viewer)); + return view; + } + @Override public List getByMinimumStars(long minimumStars) { return List.copyOf(this.recipeRepository.findAllPublicByStarsGreaterThanEqual(minimumStars)); @@ -136,7 +155,7 @@ public class RecipeServiceImpl implements RecipeService { } @Override - @PreAuthorize("#recipe.isPublic || @recipeSecurity.isViewableBy(#recipe, #viewer)") + @PreAuthorize("@recipeSecurity.isViewableBy(#recipe, #viewer)") public String getRenderedMarkdown(Recipe recipe, User viewer) { RecipeEntity entity = (RecipeEntity) recipe; if (entity.getCachedRenderedText() == null) { @@ -164,7 +183,7 @@ public class RecipeServiceImpl implements RecipeService { } @Override - @PreAuthorize("#recipe.isPublic || @recipeSecurity.isViewableBy(#recipe, #giver)") + @PreAuthorize("@recipeSecurity.isViewableBy(#recipe, #giver)") public RecipeStar addStar(Recipe recipe, User giver) { final RecipeStarEntity star = new RecipeStarEntity(); star.setOwner((UserEntity) giver); @@ -189,6 +208,12 @@ public class RecipeServiceImpl implements RecipeService { this.recipeStarRepository.delete(star); } + @Override + @PreAuthorize("@recipeSecurity.isViewableBy(#recipe, #viewer)") + public int getStarCount(Recipe recipe, @Nullable User viewer) { + return this.recipeRepository.getStarCount(recipe.getId()); + } + @Override @PreAuthorize("@recipeSecurity.isOwner(#recipe, #owner)") public Recipe setPublic(Recipe recipe, User owner, boolean isPublic) { @@ -222,6 +247,12 @@ public class RecipeServiceImpl implements RecipeService { return this.recipeRepository.save(entity); } + @Override + @PreAuthorize("@recipeSecurity.isViewableBy(#recipe, #viewer)") + public int getViewerCount(Recipe recipe, User viewer) { + return this.recipeRepository.getViewerCount(recipe.getId()); + } + @Override public RecipeComment getCommentById(long id) throws RecipeException { return this.recipeCommentRepository.findById(id) diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/view/RecipePageView.java b/src/main/java/app/mealsmadeeasy/api/recipe/view/RecipePageView.java new file mode 100644 index 0000000..cc6a676 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/recipe/view/RecipePageView.java @@ -0,0 +1,91 @@ +package app.mealsmadeeasy.api.recipe.view; + +import org.jetbrains.annotations.Nullable; + +import java.time.LocalDateTime; + +public class RecipePageView { + + private long id; + private LocalDateTime created; + private LocalDateTime modified; + private String title; + private String text; + private long ownerId; + private String ownerUsername; + private int starCount; + private int viewerCount; + + public long getId() { + return this.id; + } + + public void setId(long id) { + this.id = id; + } + + public LocalDateTime getCreated() { + return this.created; + } + + public void setCreated(LocalDateTime created) { + this.created = created; + } + + public LocalDateTime getModified() { + return this.modified; + } + + public void setModified(LocalDateTime modified) { + this.modified = modified; + } + + public String getTitle() { + return this.title; + } + + public void setTitle(String title) { + this.title = title; + } + + public @Nullable String getText() { + return this.text; + } + + public void setText(String text) { + this.text = text; + } + + public long getOwnerId() { + return this.ownerId; + } + + public void setOwnerId(long ownerId) { + this.ownerId = ownerId; + } + + public String getOwnerUsername() { + return this.ownerUsername; + } + + public void setOwnerUsername(String ownerUsername) { + this.ownerUsername = ownerUsername; + } + + public int getStarCount() { + return this.starCount; + } + + public void setStarCount(int starCount) { + this.starCount = starCount; + } + + public int getViewerCount() { + return this.viewerCount; + } + + public void setViewerCount(int viewerCount) { + this.viewerCount = viewerCount; + } + +}