Compare commits
No commits in common. "c088cb2253320a51a2239f21db15f989a967c3e6" and "f272db9bddffd126de25aa45aa0f6f1c16118570" have entirely different histories.
c088cb2253
...
f272db9bdd
@ -9,7 +9,6 @@ import app.mealsmadeeasy.api.recipe.body.RecipeDraftUpdateBody;
|
||||
import app.mealsmadeeasy.api.user.User;
|
||||
import app.mealsmadeeasy.api.user.UserCreateException;
|
||||
import app.mealsmadeeasy.api.user.UserService;
|
||||
import app.mealsmadeeasy.api.util.NoSuchEntityWithIdException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@ -27,7 +26,6 @@ import java.util.UUID;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
@ -84,10 +82,12 @@ public class RecipeDraftsControllerIntegrationTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenNoDraftAndNoPrincipal_getReturnsUnauthorized() throws Exception {
|
||||
public void whenNoUser_getReturnsUnauthorized() throws Exception {
|
||||
this.mockMvc.perform(
|
||||
get("/recipe-drafts/{fakeId}", UUID.randomUUID().toString())
|
||||
).andExpect(status().isUnauthorized());
|
||||
)
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(jsonPath("$.message", is(notNullValue())));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -112,26 +112,6 @@ public class RecipeDraftsControllerIntegrationTests {
|
||||
.andExpect(jsonPath("$.mainImage", is(nullValue())));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenDraftExistsButNoPrincipal_returnsUnauthorized() throws Exception {
|
||||
final User owner = this.seedUser();
|
||||
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
|
||||
this.mockMvc.perform(
|
||||
get("/recipe-drafts/{id}", recipeDraft.getId())
|
||||
).andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenDraftExistsButWrongPrincipal_returnsForbidden() throws Exception {
|
||||
final User owner = this.seedUser();
|
||||
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
|
||||
final User wrongViewer = this.seedUser();
|
||||
this.mockMvc.perform(
|
||||
get("/recipe-drafts/{id}", recipeDraft.getId())
|
||||
.header("Authorization", "Bearer " + this.getAccessToken(wrongViewer))
|
||||
).andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenDraftsExist_returnDrafts() throws Exception {
|
||||
final User owner = this.seedUser();
|
||||
@ -146,21 +126,6 @@ public class RecipeDraftsControllerIntegrationTests {
|
||||
.andExpect(jsonPath("$", hasSize(2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenDraftsExistButWrongPrincipal_returnsNoDrafts() throws Exception {
|
||||
final User owner = this.seedUser();
|
||||
this.recipeService.createDraft(owner); // seed 1
|
||||
this.recipeService.createDraft(owner); // seed 2
|
||||
final User wrongViewer = this.seedUser();
|
||||
this.mockMvc.perform(
|
||||
get("/recipe-drafts")
|
||||
.header("Authorization", "Bearer " + this.getAccessToken(wrongViewer))
|
||||
)
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$").isArray())
|
||||
.andExpect(jsonPath("$", hasSize(0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void manualDraft_returnsDraft() throws Exception {
|
||||
final User owner = this.seedUser();
|
||||
@ -201,14 +166,16 @@ public class RecipeDraftsControllerIntegrationTests {
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.created", is(notNullValue())))
|
||||
.andExpect(jsonPath("$.state", is(RecipeDraft.State.INFER.toString())))
|
||||
.andExpect(jsonPath("$.owner.id", is(owner.getId())))
|
||||
.andExpect(jsonPath("$.owner.username", is(owner.getUsername())));
|
||||
.andExpect(jsonPath("$.owner.id", is(owner.getId())));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static RecipeDraftUpdateBody getTestRecipeDraftUpdateBody() {
|
||||
return RecipeDraftUpdateBody.builder()
|
||||
@Test
|
||||
public void whenUpdate_returnsUpdated() throws Exception {
|
||||
final User owner = this.seedUser();
|
||||
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
|
||||
final RecipeDraftUpdateBody updateBody = RecipeDraftUpdateBody.builder()
|
||||
.title("Test Title")
|
||||
.slug("test-slug")
|
||||
.preparationTime(15)
|
||||
@ -223,13 +190,6 @@ public class RecipeDraftsControllerIntegrationTests {
|
||||
.build()
|
||||
))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenUpdate_returnsUpdated() throws Exception {
|
||||
final User owner = this.seedUser();
|
||||
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
|
||||
final RecipeDraftUpdateBody updateBody = getTestRecipeDraftUpdateBody();
|
||||
this.mockMvc.perform(
|
||||
put("/recipe-drafts/{id}", recipeDraft.getId())
|
||||
.header("Authorization", "Bearer " + this.getAccessToken(owner))
|
||||
@ -249,102 +209,4 @@ public class RecipeDraftsControllerIntegrationTests {
|
||||
.andExpect(jsonPath("$.ingredients[0].notes", is("Separated")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenUpdateNoPrincipal_returnsUnauthorized() throws Exception {
|
||||
final User owner = this.seedUser();
|
||||
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
|
||||
final RecipeDraftUpdateBody updateBody = getTestRecipeDraftUpdateBody();
|
||||
this.mockMvc.perform(
|
||||
put("/recipe-drafts/{id}", recipeDraft.getId())
|
||||
.header("Content-Type", "application/json")
|
||||
.content(this.objectMapper.writeValueAsString(updateBody))
|
||||
).andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenWrongPrincipalUpdate_returnsForbidden() throws Exception {
|
||||
final User owner = this.seedUser();
|
||||
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
|
||||
final RecipeDraftUpdateBody updateBody = getTestRecipeDraftUpdateBody();
|
||||
final User wrongModifier = this.seedUser();
|
||||
this.mockMvc.perform(
|
||||
put("/recipe-drafts/{id}", recipeDraft.getId())
|
||||
.header("Authorization", "Bearer " + this.getAccessToken(wrongModifier))
|
||||
.header("Content-Type", "application/json")
|
||||
.content(this.objectMapper.writeValueAsString(updateBody))
|
||||
).andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenDelete_draftIsDeleted() throws Exception {
|
||||
final User owner = this.seedUser();
|
||||
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
|
||||
this.mockMvc.perform(
|
||||
delete("/recipe-drafts/{id}", recipeDraft.getId())
|
||||
.header("Authorization", "Bearer " + this.getAccessToken(owner))
|
||||
)
|
||||
.andExpect(status().isNoContent());
|
||||
assertThrows(NoSuchEntityWithIdException.class, () -> this.recipeService.getDraftById(recipeDraft.getId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenDeleteWithNoPrincipal_returnsUnauthorized() throws Exception {
|
||||
final User owner = this.seedUser();
|
||||
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
|
||||
this.mockMvc.perform(
|
||||
delete("/recipe-drafts/{id}", recipeDraft.getId())
|
||||
).andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenPublish_expectDraftDeletedAndRecipeCreated() throws Exception {
|
||||
final User owner = this.seedUser();
|
||||
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
|
||||
recipeDraft.setTitle("Test Title");
|
||||
recipeDraft.setSlug("test-slug");
|
||||
recipeDraft.setRawText("# Hello, World!");
|
||||
this.recipeService.saveDraft(recipeDraft);
|
||||
this.mockMvc.perform(
|
||||
post("/recipe-drafts/{id}/publish", recipeDraft.getId())
|
||||
.header("Authorization", "Bearer " + this.getAccessToken(owner))
|
||||
)
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.id", is(notNullValue())))
|
||||
.andExpect(jsonPath("$.created", is(notNullValue())))
|
||||
.andExpect(jsonPath("$.modified", is(nullValue())))
|
||||
.andExpect(jsonPath("$.title", is("Test Title")))
|
||||
.andExpect(jsonPath("$.slug", is("test-slug")))
|
||||
.andExpect(jsonPath("$.preparationTime", is(nullValue())))
|
||||
.andExpect(jsonPath("$.cookingTime", is(nullValue())))
|
||||
.andExpect(jsonPath("$.totalTime", is(nullValue())))
|
||||
.andExpect(jsonPath("$.text", is("<h1>Hello, World!</h1>")))
|
||||
.andExpect(jsonPath("$.owner.id", is(owner.getId())))
|
||||
.andExpect(jsonPath("$.owner.username", is(owner.getUsername())))
|
||||
.andExpect(jsonPath("$.starCount", is(0)))
|
||||
.andExpect(jsonPath("$.viewerCount", is(0)))
|
||||
.andExpect(jsonPath("$.mainImage", is(nullValue())))
|
||||
.andExpect(jsonPath("$.public", is(false)));
|
||||
assertThrows(NoSuchEntityWithIdException.class, () -> this.recipeService.getDraftById(recipeDraft.getId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenPublishWithNoPrincipal_returnsUnauthorized() throws Exception {
|
||||
final User owner = this.seedUser();
|
||||
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
|
||||
this.mockMvc.perform(
|
||||
post("/recipe-drafts/{id}/publish", recipeDraft.getId())
|
||||
).andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenPublishWrongPrincipal_returnsForbidden() throws Exception {
|
||||
final User owner = this.seedUser();
|
||||
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
|
||||
final User wrongPublisher = this.seedUser();
|
||||
this.mockMvc.perform(
|
||||
post("/recipe-drafts/{id}/publish", recipeDraft.getId())
|
||||
.header("Authorization", "Bearer " + this.getAccessToken(wrongPublisher))
|
||||
).andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
package app.mealsmadeeasy.api;
|
||||
|
||||
import app.mealsmadeeasy.api.security.EndpointAuthConfigurator;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
@ -11,16 +7,6 @@ import org.springframework.web.bind.annotation.ResponseBody;
|
||||
@Controller
|
||||
public final class GreetingController {
|
||||
|
||||
@Component
|
||||
public static class GreetingEndpointAuthConfigurator implements EndpointAuthConfigurator {
|
||||
|
||||
@Override
|
||||
public void configure(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
|
||||
registry.requestMatchers("/greeting").permitAll();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@GetMapping("/greeting")
|
||||
@ResponseBody
|
||||
public String get() {
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
package app.mealsmadeeasy.api.auth;
|
||||
|
||||
import app.mealsmadeeasy.api.security.EndpointAuthConfigurator;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class AuthEndpointAuthConfigurator implements EndpointAuthConfigurator {
|
||||
|
||||
@Override
|
||||
public void configure(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
|
||||
registry.requestMatchers("/auth/**").permitAll();
|
||||
}
|
||||
|
||||
}
|
||||
@ -91,6 +91,9 @@ public class ImageController {
|
||||
@RequestParam(required = false) Set<String> viewers,
|
||||
@AuthenticationPrincipal User principal
|
||||
) throws IOException, ImageException {
|
||||
if (principal == null) {
|
||||
throw new AccessDeniedException("Must be logged in.");
|
||||
}
|
||||
final var specBuilder = ImageCreateSpec.builder()
|
||||
.alt(alt)
|
||||
.caption(caption)
|
||||
@ -116,6 +119,9 @@ public class ImageController {
|
||||
@PathVariable String filename,
|
||||
@RequestBody ImageUpdateBody body
|
||||
) {
|
||||
if (principal == null) {
|
||||
throw new AccessDeniedException("Must be logged in.");
|
||||
}
|
||||
final User owner = this.userService.getUser(username);
|
||||
final Image image = this.imageService.getByOwnerAndFilename(owner, filename, principal);
|
||||
final Image updated = this.imageService.update(image, principal, this.getImageUpdateSpec(body));
|
||||
@ -128,6 +134,9 @@ public class ImageController {
|
||||
@PathVariable String username,
|
||||
@PathVariable String filename
|
||||
) throws IOException {
|
||||
if (principal == null) {
|
||||
throw new AccessDeniedException("Must be logged in.");
|
||||
}
|
||||
final User owner = this.userService.getUser(username);
|
||||
final Image image = this.imageService.getByOwnerAndFilename(owner, filename, principal);
|
||||
this.imageService.deleteImage(image, principal);
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
package app.mealsmadeeasy.api.image;
|
||||
|
||||
import app.mealsmadeeasy.api.security.EndpointAuthConfigurator;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ImageEndpointAuthConfigurator implements EndpointAuthConfigurator {
|
||||
|
||||
@Override
|
||||
public void configure(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
|
||||
registry.requestMatchers(HttpMethod.GET, "/images/**").permitAll();
|
||||
registry.requestMatchers(HttpMethod.POST, "/images/**").authenticated();
|
||||
registry.requestMatchers(HttpMethod.PUT, "/images/**").authenticated();
|
||||
registry.requestMatchers(HttpMethod.DELETE, "/images/**").authenticated();
|
||||
}
|
||||
|
||||
}
|
||||
@ -10,6 +10,7 @@ import app.mealsmadeeasy.api.recipe.spec.RecipeDraftUpdateSpec;
|
||||
import app.mealsmadeeasy.api.recipe.view.FullRecipeView;
|
||||
import app.mealsmadeeasy.api.recipe.view.RecipeDraftView;
|
||||
import app.mealsmadeeasy.api.user.User;
|
||||
import app.mealsmadeeasy.api.util.MustBeLoggedInException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@ -34,6 +35,9 @@ public class RecipeDraftsController {
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<RecipeDraftView>> getAllDraftsForUser(@AuthenticationPrincipal User user) {
|
||||
if (user == null) {
|
||||
throw new MustBeLoggedInException();
|
||||
}
|
||||
final List<RecipeDraft> recipeDrafts = this.recipeService.getDrafts(user);
|
||||
return ResponseEntity.ok(recipeDrafts.stream()
|
||||
.map(recipeDraft -> this.draftToViewConverter.convert(recipeDraft, user))
|
||||
@ -46,12 +50,18 @@ public class RecipeDraftsController {
|
||||
@PathVariable UUID id,
|
||||
@AuthenticationPrincipal User viewer
|
||||
) {
|
||||
if (viewer == null) {
|
||||
throw new MustBeLoggedInException();
|
||||
}
|
||||
final RecipeDraft recipeDraft = this.recipeService.getDraftByIdWithViewer(id, viewer);
|
||||
return ResponseEntity.ok(this.draftToViewConverter.convert(recipeDraft, viewer));
|
||||
}
|
||||
|
||||
@PostMapping("/manual")
|
||||
public ResponseEntity<RecipeDraftView> createManualDraft(@AuthenticationPrincipal User owner) {
|
||||
if (owner == null) {
|
||||
throw new MustBeLoggedInException();
|
||||
}
|
||||
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(this.draftToViewConverter.convert(recipeDraft, owner));
|
||||
}
|
||||
@ -62,6 +72,9 @@ public class RecipeDraftsController {
|
||||
@RequestParam MultipartFile sourceFile,
|
||||
@RequestParam String sourceFileName
|
||||
) throws IOException {
|
||||
if (owner == null) {
|
||||
throw new MustBeLoggedInException();
|
||||
}
|
||||
final File file = this.fileService.create(
|
||||
sourceFile.getInputStream(),
|
||||
sourceFileName,
|
||||
@ -78,6 +91,9 @@ public class RecipeDraftsController {
|
||||
@PathVariable UUID id,
|
||||
@RequestBody RecipeDraftUpdateBody updateBody
|
||||
) {
|
||||
if (modifier == null) {
|
||||
throw new MustBeLoggedInException();
|
||||
}
|
||||
final RecipeDraftUpdateSpec spec = this.updateBodyToSpecConverter.convert(updateBody, modifier);
|
||||
final RecipeDraft updated = this.recipeService.updateDraft(id, spec, modifier);
|
||||
return ResponseEntity.ok(this.draftToViewConverter.convert(updated, modifier));
|
||||
@ -88,15 +104,21 @@ public class RecipeDraftsController {
|
||||
@AuthenticationPrincipal User modifier,
|
||||
@PathVariable UUID id
|
||||
) {
|
||||
if (modifier == null) {
|
||||
throw new MustBeLoggedInException();
|
||||
}
|
||||
this.recipeService.deleteDraft(id, modifier);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/publish")
|
||||
@PostMapping("/{id}")
|
||||
public ResponseEntity<FullRecipeView> publishRecipeDraft(
|
||||
@AuthenticationPrincipal User modifier,
|
||||
@PathVariable UUID id
|
||||
) {
|
||||
if (modifier == null) {
|
||||
throw new MustBeLoggedInException();
|
||||
}
|
||||
final Recipe recipe = this.recipeService.publishDraft(id, modifier);
|
||||
final FullRecipeView view = this.recipeToFullViewConverter.convert(recipe, false, modifier);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(view);
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
package app.mealsmadeeasy.api.recipe;
|
||||
|
||||
import app.mealsmadeeasy.api.security.EndpointAuthConfigurator;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class RecipeDraftsEndpointAuthConfigurator implements EndpointAuthConfigurator {
|
||||
|
||||
@Override
|
||||
public void configure(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
|
||||
registry.requestMatchers("/recipe-drafts/**").authenticated();
|
||||
}
|
||||
|
||||
}
|
||||
@ -244,9 +244,9 @@ public class RecipeService {
|
||||
}
|
||||
|
||||
public RecipeDraft getDraftById(UUID id) {
|
||||
return this.recipeDraftRepository.findById(id).orElseThrow(
|
||||
() -> new NoSuchEntityWithIdException(RecipeDraft.class, id)
|
||||
);
|
||||
return this.recipeDraftRepository.findById(id).orElseThrow(() -> new RuntimeException(
|
||||
"RecipeDraft with id " + id + " not found"
|
||||
));
|
||||
}
|
||||
|
||||
@PostAuthorize("@recipeDraftSecurity.isViewableBy(returnObject, #viewer)")
|
||||
|
||||
@ -23,6 +23,7 @@ import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@ -123,6 +124,9 @@ public class RecipesController {
|
||||
@PathVariable String slug,
|
||||
@Nullable @AuthenticationPrincipal User principal
|
||||
) {
|
||||
if (principal == null) {
|
||||
throw new AccessDeniedException("Must be logged in to star a recipe.");
|
||||
}
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(this.recipeStarService.create(username, slug, principal));
|
||||
}
|
||||
|
||||
@ -132,6 +136,9 @@ public class RecipesController {
|
||||
@PathVariable String slug,
|
||||
@Nullable @AuthenticationPrincipal User principal
|
||||
) {
|
||||
if (principal == null) {
|
||||
throw new AccessDeniedException("Must be logged in to get a recipe star.");
|
||||
}
|
||||
final @Nullable RecipeStar star = this.recipeStarService.find(username, slug, principal).orElse(null);
|
||||
if (star != null) {
|
||||
return ResponseEntity.ok(Map.of("isStarred", true, "star", star));
|
||||
@ -146,6 +153,9 @@ public class RecipesController {
|
||||
@PathVariable String slug,
|
||||
@Nullable @AuthenticationPrincipal User principal
|
||||
) {
|
||||
if (principal == null) {
|
||||
throw new AccessDeniedException("Must be logged in to delete a recipe star.");
|
||||
}
|
||||
this.recipeStarService.delete(username, slug, principal);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
@ -173,6 +183,9 @@ public class RecipesController {
|
||||
@RequestBody RecipeCommentCreateBody body,
|
||||
@Nullable @AuthenticationPrincipal User principal
|
||||
) {
|
||||
if (principal == null) {
|
||||
throw new AccessDeniedException("Must be logged in to comment on a recipe.");
|
||||
}
|
||||
final RecipeComment comment = this.recipeCommentService.create(username, slug, principal, body);
|
||||
return ResponseEntity.ok(RecipeCommentView.from(comment, false));
|
||||
}
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
package app.mealsmadeeasy.api.recipe;
|
||||
|
||||
import app.mealsmadeeasy.api.security.EndpointAuthConfigurator;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class RecipesEndpointAuthConfigurator implements EndpointAuthConfigurator {
|
||||
|
||||
@Override
|
||||
public void configure(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
|
||||
registry.requestMatchers(HttpMethod.GET, "/recipes/**").permitAll();
|
||||
registry.requestMatchers(HttpMethod.POST, "/recipes/**").authenticated();
|
||||
registry.requestMatchers(HttpMethod.PUT, "/recipes/**").authenticated();
|
||||
registry.requestMatchers(HttpMethod.DELETE, "/recipes/**").authenticated();
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
package app.mealsmadeeasy.api.security;
|
||||
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
|
||||
|
||||
public interface EndpointAuthConfigurator {
|
||||
void configure(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry);
|
||||
}
|
||||
@ -19,8 +19,6 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
@ -28,25 +26,15 @@ public class SecurityConfiguration {
|
||||
|
||||
private final JpaUserDetailsService jpaUserDetailsService;
|
||||
private final BeanFactory beanFactory;
|
||||
private final List<EndpointAuthConfigurator> endpointAuthConfigurators;
|
||||
|
||||
public SecurityConfiguration(
|
||||
JpaUserDetailsService jpaUserDetailsService,
|
||||
BeanFactory beanFactory,
|
||||
List<EndpointAuthConfigurator> endpointAuthConfigurators
|
||||
) {
|
||||
public SecurityConfiguration(JpaUserDetailsService jpaUserDetailsService, BeanFactory beanFactory) {
|
||||
this.jpaUserDetailsService = jpaUserDetailsService;
|
||||
this.beanFactory = beanFactory;
|
||||
this.endpointAuthConfigurators = endpointAuthConfigurators;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
|
||||
httpSecurity.authorizeHttpRequests(requests -> {
|
||||
this.endpointAuthConfigurators.forEach(endpointAuthConfigurator -> {
|
||||
endpointAuthConfigurator.configure(requests);
|
||||
});
|
||||
});
|
||||
httpSecurity.authorizeHttpRequests(requests -> requests.anyRequest().permitAll());
|
||||
httpSecurity.csrf(AbstractHttpConfigurer::disable);
|
||||
httpSecurity.cors(Customizer.withDefaults());
|
||||
httpSecurity.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
package app.mealsmadeeasy.api.signup;
|
||||
|
||||
import app.mealsmadeeasy.api.security.EndpointAuthConfigurator;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class SignUpEndpointAuthConfigurator implements EndpointAuthConfigurator {
|
||||
|
||||
@Override
|
||||
public void configure(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
|
||||
registry.requestMatchers("/sign-up/**").permitAll();
|
||||
}
|
||||
|
||||
}
|
||||
@ -22,6 +22,16 @@ public class ExceptionHandlers {
|
||||
));
|
||||
}
|
||||
|
||||
public record MustBeLoggedInExceptionView(String message) {}
|
||||
|
||||
@ExceptionHandler(MustBeLoggedInException.class)
|
||||
public ResponseEntity<MustBeLoggedInExceptionView> handleMustBeLoggedInException(
|
||||
MustBeLoggedInException e
|
||||
) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(new MustBeLoggedInExceptionView(e.getMessage()));
|
||||
}
|
||||
|
||||
public record NoSuchEntityWithUsernameAndSlugExceptionView(
|
||||
String entityName,
|
||||
String username,
|
||||
@ -46,28 +56,4 @@ public class ExceptionHandlers {
|
||||
));
|
||||
}
|
||||
|
||||
public record NoSuchEntityWithUsernameAndFilenameExceptionView(
|
||||
String entityName,
|
||||
String username,
|
||||
String filename,
|
||||
String message
|
||||
) {}
|
||||
|
||||
@ExceptionHandler(NoSuchEntityWithUsernameAndFilenameException.class)
|
||||
public ResponseEntity<NoSuchEntityWithUsernameAndFilenameExceptionView> handleNoSuchEntityWithUsernameAndFilename(
|
||||
NoSuchEntityWithUsernameAndFilenameException e
|
||||
) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new NoSuchEntityWithUsernameAndFilenameExceptionView(
|
||||
e.getEntityType().getSimpleName(),
|
||||
e.getUsername(),
|
||||
e.getFilename(),
|
||||
String.format(
|
||||
"No such entity %s for username %s and filename %s",
|
||||
e.getEntityType().getSimpleName(),
|
||||
e.getUsername(),
|
||||
e.getFilename()
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
package app.mealsmadeeasy.api.util;
|
||||
|
||||
public class MustBeLoggedInException extends RuntimeException {
|
||||
|
||||
public MustBeLoggedInException() {
|
||||
super("Must be logged in to perform the requested operation.");
|
||||
}
|
||||
|
||||
public MustBeLoggedInException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user