RecipeController for getting RecipePageView.

This commit is contained in:
JesseBrault0709 2024-07-09 15:11:05 +02:00
parent 97bbab3cf0
commit 019210d334
8 changed files with 172 additions and 18 deletions

View File

@ -35,7 +35,7 @@ public class RecipeControllerTests {
} }
private Recipe createTestRecipe(User owner) { private Recipe createTestRecipe(User owner) {
return this.recipeService.create(owner, "Test Recipe", "Hello, World!"); return this.recipeService.create(owner, "Test Recipe", "# Hello, World!");
} }
@Test @Test
@ -46,7 +46,12 @@ public class RecipeControllerTests {
this.mockMvc.perform(get("/recipe/{id}", recipe.getId())) this.mockMvc.perform(get("/recipe/{id}", recipe.getId()))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1)) .andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.title").value("Test Recipe")); .andExpect(jsonPath("$.title").value("Test Recipe"))
.andExpect(jsonPath("$.text").value("<h1>Hello, World!</h1>"))
.andExpect(jsonPath("$.ownerId").value(owner.getId()))
.andExpect(jsonPath("$.ownerUsername").value(owner.getUsername()))
.andExpect(jsonPath("$.starCount").value(0))
.andExpect(jsonPath("$.viewerCount").value(0));
} }
} }

View File

@ -1,7 +1,7 @@
package app.mealsmadeeasy.api.recipe; package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.recipe.view.RecipeExceptionView; 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 app.mealsmadeeasy.api.user.User;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.annotation.AuthenticationPrincipal;
@ -26,15 +26,9 @@ public class RecipeController {
} }
@GetMapping("/{id}") @GetMapping("/{id}")
public ResponseEntity<RecipeGetView> getById(@PathVariable long id, @AuthenticationPrincipal User user) public ResponseEntity<RecipePageView> getById(@PathVariable long id, @AuthenticationPrincipal User user)
throws RecipeException { throws RecipeException {
final Recipe recipe; return ResponseEntity.ok(this.recipeService.getPageViewById(id, user));
if (user != null) {
recipe = this.recipeService.getById(id, user);
} else {
recipe = this.recipeService.getById(id);
}
return ResponseEntity.ok(new RecipeGetView(recipe.getId(), recipe.getTitle()));
} }
} }

View File

@ -28,4 +28,10 @@ public interface RecipeRepository extends JpaRepository<RecipeEntity, Long> {
@EntityGraph(attributePaths = { "stars" }) @EntityGraph(attributePaths = { "stars" })
Optional<RecipeEntity> findByIdWithStars(long id); Optional<RecipeEntity> 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);
} }

View File

@ -1,9 +1,11 @@
package app.mealsmadeeasy.api.recipe; package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.user.User; import app.mealsmadeeasy.api.user.User;
import org.jetbrains.annotations.Nullable;
public interface RecipeSecurity { public interface RecipeSecurity {
boolean isOwner(Recipe recipe, User user); boolean isOwner(Recipe recipe, User user);
boolean isOwner(long recipeId, User user) throws RecipeException; 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;
} }

View File

@ -1,6 +1,7 @@
package app.mealsmadeeasy.api.recipe; package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.user.User; import app.mealsmadeeasy.api.user.User;
import org.jetbrains.annotations.Nullable;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.Objects; import java.util.Objects;
@ -29,10 +30,18 @@ public class RecipeSecurityImpl implements RecipeSecurity {
} }
@Override @Override
public boolean isViewableBy(Recipe recipe, User user) { public boolean isViewableBy(Recipe recipe, @Nullable User user) {
if (Objects.equals(recipe.getOwner().getId(), user.getId())) { 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; return true;
} else { } else {
// check if viewer
final RecipeEntity withViewers = this.recipeRepository.getByIdWithViewers(recipe.getId()); final RecipeEntity withViewers = this.recipeRepository.getByIdWithViewers(recipe.getId());
for (final User viewer : withViewers.getViewers()) { for (final User viewer : withViewers.getViewers()) {
if (viewer.getId() != null && viewer.getId().equals(user.getId())) { 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; 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);
}
} }

View File

@ -2,7 +2,9 @@ package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.recipe.comment.RecipeComment; import app.mealsmadeeasy.api.recipe.comment.RecipeComment;
import app.mealsmadeeasy.api.recipe.star.RecipeStar; import app.mealsmadeeasy.api.recipe.star.RecipeStar;
import app.mealsmadeeasy.api.recipe.view.RecipePageView;
import app.mealsmadeeasy.api.user.User; import app.mealsmadeeasy.api.user.User;
import org.jetbrains.annotations.Nullable;
import java.util.List; import java.util.List;
@ -17,6 +19,8 @@ public interface RecipeService {
Recipe getByIdWithStars(long id) throws RecipeException; Recipe getByIdWithStars(long id) throws RecipeException;
Recipe getByIdWithStars(long id, User viewer) throws RecipeException; Recipe getByIdWithStars(long id, User viewer) throws RecipeException;
RecipePageView getPageViewById(long id, @Nullable User viewer) throws RecipeException;
List<Recipe> getByMinimumStars(long minimumStars); List<Recipe> getByMinimumStars(long minimumStars);
List<Recipe> getByMinimumStars(long minimumStars, User viewer); List<Recipe> getByMinimumStars(long minimumStars, User viewer);
@ -33,12 +37,14 @@ public interface RecipeService {
RecipeStar addStar(Recipe recipe, User giver) throws RecipeException; RecipeStar addStar(Recipe recipe, User giver) throws RecipeException;
void deleteStarByUser(Recipe recipe, User giver) throws RecipeException; void deleteStarByUser(Recipe recipe, User giver) throws RecipeException;
void deleteStar(RecipeStar recipeStar); void deleteStar(RecipeStar recipeStar);
int getStarCount(Recipe recipe, @Nullable User viewer);
Recipe setPublic(Recipe recipe, User owner, boolean isPublic); Recipe setPublic(Recipe recipe, User owner, boolean isPublic);
Recipe addViewer(Recipe recipe, User user); Recipe addViewer(Recipe recipe, User user);
Recipe removeViewer(Recipe recipe, User user); Recipe removeViewer(Recipe recipe, User user);
Recipe clearViewers(Recipe recipe); Recipe clearViewers(Recipe recipe);
int getViewerCount(Recipe recipe, @Nullable User viewer);
RecipeComment getCommentById(long id) throws RecipeException; RecipeComment getCommentById(long id) throws RecipeException;
RecipeComment addComment(Recipe recipe, String rawCommentText, User commenter); RecipeComment addComment(Recipe recipe, String rawCommentText, User commenter);

View File

@ -6,11 +6,13 @@ import app.mealsmadeeasy.api.recipe.comment.RecipeCommentRepository;
import app.mealsmadeeasy.api.recipe.star.RecipeStar; import app.mealsmadeeasy.api.recipe.star.RecipeStar;
import app.mealsmadeeasy.api.recipe.star.RecipeStarEntity; import app.mealsmadeeasy.api.recipe.star.RecipeStarEntity;
import app.mealsmadeeasy.api.recipe.star.RecipeStarRepository; 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.User;
import app.mealsmadeeasy.api.user.UserEntity; import app.mealsmadeeasy.api.user.UserEntity;
import app.mealsmadeeasy.api.user.UserRepository; import app.mealsmadeeasy.api.user.UserRepository;
import org.commonmark.parser.Parser; import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer; import org.commonmark.renderer.html.HtmlRenderer;
import org.jetbrains.annotations.Nullable;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.safety.Safelist; import org.jsoup.safety.Safelist;
import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.access.prepost.PostAuthorize;
@ -82,7 +84,7 @@ public class RecipeServiceImpl implements RecipeService {
} }
@Override @Override
@PostAuthorize("returnObject.isPublic || @recipeSecurity.isViewableBy(returnObject, #viewer)") @PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)")
public Recipe getById(long id, User viewer) throws RecipeException { public Recipe getById(long id, User viewer) throws RecipeException {
return this.recipeRepository.findById(id).orElseThrow(() -> new RecipeException( return this.recipeRepository.findById(id).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_ID, RecipeException.Type.INVALID_ID,
@ -100,7 +102,7 @@ public class RecipeServiceImpl implements RecipeService {
} }
@Override @Override
@PostAuthorize("returnObject.isPublic || @recipeSecurity.isViewableBy(returnObject, #viewer)") @PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)")
public Recipe getByIdWithStars(long id, User viewer) throws RecipeException { public Recipe getByIdWithStars(long id, User viewer) throws RecipeException {
return this.recipeRepository.findByIdWithStars(id).orElseThrow(() -> new RecipeException( return this.recipeRepository.findByIdWithStars(id).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_ID, 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 @Override
public List<Recipe> getByMinimumStars(long minimumStars) { public List<Recipe> getByMinimumStars(long minimumStars) {
return List.copyOf(this.recipeRepository.findAllPublicByStarsGreaterThanEqual(minimumStars)); return List.copyOf(this.recipeRepository.findAllPublicByStarsGreaterThanEqual(minimumStars));
@ -136,7 +155,7 @@ public class RecipeServiceImpl implements RecipeService {
} }
@Override @Override
@PreAuthorize("#recipe.isPublic || @recipeSecurity.isViewableBy(#recipe, #viewer)") @PreAuthorize("@recipeSecurity.isViewableBy(#recipe, #viewer)")
public String getRenderedMarkdown(Recipe recipe, User viewer) { public String getRenderedMarkdown(Recipe recipe, User viewer) {
RecipeEntity entity = (RecipeEntity) recipe; RecipeEntity entity = (RecipeEntity) recipe;
if (entity.getCachedRenderedText() == null) { if (entity.getCachedRenderedText() == null) {
@ -164,7 +183,7 @@ public class RecipeServiceImpl implements RecipeService {
} }
@Override @Override
@PreAuthorize("#recipe.isPublic || @recipeSecurity.isViewableBy(#recipe, #giver)") @PreAuthorize("@recipeSecurity.isViewableBy(#recipe, #giver)")
public RecipeStar addStar(Recipe recipe, User giver) { public RecipeStar addStar(Recipe recipe, User giver) {
final RecipeStarEntity star = new RecipeStarEntity(); final RecipeStarEntity star = new RecipeStarEntity();
star.setOwner((UserEntity) giver); star.setOwner((UserEntity) giver);
@ -189,6 +208,12 @@ public class RecipeServiceImpl implements RecipeService {
this.recipeStarRepository.delete(star); 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 @Override
@PreAuthorize("@recipeSecurity.isOwner(#recipe, #owner)") @PreAuthorize("@recipeSecurity.isOwner(#recipe, #owner)")
public Recipe setPublic(Recipe recipe, User owner, boolean isPublic) { public Recipe setPublic(Recipe recipe, User owner, boolean isPublic) {
@ -222,6 +247,12 @@ public class RecipeServiceImpl implements RecipeService {
return this.recipeRepository.save(entity); 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 @Override
public RecipeComment getCommentById(long id) throws RecipeException { public RecipeComment getCommentById(long id) throws RecipeException {
return this.recipeCommentRepository.findById(id) return this.recipeCommentRepository.findById(id)

View File

@ -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;
}
}