diff --git a/summer2024-backend/src/main/java/fr/kektus/summer2024/converters/LocationConverter.java b/summer2024-backend/src/main/java/fr/kektus/summer2024/converters/LocationConverter.java index fddd483..d05d549 100644 --- a/summer2024-backend/src/main/java/fr/kektus/summer2024/converters/LocationConverter.java +++ b/summer2024-backend/src/main/java/fr/kektus/summer2024/converters/LocationConverter.java @@ -8,9 +8,9 @@ import jakarta.enterprise.context.ApplicationScoped; public class LocationConverter implements Converter { @Override public Location toLeft(LocationEntity locationEntity) { return new Location().withId(locationEntity.id) - .withLatitude(locationEntity.latitude) - .withLongitude(locationEntity.longitude) - .withTimestamp(locationEntity.timestamp); + .withLatitude(locationEntity.latitude) + .withLongitude(locationEntity.longitude) + .withTimestamp(locationEntity.timestamp); } @Override public LocationEntity toRight(Location location) { diff --git a/summer2024-backend/src/main/java/fr/kektus/summer2024/converters/PublicPostConverter.java b/summer2024-backend/src/main/java/fr/kektus/summer2024/converters/PublicPostConverter.java deleted file mode 100644 index 82f24a7..0000000 --- a/summer2024-backend/src/main/java/fr/kektus/summer2024/converters/PublicPostConverter.java +++ /dev/null @@ -1,25 +0,0 @@ -package fr.kektus.summer2024.converters; - -import fr.kektus.summer2024.data.model.Post; -import fr.kektus.summer2024.domain.service.AssetService; -import fr.kektus.summer2024.presentation.rest.PostApi; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; - -@ApplicationScoped -public class PublicPostConverter { - @Inject AssetService assetService; - - public PostApi.PostDto toPostDto(Post post) { - return new PostApi.PostDto().withId(post.id) - .withDate(post.date) - .withDescription(post.description) - .withAssets(post.assets.stream() - .map(assetService::getPresignedUrlForAsset) - .toList()) - .withLocation(new PostApi.PostDto.Location().withCity(post.city) - .withCountry(post.country) - .withLat(post.latitude) - .withLon(post.longitude)); - } -} diff --git a/summer2024-backend/src/main/java/fr/kektus/summer2024/domain/entity/PostNestedEntity.java b/summer2024-backend/src/main/java/fr/kektus/summer2024/domain/entity/PostNestedEntity.java index 0b6ba7e..0a945ee 100644 --- a/summer2024-backend/src/main/java/fr/kektus/summer2024/domain/entity/PostNestedEntity.java +++ b/summer2024-backend/src/main/java/fr/kektus/summer2024/domain/entity/PostNestedEntity.java @@ -1,6 +1,5 @@ package fr.kektus.summer2024.domain.entity; -import fr.kektus.summer2024.data.model.Asset; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/summer2024-backend/src/main/java/fr/kektus/summer2024/domain/service/AssetService.java b/summer2024-backend/src/main/java/fr/kektus/summer2024/domain/service/AssetService.java index 342d2e5..933b97e 100644 --- a/summer2024-backend/src/main/java/fr/kektus/summer2024/domain/service/AssetService.java +++ b/summer2024-backend/src/main/java/fr/kektus/summer2024/domain/service/AssetService.java @@ -6,6 +6,7 @@ import fr.kektus.summer2024.domain.entity.PresignedAsset; import io.minio.GetPresignedObjectUrlArgs; import io.minio.MinioClient; import io.minio.PostPolicy; +import io.minio.RemoveObjectArgs; import io.minio.errors.MinioException; import io.minio.http.Method; import jakarta.enterprise.context.ApplicationScoped; @@ -62,4 +63,15 @@ public class AssetService { throw new InternalServerErrorException(e); } } + + @Transactional + public void deleteAsset(Asset asset) { + try { + minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(asset.filename).build()); + } catch (MinioException | InvalidKeyException | IOException | NoSuchAlgorithmException e) { + throw new InternalServerErrorException(e); + } + + assetRepository.delete(asset); + } } diff --git a/summer2024-backend/src/main/java/fr/kektus/summer2024/domain/service/AuthService.java b/summer2024-backend/src/main/java/fr/kektus/summer2024/domain/service/AuthService.java index 3a5de4c..9c1c4ad 100644 --- a/summer2024-backend/src/main/java/fr/kektus/summer2024/domain/service/AuthService.java +++ b/summer2024-backend/src/main/java/fr/kektus/summer2024/domain/service/AuthService.java @@ -6,7 +6,6 @@ import jakarta.inject.Inject; import jakarta.ws.rs.NotAuthorizedException; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.logging.Logger; -import org.wildfly.common.Assert; @ApplicationScoped public class AuthService { diff --git a/summer2024-backend/src/main/java/fr/kektus/summer2024/domain/service/PostService.java b/summer2024-backend/src/main/java/fr/kektus/summer2024/domain/service/PostService.java index 5c1c94a..d7cd0b7 100644 --- a/summer2024-backend/src/main/java/fr/kektus/summer2024/domain/service/PostService.java +++ b/summer2024-backend/src/main/java/fr/kektus/summer2024/domain/service/PostService.java @@ -1,6 +1,5 @@ package fr.kektus.summer2024.domain.service; -import fr.kektus.summer2024.data.model.Asset; import fr.kektus.summer2024.data.model.Post; import fr.kektus.summer2024.data.repository.AssetRepository; import fr.kektus.summer2024.data.repository.PostRepository; @@ -11,6 +10,8 @@ import io.quarkus.panache.common.Sort; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.transaction.Transactional; +import jakarta.ws.rs.BadRequestException; +import org.jboss.logging.Logger; import org.wildfly.common.Assert; import java.time.ZonedDateTime; @@ -21,10 +22,11 @@ public class PostService { @Inject PostRepository postRepository; @Inject AssetRepository assetRepository; @Inject AssetService assetService; + @Inject Logger log; @Transactional public List getAllPosts() { - return postRepository.findAll().stream().map(post -> new PostNestedEntity() + return postRepository.findAll(Sort.descending("id")).stream().map(post -> new PostNestedEntity() .withId(post.id) .withDescription(post.description) .withDate(post.date) @@ -38,10 +40,6 @@ public class PostService { ).toList())).toList(); } - public List getAllPostsOrdered() { - return postRepository.findAll(Sort.descending("id")).stream().toList(); - } - @Transactional public void createPost(PostEntity post) { Assert.assertNotNull(post); @@ -53,19 +51,73 @@ public class PostService { Assert.assertNotNull(post.assets); Assert.assertFalse(post.assets.isEmpty()); - List postAssets = post.assets.stream().map(assetRepository::findById).toList(); - Post model = new Post().withDescription(post.description) - .withDate(ZonedDateTime.now()) - .withLatitude(post.latitude) - .withLongitude(post.longitude) - .withCity(post.city) - .withCountry(post.country) - .withAssets(postAssets); + final Post model = new Post().withDescription(post.description) + .withDate(ZonedDateTime.now()) + .withLatitude(post.latitude) + .withLongitude(post.longitude) + .withCity(post.city) + .withCountry(post.country); postRepository.persist(model); - postAssets.forEach(asset -> asset.setPost(model)); - postAssets.forEach(asset -> assetRepository.persist(asset)); + + final int updatedAssetCount = assetRepository.update("post.id = ?1 where post.id is null and id in ?2", + model.id, + post.assets); + if (updatedAssetCount != post.assets.size()) { + log.debug(String.format("post create error: updated %d assets, expected %d", + updatedAssetCount, + post.assets.size())); + throw new BadRequestException("one ore more asset does not exist or is already in a post"); + } post.id = model.id; post.date = model.date; } + + public PostNestedEntity getPost(Long id) { + final Post post = postRepository.findById(id); + + return new PostNestedEntity() + .withId(post.id) + .withDescription(post.description) + .withDate(post.date) + .withLatitude(post.latitude) + .withLongitude(post.longitude) + .withCity(post.city) + .withCountry(post.country) + .withAssets(post.assets.stream().map(asset -> new AssetEntity() + .withId(asset.id) + .withPresignedUrl(assetService.getPresignedUrlForAsset(asset)) + ).toList()); + } + + @Transactional + public PostEntity updatePost(PostEntity post) { + final Post model = postRepository.findById(post.id); + + assetRepository.update("post.id = null where post.id = ?1 and id not in ?2", post.id, post.assets); + final var updatedAssetCount = assetRepository.update( + "post.id = ?1 where (post.id is null or post.id = ?1) and id in ?2", + post.id, + post.assets); + if (updatedAssetCount != post.assets.size()) + throw new BadRequestException("one ore more asset does not exist or is already in a post"); + + model.setDescription(post.description); + model.setLatitude(post.latitude); + model.setLongitude(post.longitude); + model.setCity(post.city); + model.setCountry(post.country); + + postRepository.persist(model); + + post.date = model.date; + return post; + } + + @Transactional + public void deletePost(Long id) { + final Post post = postRepository.findById(id); + post.assets.forEach(assetService::deleteAsset); + postRepository.delete(post); + } } diff --git a/summer2024-backend/src/main/java/fr/kektus/summer2024/presentation/rest/AdminAssetApi.java b/summer2024-backend/src/main/java/fr/kektus/summer2024/presentation/rest/AdminAssetApi.java index c65ee2e..34f0a2b 100644 --- a/summer2024-backend/src/main/java/fr/kektus/summer2024/presentation/rest/AdminAssetApi.java +++ b/summer2024-backend/src/main/java/fr/kektus/summer2024/presentation/rest/AdminAssetApi.java @@ -17,7 +17,7 @@ import org.wildfly.common.Assert; import java.util.Map; -@Path("/admin/assets") +@Path("/assets") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public class AdminAssetApi { @@ -25,7 +25,8 @@ public class AdminAssetApi { @Inject AuthService authService; @POST - public CreateAssets.Response createAsset(@RestHeader("X-admin-token") final String adminToken, CreateAssets.Request request) { + public CreateAssets.Response createAsset(@RestHeader("X-admin-token") final String adminToken, + CreateAssets.Request request) { if (!authService.isAdminTokenValid(adminToken)) throw new NotAuthorizedException("provided admin token is invalid"); diff --git a/summer2024-backend/src/main/java/fr/kektus/summer2024/presentation/rest/AdminAuthApi.java b/summer2024-backend/src/main/java/fr/kektus/summer2024/presentation/rest/AdminAuthApi.java index 9915a40..25f5156 100644 --- a/summer2024-backend/src/main/java/fr/kektus/summer2024/presentation/rest/AdminAuthApi.java +++ b/summer2024-backend/src/main/java/fr/kektus/summer2024/presentation/rest/AdminAuthApi.java @@ -1,9 +1,7 @@ package fr.kektus.summer2024.presentation.rest; -import fr.kektus.summer2024.domain.entity.PostEntity; import fr.kektus.summer2024.domain.service.AuthService; import jakarta.inject.Inject; -import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.Path; @@ -14,7 +12,7 @@ import lombok.NoArgsConstructor; import lombok.With; import org.jboss.resteasy.reactive.RestHeader; -@Path("/admin/auth") +@Path("/auth") @Produces(MediaType.APPLICATION_JSON) public class AdminAuthApi { @Inject AuthService authService; diff --git a/summer2024-backend/src/main/java/fr/kektus/summer2024/presentation/rest/AdminLocationApi.java b/summer2024-backend/src/main/java/fr/kektus/summer2024/presentation/rest/AdminLocationApi.java deleted file mode 100644 index 908163c..0000000 --- a/summer2024-backend/src/main/java/fr/kektus/summer2024/presentation/rest/AdminLocationApi.java +++ /dev/null @@ -1,47 +0,0 @@ -package fr.kektus.summer2024.presentation.rest; - -import fr.kektus.summer2024.Validators; -import fr.kektus.summer2024.domain.entity.LocationEntity; -import fr.kektus.summer2024.domain.service.AuthService; -import fr.kektus.summer2024.domain.service.LocationService; -import jakarta.inject.Inject; -import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.NotAuthorizedException; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; -import lombok.AllArgsConstructor; -import lombok.NoArgsConstructor; -import lombok.With; -import org.jboss.resteasy.reactive.RestHeader; - -@Path("/admin/location") -@Consumes(MediaType.APPLICATION_JSON) -@Produces(MediaType.APPLICATION_JSON) -public class AdminLocationApi { - @Inject AuthService authService; - @Inject LocationService locationService; - - @POST - public LocationEntity createPost(@RestHeader("X-admin-token") final String adminToken, - CreateLocation.Request request) { - if (!authService.isAdminTokenValid(adminToken)) - throw new NotAuthorizedException("provided admin token is invalid"); - - Validators.assertNotNull(request, new BadRequestException()); - Validators.assertNotNull(request.latitude, new BadRequestException()); - Validators.assertNotNull(request.longitude, new BadRequestException()); - - return locationService.createLocation(request.latitude, request.longitude); - } - - public static class CreateLocation { - @NoArgsConstructor @AllArgsConstructor @With - public static class Request { - public Float latitude; - public Float longitude; - } - } -} diff --git a/summer2024-backend/src/main/java/fr/kektus/summer2024/presentation/rest/AdminPostApi.java b/summer2024-backend/src/main/java/fr/kektus/summer2024/presentation/rest/AdminPostApi.java deleted file mode 100644 index 402b7f9..0000000 --- a/summer2024-backend/src/main/java/fr/kektus/summer2024/presentation/rest/AdminPostApi.java +++ /dev/null @@ -1,65 +0,0 @@ -package fr.kektus.summer2024.presentation.rest; - -import fr.kektus.summer2024.domain.entity.PostEntity; -import fr.kektus.summer2024.domain.entity.PostNestedEntity; -import fr.kektus.summer2024.domain.service.AuthService; -import fr.kektus.summer2024.domain.service.PostService; -import jakarta.inject.Inject; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.NotAuthorizedException; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; -import lombok.AllArgsConstructor; -import lombok.NoArgsConstructor; -import lombok.With; -import org.jboss.resteasy.reactive.RestHeader; - -import java.util.List; - -@Path("/admin/posts") -@Consumes(MediaType.APPLICATION_JSON) -@Produces(MediaType.APPLICATION_JSON) -public class AdminPostApi { - @Inject PostService postService; - @Inject AuthService authService; - - @POST - public PostEntity createPost(@RestHeader("X-admin-token") final String adminToken, CreatePost.Request request) { - if (!authService.isAdminTokenValid(adminToken)) - throw new NotAuthorizedException("provided admin token is invalid"); - - PostEntity post = new PostEntity().withDescription(request.description) - .withLatitude(request.latitude) - .withLongitude(request.longitude) - .withCity(request.city) - .withCountry(request.country) - .withAssets(request.assets); - - postService.createPost(post); - - return post; - } - - @GET - public List getPosts(@RestHeader("X-admin-token") final String adminToken) { - if (!authService.isAdminTokenValid(adminToken)) - throw new NotAuthorizedException("provided admin token is invalid"); - - return postService.getAllPosts(); - } - - public static class CreatePost { - @NoArgsConstructor @AllArgsConstructor @With - public static class Request { - public String description; - public Float latitude; - public Float longitude; - public String city; - public String country; - public List assets; - } - } -} diff --git a/summer2024-backend/src/main/java/fr/kektus/summer2024/presentation/rest/LocationApi.java b/summer2024-backend/src/main/java/fr/kektus/summer2024/presentation/rest/LocationApi.java index c1c64b0..0a91f67 100644 --- a/summer2024-backend/src/main/java/fr/kektus/summer2024/presentation/rest/LocationApi.java +++ b/summer2024-backend/src/main/java/fr/kektus/summer2024/presentation/rest/LocationApi.java @@ -1,21 +1,51 @@ package fr.kektus.summer2024.presentation.rest; +import fr.kektus.summer2024.Validators; import fr.kektus.summer2024.domain.entity.LocationEntity; +import fr.kektus.summer2024.domain.service.AuthService; import fr.kektus.summer2024.domain.service.LocationService; import jakarta.inject.Inject; -import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotAuthorizedException; +import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.With; +import org.jboss.resteasy.reactive.RestHeader; @Path("/location") @Produces(MediaType.APPLICATION_JSON) public class LocationApi { + @Inject AuthService authService; @Inject LocationService locationService; @GET @Path("/last") public LocationEntity getLastLocation() { return locationService.getLatestLocation(); } + + @POST + public LocationEntity createPost(@RestHeader("X-admin-token") final String adminToken, + CreateLocation.Request request) { + if (!authService.isAdminTokenValid(adminToken)) + throw new NotAuthorizedException("provided admin token is invalid"); + + Validators.assertNotNull(request, new BadRequestException()); + Validators.assertNotNull(request.latitude, new BadRequestException()); + Validators.assertNotNull(request.longitude, new BadRequestException()); + + return locationService.createLocation(request.latitude, request.longitude); + } + + public static class CreateLocation { + @NoArgsConstructor @AllArgsConstructor @With + public static class Request { + public Float latitude; + public Float longitude; + } + } } diff --git a/summer2024-backend/src/main/java/fr/kektus/summer2024/presentation/rest/PostApi.java b/summer2024-backend/src/main/java/fr/kektus/summer2024/presentation/rest/PostApi.java index 991455b..11be66c 100644 --- a/summer2024-backend/src/main/java/fr/kektus/summer2024/presentation/rest/PostApi.java +++ b/summer2024-backend/src/main/java/fr/kektus/summer2024/presentation/rest/PostApi.java @@ -1,20 +1,25 @@ package fr.kektus.summer2024.presentation.rest; -import fr.kektus.summer2024.converters.PublicPostConverter; +import fr.kektus.summer2024.domain.entity.PostEntity; +import fr.kektus.summer2024.domain.entity.PostNestedEntity; +import fr.kektus.summer2024.domain.service.AuthService; import fr.kektus.summer2024.domain.service.PostService; import jakarta.inject.Inject; import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotAuthorizedException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import lombok.AllArgsConstructor; -import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; import lombok.With; +import org.jboss.resteasy.reactive.RestHeader; -import java.time.ZonedDateTime; import java.util.List; @Path("/posts") @@ -22,29 +27,89 @@ import java.util.List; @Produces(MediaType.APPLICATION_JSON) public class PostApi { @Inject PostService postService; - @Inject PublicPostConverter postConverter; + @Inject AuthService authService; - @NoArgsConstructor @AllArgsConstructor @With - @Getter @Setter - public static class PostDto { - @NoArgsConstructor @AllArgsConstructor @With - @Getter @Setter - public static class Location { - public Float lat; - public Float lon; - public String city; - public String country; - } + @GET + public List getAllPosts() { + return postService.getAllPosts(); + } - public Long id; - public ZonedDateTime date; - public String description; - public Location location; - public List assets; + @POST + public PostEntity createPost(@RestHeader("X-admin-token") final String adminToken, CreatePost.Request request) { + if (!authService.isAdminTokenValid(adminToken)) + throw new NotAuthorizedException("provided admin token is invalid"); + + PostEntity post = new PostEntity().withDescription(request.description) + .withLatitude(request.latitude) + .withLongitude(request.longitude) + .withCity(request.city) + .withCountry(request.country) + .withAssets(request.assets); + + postService.createPost(post); + + return post; } @GET - public List list() { - return postService.getAllPostsOrdered().stream().map(postConverter::toPostDto).toList(); + @Path("/{id}") + public PostNestedEntity getPost(@RestHeader("X-admin-token") final String adminToken, + @PathParam("id") final Long id) { + if (!authService.isAdminTokenValid(adminToken)) + throw new NotAuthorizedException("provided admin token is invalid"); + + return postService.getPost(id); + } + + @DELETE + @Path("/{id}") + public void deletePost(@RestHeader("X-admin-token") final String adminToken, @PathParam("id") final Long id) { + if (!authService.isAdminTokenValid(adminToken)) + throw new NotAuthorizedException("provided admin token is invalid"); + + postService.deletePost(id); + } + + @PUT + @Path("/{id}") + public PostEntity updatePost(@RestHeader("X-admin-token") final String adminToken, + @PathParam("id") final Long id, + final UpdatePost.Request request) { + if (!authService.isAdminTokenValid(adminToken)) + throw new NotAuthorizedException("provided admin token is invalid"); + + return postService.updatePost(new PostEntity() + .withId(id) + .withDescription(request.description) + .withLatitude(request.latitude) + .withLongitude(request.longitude) + .withCity(request.city) + .withCountry(request.country) + .withAssets(request.assets) + ); + } + + public static class CreatePost { + @NoArgsConstructor @AllArgsConstructor @With + public static class Request { + public String description; + public Float latitude; + public Float longitude; + public String city; + public String country; + public List assets; + } + } + + public static class UpdatePost { + @NoArgsConstructor @AllArgsConstructor @With + public static class Request { + public String description; + public Float latitude; + public Float longitude; + public String city; + public String country; + public List assets; + } } } diff --git a/summer2024-frontend/src/components/PostComponent.vue b/summer2024-frontend/src/components/PostComponent.vue index 186cb14..341603c 100644 --- a/summer2024-frontend/src/components/PostComponent.vue +++ b/summer2024-frontend/src/components/PostComponent.vue @@ -58,11 +58,23 @@ function postDataToggle() {