Compare commits
	
		
			10 Commits
		
	
	
		
			9b82e549ca
			...
			02e3b6df66
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 02e3b6df66 | ||
|   | 03efb5aa3d | ||
|   | 3d6577fe02 | ||
|   | 0396e8e3b0 | ||
|   | 66242845d6 | ||
|   | aeadd07f70 | ||
|   | 0d86ab6d93 | ||
|   | c163e8ee1d | ||
|   | 113227ef97 | ||
|   | 3a7c0f5b1d | 
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -35,3 +35,6 @@ out/ | |||||||
| 
 | 
 | ||||||
| ### VS Code ### | ### VS Code ### | ||||||
| .vscode/ | .vscode/ | ||||||
|  | 
 | ||||||
|  | # Custom | ||||||
|  | .env | ||||||
|  | |||||||
							
								
								
									
										11
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | |||||||
|  | FROM gradle:8.8.0-jdk21 AS build | ||||||
|  | WORKDIR /mme-api | ||||||
|  | COPY . /mme-api | ||||||
|  | RUN gradle build | ||||||
|  | 
 | ||||||
|  | FROM eclipse-temurin:21 | ||||||
|  | WORKDIR /mme-api | ||||||
|  | COPY --from=build /mme-api/build/libs/api-0.1.0-SNAPSHOT.jar . | ||||||
|  | COPY dev-data ./dev-data/ | ||||||
|  | EXPOSE 8080 | ||||||
|  | CMD ["java", "-jar", "api-0.1.0-SNAPSHOT.jar", "--spring.profiles.active=dev"] | ||||||
							
								
								
									
										17
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | |||||||
|  | # Meals Made Easy API | ||||||
|  | 
 | ||||||
|  | ## Getting Started | ||||||
|  | First, create a `.env` file with the following environment variables: | ||||||
|  | 
 | ||||||
|  | - `MINIO_ROOT_PASSWORD=<put something here>` | ||||||
|  | - `MYSQL_ROOT_PASSWORD=<put something here>` | ||||||
|  | - `MYSQL_PASSWORD=<put something here>` | ||||||
|  | 
 | ||||||
|  | Then run `docker compose up -d --build`. The `--build` option can be omitted if you | ||||||
|  | have already built the `api` from `Dockerfile`. Once Docker Compose has finished | ||||||
|  | starting everything, navigate to `http://localhost:8080/greeting`. | ||||||
|  | You should see a simple "Hello, World!" greeting. | ||||||
|  | 
 | ||||||
|  | **N.b.: the current configuration of the app is to use the `dev` Spring-Boot profile,  | ||||||
|  | which seeds the app with some simple recipes whose sources are located in the | ||||||
|  | `dev-data` directory.** | ||||||
							
								
								
									
										56
									
								
								compose.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								compose.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,56 @@ | |||||||
|  | name: meals-made-easy-api | ||||||
|  | services: | ||||||
|  |   db: | ||||||
|  |     image: mysql:latest | ||||||
|  |     ports: | ||||||
|  |       - '55001:3306' | ||||||
|  |       - '55000:33060' | ||||||
|  |     env_file: .env | ||||||
|  |     environment: | ||||||
|  |       MYSQL_DATABASE: meals_made_easy_api | ||||||
|  |       MYSQL_USER: meals-made-easy-api-user | ||||||
|  |     healthcheck: | ||||||
|  |       test: mysqladmin ping -u $$MYSQL_USER --password=$$MYSQL_PASSWORD | ||||||
|  |       interval: 5s | ||||||
|  |       timeout: 10s | ||||||
|  |       retries: 10 | ||||||
|  |     volumes: | ||||||
|  |       - mysql-data:/var/lib/mysql | ||||||
|  |   minio: | ||||||
|  |     image: minio/minio:latest | ||||||
|  |     ports: | ||||||
|  |       - 9000:9000 | ||||||
|  |       - 9001:9001 | ||||||
|  |     env_file: | ||||||
|  |       - .env | ||||||
|  |     environment: | ||||||
|  |       MINIO_ROOT_USER: minio-root | ||||||
|  |     volumes: | ||||||
|  |       - minio-data:/data | ||||||
|  |     command: | ||||||
|  |       - server | ||||||
|  |       - /data | ||||||
|  |       - --console-address | ||||||
|  |       - :9001 | ||||||
|  |   api: | ||||||
|  |     build: . | ||||||
|  |     depends_on: | ||||||
|  |       db: | ||||||
|  |         condition: service_healthy | ||||||
|  |       minio: | ||||||
|  |         condition: service_started | ||||||
|  |     env_file: | ||||||
|  |       - .env | ||||||
|  |     environment: | ||||||
|  |       MYSQL_HOST: db | ||||||
|  |       MYSQL_PORT: 3306 | ||||||
|  |       MYSQL_DATABASE: meals_made_easy_api | ||||||
|  |       MYSQL_USERNAME: meals-made-easy-api-user | ||||||
|  |       MINIO_HOST: minio | ||||||
|  |       MINIO_PORT: 9000 | ||||||
|  |       MINIO_ROOT_USER: minio-root | ||||||
|  |     ports: | ||||||
|  |       - 8080:8080 | ||||||
|  | volumes: | ||||||
|  |   mysql-data: | ||||||
|  |   minio-data: | ||||||
| @ -3,17 +3,33 @@ package app.mealsmadeeasy.api.recipe; | |||||||
| import app.mealsmadeeasy.api.auth.AuthService; | import app.mealsmadeeasy.api.auth.AuthService; | ||||||
| import app.mealsmadeeasy.api.auth.LoginDetails; | import app.mealsmadeeasy.api.auth.LoginDetails; | ||||||
| import app.mealsmadeeasy.api.auth.LoginException; | import app.mealsmadeeasy.api.auth.LoginException; | ||||||
|  | import app.mealsmadeeasy.api.image.Image; | ||||||
|  | import app.mealsmadeeasy.api.image.ImageService; | ||||||
|  | import app.mealsmadeeasy.api.image.S3ImageServiceTests; | ||||||
|  | import app.mealsmadeeasy.api.image.spec.ImageCreateInfoSpec; | ||||||
| import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec; | import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec; | ||||||
|  | import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec; | ||||||
| import app.mealsmadeeasy.api.recipe.star.RecipeStarService; | import app.mealsmadeeasy.api.recipe.star.RecipeStarService; | ||||||
| import app.mealsmadeeasy.api.user.User; | import app.mealsmadeeasy.api.user.User; | ||||||
| import app.mealsmadeeasy.api.user.UserCreateException; | import app.mealsmadeeasy.api.user.UserCreateException; | ||||||
| import app.mealsmadeeasy.api.user.UserService; | import app.mealsmadeeasy.api.user.UserService; | ||||||
|  | import com.fasterxml.jackson.core.JsonProcessingException; | ||||||
|  | import com.fasterxml.jackson.databind.ObjectMapper; | ||||||
| import org.junit.jupiter.api.Test; | import org.junit.jupiter.api.Test; | ||||||
| import org.springframework.beans.factory.annotation.Autowired; | import org.springframework.beans.factory.annotation.Autowired; | ||||||
| import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; | ||||||
| import org.springframework.boot.test.context.SpringBootTest; | import org.springframework.boot.test.context.SpringBootTest; | ||||||
|  | import org.springframework.http.MediaType; | ||||||
| import org.springframework.test.annotation.DirtiesContext; | import org.springframework.test.annotation.DirtiesContext; | ||||||
|  | import org.springframework.test.context.DynamicPropertyRegistry; | ||||||
|  | import org.springframework.test.context.DynamicPropertySource; | ||||||
| import org.springframework.test.web.servlet.MockMvc; | import org.springframework.test.web.servlet.MockMvc; | ||||||
|  | import org.testcontainers.containers.MinIOContainer; | ||||||
|  | import org.testcontainers.junit.jupiter.Container; | ||||||
|  | import org.testcontainers.junit.jupiter.Testcontainers; | ||||||
|  | import org.testcontainers.utility.DockerImageName; | ||||||
|  | 
 | ||||||
|  | import java.io.InputStream; | ||||||
| 
 | 
 | ||||||
| import static org.hamcrest.Matchers.hasSize; | import static org.hamcrest.Matchers.hasSize; | ||||||
| import static org.hamcrest.Matchers.nullValue; | import static org.hamcrest.Matchers.nullValue; | ||||||
| @ -21,10 +37,27 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder | |||||||
| import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; | ||||||
| import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | ||||||
| 
 | 
 | ||||||
|  | @Testcontainers | ||||||
| @SpringBootTest | @SpringBootTest | ||||||
| @AutoConfigureMockMvc | @AutoConfigureMockMvc | ||||||
| public class RecipeControllerTests { | public class RecipeControllerTests { | ||||||
| 
 | 
 | ||||||
|  |     @Container | ||||||
|  |     private static final MinIOContainer container = new MinIOContainer( | ||||||
|  |             DockerImageName.parse("minio/minio:latest") | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     @DynamicPropertySource | ||||||
|  |     public static void minioProperties(DynamicPropertyRegistry registry) { | ||||||
|  |         registry.add("app.mealsmadeeasy.api.minio.endpoint", container::getS3URL); | ||||||
|  |         registry.add("app.mealsmadeeasy.api.minio.accessKey", container::getUserName); | ||||||
|  |         registry.add("app.mealsmadeeasy.api.minio.secretKey", container::getPassword); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static InputStream getHal9000() { | ||||||
|  |         return S3ImageServiceTests.class.getClassLoader().getResourceAsStream("HAL9000.svg"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Autowired |     @Autowired | ||||||
|     private MockMvc mockMvc; |     private MockMvc mockMvc; | ||||||
| 
 | 
 | ||||||
| @ -40,6 +73,12 @@ public class RecipeControllerTests { | |||||||
|     @Autowired |     @Autowired | ||||||
|     private AuthService authService; |     private AuthService authService; | ||||||
| 
 | 
 | ||||||
|  |     @Autowired | ||||||
|  |     private ImageService imageService; | ||||||
|  | 
 | ||||||
|  |     @Autowired | ||||||
|  |     private ObjectMapper objectMapper; | ||||||
|  | 
 | ||||||
|     private User createTestUser(String username) { |     private User createTestUser(String username) { | ||||||
|         try { |         try { | ||||||
|             return this.userService.createUser(username, username + "@test.com", "test"); |             return this.userService.createUser(username, username + "@test.com", "test"); | ||||||
| @ -70,6 +109,20 @@ public class RecipeControllerTests { | |||||||
|                 .getToken(); |                 .getToken(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private Image createHal9000(User owner) { | ||||||
|  |         try (final InputStream hal9000 = getHal9000()) { | ||||||
|  |             return this.imageService.create( | ||||||
|  |                     owner, | ||||||
|  |                     "HAL9000.svg", | ||||||
|  |                     hal9000, | ||||||
|  |                     27881L, | ||||||
|  |                     new ImageCreateInfoSpec() | ||||||
|  |             ); | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             throw new RuntimeException(e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Test |     @Test | ||||||
|     @DirtiesContext |     @DirtiesContext | ||||||
|     public void getRecipePageViewByIdPublicRecipeNoPrincipal() throws Exception { |     public void getRecipePageViewByIdPublicRecipeNoPrincipal() throws Exception { | ||||||
| @ -94,6 +147,7 @@ public class RecipeControllerTests { | |||||||
|                 .andExpect(jsonPath("$.recipe.starCount").value(0)) |                 .andExpect(jsonPath("$.recipe.starCount").value(0)) | ||||||
|                 .andExpect(jsonPath("$.recipe.viewerCount").value(0)) |                 .andExpect(jsonPath("$.recipe.viewerCount").value(0)) | ||||||
|                 .andExpect(jsonPath("$.recipe.isPublic").value(true)) |                 .andExpect(jsonPath("$.recipe.isPublic").value(true)) | ||||||
|  |                 .andExpect(jsonPath("$.recipe.mainImage").value(nullValue())) | ||||||
|                 .andExpect(jsonPath("$.isStarred").value(nullValue())) |                 .andExpect(jsonPath("$.isStarred").value(nullValue())) | ||||||
|                 .andExpect(jsonPath("$.isOwner").value(nullValue())); |                 .andExpect(jsonPath("$.isOwner").value(nullValue())); | ||||||
|     } |     } | ||||||
| @ -165,7 +219,8 @@ public class RecipeControllerTests { | |||||||
|                 .andExpect(jsonPath("$.content[0].owner.id").value(owner.getId())) |                 .andExpect(jsonPath("$.content[0].owner.id").value(owner.getId())) | ||||||
|                 .andExpect(jsonPath("$.content[0].owner.username").value(owner.getUsername())) |                 .andExpect(jsonPath("$.content[0].owner.username").value(owner.getUsername())) | ||||||
|                 .andExpect(jsonPath("$.content[0].isPublic").value(true)) |                 .andExpect(jsonPath("$.content[0].isPublic").value(true)) | ||||||
|                 .andExpect(jsonPath("$.content[0].starCount").value(0)); |                 .andExpect(jsonPath("$.content[0].starCount").value(0)) | ||||||
|  |                 .andExpect(jsonPath("$.content[0].mainImage").value(nullValue())); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
| @ -187,6 +242,83 @@ public class RecipeControllerTests { | |||||||
|                 .andExpect(jsonPath("$.content", hasSize(3))); |                 .andExpect(jsonPath("$.content", hasSize(3))); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private String getUpdateBody() throws JsonProcessingException { | ||||||
|  |         final RecipeUpdateSpec spec = new RecipeUpdateSpec(); | ||||||
|  |         spec.setTitle("Updated Test Recipe"); | ||||||
|  |         spec.setPreparationTime(15); | ||||||
|  |         spec.setCookingTime(30); | ||||||
|  |         spec.setTotalTime(45); | ||||||
|  |         spec.setRawText("# Hello, Updated World!"); | ||||||
|  |         spec.setIsPublic(true); | ||||||
|  |         return this.objectMapper.writeValueAsString(spec); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     @DirtiesContext | ||||||
|  |     public void updateRecipe() throws Exception { | ||||||
|  |         final User owner = this.createTestUser("owner"); | ||||||
|  |         final Recipe recipe = this.createTestRecipe(owner, false); | ||||||
|  |         final String accessToken = this.getAccessToken(owner); | ||||||
|  |         final String body = this.getUpdateBody(); | ||||||
|  |         this.mockMvc.perform( | ||||||
|  |                 post("/recipes/{username}/{slug}", owner.getUsername(), recipe.getSlug()) | ||||||
|  |                         .header("Authorization", "Bearer " + accessToken) | ||||||
|  |                         .contentType(MediaType.APPLICATION_JSON) | ||||||
|  |                         .content(body) | ||||||
|  |         ) | ||||||
|  |                 .andExpect(status().isOk()) | ||||||
|  |                 .andExpect(jsonPath("$.recipe.id").value(recipe.getId())) | ||||||
|  |                 .andExpect(jsonPath("$.recipe.title").value("Updated Test Recipe")) | ||||||
|  |                 .andExpect(jsonPath("$.recipe.preparationTime").value(15)) | ||||||
|  |                 .andExpect(jsonPath("$.recipe.cookingTime").value(30)) | ||||||
|  |                 .andExpect(jsonPath("$.recipe.totalTime").value(45)) | ||||||
|  |                 .andExpect(jsonPath("$.recipe.text").value("<h1>Hello, Updated World!</h1>")) | ||||||
|  |                 .andExpect(jsonPath("$.recipe.rawText").value("# Hello, Updated World!")) | ||||||
|  |                 .andExpect(jsonPath("$.recipe.owner.id").value(owner.getId())) | ||||||
|  |                 .andExpect(jsonPath("$.recipe.owner.username").value(owner.getUsername())) | ||||||
|  |                 .andExpect(jsonPath("$.recipe.starCount").value(0)) | ||||||
|  |                 .andExpect(jsonPath("$.recipe.viewerCount").value(0)) | ||||||
|  |                 .andExpect(jsonPath("$.recipe.isPublic").value(true)) | ||||||
|  |                 .andExpect(jsonPath("$.recipe.mainImage").value(nullValue())) | ||||||
|  |                 .andExpect(jsonPath("$.isStarred").value(false)) | ||||||
|  |                 .andExpect(jsonPath("$.isOwner").value(true)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     @DirtiesContext | ||||||
|  |     public void updateRecipeReturnsViewWithMainImage() throws Exception { | ||||||
|  |         final User owner = this.createTestUser("owner"); | ||||||
|  | 
 | ||||||
|  |         final Image hal9000 = this.createHal9000(owner); | ||||||
|  | 
 | ||||||
|  |         final RecipeCreateSpec createSpec = new RecipeCreateSpec(); | ||||||
|  |         createSpec.setTitle("Test Recipe"); | ||||||
|  |         createSpec.setSlug("test-recipe"); | ||||||
|  |         createSpec.setPublic(false); | ||||||
|  |         createSpec.setRawText("# Hello, World!"); | ||||||
|  |         createSpec.setMainImage(hal9000); | ||||||
|  |         Recipe recipe = this.recipeService.create(owner, createSpec); | ||||||
|  | 
 | ||||||
|  |         final RecipeUpdateSpec updateSpec = new RecipeUpdateSpec(); | ||||||
|  |         updateSpec.setTitle("Updated Test Recipe"); | ||||||
|  |         updateSpec.setRawText("# Hello, Updated World!"); | ||||||
|  |         final RecipeUpdateSpec.MainImageUpdateSpec mainImageUpdateSpec = new RecipeUpdateSpec.MainImageUpdateSpec(); | ||||||
|  |         mainImageUpdateSpec.setUsername(hal9000.getOwner().getUsername()); | ||||||
|  |         mainImageUpdateSpec.setFilename(hal9000.getUserFilename()); | ||||||
|  |         updateSpec.setMainImage(mainImageUpdateSpec); | ||||||
|  |         final String body = this.objectMapper.writeValueAsString(updateSpec); | ||||||
|  | 
 | ||||||
|  |         final String accessToken = this.getAccessToken(owner); | ||||||
|  |         this.mockMvc.perform( | ||||||
|  |                 post("/recipes/{username}/{slug}", owner.getUsername(), recipe.getSlug()) | ||||||
|  |                         .header("Authorization", "Bearer " + accessToken) | ||||||
|  |                         .contentType(MediaType.APPLICATION_JSON) | ||||||
|  |                         .content(body) | ||||||
|  |         ) | ||||||
|  |                 .andExpect(status().isOk()) | ||||||
|  |                 .andExpect(jsonPath("$.recipe.mainImage").isMap()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Test |     @Test | ||||||
|     @DirtiesContext |     @DirtiesContext | ||||||
|     public void addStarToRecipe() throws Exception { |     public void addStarToRecipe() throws Exception { | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| package app.mealsmadeeasy.api.recipe; | package app.mealsmadeeasy.api.recipe; | ||||||
| 
 | 
 | ||||||
|  | import app.mealsmadeeasy.api.image.ImageException; | ||||||
| import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec; | import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec; | ||||||
| import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec; | import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec; | ||||||
| import app.mealsmadeeasy.api.recipe.star.RecipeStar; | import app.mealsmadeeasy.api.recipe.star.RecipeStar; | ||||||
| @ -162,13 +163,18 @@ public class RecipeServiceTests { | |||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     @DirtiesContext |     @DirtiesContext | ||||||
|     public void getByIdWithStarsOkayWhenPublicRecipeWithViewer() throws RecipeException { |     public void getByIdWithStarsOkayWhenPublicRecipeWithViewer() throws RecipeException, ImageException { | ||||||
|         final User owner = this.createTestUser("recipeOwner"); |         final User owner = this.createTestUser("recipeOwner"); | ||||||
|         final User viewer = this.createTestUser("viewer"); |         final User viewer = this.createTestUser("viewer"); | ||||||
|         final Recipe notYetPublicRecipe = this.createTestRecipe(owner); |         final Recipe notYetPublicRecipe = this.createTestRecipe(owner); | ||||||
|         final RecipeUpdateSpec updateSpec = new RecipeUpdateSpec(); |         final RecipeUpdateSpec updateSpec = new RecipeUpdateSpec(notYetPublicRecipe); | ||||||
|         updateSpec.setPublic(true); |         updateSpec.setIsPublic(true); | ||||||
|         final Recipe publicRecipe = this.recipeService.update(notYetPublicRecipe.getId(), updateSpec, owner); |         final Recipe publicRecipe = this.recipeService.update( | ||||||
|  |                 notYetPublicRecipe.getOwner().getUsername(), | ||||||
|  |                 notYetPublicRecipe.getSlug(), | ||||||
|  |                 updateSpec, | ||||||
|  |                 owner | ||||||
|  |         ); | ||||||
|         assertDoesNotThrow(() -> this.recipeService.getByIdWithStars(publicRecipe.getId(), viewer)); |         assertDoesNotThrow(() -> this.recipeService.getByIdWithStars(publicRecipe.getId(), viewer)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -304,7 +310,7 @@ public class RecipeServiceTests { | |||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     @DirtiesContext |     @DirtiesContext | ||||||
|     public void updateRawText() throws RecipeException { |     public void updateRawText() throws RecipeException, ImageException { | ||||||
|         final User owner = this.createTestUser("recipeOwner"); |         final User owner = this.createTestUser("recipeOwner"); | ||||||
|         final RecipeCreateSpec createSpec = new RecipeCreateSpec(); |         final RecipeCreateSpec createSpec = new RecipeCreateSpec(); | ||||||
|         createSpec.setSlug("my-recipe"); |         createSpec.setSlug("my-recipe"); | ||||||
| @ -312,9 +318,14 @@ public class RecipeServiceTests { | |||||||
|         createSpec.setRawText("# A Heading"); |         createSpec.setRawText("# A Heading"); | ||||||
|         Recipe recipe = this.recipeService.create(owner, createSpec); |         Recipe recipe = this.recipeService.create(owner, createSpec); | ||||||
|         final String newRawText = "# A Heading\n## A Subheading"; |         final String newRawText = "# A Heading\n## A Subheading"; | ||||||
|         final RecipeUpdateSpec updateSpec = new RecipeUpdateSpec(); |         final RecipeUpdateSpec updateSpec = new RecipeUpdateSpec(recipe); | ||||||
|         updateSpec.setRawText(newRawText); |         updateSpec.setRawText(newRawText); | ||||||
|         recipe = this.recipeService.update(recipe.getId(), updateSpec, owner); |         recipe = this.recipeService.update( | ||||||
|  |                 recipe.getOwner().getUsername(), | ||||||
|  |                 recipe.getSlug(), | ||||||
|  |                 updateSpec, | ||||||
|  |                 owner | ||||||
|  |         ); | ||||||
|         assertThat(recipe.getRawText(), is(newRawText)); |         assertThat(recipe.getRawText(), is(newRawText)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -328,7 +339,12 @@ public class RecipeServiceTests { | |||||||
|         updateSpec.setRawText("should fail"); |         updateSpec.setRawText("should fail"); | ||||||
|         assertThrows( |         assertThrows( | ||||||
|                 AccessDeniedException.class, |                 AccessDeniedException.class, | ||||||
|                 () -> this.recipeService.update(recipe.getId(), updateSpec, notOwner) |                 () -> this.recipeService.update( | ||||||
|  |                         recipe.getOwner().getUsername(), | ||||||
|  |                         recipe.getSlug(), | ||||||
|  |                         updateSpec, | ||||||
|  |                         notOwner | ||||||
|  |                 ) | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										740
									
								
								src/integrationTest/resources/HAL9000.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										740
									
								
								src/integrationTest/resources/HAL9000.svg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,740 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||||
|  | <!-- Created with Inkscape (http://www.inkscape.org/) --> | ||||||
|  | 
 | ||||||
|  | <svg | ||||||
|  |         xmlns:dc="http://purl.org/dc/elements/1.1/" | ||||||
|  |         xmlns:cc="http://creativecommons.org/ns#" | ||||||
|  |         xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | ||||||
|  |         xmlns="http://www.w3.org/2000/svg" | ||||||
|  |         xmlns:xlink="http://www.w3.org/1999/xlink" | ||||||
|  |         xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | ||||||
|  |         xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | ||||||
|  |         version="1.1" | ||||||
|  |         width="256" | ||||||
|  |         height="256" | ||||||
|  |         id="svg2" | ||||||
|  |         inkscape:version="0.48.2 r9819" | ||||||
|  |         sodipodi:docname="HAL9000.svg"> | ||||||
|  |   <title | ||||||
|  |      id="title3116">HAL9000</title> | ||||||
|  |   <sodipodi:namedview | ||||||
|  |      pagecolor="#ffffff" | ||||||
|  |      bordercolor="#666666" | ||||||
|  |      borderopacity="1" | ||||||
|  |      objecttolerance="10" | ||||||
|  |      gridtolerance="10" | ||||||
|  |      guidetolerance="10" | ||||||
|  |      inkscape:pageopacity="0" | ||||||
|  |      inkscape:pageshadow="2" | ||||||
|  |      inkscape:window-width="2560" | ||||||
|  |      inkscape:window-height="1292" | ||||||
|  |      id="namedview139" | ||||||
|  |      showgrid="false" | ||||||
|  |      inkscape:zoom="2.6074563" | ||||||
|  |      inkscape:cx="94.488987" | ||||||
|  |      inkscape:cy="146.92353" | ||||||
|  |      inkscape:window-x="0" | ||||||
|  |      inkscape:window-y="0" | ||||||
|  |      inkscape:window-maximized="1" | ||||||
|  |      inkscape:current-layer="svg2" /> | ||||||
|  |   <defs | ||||||
|  |      id="defs4"> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient4716"> | ||||||
|  |       <stop | ||||||
|  |          id="stop4718" | ||||||
|  |          style="stop-color:#0a1314;stop-opacity:1" | ||||||
|  |          offset="0" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop4720" | ||||||
|  |          style="stop-color:#0a1314;stop-opacity:0" | ||||||
|  |          offset="1" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient4698"> | ||||||
|  |       <stop | ||||||
|  |          id="stop4700" | ||||||
|  |          style="stop-color:#424a4b;stop-opacity:1" | ||||||
|  |          offset="0" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop4702" | ||||||
|  |          style="stop-color:#424a4b;stop-opacity:0" | ||||||
|  |          offset="1" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient4680"> | ||||||
|  |       <stop | ||||||
|  |          id="stop4682" | ||||||
|  |          style="stop-color:#0e191c;stop-opacity:1" | ||||||
|  |          offset="0" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop4684" | ||||||
|  |          style="stop-color:#0e191c;stop-opacity:0" | ||||||
|  |          offset="1" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient4662"> | ||||||
|  |       <stop | ||||||
|  |          id="stop4664" | ||||||
|  |          style="stop-color:#f5f4f1;stop-opacity:1" | ||||||
|  |          offset="0" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop4666" | ||||||
|  |          style="stop-color:#f5f4f1;stop-opacity:0" | ||||||
|  |          offset="1" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient4632"> | ||||||
|  |       <stop | ||||||
|  |          id="stop4634" | ||||||
|  |          style="stop-color:#626463;stop-opacity:1" | ||||||
|  |          offset="0" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop4636" | ||||||
|  |          style="stop-color:#626463;stop-opacity:0" | ||||||
|  |          offset="1" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient4622"> | ||||||
|  |       <stop | ||||||
|  |          id="stop4624" | ||||||
|  |          style="stop-color:#b9b5b4;stop-opacity:1" | ||||||
|  |          offset="0" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop4626" | ||||||
|  |          style="stop-color:#9d9290;stop-opacity:0" | ||||||
|  |          offset="1" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient4594"> | ||||||
|  |       <stop | ||||||
|  |          id="stop4596" | ||||||
|  |          style="stop-color:#473e3e;stop-opacity:1" | ||||||
|  |          offset="0" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop4598" | ||||||
|  |          style="stop-color:#473e3e;stop-opacity:0" | ||||||
|  |          offset="1" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient4584"> | ||||||
|  |       <stop | ||||||
|  |          id="stop4586" | ||||||
|  |          style="stop-color:#b1aba9;stop-opacity:1" | ||||||
|  |          offset="0" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop4588" | ||||||
|  |          style="stop-color:#b1aba9;stop-opacity:0" | ||||||
|  |          offset="1" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient4572"> | ||||||
|  |       <stop | ||||||
|  |          id="stop4574" | ||||||
|  |          style="stop-color:#e0d8d4;stop-opacity:1" | ||||||
|  |          offset="0" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop4580" | ||||||
|  |          style="stop-color:#e0d8d4;stop-opacity:0.45490196" | ||||||
|  |          offset="0.67647064" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop4576" | ||||||
|  |          style="stop-color:#e0d8d4;stop-opacity:0" | ||||||
|  |          offset="1" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient4562"> | ||||||
|  |       <stop | ||||||
|  |          id="stop4564" | ||||||
|  |          style="stop-color:#cfcdc7;stop-opacity:1" | ||||||
|  |          offset="0" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop4566" | ||||||
|  |          style="stop-color:#cfcdc7;stop-opacity:0" | ||||||
|  |          offset="1" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient4546"> | ||||||
|  |       <stop | ||||||
|  |          id="stop4548" | ||||||
|  |          style="stop-color:#444544;stop-opacity:1" | ||||||
|  |          offset="0" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop4550" | ||||||
|  |          style="stop-color:#706062;stop-opacity:0" | ||||||
|  |          offset="1" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient4454"> | ||||||
|  |       <stop | ||||||
|  |          id="stop4456" | ||||||
|  |          style="stop-color:#bebbb6;stop-opacity:1" | ||||||
|  |          offset="0" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop4458" | ||||||
|  |          style="stop-color:#bebbb6;stop-opacity:0" | ||||||
|  |          offset="1" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient4446"> | ||||||
|  |       <stop | ||||||
|  |          id="stop4448" | ||||||
|  |          style="stop-color:#8f908a;stop-opacity:1" | ||||||
|  |          offset="0" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop4450" | ||||||
|  |          style="stop-color:#8f908a;stop-opacity:0" | ||||||
|  |          offset="1" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient4438"> | ||||||
|  |       <stop | ||||||
|  |          id="stop4440" | ||||||
|  |          style="stop-color:#ffffff;stop-opacity:1" | ||||||
|  |          offset="0" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop4442" | ||||||
|  |          style="stop-color:#ffffff;stop-opacity:0" | ||||||
|  |          offset="1" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient4430"> | ||||||
|  |       <stop | ||||||
|  |          id="stop4432" | ||||||
|  |          style="stop-color:#ea3231;stop-opacity:1" | ||||||
|  |          offset="0" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop4434" | ||||||
|  |          style="stop-color:#ea3231;stop-opacity:0" | ||||||
|  |          offset="1" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient3971"> | ||||||
|  |       <stop | ||||||
|  |          id="stop3973" | ||||||
|  |          style="stop-color:#ea1117;stop-opacity:1" | ||||||
|  |          offset="0" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop3975" | ||||||
|  |          style="stop-color:#d3070e;stop-opacity:1" | ||||||
|  |          offset="0.36951563" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop3977" | ||||||
|  |          style="stop-color:#c10914;stop-opacity:0" | ||||||
|  |          offset="1" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient3862"> | ||||||
|  |       <stop | ||||||
|  |          id="stop3864" | ||||||
|  |          style="stop-color:#ea1117;stop-opacity:1" | ||||||
|  |          offset="0" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop3866" | ||||||
|  |          style="stop-color:#ea1117;stop-opacity:0" | ||||||
|  |          offset="1" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient3798"> | ||||||
|  |       <stop | ||||||
|  |          id="stop3800" | ||||||
|  |          style="stop-color:#f8ee46;stop-opacity:1" | ||||||
|  |          offset="0" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop3802" | ||||||
|  |          style="stop-color:#d3321c;stop-opacity:0" | ||||||
|  |          offset="1" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient3774"> | ||||||
|  |       <stop | ||||||
|  |          id="stop3776" | ||||||
|  |          style="stop-color:#ea1117;stop-opacity:1" | ||||||
|  |          offset="0" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop3782" | ||||||
|  |          style="stop-color:#cd0d14;stop-opacity:1" | ||||||
|  |          offset="0.36951563" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop3778" | ||||||
|  |          style="stop-color:#c10914;stop-opacity:0" | ||||||
|  |          offset="1" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <filter | ||||||
|  |        color-interpolation-filters="sRGB" | ||||||
|  |        id="filter4426"> | ||||||
|  |       <feGaussianBlur | ||||||
|  |          id="feGaussianBlur4428" | ||||||
|  |          stdDeviation="0.79644532" /> | ||||||
|  |     </filter> | ||||||
|  |     <filter | ||||||
|  |        x="-0.014859335" | ||||||
|  |        y="-0.10673607" | ||||||
|  |        width="1.0297188" | ||||||
|  |        height="1.2134721" | ||||||
|  |        color-interpolation-filters="sRGB" | ||||||
|  |        id="filter4487"> | ||||||
|  |       <feGaussianBlur | ||||||
|  |          id="feGaussianBlur4489" | ||||||
|  |          stdDeviation="0.49337636" /> | ||||||
|  |     </filter> | ||||||
|  |     <filter | ||||||
|  |        color-interpolation-filters="sRGB" | ||||||
|  |        id="filter4511"> | ||||||
|  |       <feGaussianBlur | ||||||
|  |          id="feGaussianBlur4513" | ||||||
|  |          stdDeviation="0.36832783" /> | ||||||
|  |     </filter> | ||||||
|  |     <radialGradient | ||||||
|  |        cx="355.6181" | ||||||
|  |        cy="263.47437" | ||||||
|  |        r="214.64285" | ||||||
|  |        fx="355.6181" | ||||||
|  |        fy="263.47437" | ||||||
|  |        id="radialGradient4735" | ||||||
|  |        xlink:href="#linearGradient4662" | ||||||
|  |        gradientUnits="userSpaceOnUse" | ||||||
|  |        gradientTransform="matrix(0.49590542,0.02361451,-0.04756507,0.99886814,191.79733,-8.0995301)" /> | ||||||
|  |     <radialGradient | ||||||
|  |        cx="688.20172" | ||||||
|  |        cy="322.61343" | ||||||
|  |        r="214.64285" | ||||||
|  |        fx="688.20172" | ||||||
|  |        fy="322.61343" | ||||||
|  |        id="radialGradient4737" | ||||||
|  |        xlink:href="#linearGradient4662" | ||||||
|  |        gradientUnits="userSpaceOnUse" | ||||||
|  |        gradientTransform="matrix(0.18891652,0.20859515,-0.74120407,0.67127976,623.54686,-26.528142)" /> | ||||||
|  |     <radialGradient | ||||||
|  |        cx="-50.826534" | ||||||
|  |        cy="568.55469" | ||||||
|  |        r="214.64285" | ||||||
|  |        fx="-50.826534" | ||||||
|  |        fy="568.55469" | ||||||
|  |        id="radialGradient4739" | ||||||
|  |        xlink:href="#linearGradient4662" | ||||||
|  |        gradientUnits="userSpaceOnUse" | ||||||
|  |        gradientTransform="matrix(0.24198071,-0.24251704,0.72109578,0.71950102,-205.95295,-86.121137)" /> | ||||||
|  |     <radialGradient | ||||||
|  |        cx="143.2925" | ||||||
|  |        cy="560.57587" | ||||||
|  |        r="214.64285" | ||||||
|  |        fx="143.2925" | ||||||
|  |        fy="560.57587" | ||||||
|  |        id="radialGradient4741" | ||||||
|  |        xlink:href="#linearGradient4680" | ||||||
|  |        gradientUnits="userSpaceOnUse" | ||||||
|  |        gradientTransform="matrix(0.55737084,1.1227092,-1.0153425,0.5040684,651.47245,105.56632)" /> | ||||||
|  |     <radialGradient | ||||||
|  |        cx="359.53653" | ||||||
|  |        cy="680.74078" | ||||||
|  |        r="214.64285" | ||||||
|  |        fx="359.53653" | ||||||
|  |        fy="680.74078" | ||||||
|  |        id="radialGradient4743" | ||||||
|  |        xlink:href="#linearGradient4698" | ||||||
|  |        gradientUnits="userSpaceOnUse" | ||||||
|  |        gradientTransform="matrix(0.59429926,-0.0196788,0.03309447,0.99945223,124.48576,7.5040537)" /> | ||||||
|  |     <radialGradient | ||||||
|  |        cx="549.07318" | ||||||
|  |        cy="531.27026" | ||||||
|  |        r="214.64285" | ||||||
|  |        fx="549.07318" | ||||||
|  |        fy="531.27026" | ||||||
|  |        id="radialGradient4745" | ||||||
|  |        xlink:href="#linearGradient4716" | ||||||
|  |        gradientUnits="userSpaceOnUse" | ||||||
|  |        gradientTransform="matrix(-0.28951144,0.61897854,-1.0261698,-0.47996685,1253.2096,446.39787)" /> | ||||||
|  |     <radialGradient | ||||||
|  |        cx="137.3555" | ||||||
|  |        cy="130.31177" | ||||||
|  |        r="87.547356" | ||||||
|  |        fx="137.3555" | ||||||
|  |        fy="130.31177" | ||||||
|  |        id="radialGradient4965" | ||||||
|  |        xlink:href="#linearGradient3774" | ||||||
|  |        gradientUnits="userSpaceOnUse" /> | ||||||
|  |     <radialGradient | ||||||
|  |        cx="137.3555" | ||||||
|  |        cy="130.31177" | ||||||
|  |        r="87.547356" | ||||||
|  |        fx="137.3555" | ||||||
|  |        fy="130.31177" | ||||||
|  |        id="radialGradient4967" | ||||||
|  |        xlink:href="#linearGradient3971" | ||||||
|  |        gradientUnits="userSpaceOnUse" /> | ||||||
|  |     <radialGradient | ||||||
|  |        cx="126.875" | ||||||
|  |        cy="125.125" | ||||||
|  |        r="44.125" | ||||||
|  |        fx="126.875" | ||||||
|  |        fy="125.125" | ||||||
|  |        id="radialGradient4969" | ||||||
|  |        xlink:href="#linearGradient3862" | ||||||
|  |        gradientUnits="userSpaceOnUse" /> | ||||||
|  |     <radialGradient | ||||||
|  |        cx="493.20697" | ||||||
|  |        cy="120.2355" | ||||||
|  |        r="20.152544" | ||||||
|  |        fx="493.20697" | ||||||
|  |        fy="120.2355" | ||||||
|  |        id="radialGradient4971" | ||||||
|  |        xlink:href="#linearGradient3798" | ||||||
|  |        gradientUnits="userSpaceOnUse" | ||||||
|  |        gradientTransform="matrix(0.75621888,0,0,0.75621888,120.23455,29.311144)" /> | ||||||
|  |     <linearGradient | ||||||
|  |        x1="59.75" | ||||||
|  |        y1="853.86218" | ||||||
|  |        x2="63.5" | ||||||
|  |        y2="848.86218" | ||||||
|  |        id="linearGradient4973" | ||||||
|  |        xlink:href="#linearGradient4454" | ||||||
|  |        gradientUnits="userSpaceOnUse" /> | ||||||
|  |     <linearGradient | ||||||
|  |        x1="65.5" | ||||||
|  |        y1="878.36218" | ||||||
|  |        x2="89" | ||||||
|  |        y2="879.86218" | ||||||
|  |        id="linearGradient4975" | ||||||
|  |        xlink:href="#linearGradient4438" | ||||||
|  |        gradientUnits="userSpaceOnUse" /> | ||||||
|  |     <linearGradient | ||||||
|  |        x1="159.09903" | ||||||
|  |        y1="895.73804" | ||||||
|  |        x2="155.03316" | ||||||
|  |        y2="895.2077" | ||||||
|  |        id="linearGradient4977" | ||||||
|  |        xlink:href="#linearGradient4430" | ||||||
|  |        gradientUnits="userSpaceOnUse" /> | ||||||
|  |     <linearGradient | ||||||
|  |        x1="201.5" | ||||||
|  |        y1="859.36218" | ||||||
|  |        x2="192.5" | ||||||
|  |        y2="865.86218" | ||||||
|  |        id="linearGradient4979" | ||||||
|  |        xlink:href="#linearGradient4446" | ||||||
|  |        gradientUnits="userSpaceOnUse" /> | ||||||
|  |     <radialGradient | ||||||
|  |        cx="565.67145" | ||||||
|  |        cy="446.36511" | ||||||
|  |        r="214.64285" | ||||||
|  |        fx="565.67145" | ||||||
|  |        fy="446.36511" | ||||||
|  |        id="radialGradient4981" | ||||||
|  |        xlink:href="#linearGradient4546" | ||||||
|  |        gradientUnits="userSpaceOnUse" | ||||||
|  |        gradientTransform="matrix(-0.01141222,0.7200172,-0.98807046,-0.01566569,1003.895,64.801116)" /> | ||||||
|  |     <radialGradient | ||||||
|  |        cx="525.8512" | ||||||
|  |        cy="583.23352" | ||||||
|  |        r="214.64285" | ||||||
|  |        fx="525.8512" | ||||||
|  |        fy="583.23352" | ||||||
|  |        id="radialGradient4983" | ||||||
|  |        xlink:href="#linearGradient4562" | ||||||
|  |        gradientUnits="userSpaceOnUse" | ||||||
|  |        gradientTransform="matrix(-0.13019845,0.14665728,-0.25941753,-0.23030421,735.49435,657.5781)" /> | ||||||
|  |     <radialGradient | ||||||
|  |        cx="338.53104" | ||||||
|  |        cy="703.86841" | ||||||
|  |        r="214.64285" | ||||||
|  |        fx="338.53104" | ||||||
|  |        fy="703.86841" | ||||||
|  |        id="radialGradient4985" | ||||||
|  |        xlink:href="#linearGradient4572" | ||||||
|  |        gradientUnits="userSpaceOnUse" | ||||||
|  |        gradientTransform="matrix(-0.27995439,0.0091529,-0.01235873,-0.37800452,464.47395,950.87477)" /> | ||||||
|  |     <radialGradient | ||||||
|  |        cx="241.97748" | ||||||
|  |        cy="591.31604" | ||||||
|  |        r="214.64285" | ||||||
|  |        fx="241.97748" | ||||||
|  |        fy="591.31604" | ||||||
|  |        id="radialGradient4987" | ||||||
|  |        xlink:href="#linearGradient4584" | ||||||
|  |        gradientUnits="userSpaceOnUse" | ||||||
|  |        gradientTransform="matrix(0.0653018,0.07876991,-0.51364189,0.42581899,480.05196,325.10195)" /> | ||||||
|  |     <radialGradient | ||||||
|  |        cx="148.8054" | ||||||
|  |        cy="479.24811" | ||||||
|  |        r="214.64285" | ||||||
|  |        fx="148.8054" | ||||||
|  |        fy="479.24811" | ||||||
|  |        id="radialGradient4989" | ||||||
|  |        xlink:href="#linearGradient4594" | ||||||
|  |        gradientUnits="userSpaceOnUse" | ||||||
|  |        gradientTransform="matrix(0.03414964,0.69195514,-1.3162287,0.06495088,774.52389,345.15386)" /> | ||||||
|  |     <radialGradient | ||||||
|  |        cx="361.88593" | ||||||
|  |        cy="270.58835" | ||||||
|  |        r="214.64285" | ||||||
|  |        fx="361.88593" | ||||||
|  |        fy="270.58835" | ||||||
|  |        id="radialGradient4991" | ||||||
|  |        xlink:href="#linearGradient4622" | ||||||
|  |        gradientUnits="userSpaceOnUse" | ||||||
|  |        gradientTransform="matrix(0.50253939,0.0288342,-0.05728614,0.9983578,195.52495,-9.9903306)" /> | ||||||
|  |     <radialGradient | ||||||
|  |        cx="113.19639" | ||||||
|  |        cy="362.84845" | ||||||
|  |        r="214.64285" | ||||||
|  |        fx="113.19639" | ||||||
|  |        fy="362.84845" | ||||||
|  |        id="radialGradient4993" | ||||||
|  |        xlink:href="#linearGradient4632" | ||||||
|  |        gradientUnits="userSpaceOnUse" | ||||||
|  |        gradientTransform="matrix(-0.08238352,0.11533689,-0.81373347,-0.58123819,490.52721,552.42839)" /> | ||||||
|  |   </defs> | ||||||
|  |   <metadata | ||||||
|  |      id="metadata7"> | ||||||
|  |     <rdf:RDF> | ||||||
|  |       <cc:Work | ||||||
|  |          rdf:about=""> | ||||||
|  |         <dc:format>image/svg+xml</dc:format> | ||||||
|  |         <dc:type | ||||||
|  |            rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> | ||||||
|  |         <dc:title>HAL9000</dc:title> | ||||||
|  |         <dc:date></dc:date> | ||||||
|  |         <dc:creator> | ||||||
|  |           <cc:Agent> | ||||||
|  |             <dc:title>MorningLemon</dc:title> | ||||||
|  |           </cc:Agent> | ||||||
|  |         </dc:creator> | ||||||
|  |         <dc:language>German</dc:language> | ||||||
|  |         <dc:subject> | ||||||
|  |           <rdf:Bag> | ||||||
|  |             <rdf:li>HAL</rdf:li> | ||||||
|  |             <rdf:li>9000</rdf:li> | ||||||
|  |             <rdf:li>HAL9000</rdf:li> | ||||||
|  |             <rdf:li>robot</rdf:li> | ||||||
|  |             <rdf:li>space</rdf:li> | ||||||
|  |           </rdf:Bag> | ||||||
|  |         </dc:subject> | ||||||
|  |         <dc:description>The famous red eye of HAL 9000 from Stanley Kubricks Film "2001: A Space Odyssey".</dc:description> | ||||||
|  |         <cc:license | ||||||
|  |            rdf:resource="http://creativecommons.org/licenses/by/3.0/" /> | ||||||
|  |       </cc:Work> | ||||||
|  |       <cc:License | ||||||
|  |          rdf:about="http://creativecommons.org/licenses/by/3.0/"> | ||||||
|  |         <cc:permits | ||||||
|  |            rdf:resource="http://creativecommons.org/ns#Reproduction" /> | ||||||
|  |         <cc:permits | ||||||
|  |            rdf:resource="http://creativecommons.org/ns#Distribution" /> | ||||||
|  |         <cc:requires | ||||||
|  |            rdf:resource="http://creativecommons.org/ns#Notice" /> | ||||||
|  |         <cc:requires | ||||||
|  |            rdf:resource="http://creativecommons.org/ns#Attribution" /> | ||||||
|  |         <cc:permits | ||||||
|  |            rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /> | ||||||
|  |       </cc:License> | ||||||
|  |     </rdf:RDF> | ||||||
|  |   </metadata> | ||||||
|  |   <g | ||||||
|  |      transform="translate(0,-796.36218)" | ||||||
|  |      id="layer1"> | ||||||
|  |     <g | ||||||
|  |        transform="translate(-727,-21)" | ||||||
|  |        id="g4726"> | ||||||
|  |       <path | ||||||
|  |          d="m 568.57141,471.29074 a 214.64285,214.64285 0 1 1 -429.2857,0 214.64285,214.64285 0 1 1 429.2857,0 z" | ||||||
|  |          transform="matrix(0.59186864,0,0,0.59186864,645.52079,666.41997)" | ||||||
|  |          id="path4724" | ||||||
|  |          style="fill:#5d5f5f;fill-opacity:1;stroke:none" /> | ||||||
|  |       <path | ||||||
|  |          d="m 568.57141,471.29074 a 214.64285,214.64285 0 1 1 -429.2857,0 214.64285,214.64285 0 1 1 429.2857,0 z" | ||||||
|  |          transform="matrix(0.59186864,0,0,0.59186864,645.52079,666.41997)" | ||||||
|  |          id="path4642" | ||||||
|  |          style="fill:url(#radialGradient4735);fill-opacity:1;stroke:none" /> | ||||||
|  |       <path | ||||||
|  |          d="m 568.57141,471.29074 a 214.64285,214.64285 0 1 1 -429.2857,0 214.64285,214.64285 0 1 1 429.2857,0 z" | ||||||
|  |          transform="matrix(0.59186864,0,0,0.59186864,645.52079,666.41997)" | ||||||
|  |          id="path4670" | ||||||
|  |          style="fill:url(#radialGradient4737);fill-opacity:1;stroke:none" /> | ||||||
|  |       <path | ||||||
|  |          d="m 568.57141,471.29074 a 214.64285,214.64285 0 1 1 -429.2857,0 214.64285,214.64285 0 1 1 429.2857,0 z" | ||||||
|  |          transform="matrix(0.59186864,0,0,0.59186864,645.52079,666.41997)" | ||||||
|  |          id="path4674" | ||||||
|  |          style="fill:url(#radialGradient4739);fill-opacity:1;stroke:none" /> | ||||||
|  |       <path | ||||||
|  |          d="m 568.57141,471.29074 a 214.64285,214.64285 0 1 1 -429.2857,0 214.64285,214.64285 0 1 1 429.2857,0 z" | ||||||
|  |          transform="matrix(0.59186864,0,0,0.59186864,645.52079,666.41997)" | ||||||
|  |          id="path4678" | ||||||
|  |          style="fill:url(#radialGradient4741);fill-opacity:1;stroke:none" /> | ||||||
|  |       <path | ||||||
|  |          d="m 568.57141,471.29074 a 214.64285,214.64285 0 1 1 -429.2857,0 214.64285,214.64285 0 1 1 429.2857,0 z" | ||||||
|  |          transform="matrix(0.59186864,0,0,0.59186864,645.52079,666.41997)" | ||||||
|  |          id="path4688" | ||||||
|  |          style="fill:url(#radialGradient4743);fill-opacity:1;stroke:none" /> | ||||||
|  |       <path | ||||||
|  |          d="m 568.57141,471.29074 a 214.64285,214.64285 0 1 1 -429.2857,0 214.64285,214.64285 0 1 1 429.2857,0 z" | ||||||
|  |          transform="matrix(0.59186864,0,0,0.59186864,645.52079,666.41997)" | ||||||
|  |          id="path4706" | ||||||
|  |          style="fill:url(#radialGradient4745);fill-opacity:1;stroke:none" /> | ||||||
|  |     </g> | ||||||
|  |     <g | ||||||
|  |        id="g4924"> | ||||||
|  |       <path | ||||||
|  |          d="m 568.57141,471.29074 a 214.64285,214.64285 0 1 1 -429.2857,0 214.64285,214.64285 0 1 1 429.2857,0 z" | ||||||
|  |          transform="matrix(0.56551391,0,0,0.56551391,-72.151523,657.84071)" | ||||||
|  |          id="path3766" | ||||||
|  |          style="fill:#706062;fill-opacity:1;stroke:none" /> | ||||||
|  |       <path | ||||||
|  |          d="m 568.57141,471.29074 a 214.64285,214.64285 0 1 1 -429.2857,0 214.64285,214.64285 0 1 1 429.2857,0 z" | ||||||
|  |          transform="matrix(0.56551391,0,0,0.56551391,-72.151523,657.84071)" | ||||||
|  |          id="path4556" | ||||||
|  |          style="fill:#767676;fill-opacity:1;stroke:none" /> | ||||||
|  |       <path | ||||||
|  |          d="m 568.57141,471.29074 a 214.64285,214.64285 0 1 1 -429.2857,0 214.64285,214.64285 0 1 1 429.2857,0 z" | ||||||
|  |          transform="matrix(0.56551391,0,0,0.56551391,-72.151523,657.84071)" | ||||||
|  |          id="path4544" | ||||||
|  |          style="fill:url(#radialGradient4981);fill-opacity:1;stroke:none" /> | ||||||
|  |       <path | ||||||
|  |          d="m 568.57141,471.29074 a 214.64285,214.64285 0 1 1 -429.2857,0 214.64285,214.64285 0 1 1 429.2857,0 z" | ||||||
|  |          transform="matrix(0.56551391,0,0,0.56551391,-72.151523,657.84071)" | ||||||
|  |          id="path4560" | ||||||
|  |          style="fill:url(#radialGradient4983);fill-opacity:1;stroke:none" /> | ||||||
|  |       <path | ||||||
|  |          d="m 568.57141,471.29074 a 214.64285,214.64285 0 1 1 -429.2857,0 214.64285,214.64285 0 1 1 429.2857,0 z" | ||||||
|  |          transform="matrix(0.56551391,0,0,0.56551391,-72.151523,657.84071)" | ||||||
|  |          id="path4570" | ||||||
|  |          style="fill:url(#radialGradient4985);fill-opacity:1;stroke:none" /> | ||||||
|  |       <path | ||||||
|  |          d="m 568.57141,471.29074 a 214.64285,214.64285 0 1 1 -429.2857,0 214.64285,214.64285 0 1 1 429.2857,0 z" | ||||||
|  |          transform="matrix(0.56551391,0,0,0.56551391,-72.151523,657.84071)" | ||||||
|  |          id="path4582" | ||||||
|  |          style="fill:url(#radialGradient4987);fill-opacity:1;stroke:none" /> | ||||||
|  |       <path | ||||||
|  |          d="m 568.57141,471.29074 a 214.64285,214.64285 0 1 1 -429.2857,0 214.64285,214.64285 0 1 1 429.2857,0 z" | ||||||
|  |          transform="matrix(0.56551391,0,0,0.56551391,-72.151523,657.84071)" | ||||||
|  |          id="path4592" | ||||||
|  |          style="fill:url(#radialGradient4989);fill-opacity:1;stroke:none" /> | ||||||
|  |       <path | ||||||
|  |          d="m 568.57141,471.29074 a 214.64285,214.64285 0 1 1 -429.2857,0 214.64285,214.64285 0 1 1 429.2857,0 z" | ||||||
|  |          transform="matrix(0.56551391,0,0,0.56551391,-72.151523,657.84071)" | ||||||
|  |          id="path4602" | ||||||
|  |          style="fill:url(#radialGradient4991);fill-opacity:1;stroke:none" /> | ||||||
|  |       <path | ||||||
|  |          d="m 568.57141,471.29074 a 214.64285,214.64285 0 1 1 -429.2857,0 214.64285,214.64285 0 1 1 429.2857,0 z" | ||||||
|  |          transform="matrix(0.56551391,0,0,0.56551391,-72.151523,657.84071)" | ||||||
|  |          id="path4630" | ||||||
|  |          style="fill:url(#radialGradient4993);fill-opacity:1;stroke:none" /> | ||||||
|  |     </g> | ||||||
|  |     <g | ||||||
|  |        transform="translate(-2.2167876e-7,-3.5831251e-6)" | ||||||
|  |        id="g4879"> | ||||||
|  |       <g | ||||||
|  |          id="g4800"> | ||||||
|  |         <path | ||||||
|  |            d="m 568.57141,471.29074 a 214.64285,214.64285 0 1 1 -429.2857,0 214.64285,214.64285 0 1 1 429.2857,0 z" | ||||||
|  |            transform="matrix(0.51198087,0,0,0.51198087,-53.204651,683.07034)" | ||||||
|  |            id="path3768" | ||||||
|  |            style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-opacity:1" /> | ||||||
|  |         <g | ||||||
|  |            id="g4785"> | ||||||
|  |           <path | ||||||
|  |              d="m 224.15285,130.31177 a 86.797356,86.797356 0 1 1 -173.594706,0 86.797356,86.797356 0 1 1 173.594706,0 z" | ||||||
|  |              transform="matrix(0.74941633,0,0,0.74941633,25.063546,826.70441)" | ||||||
|  |              id="path3874" | ||||||
|  |              style="opacity:0.38050316;fill:url(#radialGradient4965);fill-opacity:1;stroke:none" /> | ||||||
|  |           <path | ||||||
|  |              d="m 224.15285,130.31177 a 86.797356,86.797356 0 1 1 -173.594706,0 86.797356,86.797356 0 1 1 173.594706,0 z" | ||||||
|  |              transform="translate(-9.3554993,794.05041)" | ||||||
|  |              id="path3772" | ||||||
|  |              style="fill:url(#radialGradient4967);fill-opacity:1;stroke:none" /> | ||||||
|  |           <path | ||||||
|  |              d="m 170.25,125.125 a 43.375,43.375 0 1 1 -86.75,0 43.375,43.375 0 1 1 86.75,0 z" | ||||||
|  |              transform="translate(1.125,801.23718)" | ||||||
|  |              id="path3860" | ||||||
|  |              style="fill:url(#radialGradient4969);fill-opacity:1;stroke:#ef1d00;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:0;stroke-dasharray:none;stroke-dashoffset:0" /> | ||||||
|  |           <path | ||||||
|  |              d="m 513.35951,120.2355 a 20.152544,20.152544 0 1 1 -40.30508,0 20.152544,20.152544 0 1 1 40.30508,0 z" | ||||||
|  |              transform="matrix(1.6843175,0,0,1.6843175,-702.71713,721.84743)" | ||||||
|  |              id="path3796" | ||||||
|  |              style="fill:url(#radialGradient4971);fill-opacity:1;stroke:none" /> | ||||||
|  |           <path | ||||||
|  |              d="m 498.51029,117.9374 a 4.0658641,4.0658641 0 1 1 -8.13173,0 4.0658641,4.0658641 0 1 1 8.13173,0 z" | ||||||
|  |              transform="matrix(0.86956522,0,0,0.86956522,-301.95168,821.80792)" | ||||||
|  |              id="path3818" | ||||||
|  |              style="fill:#f4f846;fill-opacity:1;stroke:none" /> | ||||||
|  |         </g> | ||||||
|  |       </g> | ||||||
|  |       <g | ||||||
|  |          id="g4859"> | ||||||
|  |         <g | ||||||
|  |            id="g4393" | ||||||
|  |            style="filter:url(#filter4426)"> | ||||||
|  |           <path | ||||||
|  |              d="m 98.20252,833.52304 2.12132,8.3085 c 15.36068,-5.88046 45.97986,-3.83259 61.87184,3.71231 l 1.59099,-8.3085 C 143.78528,828.55429 112.399,829.1646 98.20252,833.52304 z" | ||||||
|  |              id="path4007" | ||||||
|  |              style="fill:#c9c9bf;fill-opacity:1;stroke:none" /> | ||||||
|  |           <path | ||||||
|  |              d="m 58.60454,849.25616 c -13.04121,17.05062 -17.38118,27.71714 -19.09189,37.29989 3.26274,-1.56268 4.80687,-4.56751 6.34116,-7.26083 6.11733,-9.48606 12.36426,-19.07676 21.05923,-25.44286 z" | ||||||
|  |              id="path4009" | ||||||
|  |              style="fill:url(#linearGradient4973);fill-opacity:1;stroke:none" /> | ||||||
|  |           <path | ||||||
|  |              d="m 58.60454,877.71721 11.3137,-8.3085 9.54595,9.36916 -10.78338,4.5962 z" | ||||||
|  |              id="path4011" | ||||||
|  |              style="opacity:0.53773588;fill:url(#linearGradient4975);fill-opacity:1;stroke:none" /> | ||||||
|  |           <path | ||||||
|  |              d="m 111.10722,853.49881 c 9.39243,-1.29788 19.13002,-0.86996 28.99137,0.17677 0.56932,2.02341 0.78297,4.40249 0.88389,6.89429 -9.25131,-0.68705 -18.50263,-1.334 -27.75394,-0.17677 -0.425,-2.01599 -1.22929,-4.41128 -2.12132,-6.89429 z" | ||||||
|  |              id="path4013" | ||||||
|  |              style="opacity:0.53773588;fill:#ffffff;fill-opacity:1;stroke:none" /> | ||||||
|  |           <path | ||||||
|  |              d="m 178.81269,871.17648 -5.12652,6.54073 c 2.70117,0.95519 6.19764,2.10921 10.78337,3.53554 1.18577,-1.42148 2.82121,-3.29262 4.94975,-5.65686 -2.93649,-1.67282 -6.26255,-3.21578 -10.6066,-4.41941 z" | ||||||
|  |              id="path4015" | ||||||
|  |              style="fill:#816461;fill-opacity:1;stroke:none" /> | ||||||
|  |           <path | ||||||
|  |              d="m 91.66178,896.98587 5.12652,-3.88909 2.2981,3.53554 -3.88909,3.18198 z" | ||||||
|  |              id="path4017" | ||||||
|  |              style="fill:#f15e4f;fill-opacity:1;stroke:none" /> | ||||||
|  |           <path | ||||||
|  |              d="m 118.88539,882.31341 c 5.81338,-0.94098 11.07456,-0.22537 16.44023,0.17677 l -0.35355,3.18198 c -5.24438,-0.1614 -10.48875,-0.5541 -15.73313,0.17678 z" | ||||||
|  |              id="path4019" | ||||||
|  |              style="fill:#ec4e3e;fill-opacity:1;stroke:none" /> | ||||||
|  |           <path | ||||||
|  |              d="m 119.76927,895.21811 c 7.32697,-1.53523 13.82111,-1.00484 19.26866,2.12132 l 1.41422,-2.82843 c -7.72213,-3.49102 -16.3628,-3.26623 -21.38998,-1.59099 z" | ||||||
|  |              id="path4021" | ||||||
|  |              style="fill:#e66044;fill-opacity:1;stroke:none" /> | ||||||
|  |           <path | ||||||
|  |              d="m 160.25114,898.75364 2.12132,-3.0052 -4.94975,-2.2981 -2.65165,2.2981 z" | ||||||
|  |              id="path4023" | ||||||
|  |              style="fill:url(#linearGradient4977);fill-opacity:1;stroke:none" /> | ||||||
|  |           <path | ||||||
|  |              d="m 155.30139,906.70859 c -2.27582,-3.53407 -2.72679,-5.37411 -4.77297,-7.07107 l -1.59099,1.06066 c 2.46956,2.24316 4.48363,4.82793 6.01041,7.77818 z" | ||||||
|  |              id="path4025" | ||||||
|  |              style="fill:#f74639;fill-opacity:1;stroke:none" /> | ||||||
|  |           <path | ||||||
|  |              d="m 132.4972,901.40529 c -2.88735,-0.75674 -5.77471,-0.58908 -8.66206,-0.17678 l 0,-1.94454 c 2.70228,-0.8158 5.54755,-0.77368 8.48528,-0.17678 z" | ||||||
|  |              id="path4027" | ||||||
|  |              style="fill:#ef4d2b;fill-opacity:1;stroke:none" /> | ||||||
|  |           <path | ||||||
|  |              d="m 108.63234,900.52141 c -3.03319,2.75901 -5.03926,5.90318 -6.18718,9.36916 l -1.06066,-1.06066 c 1.17722,-3.76435 2.99712,-7.35399 6.0104,-10.42982 z" | ||||||
|  |              id="path4029" | ||||||
|  |              style="fill:#eb5241;fill-opacity:1;stroke:none" /> | ||||||
|  |           <path | ||||||
|  |              d="m 110.57688,908.47636 1.06067,-2.47488 -2.65166,-1.06066 -1.23743,2.47488 z" | ||||||
|  |              id="path4031" | ||||||
|  |              style="fill:#f7432e;fill-opacity:1;stroke:none" /> | ||||||
|  |           <path | ||||||
|  |              d="m 146.99288,908.29958 1.23744,-1.94454 -2.12132,-1.23744 -1.76777,1.41421 z" | ||||||
|  |              id="path4033" | ||||||
|  |              style="fill:#f7432e;fill-opacity:1;stroke:none" /> | ||||||
|  |           <path | ||||||
|  |              d="m 191.57094,855.65046 4.77297,-4.06587 c 8.44599,4.49145 14.50078,13.36678 20.15255,22.98097 l -2.47488,2.47488 c -7.3081,-7.48088 -14.67726,-14.83965 -22.45064,-21.38998 z" | ||||||
|  |              id="path4035" | ||||||
|  |              style="fill:url(#linearGradient4979);fill-opacity:1;stroke:none" /> | ||||||
|  |         </g> | ||||||
|  |         <path | ||||||
|  |            d="m 128,26.25 c -12.15497,0 -23.81896,2.153186 -34.625,6.0625 l 2.34375,0.15625 C 105.86477,29.075845 116.71287,28.25 128,28.25 c 15.02248,0 29.28979,2.249441 42.125,8.09375 L 173.0625,36.75 C 159.47932,30.02936 144.18114,26.25 128,26.25 z" | ||||||
|  |            transform="translate(0,796.36218)" | ||||||
|  |            id="path4462" | ||||||
|  |            style="opacity:0.93081761;fill:#35373c;fill-opacity:1;stroke:none;filter:url(#filter4487)" /> | ||||||
|  |         <path | ||||||
|  |            d="M 49.71875,63 C 46.008058,67.46453 42.663152,72.251252 39.75,77.3125 l 0.1875,0.03125 c 1.24315,-2.050869 3.554075,-4.055585 4.931758,-6.00967 1.73058,-2.454631 3.566499,-5.829368 5.505742,-8.11533 z" | ||||||
|  |            transform="translate(0,796.36218)" | ||||||
|  |            id="path4469" | ||||||
|  |            style="opacity:0.39308178;fill:#bab7b2;fill-opacity:1;stroke:none;filter:url(#filter4511)" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 27 KiB | 
| @ -2,8 +2,10 @@ package app.mealsmadeeasy.api; | |||||||
| 
 | 
 | ||||||
| import org.springframework.boot.SpringApplication; | import org.springframework.boot.SpringApplication; | ||||||
| import org.springframework.boot.autoconfigure.SpringBootApplication; | import org.springframework.boot.autoconfigure.SpringBootApplication; | ||||||
|  | import org.springframework.scheduling.annotation.EnableScheduling; | ||||||
| 
 | 
 | ||||||
| @SpringBootApplication | @SpringBootApplication | ||||||
|  | @EnableScheduling | ||||||
| public class MealsMadeEasyApiApplication { | public class MealsMadeEasyApiApplication { | ||||||
| 
 | 
 | ||||||
| 	public static void main(String[] args) { | 	public static void main(String[] args) { | ||||||
|  | |||||||
| @ -2,8 +2,10 @@ package app.mealsmadeeasy.api.auth; | |||||||
| 
 | 
 | ||||||
| import app.mealsmadeeasy.api.jwt.JwtService; | import app.mealsmadeeasy.api.jwt.JwtService; | ||||||
| import app.mealsmadeeasy.api.user.UserEntity; | import app.mealsmadeeasy.api.user.UserEntity; | ||||||
|  | import jakarta.transaction.Transactional; | ||||||
| import org.jetbrains.annotations.Nullable; | import org.jetbrains.annotations.Nullable; | ||||||
| import org.springframework.beans.factory.annotation.Value; | import org.springframework.beans.factory.annotation.Value; | ||||||
|  | import org.springframework.scheduling.annotation.Scheduled; | ||||||
| import org.springframework.security.authentication.AuthenticationManager; | import org.springframework.security.authentication.AuthenticationManager; | ||||||
| import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||||||
| import org.springframework.security.core.Authentication; | import org.springframework.security.core.Authentication; | ||||||
| @ -12,9 +14,10 @@ import org.springframework.stereotype.Service; | |||||||
| 
 | 
 | ||||||
| import java.time.LocalDateTime; | import java.time.LocalDateTime; | ||||||
| import java.util.UUID; | import java.util.UUID; | ||||||
|  | import java.util.concurrent.TimeUnit; | ||||||
| 
 | 
 | ||||||
| @Service | @Service | ||||||
| public final class AuthServiceImpl implements AuthService { | public class AuthServiceImpl implements AuthService { | ||||||
| 
 | 
 | ||||||
|     private final AuthenticationManager authenticationManager; |     private final AuthenticationManager authenticationManager; | ||||||
|     private final JwtService jwtService; |     private final JwtService jwtService; | ||||||
| @ -60,11 +63,13 @@ public final class AuthServiceImpl implements AuthService { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  |     @Transactional | ||||||
|     public void logout(String refreshToken) { |     public void logout(String refreshToken) { | ||||||
|         this.refreshTokenRepository.findByToken(refreshToken).ifPresent(this.refreshTokenRepository::delete); |         this.refreshTokenRepository.findByToken(refreshToken).ifPresent(this.refreshTokenRepository::delete); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  |     @Transactional | ||||||
|     public LoginDetails refresh(@Nullable String refreshToken) throws LoginException { |     public LoginDetails refresh(@Nullable String refreshToken) throws LoginException { | ||||||
|         if (refreshToken == null) { |         if (refreshToken == null) { | ||||||
|             throw new LoginException(LoginExceptionReason.NO_REFRESH_TOKEN, "No refresh token provided."); |             throw new LoginException(LoginExceptionReason.NO_REFRESH_TOKEN, "No refresh token provided."); | ||||||
| @ -73,17 +78,18 @@ public final class AuthServiceImpl implements AuthService { | |||||||
|         final RefreshTokenEntity old = this.refreshTokenRepository.findByToken(refreshToken) |         final RefreshTokenEntity old = this.refreshTokenRepository.findByToken(refreshToken) | ||||||
|                 .orElseThrow(() -> new LoginException( |                 .orElseThrow(() -> new LoginException( | ||||||
|                         LoginExceptionReason.INVALID_REFRESH_TOKEN, |                         LoginExceptionReason.INVALID_REFRESH_TOKEN, | ||||||
|                         "No such refresh-token: " + refreshToken |                         "No such refresh token: " + refreshToken | ||||||
|                 )); |                 )); | ||||||
|         if (old.isRevoked()) { |         if (old.isRevoked() || old.isDeleted()) { | ||||||
|             throw new LoginException(LoginExceptionReason.INVALID_REFRESH_TOKEN, "RefreshToken is revoked."); |             throw new LoginException(LoginExceptionReason.INVALID_REFRESH_TOKEN, "Invalid refresh token."); | ||||||
|         } |         } | ||||||
|         if (old.getExpires().isBefore(LocalDateTime.now())) { |         if (old.getExpires().isBefore(LocalDateTime.now())) { | ||||||
|             throw new LoginException(LoginExceptionReason.EXPIRED_REFRESH_TOKEN, "RefreshToken is expired."); |             throw new LoginException(LoginExceptionReason.EXPIRED_REFRESH_TOKEN, "Refresh token is expired."); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         final UserEntity principal = old.getOwner(); |         final UserEntity principal = old.getOwner(); | ||||||
|         this.refreshTokenRepository.delete(old); |         old.setDeleted(true); | ||||||
|  |         this.refreshTokenRepository.save(old); | ||||||
| 
 | 
 | ||||||
|         final String username = principal.getUsername(); |         final String username = principal.getUsername(); | ||||||
|         return new LoginDetails( |         return new LoginDetails( | ||||||
| @ -93,4 +99,9 @@ public final class AuthServiceImpl implements AuthService { | |||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Scheduled(fixedDelay = 60, timeUnit = TimeUnit.SECONDS) | ||||||
|  |     public void cleanUpDeletedRefreshTokens() { | ||||||
|  |         this.refreshTokenRepository.deleteAllWhereSoftDeleted(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -7,4 +7,5 @@ import java.time.LocalDateTime; | |||||||
| public interface RefreshToken extends AuthToken { | public interface RefreshToken extends AuthToken { | ||||||
|     LocalDateTime getIssued(); |     LocalDateTime getIssued(); | ||||||
|     boolean isRevoked(); |     boolean isRevoked(); | ||||||
|  |     boolean isDeleted(); | ||||||
| } | } | ||||||
|  | |||||||
| @ -30,6 +30,9 @@ public class RefreshTokenEntity implements RefreshToken { | |||||||
|     @ManyToOne |     @ManyToOne | ||||||
|     private UserEntity owner; |     private UserEntity owner; | ||||||
| 
 | 
 | ||||||
|  |     @Column(nullable = false) | ||||||
|  |     private Boolean deleted = false; | ||||||
|  | 
 | ||||||
|     public Long getId() { |     public Long getId() { | ||||||
|         return this.id; |         return this.id; | ||||||
|     } |     } | ||||||
| @ -70,7 +73,7 @@ public class RefreshTokenEntity implements RefreshToken { | |||||||
|         return this.revoked; |         return this.revoked; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void setRevoked(Boolean revoked) { |     public void setRevoked(boolean revoked) { | ||||||
|         this.revoked = revoked; |         this.revoked = revoked; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -82,6 +85,15 @@ public class RefreshTokenEntity implements RefreshToken { | |||||||
|         this.owner = owner; |         this.owner = owner; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean isDeleted() { | ||||||
|  |         return this.deleted; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setDeleted(boolean deleted) { | ||||||
|  |         this.deleted = deleted; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     public long getLifetime() { |     public long getLifetime() { | ||||||
|         return ChronoUnit.SECONDS.between(this.issued, this.expiration); |         return ChronoUnit.SECONDS.between(this.issued, this.expiration); | ||||||
|  | |||||||
| @ -1,9 +1,19 @@ | |||||||
| package app.mealsmadeeasy.api.auth; | package app.mealsmadeeasy.api.auth; | ||||||
| 
 | 
 | ||||||
|  | import jakarta.transaction.Transactional; | ||||||
| import org.springframework.data.jpa.repository.JpaRepository; | import org.springframework.data.jpa.repository.JpaRepository; | ||||||
|  | import org.springframework.data.jpa.repository.Modifying; | ||||||
|  | import org.springframework.data.jpa.repository.Query; | ||||||
| 
 | 
 | ||||||
| import java.util.Optional; | import java.util.Optional; | ||||||
| 
 | 
 | ||||||
| public interface RefreshTokenRepository extends JpaRepository<RefreshTokenEntity, Long> { | public interface RefreshTokenRepository extends JpaRepository<RefreshTokenEntity, Long> { | ||||||
|  | 
 | ||||||
|     Optional<RefreshTokenEntity> findByToken(String token); |     Optional<RefreshTokenEntity> findByToken(String token); | ||||||
|  | 
 | ||||||
|  |     @Modifying | ||||||
|  |     @Transactional | ||||||
|  |     @Query("DELETE FROM RefreshToken t WHERE t.deleted = true") | ||||||
|  |     void deleteAllWhereSoftDeleted(); | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -3,7 +3,10 @@ package app.mealsmadeeasy.api.image; | |||||||
| public class ImageException extends Exception { | public class ImageException extends Exception { | ||||||
| 
 | 
 | ||||||
|     public enum Type { |     public enum Type { | ||||||
|         INVALID_ID, IMAGE_NOT_FOUND, UNKNOWN_MIME_TYPE |         INVALID_ID, | ||||||
|  |         INVALID_USERNAME_OR_FILENAME, | ||||||
|  |         IMAGE_NOT_FOUND, | ||||||
|  |         UNKNOWN_MIME_TYPE | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private final Type type; |     private final Type type; | ||||||
|  | |||||||
| @ -17,6 +17,7 @@ public interface ImageService { | |||||||
| 
 | 
 | ||||||
|     Image getById(long id, @Nullable User viewer) throws ImageException; |     Image getById(long id, @Nullable User viewer) throws ImageException; | ||||||
|     Image getByOwnerAndFilename(User owner, String filename, User viewer) throws ImageException; |     Image getByOwnerAndFilename(User owner, String filename, User viewer) throws ImageException; | ||||||
|  |     Image getByUsernameAndFilename(String username, String filename, User viewer) throws ImageException; | ||||||
| 
 | 
 | ||||||
|     InputStream getImageContent(Image image, @Nullable User viewer) throws IOException; |     InputStream getImageContent(Image image, @Nullable User viewer) throws IOException; | ||||||
|     List<Image> getImagesOwnedBy(User user); |     List<Image> getImagesOwnedBy(User user); | ||||||
|  | |||||||
| @ -17,4 +17,7 @@ public interface S3ImageRepository extends JpaRepository<S3ImageEntity, Long> { | |||||||
|     List<S3ImageEntity> findAllByOwner(UserEntity owner); |     List<S3ImageEntity> findAllByOwner(UserEntity owner); | ||||||
|     Optional<S3ImageEntity> findByOwnerAndUserFilename(UserEntity owner, String filename); |     Optional<S3ImageEntity> findByOwnerAndUserFilename(UserEntity owner, String filename); | ||||||
| 
 | 
 | ||||||
|  |     @Query("SELECT image from Image image WHERE image.owner.username = ?1 AND image.userFilename = ?2") | ||||||
|  |     Optional<S3ImageEntity> findByOwnerUsernameAndFilename(String username, String filename); | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -139,6 +139,17 @@ public class S3ImageService implements ImageService { | |||||||
|                 )); |                 )); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     @PostAuthorize("@imageSecurity.isViewableBy(returnObject, #viewer)") | ||||||
|  |     public Image getByUsernameAndFilename(String username, String filename, User viewer) throws ImageException { | ||||||
|  |         return this.imageRepository.findByOwnerUsernameAndFilename(username, filename).orElseThrow( | ||||||
|  |                 () -> new ImageException( | ||||||
|  |                         ImageException.Type.INVALID_USERNAME_OR_FILENAME, | ||||||
|  |                         "No such Image for username " + username + " and filename " + filename | ||||||
|  |                 ) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     @PreAuthorize("@imageSecurity.isViewableBy(#image, #viewer)") |     @PreAuthorize("@imageSecurity.isViewableBy(#image, #viewer)") | ||||||
|     public InputStream getImageContent(Image image, User viewer) throws IOException { |     public InputStream getImageContent(Image image, User viewer) throws IOException { | ||||||
|  | |||||||
| @ -24,5 +24,5 @@ public interface Recipe { | |||||||
|     boolean isPublic(); |     boolean isPublic(); | ||||||
|     Set<User> getViewers(); |     Set<User> getViewers(); | ||||||
|     Set<RecipeComment> getComments(); |     Set<RecipeComment> getComments(); | ||||||
|     Image getMainImage(); |     @Nullable Image getMainImage(); | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,5 +1,7 @@ | |||||||
| package app.mealsmadeeasy.api.recipe; | package app.mealsmadeeasy.api.recipe; | ||||||
| 
 | 
 | ||||||
|  | import app.mealsmadeeasy.api.image.ImageException; | ||||||
|  | import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec; | ||||||
| import app.mealsmadeeasy.api.recipe.star.RecipeStar; | import app.mealsmadeeasy.api.recipe.star.RecipeStar; | ||||||
| import app.mealsmadeeasy.api.recipe.star.RecipeStarService; | import app.mealsmadeeasy.api.recipe.star.RecipeStarService; | ||||||
| import app.mealsmadeeasy.api.recipe.view.FullRecipeView; | import app.mealsmadeeasy.api.recipe.view.FullRecipeView; | ||||||
| @ -42,6 +44,14 @@ public class RecipeController { | |||||||
|         )); |         )); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private Map<String, Object> getFullViewWrapper(String username, String slug, FullRecipeView view, @Nullable User viewer) { | ||||||
|  |         Map<String, Object> wrapper = new HashMap<>(); | ||||||
|  |         wrapper.put("recipe", view); | ||||||
|  |         wrapper.put("isStarred", this.recipeService.isStarer(username, slug, viewer)); | ||||||
|  |         wrapper.put("isOwner", this.recipeService.isOwner(username, slug, viewer)); | ||||||
|  |         return wrapper; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @GetMapping("/{username}/{slug}") |     @GetMapping("/{username}/{slug}") | ||||||
|     public ResponseEntity<Map<String, Object>> getByUsernameAndSlug( |     public ResponseEntity<Map<String, Object>> getByUsernameAndSlug( | ||||||
|             @PathVariable String username, |             @PathVariable String username, | ||||||
| @ -55,11 +65,20 @@ public class RecipeController { | |||||||
|                 includeRawText, |                 includeRawText, | ||||||
|                 viewer |                 viewer | ||||||
|         ); |         ); | ||||||
|         final Map<String, Object> body = new HashMap<>(); |         return ResponseEntity.ok(this.getFullViewWrapper(username, slug, view, viewer)); | ||||||
|         body.put("recipe", view); |     } | ||||||
|         body.put("isStarred", this.recipeService.isStarer(username, slug, viewer)); | 
 | ||||||
|         body.put("isOwner", this.recipeService.isOwner(username, slug, viewer)); |     @PostMapping("/{username}/{slug}") | ||||||
|         return ResponseEntity.ok(body); |     public ResponseEntity<Map<String, Object>> updateByUsernameAndSlug( | ||||||
|  |             @PathVariable String username, | ||||||
|  |             @PathVariable String slug, | ||||||
|  |             @RequestParam(defaultValue = "true") boolean includeRawText, | ||||||
|  |             @RequestBody RecipeUpdateSpec updateSpec, | ||||||
|  |             @AuthenticationPrincipal User principal | ||||||
|  |     ) throws ImageException, RecipeException { | ||||||
|  |         final Recipe updated = this.recipeService.update(username, slug, updateSpec, principal); | ||||||
|  |         final FullRecipeView view = this.recipeService.toFullRecipeView(updated, includeRawText, principal); | ||||||
|  |         return ResponseEntity.ok(this.getFullViewWrapper(username, slug, view, principal)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @GetMapping |     @GetMapping | ||||||
|  | |||||||
| @ -153,11 +153,11 @@ public final class RecipeEntity implements Recipe { | |||||||
|         this.rawText = rawText; |         this.rawText = rawText; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public String getCachedRenderedText() { |     public @Nullable String getCachedRenderedText() { | ||||||
|         return this.cachedRenderedText; |         return this.cachedRenderedText; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void setCachedRenderedText(String cachedRenderedText) { |     public void setCachedRenderedText(@Nullable String cachedRenderedText) { | ||||||
|         this.cachedRenderedText = cachedRenderedText; |         this.cachedRenderedText = cachedRenderedText; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -224,11 +224,11 @@ public final class RecipeEntity implements Recipe { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public S3ImageEntity getMainImage() { |     public @Nullable S3ImageEntity getMainImage() { | ||||||
|         return this.mainImage; |         return this.mainImage; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void setMainImage(S3ImageEntity image) { |     public void setMainImage(@Nullable S3ImageEntity image) { | ||||||
|         this.mainImage = image; |         this.mainImage = image; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ 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 isOwner(String username, String slug, @Nullable User user) throws RecipeException; | ||||||
|     boolean isViewableBy(Recipe recipe, @Nullable User user) throws RecipeException; |     boolean isViewableBy(Recipe recipe, @Nullable User user) throws RecipeException; | ||||||
|     boolean isViewableBy(String ownerUsername, String slug, @Nullable User user) throws RecipeException; |     boolean isViewableBy(String ownerUsername, String slug, @Nullable User user) throws RecipeException; | ||||||
|     boolean isViewableBy(long recipeId, @Nullable User user) throws RecipeException; |     boolean isViewableBy(long recipeId, @Nullable User user) throws RecipeException; | ||||||
|  | |||||||
| @ -29,6 +29,17 @@ public class RecipeSecurityImpl implements RecipeSecurity { | |||||||
|         return this.isOwner(recipe, user); |         return this.isOwner(recipe, user); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean isOwner(String username, String slug, @Nullable User user) throws RecipeException { | ||||||
|  |         final Recipe recipe = this.recipeRepository.findByOwnerUsernameAndSlug(username, slug).orElseThrow( | ||||||
|  |                 () -> new RecipeException( | ||||||
|  |                         RecipeException.Type.INVALID_USERNAME_OR_SLUG, | ||||||
|  |                         "No such Recipe for username " + username + " and slug " + slug | ||||||
|  |                 ) | ||||||
|  |         ); | ||||||
|  |         return this.isOwner(recipe, user); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     public boolean isViewableBy(Recipe recipe, @Nullable User user) throws RecipeException { |     public boolean isViewableBy(Recipe recipe, @Nullable User user) throws RecipeException { | ||||||
|         if (recipe.isPublic()) { |         if (recipe.isPublic()) { | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| package app.mealsmadeeasy.api.recipe; | package app.mealsmadeeasy.api.recipe; | ||||||
| 
 | 
 | ||||||
|  | import app.mealsmadeeasy.api.image.ImageException; | ||||||
| import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec; | import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec; | ||||||
| import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec; | import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec; | ||||||
| import app.mealsmadeeasy.api.recipe.view.FullRecipeView; | import app.mealsmadeeasy.api.recipe.view.FullRecipeView; | ||||||
| @ -34,7 +35,8 @@ public interface RecipeService { | |||||||
|     List<Recipe> getRecipesViewableBy(User viewer); |     List<Recipe> getRecipesViewableBy(User viewer); | ||||||
|     List<Recipe> getRecipesOwnedBy(User owner); |     List<Recipe> getRecipesOwnedBy(User owner); | ||||||
| 
 | 
 | ||||||
|     Recipe update(long id, RecipeUpdateSpec spec, User modifier) throws RecipeException; |     Recipe update(String username, String slug, RecipeUpdateSpec spec, User modifier) | ||||||
|  |             throws RecipeException, ImageException; | ||||||
| 
 | 
 | ||||||
|     Recipe addViewer(long id, User modifier, User viewer) throws RecipeException; |     Recipe addViewer(long id, User modifier, User viewer) throws RecipeException; | ||||||
|     Recipe removeViewer(long id, User modifier, User viewer) throws RecipeException; |     Recipe removeViewer(long id, User modifier, User viewer) throws RecipeException; | ||||||
| @ -42,6 +44,9 @@ public interface RecipeService { | |||||||
| 
 | 
 | ||||||
|     void deleteRecipe(long id, User modifier); |     void deleteRecipe(long id, User modifier); | ||||||
| 
 | 
 | ||||||
|  |     FullRecipeView toFullRecipeView(Recipe recipe, boolean includeRawText, @Nullable User viewer); | ||||||
|  |     RecipeInfoView toRecipeInfoView(Recipe recipe, @Nullable User viewer); | ||||||
|  | 
 | ||||||
|     @Contract("_, _, null -> null") |     @Contract("_, _, null -> null") | ||||||
|     @Nullable Boolean isStarer(String username, String slug, @Nullable User viewer); |     @Nullable Boolean isStarer(String username, String slug, @Nullable User viewer); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| package app.mealsmadeeasy.api.recipe; | package app.mealsmadeeasy.api.recipe; | ||||||
| 
 | 
 | ||||||
| import app.mealsmadeeasy.api.image.Image; | import app.mealsmadeeasy.api.image.Image; | ||||||
|  | import app.mealsmadeeasy.api.image.ImageException; | ||||||
| import app.mealsmadeeasy.api.image.ImageService; | import app.mealsmadeeasy.api.image.ImageService; | ||||||
| import app.mealsmadeeasy.api.image.S3ImageEntity; | import app.mealsmadeeasy.api.image.S3ImageEntity; | ||||||
| import app.mealsmadeeasy.api.image.view.ImageView; | import app.mealsmadeeasy.api.image.view.ImageView; | ||||||
| @ -199,46 +200,38 @@ public class RecipeServiceImpl implements RecipeService { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     @PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)") |     @PreAuthorize("@recipeSecurity.isOwner(#username, #slug, #modifier)") | ||||||
|     public Recipe update(long id, RecipeUpdateSpec spec, User modifier) throws RecipeException { |     public Recipe update(String username, String slug, RecipeUpdateSpec spec, User modifier) | ||||||
|         final RecipeEntity entity = this.findRecipeEntity(id); |             throws RecipeException, ImageException { | ||||||
|         boolean didModify = false; |         final RecipeEntity recipe = this.recipeRepository.findByOwnerUsernameAndSlug(username, slug).orElseThrow(() -> | ||||||
|         if (spec.getSlug() != null) { |                 new RecipeException( | ||||||
|             entity.setSlug(spec.getSlug()); |                         RecipeException.Type.INVALID_USERNAME_OR_SLUG, | ||||||
|             didModify = true; |                         "No such Recipe for username " + username + " and slug: " + slug | ||||||
|  |                 ) | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         recipe.setTitle(spec.getTitle()); | ||||||
|  |         recipe.setPreparationTime(spec.getPreparationTime()); | ||||||
|  |         recipe.setCookingTime(spec.getCookingTime()); | ||||||
|  |         recipe.setTotalTime(spec.getTotalTime()); | ||||||
|  |         recipe.setRawText(spec.getRawText()); | ||||||
|  |         recipe.setCachedRenderedText(null); | ||||||
|  |         recipe.setPublic(spec.getIsPublic()); | ||||||
|  | 
 | ||||||
|  |         final S3ImageEntity mainImage; | ||||||
|  |         if (spec.getMainImage() == null) { | ||||||
|  |             mainImage = null; | ||||||
|  |         } else { | ||||||
|  |             mainImage = (S3ImageEntity) this.imageService.getByUsernameAndFilename( | ||||||
|  |                     spec.getMainImage().getUsername(), | ||||||
|  |                     spec.getMainImage().getFilename(), | ||||||
|  |                     modifier | ||||||
|  |             ); | ||||||
|         } |         } | ||||||
|         if (spec.getTitle() != null) { |         recipe.setMainImage(mainImage); | ||||||
|             entity.setTitle(spec.getTitle()); | 
 | ||||||
|             didModify = true; |         recipe.setModified(LocalDateTime.now()); | ||||||
|         } |         return this.recipeRepository.save(recipe); | ||||||
|         if (spec.getPreparationTime() != null) { |  | ||||||
|             entity.setPreparationTime(spec.getPreparationTime()); |  | ||||||
|             didModify = true; |  | ||||||
|         } |  | ||||||
|         if (spec.getCookingTime() != null) { |  | ||||||
|             entity.setCookingTime(spec.getCookingTime()); |  | ||||||
|             didModify = true; |  | ||||||
|         } |  | ||||||
|         if (spec.getTotalTime() != null) { |  | ||||||
|             entity.setTotalTime(spec.getTotalTime()); |  | ||||||
|             didModify = true; |  | ||||||
|         } |  | ||||||
|         if (spec.getRawText() != null) { |  | ||||||
|             entity.setRawText(spec.getRawText()); |  | ||||||
|             didModify = true; |  | ||||||
|         } |  | ||||||
|         if (spec.getPublic() != null) { |  | ||||||
|             entity.setPublic(spec.getPublic()); |  | ||||||
|             didModify = true; |  | ||||||
|         } |  | ||||||
|         if (spec.getMainImage() != null) { |  | ||||||
|             entity.setMainImage((S3ImageEntity) spec.getMainImage()); |  | ||||||
|             didModify = true; |  | ||||||
|         } |  | ||||||
|         if (didModify) { |  | ||||||
|             entity.setModified(LocalDateTime.now()); |  | ||||||
|         } |  | ||||||
|         return this.recipeRepository.save(entity); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
| @ -277,6 +270,16 @@ public class RecipeServiceImpl implements RecipeService { | |||||||
|         this.recipeRepository.deleteById(id); |         this.recipeRepository.deleteById(id); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     public FullRecipeView toFullRecipeView(Recipe recipe, boolean includeRawText, @Nullable User viewer) { | ||||||
|  |         return this.getFullView((RecipeEntity) recipe, includeRawText, viewer); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public RecipeInfoView toRecipeInfoView(Recipe recipe, @Nullable User viewer) { | ||||||
|  |         return this.getInfoView((RecipeEntity) recipe, viewer); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     @PreAuthorize("@recipeSecurity.isViewableBy(#username, #slug, #viewer)") |     @PreAuthorize("@recipeSecurity.isViewableBy(#username, #slug, #viewer)") | ||||||
|     @Contract("_, _, null -> null") |     @Contract("_, _, null -> null") | ||||||
|  | |||||||
| @ -1,25 +1,65 @@ | |||||||
| package app.mealsmadeeasy.api.recipe.spec; | package app.mealsmadeeasy.api.recipe.spec; | ||||||
| 
 | 
 | ||||||
| import app.mealsmadeeasy.api.image.Image; | import app.mealsmadeeasy.api.image.Image; | ||||||
|  | import app.mealsmadeeasy.api.recipe.Recipe; | ||||||
| import org.jetbrains.annotations.Nullable; | import org.jetbrains.annotations.Nullable; | ||||||
| 
 | 
 | ||||||
|  | // For now, we cannot change slug after creation. | ||||||
|  | // In the future, we may be able to have redirects from | ||||||
|  | // old slugs to new slugs. | ||||||
| public class RecipeUpdateSpec { | public class RecipeUpdateSpec { | ||||||
| 
 | 
 | ||||||
|     private @Nullable String slug; |     public static class MainImageUpdateSpec { | ||||||
|     private @Nullable String title; | 
 | ||||||
|  |         private String username; | ||||||
|  |         private String filename; | ||||||
|  | 
 | ||||||
|  |         public String getUsername() { | ||||||
|  |             return this.username; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public void setUsername(String username) { | ||||||
|  |             this.username = username; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public String getFilename() { | ||||||
|  |             return this.filename; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public void setFilename(String filename) { | ||||||
|  |             this.filename = filename; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private String title; | ||||||
|     private @Nullable Integer preparationTime; |     private @Nullable Integer preparationTime; | ||||||
|     private @Nullable Integer cookingTime; |     private @Nullable Integer cookingTime; | ||||||
|     private @Nullable Integer totalTime; |     private @Nullable Integer totalTime; | ||||||
|     private @Nullable String rawText; |     private String rawText; | ||||||
|     private @Nullable Boolean isPublic; |     private boolean isPublic; | ||||||
|     private @Nullable Image mainImage; |     private @Nullable MainImageUpdateSpec mainImage; | ||||||
| 
 | 
 | ||||||
|     public @Nullable String getSlug() { |     public RecipeUpdateSpec() {} | ||||||
|         return this.slug; | 
 | ||||||
|  |     /** | ||||||
|  |      * Convenience constructor for testing purposes. | ||||||
|  |      * | ||||||
|  |      * @param recipe the Recipe to copy from | ||||||
|  |      */ | ||||||
|  |     public RecipeUpdateSpec(Recipe recipe) { | ||||||
|  |         this.title = recipe.getTitle(); | ||||||
|  |         this.preparationTime = recipe.getPreparationTime(); | ||||||
|  |         this.cookingTime = recipe.getCookingTime(); | ||||||
|  |         this.totalTime = recipe.getTotalTime(); | ||||||
|  |         this.rawText = recipe.getRawText(); | ||||||
|  |         this.isPublic = recipe.isPublic(); | ||||||
|  |         final @Nullable Image mainImage = recipe.getMainImage(); | ||||||
|  |         if (mainImage != null) { | ||||||
|  |             this.mainImage = new MainImageUpdateSpec(); | ||||||
|  |             this.mainImage.setUsername(mainImage.getOwner().getUsername()); | ||||||
|  |             this.mainImage.setFilename(mainImage.getUserFilename()); | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|     public void setSlug(@Nullable String slug) { |  | ||||||
|         this.slug = slug; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public @Nullable String getTitle() { |     public @Nullable String getTitle() { | ||||||
| @ -62,19 +102,19 @@ public class RecipeUpdateSpec { | |||||||
|         this.rawText = rawText; |         this.rawText = rawText; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public @Nullable Boolean getPublic() { |     public boolean getIsPublic() { | ||||||
|         return this.isPublic; |         return this.isPublic; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void setPublic(@Nullable Boolean isPublic) { |     public void setIsPublic(boolean isPublic) { | ||||||
|         this.isPublic = isPublic; |         this.isPublic = isPublic; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public @Nullable Image getMainImage() { |     public @Nullable MainImageUpdateSpec getMainImage() { | ||||||
|         return this.mainImage; |         return this.mainImage; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void setMainImage(@Nullable Image mainImage) { |     public void setMainImage(@Nullable MainImageUpdateSpec mainImage) { | ||||||
|         this.mainImage = mainImage; |         this.mainImage = mainImage; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -17,7 +17,7 @@ public class FullRecipeView { | |||||||
|             boolean includeRawText, |             boolean includeRawText, | ||||||
|             int starCount, |             int starCount, | ||||||
|             int viewerCount, |             int viewerCount, | ||||||
|             ImageView mainImage |             @Nullable ImageView mainImage | ||||||
|     ) { |     ) { | ||||||
|         final FullRecipeView view = new FullRecipeView(); |         final FullRecipeView view = new FullRecipeView(); | ||||||
|         view.setId(recipe.getId()); |         view.setId(recipe.getId()); | ||||||
| @ -53,7 +53,7 @@ public class FullRecipeView { | |||||||
|     private UserInfoView owner; |     private UserInfoView owner; | ||||||
|     private int starCount; |     private int starCount; | ||||||
|     private int viewerCount; |     private int viewerCount; | ||||||
|     private ImageView mainImage; |     private @Nullable ImageView mainImage; | ||||||
|     private boolean isPublic; |     private boolean isPublic; | ||||||
| 
 | 
 | ||||||
|     public long getId() { |     public long getId() { | ||||||
| @ -161,11 +161,11 @@ public class FullRecipeView { | |||||||
|         this.viewerCount = viewerCount; |         this.viewerCount = viewerCount; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public ImageView getMainImage() { |     public @Nullable ImageView getMainImage() { | ||||||
|         return this.mainImage; |         return this.mainImage; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void setMainImage(ImageView mainImage) { |     public void setMainImage(@Nullable ImageView mainImage) { | ||||||
|         this.mainImage = mainImage; |         this.mainImage = mainImage; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,13 +1,13 @@ | |||||||
| spring.application.name=meals-made-easy-api | spring.application.name=meals-made-easy-api | ||||||
| spring.jpa.hibernate.ddl-auto=create-drop | spring.jpa.hibernate.ddl-auto=create-drop | ||||||
| spring.datasource.url=jdbc:mysql://localhost:55001/meals_made_easy_api | spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:meals_made_easy_api} | ||||||
| spring.datasource.username=meals-made-easy-api-user | spring.datasource.username=${MYSQL_USERNAME:meals-made-easy-api-user} | ||||||
| spring.datasource.password=devpass | spring.datasource.password=${MYSQL_PASSWORD} | ||||||
| spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver | ||||||
| app.mealsmadeeasy.api.baseUrl=http://localhost:8080 | app.mealsmadeeasy.api.baseUrl=http://localhost:8080 | ||||||
| app.mealsmadeeasy.api.security.access-token-lifetime=60 | app.mealsmadeeasy.api.security.access-token-lifetime=60 | ||||||
| app.mealsmadeeasy.api.security.refresh-token-lifetime=3600 | app.mealsmadeeasy.api.security.refresh-token-lifetime=3600 | ||||||
| app.mealsmadeeasy.api.minio.endpoint=http://localhost:9000 | app.mealsmadeeasy.api.minio.endpoint=http://${MINIO_HOST:localhost}:${MINIO_PORT:9000} | ||||||
| app.mealsmadeeasy.api.minio.accessKey=minio-root | app.mealsmadeeasy.api.minio.accessKey=${MINIO_ROOT_USER} | ||||||
| app.mealsmadeeasy.api.minio.secretKey=test0123 | app.mealsmadeeasy.api.minio.secretKey=${MINIO_ROOT_PASSWORD} | ||||||
| app.mealsmadeeasy.api.images.bucketName=images | app.mealsmadeeasy.api.images.bucketName=images | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user