init backend, added admin page in front

Signed-off-by: Nicolas Froger <nicolas@kektus.xyz>
This commit is contained in:
Nicolas Froger 2024-07-25 03:00:51 +02:00
commit ddc6c64f0f
No known key found for this signature in database
89 changed files with 5083 additions and 9 deletions

View file

@ -0,0 +1,97 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
#
# Before building the container image run:
#
# ./mvnw package
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/summer2024-backend-jvm .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/summer2024-backend-jvm
#
# If you want to include the debug port into your docker image
# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005.
# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005
# when running the container
#
# Then run the container using :
#
# docker run -i --rm -p 8080:8080 quarkus/summer2024-backend-jvm
#
# This image uses the `run-java.sh` script to run the application.
# This scripts computes the command line to execute your Java application, and
# includes memory/GC tuning.
# You can configure the behavior using the following environment properties:
# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class")
# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options
# in JAVA_OPTS (example: "-Dsome.property=foo")
# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is
# used to calculate a default maximal heap memory based on a containers restriction.
# If used in a container without any memory constraints for the container then this
# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio
# of the container available memory as set here. The default is `50` which means 50%
# of the available memory is used as an upper boundary. You can skip this mechanism by
# setting this value to `0` in which case no `-Xmx` option is added.
# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This
# is used to calculate a default initial heap memory based on the maximum heap memory.
# If used in a container without any memory constraints for the container then this
# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio
# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx`
# is used as the initial heap size. You can skip this mechanism by setting this value
# to `0` in which case no `-Xms` option is added (example: "25")
# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS.
# This is used to calculate the maximum value of the initial heap memory. If used in
# a container without any memory constraints for the container then this option has
# no effect. If there is a memory constraint then `-Xms` is limited to the value set
# here. The default is 4096MB which means the calculated value of `-Xms` never will
# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096")
# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output
# when things are happening. This option, if set to true, will set
# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true").
# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example:
# true").
# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787").
# - CONTAINER_CORE_LIMIT: A calculated core limit as described in
# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2")
# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024").
# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion.
# (example: "20")
# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking.
# (example: "40")
# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection.
# (example: "4")
# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus
# previous GC times. (example: "90")
# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20")
# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100")
# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should
# contain the necessary JRE command-line options to specify the required GC, which
# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC).
# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080")
# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080")
# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be
# accessed directly. (example: "foo.example.com,bar.example.com")
#
###
FROM registry.access.redhat.com/ubi8/openjdk-21:1.19
ENV LANGUAGE='en_US:en'
# We make four distinct layers so if there are application changes the library layers can be re-used
COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/
COPY --chown=185 target/quarkus-app/*.jar /deployments/
COPY --chown=185 target/quarkus-app/app/ /deployments/app/
COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/
EXPOSE 8080
USER 185
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]

View file

@ -0,0 +1,93 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
#
# Before building the container image run:
#
# ./mvnw package -Dquarkus.package.jar.type=legacy-jar
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/summer2024-backend-legacy-jar .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/summer2024-backend-legacy-jar
#
# If you want to include the debug port into your docker image
# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005.
# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005
# when running the container
#
# Then run the container using :
#
# docker run -i --rm -p 8080:8080 quarkus/summer2024-backend-legacy-jar
#
# This image uses the `run-java.sh` script to run the application.
# This scripts computes the command line to execute your Java application, and
# includes memory/GC tuning.
# You can configure the behavior using the following environment properties:
# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class")
# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options
# in JAVA_OPTS (example: "-Dsome.property=foo")
# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is
# used to calculate a default maximal heap memory based on a containers restriction.
# If used in a container without any memory constraints for the container then this
# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio
# of the container available memory as set here. The default is `50` which means 50%
# of the available memory is used as an upper boundary. You can skip this mechanism by
# setting this value to `0` in which case no `-Xmx` option is added.
# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This
# is used to calculate a default initial heap memory based on the maximum heap memory.
# If used in a container without any memory constraints for the container then this
# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio
# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx`
# is used as the initial heap size. You can skip this mechanism by setting this value
# to `0` in which case no `-Xms` option is added (example: "25")
# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS.
# This is used to calculate the maximum value of the initial heap memory. If used in
# a container without any memory constraints for the container then this option has
# no effect. If there is a memory constraint then `-Xms` is limited to the value set
# here. The default is 4096MB which means the calculated value of `-Xms` never will
# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096")
# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output
# when things are happening. This option, if set to true, will set
# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true").
# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example:
# true").
# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787").
# - CONTAINER_CORE_LIMIT: A calculated core limit as described in
# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2")
# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024").
# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion.
# (example: "20")
# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking.
# (example: "40")
# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection.
# (example: "4")
# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus
# previous GC times. (example: "90")
# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20")
# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100")
# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should
# contain the necessary JRE command-line options to specify the required GC, which
# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC).
# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080")
# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080")
# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be
# accessed directly. (example: "foo.example.com,bar.example.com")
#
###
FROM registry.access.redhat.com/ubi8/openjdk-21:1.19
ENV LANGUAGE='en_US:en'
COPY target/lib/* /deployments/lib/
COPY target/*-runner.jar /deployments/quarkus-run.jar
EXPOSE 8080
USER 185
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]

View file

@ -0,0 +1,27 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode.
#
# Before building the container image run:
#
# ./mvnw package -Dnative
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.native -t quarkus/summer2024-backend .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/summer2024-backend
#
###
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9
WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
&& chown 1001:root /work
COPY --chown=1001:root target/*-runner /work/application
EXPOSE 8080
USER 1001
ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"]

View file

@ -0,0 +1,30 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode.
# It uses a micro base image, tuned for Quarkus native executables.
# It reduces the size of the resulting container image.
# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image.
#
# Before building the container image run:
#
# ./mvnw package -Dnative
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/summer2024-backend .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/summer2024-backend
#
###
FROM quay.io/quarkus/quarkus-micro-image:2.0
WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
&& chown 1001:root /work
COPY --chown=1001:root target/*-runner /work/application
EXPOSE 8080
USER 1001
ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"]

View file

@ -0,0 +1,23 @@
package fr.kektus.summer2024;
import io.smallrye.common.constraint.NotNull;
import jakarta.ws.rs.InternalServerErrorException;
import jakarta.ws.rs.WebApplicationException;
public enum Validators {
;
public static void assertNotNull(final Exception exception) {
if (exception == null) {
throw new InternalServerErrorException("Null exception was passed");
}
}
public static void assertNotNull(final Object object, final @NotNull WebApplicationException exception) {
assertNotNull(exception);
if (object == null) {
throw exception;
}
}
}

View file

@ -0,0 +1,7 @@
package fr.kektus.summer2024.converters;
public interface Converter<TYPE_LEFT, TYPE_RIGHT> {
public TYPE_LEFT toLeft(TYPE_RIGHT right);
public TYPE_RIGHT toRight(TYPE_LEFT left);
}

View file

@ -0,0 +1,25 @@
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

@ -0,0 +1,22 @@
package fr.kektus.summer2024.data.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.With;
@Entity
@AllArgsConstructor @NoArgsConstructor @With
@Getter @Setter
public class Asset {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) public Long id;
public String filename;
@ManyToOne @JoinColumn(name = "post_id") public Post post;
}

View file

@ -0,0 +1,31 @@
package fr.kektus.summer2024.data.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.With;
import org.hibernate.Length;
import java.time.ZonedDateTime;
import java.util.List;
@Entity
@NoArgsConstructor @AllArgsConstructor
@With @Getter @Setter
public class Post {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) public Long id;
public ZonedDateTime date;
@Column(length = Length.LONG) public String description;
public Float latitude;
public Float longitude;
@Column(length = 64) public String city;
@Column(length = 64) public String country;
@OneToMany(mappedBy = "post") public List<Asset> assets;
}

View file

@ -0,0 +1,9 @@
package fr.kektus.summer2024.data.repository;
import fr.kektus.summer2024.data.model.Asset;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class AssetRepository implements PanacheRepository<Asset> {
}

View file

@ -0,0 +1,9 @@
package fr.kektus.summer2024.data.repository;
import fr.kektus.summer2024.data.model.Post;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class PostRepository implements PanacheRepository<Post> {
}

View file

@ -0,0 +1,11 @@
package fr.kektus.summer2024.domain.entity;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.With;
@NoArgsConstructor @AllArgsConstructor @With
public class AssetEntity {
public Long id;
public String presignedUrl;
}

View file

@ -0,0 +1,23 @@
package fr.kektus.summer2024.domain.entity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.With;
import java.time.ZonedDateTime;
import java.util.List;
@NoArgsConstructor @AllArgsConstructor
@With @Getter @Setter
public class PostEntity {
public Long id;
public ZonedDateTime date;
public String description;
public Float latitude;
public Float longitude;
public String city;
public String country;
public List<Long> assets;
}

View file

@ -0,0 +1,24 @@
package fr.kektus.summer2024.domain.entity;
import fr.kektus.summer2024.data.model.Asset;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.With;
import java.time.ZonedDateTime;
import java.util.List;
@NoArgsConstructor @AllArgsConstructor
@With @Getter @Setter
public class PostNestedEntity {
public Long id;
public ZonedDateTime date;
public String description;
public Float latitude;
public Float longitude;
public String city;
public String country;
public List<AssetEntity> assets;
}

View file

@ -0,0 +1,14 @@
package fr.kektus.summer2024.domain.entity;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.With;
import java.util.Map;
@NoArgsConstructor @AllArgsConstructor @With
public class PresignedAsset {
public Long id;
public String filename;
public Map<String, String> formData;
}

View file

@ -0,0 +1,64 @@
package fr.kektus.summer2024.domain.service;
import fr.kektus.summer2024.data.model.Asset;
import fr.kektus.summer2024.data.repository.AssetRepository;
import fr.kektus.summer2024.domain.entity.PresignedAsset;
import io.minio.GetPresignedObjectUrlArgs;
import io.minio.MinioClient;
import io.minio.PostPolicy;
import io.minio.errors.MinioException;
import io.minio.http.Method;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.InternalServerErrorException;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.wildfly.common.Assert;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.ZonedDateTime;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@ApplicationScoped
public class AssetService {
@Inject AssetRepository assetRepository;
@Inject MinioClient minioClient;
@ConfigProperty(name = "kektus.assets.bucket") String bucketName;
@Transactional
public PresignedAsset createAsset(String filename) {
String actualFilename = UUID.randomUUID() + "_" + filename;
PostPolicy policy = new PostPolicy(bucketName, ZonedDateTime.now().plusMinutes(60));
policy.addEqualsCondition("key", actualFilename);
policy.addContentLengthRangeCondition(64 * 1024, 200 * 1024 * 1024);
try {
Map<String, String> formData = minioClient.getPresignedPostFormData(policy);
Asset asset = new Asset().withFilename(actualFilename);
assetRepository.persist(asset);
return new PresignedAsset().withId(asset.id).withFilename(actualFilename).withFormData(formData);
} catch (MinioException | InvalidKeyException | IOException | NoSuchAlgorithmException e) {
throw new InternalServerErrorException(e);
}
}
public String getPresignedUrlForAsset(Asset asset) {
Assert.assertNotNull(asset);
try {
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucketName)
.object(asset.filename)
.expiry(10, TimeUnit.MINUTES)
.build());
} catch (MinioException | InvalidKeyException | IOException | NoSuchAlgorithmException e) {
throw new InternalServerErrorException(e);
}
}
}

View file

@ -0,0 +1,24 @@
package fr.kektus.summer2024.domain.service;
import fr.kektus.summer2024.Validators;
import jakarta.enterprise.context.ApplicationScoped;
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 {
@Inject Logger logger;
@ConfigProperty(name = "kektus.admin.token") String adminToken;
public boolean isAdminTokenValid(final String adminToken) {
logger.info("Checking admin token");
Validators.assertNotNull(adminToken, new NotAuthorizedException("no admin token provided"));
return adminToken.equals(this.adminToken);
}
}

View file

@ -0,0 +1,71 @@
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;
import fr.kektus.summer2024.domain.entity.AssetEntity;
import fr.kektus.summer2024.domain.entity.PostEntity;
import fr.kektus.summer2024.domain.entity.PostNestedEntity;
import io.quarkus.panache.common.Sort;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.wildfly.common.Assert;
import java.time.ZonedDateTime;
import java.util.List;
@ApplicationScoped
public class PostService {
@Inject PostRepository postRepository;
@Inject AssetRepository assetRepository;
@Inject AssetService assetService;
@Transactional
public List<PostNestedEntity> getAllPosts() {
return postRepository.findAll().stream().map(post -> 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())).toList();
}
public List<Post> getAllPostsOrdered() {
return postRepository.findAll(Sort.descending("id")).stream().toList();
}
@Transactional
public void createPost(PostEntity post) {
Assert.assertNotNull(post);
Assert.assertNotNull(post.description);
Assert.assertNotNull(post.latitude);
Assert.assertNotNull(post.longitude);
Assert.assertNotNull(post.city);
Assert.assertNotNull(post.country);
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);
postRepository.persist(model);
postAssets.forEach(asset -> asset.setPost(model));
postAssets.forEach(asset -> assetRepository.persist(asset));
post.id = model.id;
post.date = model.date;
}
}

View file

@ -0,0 +1,55 @@
package fr.kektus.summer2024.presentation.rest;
import fr.kektus.summer2024.domain.service.AssetService;
import fr.kektus.summer2024.domain.service.AuthService;
import jakarta.inject.Inject;
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;
import org.wildfly.common.Assert;
import java.util.Map;
@Path("/admin/assets")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class AdminAssetApi {
@Inject AssetService assetService;
@Inject AuthService authService;
@POST
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");
Assert.assertNotNull(request);
Assert.assertNotNull(request.filename);
final var asset = assetService.createAsset(request.filename);
return new CreateAssets.Response()
.withId(asset.id)
.withFilename(asset.filename)
.withFormData(asset.formData);
}
public static class CreateAssets {
@NoArgsConstructor @AllArgsConstructor @With
public static class Request {
public String filename;
}
@NoArgsConstructor @AllArgsConstructor @With
public static class Response {
public Long id;
public String filename;
public Map<String, String> formData;
}
}
}

View file

@ -0,0 +1,36 @@
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;
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/auth")
@Produces(MediaType.APPLICATION_JSON)
public class AdminAuthApi {
@Inject AuthService authService;
@GET @Path("/check")
public CheckAuth.Response checkAuth(@RestHeader("X-admin-token") final String adminToken) {
if (!authService.isAdminTokenValid(adminToken))
throw new NotAuthorizedException("provided admin token is invalid");
return new CheckAuth.Response().withStatus("ok");
}
public static class CheckAuth {
@AllArgsConstructor @NoArgsConstructor @With
public static class Response {
public String status;
}
}
}

View file

@ -0,0 +1,65 @@
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

@ -0,0 +1,50 @@
package fr.kektus.summer2024.presentation.rest;
import fr.kektus.summer2024.converters.PublicPostConverter;
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.Path;
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 java.time.ZonedDateTime;
import java.util.List;
@Path("/posts")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class PostApi {
@Inject PostService postService;
@Inject PublicPostConverter postConverter;
@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;
}
public Long id;
public ZonedDateTime date;
public String description;
public Location location;
public List<String> assets;
}
@GET
public List<PostDto> list() {
return postService.getAllPostsOrdered().stream().map(postConverter::toPostDto).toList();
}
}

View file

@ -0,0 +1,6 @@
quarkus.analytics.disabled=true
quarkus.flyway.migrate-at-start=true
%dev.kektus.assets.bucket=assets
%dev.kektus.admin.token=hunter2
quarkus.http.cors=true
%dev.quarkus.http.cors.origins=/.*/

View file

@ -0,0 +1,15 @@
create table Post (
id bigserial not null primary key,
description text,
date timestamp(6) with time zone,
city varchar(64),
country varchar(64),
latitude float4,
longitude float4
);
create table Asset (
id bigserial not null primary key,
filename varchar(255) not null,
post_id bigint references Post
);