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 ###
|
||||
.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.LoginDetails;
|
||||
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.RecipeUpdateSpec;
|
||||
import app.mealsmadeeasy.api.recipe.star.RecipeStarService;
|
||||
import app.mealsmadeeasy.api.user.User;
|
||||
import app.mealsmadeeasy.api.user.UserCreateException;
|
||||
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.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
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.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.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.status;
|
||||
|
||||
@Testcontainers
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
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
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@ -40,6 +73,12 @@ public class RecipeControllerTests {
|
||||
@Autowired
|
||||
private AuthService authService;
|
||||
|
||||
@Autowired
|
||||
private ImageService imageService;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
private User createTestUser(String username) {
|
||||
try {
|
||||
return this.userService.createUser(username, username + "@test.com", "test");
|
||||
@ -70,6 +109,20 @@ public class RecipeControllerTests {
|
||||
.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
|
||||
@DirtiesContext
|
||||
public void getRecipePageViewByIdPublicRecipeNoPrincipal() throws Exception {
|
||||
@ -94,6 +147,7 @@ public class RecipeControllerTests {
|
||||
.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(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.username").value(owner.getUsername()))
|
||||
.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
|
||||
@ -187,6 +242,83 @@ public class RecipeControllerTests {
|
||||
.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
|
||||
@DirtiesContext
|
||||
public void addStarToRecipe() throws Exception {
|
||||
|
@ -1,5 +1,6 @@
|
||||
package app.mealsmadeeasy.api.recipe;
|
||||
|
||||
import app.mealsmadeeasy.api.image.ImageException;
|
||||
import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec;
|
||||
import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec;
|
||||
import app.mealsmadeeasy.api.recipe.star.RecipeStar;
|
||||
@ -162,13 +163,18 @@ public class RecipeServiceTests {
|
||||
|
||||
@Test
|
||||
@DirtiesContext
|
||||
public void getByIdWithStarsOkayWhenPublicRecipeWithViewer() throws RecipeException {
|
||||
public void getByIdWithStarsOkayWhenPublicRecipeWithViewer() throws RecipeException, ImageException {
|
||||
final User owner = this.createTestUser("recipeOwner");
|
||||
final User viewer = this.createTestUser("viewer");
|
||||
final Recipe notYetPublicRecipe = this.createTestRecipe(owner);
|
||||
final RecipeUpdateSpec updateSpec = new RecipeUpdateSpec();
|
||||
updateSpec.setPublic(true);
|
||||
final Recipe publicRecipe = this.recipeService.update(notYetPublicRecipe.getId(), updateSpec, owner);
|
||||
final RecipeUpdateSpec updateSpec = new RecipeUpdateSpec(notYetPublicRecipe);
|
||||
updateSpec.setIsPublic(true);
|
||||
final Recipe publicRecipe = this.recipeService.update(
|
||||
notYetPublicRecipe.getOwner().getUsername(),
|
||||
notYetPublicRecipe.getSlug(),
|
||||
updateSpec,
|
||||
owner
|
||||
);
|
||||
assertDoesNotThrow(() -> this.recipeService.getByIdWithStars(publicRecipe.getId(), viewer));
|
||||
}
|
||||
|
||||
@ -304,7 +310,7 @@ public class RecipeServiceTests {
|
||||
|
||||
@Test
|
||||
@DirtiesContext
|
||||
public void updateRawText() throws RecipeException {
|
||||
public void updateRawText() throws RecipeException, ImageException {
|
||||
final User owner = this.createTestUser("recipeOwner");
|
||||
final RecipeCreateSpec createSpec = new RecipeCreateSpec();
|
||||
createSpec.setSlug("my-recipe");
|
||||
@ -312,9 +318,14 @@ public class RecipeServiceTests {
|
||||
createSpec.setRawText("# A Heading");
|
||||
Recipe recipe = this.recipeService.create(owner, createSpec);
|
||||
final String newRawText = "# A Heading\n## A Subheading";
|
||||
final RecipeUpdateSpec updateSpec = new RecipeUpdateSpec();
|
||||
final RecipeUpdateSpec updateSpec = new RecipeUpdateSpec(recipe);
|
||||
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));
|
||||
}
|
||||
|
||||
@ -328,7 +339,12 @@ public class RecipeServiceTests {
|
||||
updateSpec.setRawText("should fail");
|
||||
assertThrows(
|
||||
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.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
public class MealsMadeEasyApiApplication {
|
||||
|
||||
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.user.UserEntity;
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@ -12,9 +14,10 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service
|
||||
public final class AuthServiceImpl implements AuthService {
|
||||
public class AuthServiceImpl implements AuthService {
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final JwtService jwtService;
|
||||
@ -60,11 +63,13 @@ public final class AuthServiceImpl implements AuthService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void logout(String refreshToken) {
|
||||
this.refreshTokenRepository.findByToken(refreshToken).ifPresent(this.refreshTokenRepository::delete);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public LoginDetails refresh(@Nullable String refreshToken) throws LoginException {
|
||||
if (refreshToken == null) {
|
||||
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)
|
||||
.orElseThrow(() -> new LoginException(
|
||||
LoginExceptionReason.INVALID_REFRESH_TOKEN,
|
||||
"No such refresh-token: " + refreshToken
|
||||
"No such refresh token: " + refreshToken
|
||||
));
|
||||
if (old.isRevoked()) {
|
||||
throw new LoginException(LoginExceptionReason.INVALID_REFRESH_TOKEN, "RefreshToken is revoked.");
|
||||
if (old.isRevoked() || old.isDeleted()) {
|
||||
throw new LoginException(LoginExceptionReason.INVALID_REFRESH_TOKEN, "Invalid refresh token.");
|
||||
}
|
||||
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();
|
||||
this.refreshTokenRepository.delete(old);
|
||||
old.setDeleted(true);
|
||||
this.refreshTokenRepository.save(old);
|
||||
|
||||
final String username = principal.getUsername();
|
||||
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 {
|
||||
LocalDateTime getIssued();
|
||||
boolean isRevoked();
|
||||
boolean isDeleted();
|
||||
}
|
||||
|
@ -30,6 +30,9 @@ public class RefreshTokenEntity implements RefreshToken {
|
||||
@ManyToOne
|
||||
private UserEntity owner;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Boolean deleted = false;
|
||||
|
||||
public Long getId() {
|
||||
return this.id;
|
||||
}
|
||||
@ -70,7 +73,7 @@ public class RefreshTokenEntity implements RefreshToken {
|
||||
return this.revoked;
|
||||
}
|
||||
|
||||
public void setRevoked(Boolean revoked) {
|
||||
public void setRevoked(boolean revoked) {
|
||||
this.revoked = revoked;
|
||||
}
|
||||
|
||||
@ -82,6 +85,15 @@ public class RefreshTokenEntity implements RefreshToken {
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDeleted() {
|
||||
return this.deleted;
|
||||
}
|
||||
|
||||
public void setDeleted(boolean deleted) {
|
||||
this.deleted = deleted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLifetime() {
|
||||
return ChronoUnit.SECONDS.between(this.issued, this.expiration);
|
||||
|
@ -1,9 +1,19 @@
|
||||
package app.mealsmadeeasy.api.auth;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
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;
|
||||
|
||||
public interface RefreshTokenRepository extends JpaRepository<RefreshTokenEntity, Long> {
|
||||
|
||||
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 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;
|
||||
|
@ -17,6 +17,7 @@ public interface ImageService {
|
||||
|
||||
Image getById(long id, @Nullable 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;
|
||||
List<Image> getImagesOwnedBy(User user);
|
||||
|
@ -17,4 +17,7 @@ public interface S3ImageRepository extends JpaRepository<S3ImageEntity, Long> {
|
||||
List<S3ImageEntity> findAllByOwner(UserEntity owner);
|
||||
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
|
||||
@PreAuthorize("@imageSecurity.isViewableBy(#image, #viewer)")
|
||||
public InputStream getImageContent(Image image, User viewer) throws IOException {
|
||||
|
@ -24,5 +24,5 @@ public interface Recipe {
|
||||
boolean isPublic();
|
||||
Set<User> getViewers();
|
||||
Set<RecipeComment> getComments();
|
||||
Image getMainImage();
|
||||
@Nullable Image getMainImage();
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
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.RecipeStarService;
|
||||
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}")
|
||||
public ResponseEntity<Map<String, Object>> getByUsernameAndSlug(
|
||||
@PathVariable String username,
|
||||
@ -55,11 +65,20 @@ public class RecipeController {
|
||||
includeRawText,
|
||||
viewer
|
||||
);
|
||||
final Map<String, Object> body = new HashMap<>();
|
||||
body.put("recipe", view);
|
||||
body.put("isStarred", this.recipeService.isStarer(username, slug, viewer));
|
||||
body.put("isOwner", this.recipeService.isOwner(username, slug, viewer));
|
||||
return ResponseEntity.ok(body);
|
||||
return ResponseEntity.ok(this.getFullViewWrapper(username, slug, view, viewer));
|
||||
}
|
||||
|
||||
@PostMapping("/{username}/{slug}")
|
||||
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
|
||||
|
@ -153,11 +153,11 @@ public final class RecipeEntity implements Recipe {
|
||||
this.rawText = rawText;
|
||||
}
|
||||
|
||||
public String getCachedRenderedText() {
|
||||
public @Nullable String getCachedRenderedText() {
|
||||
return this.cachedRenderedText;
|
||||
}
|
||||
|
||||
public void setCachedRenderedText(String cachedRenderedText) {
|
||||
public void setCachedRenderedText(@Nullable String cachedRenderedText) {
|
||||
this.cachedRenderedText = cachedRenderedText;
|
||||
}
|
||||
|
||||
@ -224,11 +224,11 @@ public final class RecipeEntity implements Recipe {
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3ImageEntity getMainImage() {
|
||||
public @Nullable S3ImageEntity getMainImage() {
|
||||
return this.mainImage;
|
||||
}
|
||||
|
||||
public void setMainImage(S3ImageEntity image) {
|
||||
public void setMainImage(@Nullable S3ImageEntity image) {
|
||||
this.mainImage = image;
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
public interface RecipeSecurity {
|
||||
boolean isOwner(Recipe recipe, User user);
|
||||
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(String ownerUsername, String slug, @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);
|
||||
}
|
||||
|
||||
@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
|
||||
public boolean isViewableBy(Recipe recipe, @Nullable User user) throws RecipeException {
|
||||
if (recipe.isPublic()) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
package app.mealsmadeeasy.api.recipe;
|
||||
|
||||
import app.mealsmadeeasy.api.image.ImageException;
|
||||
import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec;
|
||||
import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec;
|
||||
import app.mealsmadeeasy.api.recipe.view.FullRecipeView;
|
||||
@ -34,7 +35,8 @@ public interface RecipeService {
|
||||
List<Recipe> getRecipesViewableBy(User viewer);
|
||||
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 removeViewer(long id, User modifier, User viewer) throws RecipeException;
|
||||
@ -42,6 +44,9 @@ public interface RecipeService {
|
||||
|
||||
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")
|
||||
@Nullable Boolean isStarer(String username, String slug, @Nullable User viewer);
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package app.mealsmadeeasy.api.recipe;
|
||||
|
||||
import app.mealsmadeeasy.api.image.Image;
|
||||
import app.mealsmadeeasy.api.image.ImageException;
|
||||
import app.mealsmadeeasy.api.image.ImageService;
|
||||
import app.mealsmadeeasy.api.image.S3ImageEntity;
|
||||
import app.mealsmadeeasy.api.image.view.ImageView;
|
||||
@ -199,46 +200,38 @@ public class RecipeServiceImpl implements RecipeService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
|
||||
public Recipe update(long id, RecipeUpdateSpec spec, User modifier) throws RecipeException {
|
||||
final RecipeEntity entity = this.findRecipeEntity(id);
|
||||
boolean didModify = false;
|
||||
if (spec.getSlug() != null) {
|
||||
entity.setSlug(spec.getSlug());
|
||||
didModify = true;
|
||||
@PreAuthorize("@recipeSecurity.isOwner(#username, #slug, #modifier)")
|
||||
public Recipe update(String username, String slug, RecipeUpdateSpec spec, User modifier)
|
||||
throws RecipeException, ImageException {
|
||||
final RecipeEntity recipe = this.recipeRepository.findByOwnerUsernameAndSlug(username, slug).orElseThrow(() ->
|
||||
new RecipeException(
|
||||
RecipeException.Type.INVALID_USERNAME_OR_SLUG,
|
||||
"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) {
|
||||
entity.setTitle(spec.getTitle());
|
||||
didModify = true;
|
||||
}
|
||||
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);
|
||||
recipe.setMainImage(mainImage);
|
||||
|
||||
recipe.setModified(LocalDateTime.now());
|
||||
return this.recipeRepository.save(recipe);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -277,6 +270,16 @@ public class RecipeServiceImpl implements RecipeService {
|
||||
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
|
||||
@PreAuthorize("@recipeSecurity.isViewableBy(#username, #slug, #viewer)")
|
||||
@Contract("_, _, null -> null")
|
||||
|
@ -1,25 +1,65 @@
|
||||
package app.mealsmadeeasy.api.recipe.spec;
|
||||
|
||||
import app.mealsmadeeasy.api.image.Image;
|
||||
import app.mealsmadeeasy.api.recipe.Recipe;
|
||||
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 {
|
||||
|
||||
private @Nullable String slug;
|
||||
private @Nullable String title;
|
||||
public static class MainImageUpdateSpec {
|
||||
|
||||
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 cookingTime;
|
||||
private @Nullable Integer totalTime;
|
||||
private @Nullable String rawText;
|
||||
private @Nullable Boolean isPublic;
|
||||
private @Nullable Image mainImage;
|
||||
private String rawText;
|
||||
private boolean isPublic;
|
||||
private @Nullable MainImageUpdateSpec mainImage;
|
||||
|
||||
public @Nullable String getSlug() {
|
||||
return this.slug;
|
||||
}
|
||||
public RecipeUpdateSpec() {}
|
||||
|
||||
public void setSlug(@Nullable String slug) {
|
||||
this.slug = 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 @Nullable String getTitle() {
|
||||
@ -62,19 +102,19 @@ public class RecipeUpdateSpec {
|
||||
this.rawText = rawText;
|
||||
}
|
||||
|
||||
public @Nullable Boolean getPublic() {
|
||||
public boolean getIsPublic() {
|
||||
return this.isPublic;
|
||||
}
|
||||
|
||||
public void setPublic(@Nullable Boolean isPublic) {
|
||||
public void setIsPublic(boolean isPublic) {
|
||||
this.isPublic = isPublic;
|
||||
}
|
||||
|
||||
public @Nullable Image getMainImage() {
|
||||
public @Nullable MainImageUpdateSpec getMainImage() {
|
||||
return this.mainImage;
|
||||
}
|
||||
|
||||
public void setMainImage(@Nullable Image mainImage) {
|
||||
public void setMainImage(@Nullable MainImageUpdateSpec mainImage) {
|
||||
this.mainImage = mainImage;
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@ public class FullRecipeView {
|
||||
boolean includeRawText,
|
||||
int starCount,
|
||||
int viewerCount,
|
||||
ImageView mainImage
|
||||
@Nullable ImageView mainImage
|
||||
) {
|
||||
final FullRecipeView view = new FullRecipeView();
|
||||
view.setId(recipe.getId());
|
||||
@ -53,7 +53,7 @@ public class FullRecipeView {
|
||||
private UserInfoView owner;
|
||||
private int starCount;
|
||||
private int viewerCount;
|
||||
private ImageView mainImage;
|
||||
private @Nullable ImageView mainImage;
|
||||
private boolean isPublic;
|
||||
|
||||
public long getId() {
|
||||
@ -161,11 +161,11 @@ public class FullRecipeView {
|
||||
this.viewerCount = viewerCount;
|
||||
}
|
||||
|
||||
public ImageView getMainImage() {
|
||||
public @Nullable ImageView getMainImage() {
|
||||
return this.mainImage;
|
||||
}
|
||||
|
||||
public void setMainImage(ImageView mainImage) {
|
||||
public void setMainImage(@Nullable ImageView mainImage) {
|
||||
this.mainImage = mainImage;
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
spring.application.name=meals-made-easy-api
|
||||
spring.jpa.hibernate.ddl-auto=create-drop
|
||||
spring.datasource.url=jdbc:mysql://localhost:55001/meals_made_easy_api
|
||||
spring.datasource.username=meals-made-easy-api-user
|
||||
spring.datasource.password=devpass
|
||||
spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:meals_made_easy_api}
|
||||
spring.datasource.username=${MYSQL_USERNAME:meals-made-easy-api-user}
|
||||
spring.datasource.password=${MYSQL_PASSWORD}
|
||||
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
app.mealsmadeeasy.api.baseUrl=http://localhost:8080
|
||||
app.mealsmadeeasy.api.security.access-token-lifetime=60
|
||||
app.mealsmadeeasy.api.security.refresh-token-lifetime=3600
|
||||
app.mealsmadeeasy.api.minio.endpoint=http://localhost:9000
|
||||
app.mealsmadeeasy.api.minio.accessKey=minio-root
|
||||
app.mealsmadeeasy.api.minio.secretKey=test0123
|
||||
app.mealsmadeeasy.api.minio.endpoint=http://${MINIO_HOST:localhost}:${MINIO_PORT:9000}
|
||||
app.mealsmadeeasy.api.minio.accessKey=${MINIO_ROOT_USER}
|
||||
app.mealsmadeeasy.api.minio.secretKey=${MINIO_ROOT_PASSWORD}
|
||||
app.mealsmadeeasy.api.images.bucketName=images
|
Loading…
Reference in New Issue
Block a user