refactor, add post edit and delete, merge APIs, add not found view

Signed-off-by: Nicolas Froger <nicolas@kektus.xyz>
This commit is contained in:
Nicolas Froger 2024-07-27 02:20:08 +02:00
commit efde8738a8
No known key found for this signature in database
23 changed files with 803 additions and 343 deletions

View file

@ -8,9 +8,9 @@ import jakarta.enterprise.context.ApplicationScoped;
public class LocationConverter implements Converter<Location, LocationEntity> {
@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) {

View file

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

View file

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

View file

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

View file

@ -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 {

View file

@ -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<PostNestedEntity> 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<Post> 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<Asset> 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);
}
}

View file

@ -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");

View file

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

View file

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

View file

@ -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<PostNestedEntity> 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<Long> assets;
}
}
}

View file

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

View file

@ -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<PostNestedEntity> getAllPosts() {
return postService.getAllPosts();
}
public Long id;
public ZonedDateTime date;
public String description;
public Location location;
public List<String> 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<PostDto> 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<Long> 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<Long> assets;
}
}
}