commit d46130d3036e2ddf24f0c2840ba20c5a60c9ab25 Author: David Date: Sun Aug 3 02:39:47 2025 +0200 first commit diff --git a/.gitattributes b/.gitattributes new file mode 100755 index 0000000..3b41682 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..33f3ade --- /dev/null +++ b/.gitignore @@ -0,0 +1,140 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### MAC ### +.DS_Store + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +# ---> Java +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +# ---> JetBrains +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +.idea/compiler.xml +.idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +*.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +.env +.idea/ +.run/ +**/static/images/uplaod \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100755 index 0000000..d58dfb7 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..372dde9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,49 @@ +# syntax=docker/dockerfile:1 + +FROM maven:3-eclipse-temurin-23-alpine AS dependency +WORKDIR /app +COPY pom.xml ./ +COPY domain/pom.xml domain/ +COPY domain/spi/pom.xml domain/spi/ +COPY domain/api/pom.xml domain/api/ +COPY domain/service/pom.xml domain/service/ +COPY domain/data/pom.xml domain/data/ +COPY application/pom.xml application/ +COPY infrastructure/pom.xml infrastructure/ +COPY bootstrap/pom.xml bootstrap/ +COPY common/pom.xml common/ +ARG XPEDITIS_PROFILE +RUN mvn -P${XPEDITIS_PROFILE} dependency:go-offline \ +&& mvn dependency:get -Dartifact=net.bytebuddy:byte-buddy-agent:1.15.11 -B \ +&& mvn dependency:get -Dartifact=org.mapstruct:mapstruct-processor:1.6.3 -B \ +&& mvn dependency:get -Dartifact=org.projectlombok:lombok-mapstruct-binding:0.2.0 -B \ +&& mvn dependency:get -Dartifact=com.mysql:mysql-connector-j:9.1.0 -B \ +&& mvn dependency:get -Dartifact=org.flywaydb:flyway-core:10.20.1 -B \ +&& mvn dependency:get -Dartifact=org.flywaydb:flyway-mysql:10.20.1 -B + +FROM dependency AS builder +COPY domain/spi/src domain/spi/src +COPY domain/api/src domain/api/src +COPY domain/service/src domain/service/src +COPY domain/data/src domain/data/src +COPY application/src application/src +COPY infrastructure/src infrastructure/src +COPY bootstrap/src bootstrap/src +COPY common/src common/src +ARG XPEDITIS_PROFILE +RUN mvn -o clean package -P${XPEDITIS_PROFILE} -Dmaven.test.skip=true -DskipTests + +#FROM builder AS test +#RUN mvn -o clean test + +FROM eclipse-temurin:23-jdk-alpine AS production +WORKDIR /opt/app +EXPOSE 8080 +COPY --from=builder /app/bootstrap/target/bootstrap*.jar xpeditis.jar +COPY wait-for-it.sh ./ +RUN chmod +x wait-for-it.sh +COPY entrypoint.sh ./ +RUN chmod +x entrypoint.sh +ENTRYPOINT ["./entrypoint.sh"] +#CMD ["java", "-jar", "-Dserver.port=8080", "leblr.jar"] +#HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD [ "curl", "--fail", "http://localhost:8080/actuator/health", "|| exit 1" ] diff --git a/Dockerfile-springboot b/Dockerfile-springboot new file mode 100644 index 0000000..ce9b9ef --- /dev/null +++ b/Dockerfile-springboot @@ -0,0 +1,36 @@ +# syntax=docker/dockerfile:1 + +FROM eclipse-temurin:23-jdk AS dependency + +WORKDIR /app + +COPY .mvn/ .mvn +COPY mvnw ./ + +COPY pom.xml ./ +COPY domain/pom.xml domain/ +COPY domain/spi/pom.xml domain/spi/ +COPY domain/api/pom.xml domain/api/ +COPY domain/service/pom.xml domain/service/ +COPY domain/data/pom.xml domain/data/ +COPY application/pom.xml application/ +COPY infrastructure/pom.xml infrastructure/ +COPY bootstrap/pom.xml bootstrap/ +COPY common/pom.xml common/ +ARG XPEDITIS_PROFILE +RUN ./mvnw -P${XPEDITIS_PROFILE} dependency:go-offline \ +&& ./mvnw dependency:get -Dartifact=net.bytebuddy:byte-buddy-agent:1.15.11 -B \ +&& ./mvnw dependency:get -Dartifact=org.mapstruct:mapstruct-processor:1.6.3 -B \ +&& ./mvnw dependency:get -Dartifact=org.projectlombok:lombok-mapstruct-binding:0.2.0 -B + +FROM dependency AS production +COPY domain/spi/src domain/spi/src +COPY domain/api/src domain/api/src +COPY domain/service/src domain/service/src +COPY domain/data/src domain/data/src +COPY application/src application/src +COPY infrastructure/src infrastructure/src +COPY bootstrap/src bootstrap/src +COPY common/src common/src + +CMD ["./mvnw", "spring-boot:run"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef1301f --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# leblr-backend + +Le BLR backend side
+SWAGGER UI : http://localhost:8080/swagger-ui.html
+
+.\mvnw clean install flyway:migrate -Pprod
+.\mvnw clean install flyway:migrate '-Dflyway.configFiles=flyway-h2.conf' -Pdev
\ No newline at end of file diff --git a/application/pom.xml b/application/pom.xml new file mode 100755 index 0000000..dbdf3de --- /dev/null +++ b/application/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + + com.dh7789dev + xpeditis + 0.0.1-SNAPSHOT + + + application + jar + + + + com.dh7789dev + api + ${project.version} + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.0 + + + org.projectlombok + lombok + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + + org.projectlombok + lombok + ${org.projectlombok.version} + + + + + + + \ No newline at end of file diff --git a/application/src/main/java/com/dh7789dev/xpeditis/advice/GlobalExceptionHandler.java b/application/src/main/java/com/dh7789dev/xpeditis/advice/GlobalExceptionHandler.java new file mode 100644 index 0000000..380cc69 --- /dev/null +++ b/application/src/main/java/com/dh7789dev/xpeditis/advice/GlobalExceptionHandler.java @@ -0,0 +1,83 @@ +package com.dh7789dev.xpeditis.advice; + +import com.dh7789dev.xpeditis.NlsService; +import com.dh7789dev.xpeditis.dto.CustomInternalExceptionResponse; +import com.dh7789dev.xpeditis.exception.CustomInternalException; +import com.dh7789dev.xpeditis.exception.GlobalNotFoundException; +import com.dh7789dev.xpeditis.exception.StorageException; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import java.time.Instant; + +@Slf4j +//@Order(1) +@RestControllerAdvice(basePackages = {"com.dh7789dev.xpeditis.controller"} ) +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + + private static final String INTERNAL_ERROR = "internal.error"; + + private final NlsService nlsService; + + @Autowired + public GlobalExceptionHandler(NlsService nlsService) { + this.nlsService = nlsService; + } + + @ExceptionHandler(value = {IllegalArgumentException.class}) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public String handleIllegalArgumentException(HttpServletRequest req, IllegalArgumentException ex) { + return nlsService.getMessage("error.invalid.argument"); + } + + @ExceptionHandler(StorageException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public CustomInternalExceptionResponse handleFileStorageException(HttpServletRequest req, StorageException ex) { + return new CustomInternalExceptionResponse( + (String) req.getAttribute("request_id"), + "StorageError", + ex.getMessage(), + req.getRequestURL().toString()); + } + + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public CustomInternalExceptionResponse handleUnknownException(HttpServletRequest req, Exception ex) { + CustomInternalExceptionResponse response = new CustomInternalExceptionResponse(); + response.setErrorCode(nlsService.getMessage(INTERNAL_ERROR)); + response.setErrorMessage(ex.getMessage()); + response.setRequestURL(req.getRequestURL().toString()); + response.setTimestamp(Instant.now()); + response.setId((String) req.getAttribute("request_id")); + return response; + } + + @ExceptionHandler(CustomInternalException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public CustomInternalExceptionResponse handleCustomInternalException(HttpServletRequest req, CustomInternalException ex) { + CustomInternalExceptionResponse response = new CustomInternalExceptionResponse(); + response.setErrorCode(nlsService.getMessage(ex.getNlsKey(), ex.getNlsParameters())); + response.setErrorMessage(ex.getMessage()); + response.setRequestURL(req.getRequestURL().toString()); + response.setTimestamp(Instant.now()); + response.setId((String) req.getAttribute("request_id")); + return response; + } + + @ExceptionHandler(GlobalNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + //@ResponseBody + public CustomInternalExceptionResponse globalNotFoundHandler(HttpServletRequest req, GlobalNotFoundException ex) { + return new CustomInternalExceptionResponse( + (String) req.getAttribute("request_id"), + ex.getErrorCode(), + ex.getMessage(), + req.getRequestURL().toString()); + } +} diff --git a/application/src/main/java/com/dh7789dev/xpeditis/controller/IndexRestController.java b/application/src/main/java/com/dh7789dev/xpeditis/controller/IndexRestController.java new file mode 100644 index 0000000..b57e25a --- /dev/null +++ b/application/src/main/java/com/dh7789dev/xpeditis/controller/IndexRestController.java @@ -0,0 +1,35 @@ +package com.dh7789dev.xpeditis.controller; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +@RestController +@RequestMapping(name = "/", + produces = APPLICATION_JSON_VALUE) +public class IndexRestController { + + @Value("${spring.application.name}") + String name; + + @Value("${spring.application.version}") + String version; + + @Value("${spring.profiles.active}") + String springProfile; + + public record ServiceInformation(String name, + String version, + @JsonProperty("spring profile") String springProfile) { + } + + @GetMapping + public ResponseEntity index() { + return ResponseEntity.ok(new ServiceInformation(name, version, springProfile)); + } +} diff --git a/application/src/main/java/com/dh7789dev/xpeditis/controller/UploadController.java b/application/src/main/java/com/dh7789dev/xpeditis/controller/UploadController.java new file mode 100644 index 0000000..941e51e --- /dev/null +++ b/application/src/main/java/com/dh7789dev/xpeditis/controller/UploadController.java @@ -0,0 +1,72 @@ +package com.dh7789dev.xpeditis.controller; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; + +@Controller +public class UploadController { + + @Value("${file.upload-dir}") + String uploadDir; + + private final EventService eventService; + + private final GalleryService galleryService; + + public UploadController(EventService eventService, GalleryService galleryService) { + this.eventService = eventService; + this.galleryService = galleryService; + } + + @GetMapping("/uploadimage") + public String displayUploadForm() { + return "addImages"; + } + + @PostMapping(value = "/upload/{eventId}", consumes = MULTIPART_FORM_DATA_VALUE) + public String uploadImage(Model model, + @PathVariable long eventId, + @RequestParam("images") List images) { + + eventService.addImages(eventId, images); + StringBuilder fileNames = new StringBuilder(); + images.forEach(image -> fileNames.append(image.getOriginalFilename()).append(" ")); + model.addAttribute("msg", "Uploaded images: " + fileNames); + return "addImages"; + } + + @PostMapping(value = "/upload/gallery/{galleryId}", consumes = MULTIPART_FORM_DATA_VALUE) + public String uploadImageToGallery(Model model, + @PathVariable long galleryId, + @RequestParam("images") List images) { + + galleryService.addImages(galleryId, images); + StringBuilder fileNames = new StringBuilder(); + images.forEach(image -> fileNames.append(image.getOriginalFilename()).append(" ")); + model.addAttribute("msg", "Uploaded images: " + fileNames); + return "addImages"; + } + + @GetMapping("/addevent") + public String displayUploadFormToAddEvent() { + return "addEvent"; + } + + @PostMapping(value = "/upload", consumes = MULTIPART_FORM_DATA_VALUE) + public String addEvent(Model model, + @RequestPart("newEvent") Event newEvent, + @RequestPart("images") List images) { + eventService.addEvent(newEvent, images); + StringBuilder fileNames = new StringBuilder(); + images.forEach(image -> fileNames.append(image.getOriginalFilename()).append(" ")); + model.addAttribute("msg", "Uploaded images: " + fileNames); + return "addEvent"; + } +} diff --git a/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/AuthenticationRestController.java b/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/AuthenticationRestController.java new file mode 100644 index 0000000..244be1c --- /dev/null +++ b/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/AuthenticationRestController.java @@ -0,0 +1,29 @@ +package com.dh7789dev.xpeditis.controller.api.v1; + +import com.dh7789dev.xpeditis.AuthenticationService; +import com.dh7789dev.xpeditis.dto.AuthenticationRequest; +import com.dh7789dev.xpeditis.dto.AuthenticationResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +@RestController +@RequestMapping(value = "${apiPrefix}/api/v1/auth", + produces = APPLICATION_JSON_VALUE) +@RequiredArgsConstructor +public class AuthenticationRestController { + + private final AuthenticationService service; + + @PostMapping("/authenticate") + public ResponseEntity authenticate( + @RequestBody @Valid AuthenticationRequest request) { + return ResponseEntity.ok(service.authenticate(request)); + } +} diff --git a/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/UserRestController.java b/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/UserRestController.java new file mode 100644 index 0000000..cdfe0cc --- /dev/null +++ b/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/UserRestController.java @@ -0,0 +1,38 @@ +package com.dh7789dev.xpeditis.controller.api.v1; + +import com.dh7789dev.xpeditis.UserService; +import com.dh7789dev.xpeditis.dto.ChangePasswordRequest; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.security.Principal; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +@RestController +@Validated +@RequestMapping(value = "${apiPrefix}/api/v1/users", + produces = APPLICATION_JSON_VALUE) +public class UserRestController { + + private final UserService service; + + public UserRestController(UserService service) { + this.service = service; + } + + @Operation(summary = "Change password of the connected user") + @PatchMapping("/password") + public ResponseEntity changePassword( + @RequestBody ChangePasswordRequest request, + Principal connectedUser) { + service.changePassword(request, connectedUser); + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/application/src/main/java/lombok.config b/application/src/main/java/lombok.config new file mode 100755 index 0000000..8f7e8aa --- /dev/null +++ b/application/src/main/java/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true \ No newline at end of file diff --git a/bootstrap/pom.xml b/bootstrap/pom.xml new file mode 100755 index 0000000..d19abe0 --- /dev/null +++ b/bootstrap/pom.xml @@ -0,0 +1,128 @@ + + + 4.0.0 + + com.dh7789dev + xpeditis + 0.0.1-SNAPSHOT + + + bootstrap + jar + Xpeditis + + + 23 + 23 + UTF-8 + + + + + com.dh7789dev + application + ${project.version} + + + com.dh7789dev + service + ${project.version} + + + com.dh7789dev + infrastructure + ${project.version} + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + + + dev + + true + + + dev + + + + prod + + prod + + + + + + ${project.name} + + + src/main/resources + true + + + + + org.springframework.boot + spring-boot-maven-plugin + + false + + + org.projectlombok + lombok + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + org.springframework.boot + spring-boot-configuration-processor + + + + + + + \ No newline at end of file diff --git a/bootstrap/src/main/java/com/dh7789dev/xpeditis/XpeditisApplication.java b/bootstrap/src/main/java/com/dh7789dev/xpeditis/XpeditisApplication.java new file mode 100755 index 0000000..cc5beae --- /dev/null +++ b/bootstrap/src/main/java/com/dh7789dev/xpeditis/XpeditisApplication.java @@ -0,0 +1,27 @@ +package com.dh7789dev.xpeditis; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@ComponentScan(basePackages = {"com.dh7789dev.xpeditis"}) +@EnableJpaRepositories("com.dh7789dev.xpeditis.dao") +@EntityScan("com.dh7789dev.xpeditis.entity") +@EnableJpaAuditing(auditorAwareRef = "auditorProvider") +@SpringBootApplication +public class XpeditisApplication extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder applicationBuilder) { + return applicationBuilder.sources(XpeditisApplication.class); + } + + public static void main(String[] args) { + SpringApplication.run(XpeditisApplication.class, args); + } +} diff --git a/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/GlobalConfiguration.java b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/GlobalConfiguration.java new file mode 100755 index 0000000..21ad38a --- /dev/null +++ b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/GlobalConfiguration.java @@ -0,0 +1,95 @@ +package com.dh7789dev.xpeditis.configuration; + +import com.dh7789dev.xpeditis.dao.UserDao; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.ReloadableResourceBundleMessageSource; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CommonsRequestLoggingFilter; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.i18n.CookieLocaleResolver; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; + +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; + +@Configuration +@EnableTransactionManagement +@EnableAsync +public class GlobalConfiguration { + + private static final String USER_NOT_FOUND_MSG = "User %s not found"; + + private final UserDao userDao; + + public GlobalConfiguration(UserDao userDao) { + this.userDao = userDao; + } + + @Bean + public CommonsRequestLoggingFilter loggingFilter() { + CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter(); + filter.setIncludeQueryString(true); + filter.setIncludePayload(true); + filter.setIncludeClientInfo(true); + filter.setMaxPayloadLength(10000); + filter.setIncludeHeaders(false); + return filter; + } + + @Bean + public MessageSource messageSource() { + ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); + messageSource.setBasename("classpath:language/i18n"); + messageSource.setDefaultEncoding("UTF-8"); + messageSource.setDefaultLocale(Locale.FRANCE); + messageSource.setUseCodeAsDefaultMessage(true); + // Refresh cache once every hour + messageSource.setCacheSeconds(3600); + return messageSource; + } + + @Bean + public LocaleResolver localeResolver() { + // Can be Fixed, AcceptHeader, Session or Cookie + CookieLocaleResolver localeResolver = new CookieLocaleResolver(); + localeResolver.setDefaultLocale(Locale.FRANCE); + localeResolver.setDefaultTimeZone(TimeZone.getTimeZone("UTC")); + return localeResolver; + } + + @Bean + public LocaleChangeInterceptor localeChangeInterceptor() { + LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor(); + // Example: ?lang=en + localeChangeInterceptor.setParamName("lang"); + return localeChangeInterceptor; + } + + @Bean + UrlBasedCorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + //configuration.setAllowCredentials(true); + configuration.setAllowedOrigins(List.of("http://localhost:8080", "http://localhost:3000", + "http://127.0.0.1:8080", "https://leblr.preprod.weworkstudio.fr","https://leblr.fr", + "https://api.leblr.preprod.weworkstudio.fr/","https://api.leblr.fr/", "https://www.leblr.fr")); + configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH")); + configuration.setAllowedHeaders(List.of("*")); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + + @Bean + public UserDetailsService userDetailsService() { + return username -> userDao.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException(String.format(USER_NOT_FOUND_MSG, username))); + } +} diff --git a/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/InterceptorConfiguration.java b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/InterceptorConfiguration.java new file mode 100644 index 0000000..9a7655a --- /dev/null +++ b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/InterceptorConfiguration.java @@ -0,0 +1,31 @@ +package com.dh7789dev.xpeditis.configuration; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; + +@Configuration +public class InterceptorConfiguration implements WebMvcConfigurer { + + private final LocaleChangeInterceptor localeChangeInterceptor; + + private final RequestIdInterceptor requestIdInterceptor; + + @Autowired + public InterceptorConfiguration(LocaleChangeInterceptor localeChangeInterceptor, + RequestIdInterceptor requestIdInterceptor) { + this.localeChangeInterceptor = localeChangeInterceptor; + this.requestIdInterceptor = requestIdInterceptor; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + final String[] excluded = {"/error"}; + registry.addInterceptor(localeChangeInterceptor) + .excludePathPatterns(excluded); + registry.addInterceptor(requestIdInterceptor) + .excludePathPatterns(excluded); + } +} diff --git a/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/JwtAuthenticationFilter.java b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/JwtAuthenticationFilter.java new file mode 100644 index 0000000..420f7ee --- /dev/null +++ b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/JwtAuthenticationFilter.java @@ -0,0 +1,81 @@ +package com.dh7789dev.xpeditis.configuration; + +import com.dh7789dev.xpeditis.dao.TokenDao; +import com.dh7789dev.xpeditis.util.JwtUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.lang.NonNull; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private static final String AUTH_HEADER = "Authorization"; + private static final String AUTH_TYPE = "Bearer "; + + private final UserDetailsService userDetailsService; + private final JwtUtil jwtUtil; + private final TokenDao tokenDao; + + public JwtAuthenticationFilter(UserDetailsService userDetailsService, JwtUtil jwtUtil, TokenDao tokenDao) { + this.userDetailsService = userDetailsService; + this.jwtUtil = jwtUtil; + this.tokenDao = tokenDao; + } + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + + final String jwt; + final String username; + + if (request.getServletPath().contains("/api/v1/auth")) { + filterChain.doFilter(request, response); + return; + } + + final String authHeader = request.getHeader(AUTH_HEADER); + if (authHeader == null || !authHeader.startsWith(AUTH_TYPE)) { + filterChain.doFilter(request, response); + return; + } + + jwt = authHeader.substring(AUTH_TYPE.length()).trim(); + username = jwtUtil.extractUsername(jwt); + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + + boolean isTokenValid = tokenDao.findByToken(jwt) + .map(t -> !t.isExpired() && !t.isRevoked()) + .orElse(false); + if (isTokenValid && jwtUtil.isTokenValid(jwt, userDetails)) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } else { + throw new UsernameNotFoundException("Failed to authenticate with access token"); + } + } + filterChain.doFilter(request, response); + } +} diff --git a/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/LogoutService.java b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/LogoutService.java new file mode 100644 index 0000000..d412729 --- /dev/null +++ b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/LogoutService.java @@ -0,0 +1,38 @@ +package com.dh7789dev.xpeditis.configuration; + +import com.dh7789dev.xpeditis.dao.TokenDao; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LogoutService implements LogoutHandler { + + private final TokenDao tokenDao; + + @Override + public void logout( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) { + final String authHeader = request.getHeader("Authorization"); + final String jwt; + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + return; + } + jwt = authHeader.substring(7); + var storedToken = tokenDao.findByToken(jwt) + .orElse(null); + if (storedToken != null) { + storedToken.setExpired(true); + storedToken.setRevoked(true); + tokenDao.save(storedToken); + SecurityContextHolder.clearContext(); + } + } +} diff --git a/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/RequestIdInterceptor.java b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/RequestIdInterceptor.java new file mode 100644 index 0000000..d30c6b6 --- /dev/null +++ b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/RequestIdInterceptor.java @@ -0,0 +1,29 @@ +package com.dh7789dev.xpeditis.configuration; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.util.UUID; + +@Component +public class RequestIdInterceptor implements HandlerInterceptor { + + private static final String REQUEST_ID = "request_id"; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + final String requestId = UUID.randomUUID().toString(); + MDC.put(REQUEST_ID, requestId); + request.setAttribute(REQUEST_ID, requestId); + return HandlerInterceptor.super.preHandle(request, response, handler); + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + MDC.clear(); + HandlerInterceptor.super.afterCompletion(request, response, handler, ex); + } +} diff --git a/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/SecurityConfiguration.java b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/SecurityConfiguration.java new file mode 100755 index 0000000..daf15a5 --- /dev/null +++ b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/SecurityConfiguration.java @@ -0,0 +1,128 @@ +package com.dh7789dev.xpeditis.configuration; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutHandler; + +import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; +import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; + +@Configuration +@EnableWebSecurity +//@EnableMethodSecurity +public class SecurityConfiguration { + + @Value("${application.csrf.enabled}") + boolean csrfEnabled; + + private static final String ADMIN_ROLE = "ADMIN"; + + private static final String API_V1_URI = "/api/v1/**"; + + private static final String[] WHITE_LIST_URL = { + "/api/v1/auth/**", + "/actuator/health/**"}; + + private static final String[] INTERNAL_WHITE_LIST_URL = { + "/v2/api-docs", + "/v3/api-docs", + "/v3/api-docs/**", + "/swagger-resources", + "/swagger-resources/**", + "/configuration/ui", + "/configuration/security", + "/swagger-ui/**", + "/webjars/**", + "/swagger-ui.html", + "/uploadimage", + "/upload/**", + "/addevent"}; + + private static final WebExpressionAuthorizationManager INTERNAL_ACCESS = + new WebExpressionAuthorizationManager("hasIpAddress('127.0.0.1') or hasIpAddress('0:0:0:0:0:0:0:1')"); + + private final UserDetailsService userDetailsService; + + private final LogoutHandler logoutHandler; + + @Autowired + public SecurityConfiguration(UserDetailsService userDetailsService, LogoutHandler logoutHandler) { + this.userDetailsService = userDetailsService; + this.logoutHandler = logoutHandler; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { + return configuration.getAuthenticationManager(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } + + @Bean + public AuthenticationProvider authenticationProvider() { + final DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setPasswordEncoder(passwordEncoder()); + provider.setUserDetailsService(userDetailsService); + return provider; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthFilter) throws Exception { + + if (csrfEnabled) { + http.csrf(csrf -> csrf.ignoringRequestMatchers(antMatcher("/h2-console/**"))); + } else { + // csrf is disabled for dev and test + http.csrf(AbstractHttpConfigurer::disable); + } + http.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)); + http.authorizeHttpRequests(auth -> + auth.requestMatchers(WHITE_LIST_URL).permitAll() + .requestMatchers(antMatcher(HttpMethod.GET, "/")).permitAll() + .requestMatchers(antMatcher(HttpMethod.GET, API_V1_URI)).permitAll() + .requestMatchers(antMatcher(HttpMethod.GET, "/images/**")).permitAll() + .requestMatchers(antMatcher(HttpMethod.GET, "/api/v1/reservations")).hasRole(ADMIN_ROLE) + .requestMatchers(antMatcher(HttpMethod.GET, "/api/v1/reservations/**")).hasRole(ADMIN_ROLE) + .requestMatchers(antMatcher(HttpMethod.PUT, API_V1_URI)).hasRole(ADMIN_ROLE) + .requestMatchers(antMatcher(HttpMethod.DELETE, API_V1_URI)).hasRole(ADMIN_ROLE) + .requestMatchers(antMatcher(HttpMethod.POST, "/api/v1/reservations")).permitAll() + .requestMatchers(antMatcher(HttpMethod.POST, API_V1_URI)).hasRole(ADMIN_ROLE) + .requestMatchers(antMatcher("/h2-console/**")).access(INTERNAL_ACCESS) + .requestMatchers(antMatcher("/actuator/**")).access(INTERNAL_ACCESS) + .requestMatchers(INTERNAL_WHITE_LIST_URL).access(INTERNAL_ACCESS) + .anyRequest().authenticated() + ) + .sessionManagement(session -> session.sessionCreationPolicy(STATELESS)) + .authenticationProvider(authenticationProvider()) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) + .logout(logout -> logout + .logoutUrl("/api/v1/auth/logout") + .addLogoutHandler(logoutHandler) + .logoutSuccessHandler((request, + response, + authentication) -> SecurityContextHolder.clearContext())); + + return http.build(); + } +} diff --git a/bootstrap/src/main/resources/application-dev.yml b/bootstrap/src/main/resources/application-dev.yml new file mode 100644 index 0000000..c1de8b9 --- /dev/null +++ b/bootstrap/src/main/resources/application-dev.yml @@ -0,0 +1,60 @@ +--- +spring: + h2: + console: + enabled: 'true' + + datasource: + url: jdbc:h2:mem:xpeditis;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + driverClassName: org.h2.Driver + username: sa + password: '' + + sql: + init: + platform: h2 + mode: always + + jpa: + show-sql: true + properties: + hibernate: + format_sql: true +# show_sql: true + database-platform: org.hibernate.dialect.H2Dialect + hibernate: + ddl-auto: update + # Just to load initial data for the demo. DO NOT USE IT IN PRODUCTION + defer-datasource-initialization: true + + flyway: # flyway automatically uses the datasource from the application to connect to the DB + enabled: false # enables flyway database migration + + mail: + host: sandbox.smtp.mailtrap.io + port: 2525 + username: 2597bd31d265eb + password: cd126234193c89 + properties: + mail: + smtp: + ssl: + trust:"*" + auth: true + starttls: + enable: true + connectiontimeout: 5000 + timeout: 3000 + writetimeout: 5000 + +application: + email: + from: randommailjf@gmail.com + csrf: + enabled: false + security: + jwt: + secret-key: 404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970 + expiration: 86400000 # a day + refresh-token: + expiration: 604800000 # 7 days \ No newline at end of file diff --git a/bootstrap/src/main/resources/application-prod.yml b/bootstrap/src/main/resources/application-prod.yml new file mode 100644 index 0000000..1dcc1ab --- /dev/null +++ b/bootstrap/src/main/resources/application-prod.yml @@ -0,0 +1,66 @@ +--- +spring: + datasource: + url: ${SPRING_DATASOURCE_URL} + driver-class-name: com.mysql.cj.jdbc.Driver + username: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} + #hikari: + #schema: leblr + + sql: + init: + platform: mysql + mode: never + #data-locations: import_users.sql + + jpa: +# show-sql: true + properties: + hibernate: + format_sql: true + #show_sql: true + database: mysql + database-platform: org.hibernate.dialect.MySQLDialect + hibernate: + ddl-auto: validate + defer-datasource-initialization: false + #open-in-view: false + + flyway: # flyway automatically uses the datasource from the application to connect to the DB + enabled: true # enables flyway database migration + locations: classpath:db/migration/structure, classpath:db/migration/data # the location where flyway should look for migration scripts + validate-on-migrate: true + baseline-on-migrate: true + baseline-version: 0 + default-schema: leblr + + mail: + protocol: smtp + host: ssl0.ovh.net + port: 587 + username: contact@xpeditis.fr + password: + properties: + mail: + smtp: + auth: true + starttls: + enable: true + connectiontimeout: 5000 + timeout: 3000 + writetimeout: 5000 + +application: + email: + from: contact@leblr.fr + + csrf: + enabled: false + + security: + jwt: + secret-key: 404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970 + expiration: 86400000 # a day + refresh-token: + expiration: 604800000 # 7 days \ No newline at end of file diff --git a/bootstrap/src/main/resources/application.yml b/bootstrap/src/main/resources/application.yml new file mode 100755 index 0000000..b804994 --- /dev/null +++ b/bootstrap/src/main/resources/application.yml @@ -0,0 +1,52 @@ +server: + port: 8080 + +file: + upload-dir: /upload + +spring: + http: + encoding: + charset: UTF-8 + enabled: true + force: true + + application: + name: '@project.description@' + version: '@project.version@' + + profiles: + active: '@spring.profiles.active@' + + banner: + location: 'classpath:banner.txt' + +# jackson: +# date-format: yyyy-MM-dd HH:mm:ss +# time-zone: Europe/Paris + +# messages: +# basename: language/i18n + + servlet: + multipart: + enabled: true + max-file-size: 50MB + max-request-size: 50MB + #location: ${java.io.tmpdir} + +logging: + level: + org: + springframework: + web: + filter: + CommonsRequestLoggingFilter: INFO + security: + config: + annotation: + authentication: + configuration: + InitializeUserDetailsBeanManagerConfigurer: ERROR + +apiPrefix: "" \ No newline at end of file diff --git a/bootstrap/src/main/resources/banner.txt b/bootstrap/src/main/resources/banner.txt new file mode 100644 index 0000000..84eca27 --- /dev/null +++ b/bootstrap/src/main/resources/banner.txt @@ -0,0 +1,17 @@ +██╗ ██╗██████╗ ███████╗██████╗ ██╗████████╗██╗███████╗ + ╚██╗██╔╝██╔══██╗██╔════╝██╔══██╗██║╚══██╔══╝██║██╔════╝ + ╚███╔╝ ██████╔╝█████╗ ██║ ██║██║ ██║ ██║███████╗ + ██╔██╗ ██╔═══╝ ██╔══╝ ██║ ██║██║ ██║ ██║╚════██║ + ██╔╝ ██╗██║ ███████╗██████╔╝██║ ██║ ██║███████║ + ╚═╝ ╚═╝╚═╝ ╚══════╝╚═════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ + + ⚓ MARITIME LOGISTICS & SHIPPING SOLUTIONS ⚓ + 🌊 Setting Sail for Excellence 🌊 + + ═══════════════════════════════════════════════════════ + Port: ${server.port:8080} | Version: ${spring.application.version} + ═══════════════════════════════════════════════════════ + 🚢 Navigating the Digital Ocean 🚢 + Ready to Ship Your Success! + +Powered by Spring Boot ${spring-boot.version} \ No newline at end of file diff --git a/bootstrap/src/main/resources/data-h2.sql b/bootstrap/src/main/resources/data-h2.sql new file mode 100644 index 0000000..05044a4 --- /dev/null +++ b/bootstrap/src/main/resources/data-h2.sql @@ -0,0 +1,28 @@ +INSERT INTO users (username, email, password, role) +VALUES ('dbuser', 'dbuser@dev.ovh', '{bcrypt}$2y$10$.qkbukzzX21D.bqbI.B2R.tvWP90o/Y16QRWVLodw51BHft7ZWbc.', 'USER'), + ('dbadmin', 'dbadmin@dev.ovh', '{bcrypt}$2y$10$kp1V7UYDEWn17WSK16UcmOnFd1mPFVF6UkLrOOCGtf24HOYt8p1iC', 'ADMIN'); + +INSERT INTO event (name, description, date) +VALUES ('toto', 'hello', '2025-01-01'), + ('tata', 'hola', '2025-01-10'); + +INSERT INTO gallery (name, description) +VALUES ('gallery', ''); + +INSERT INTO menu_category (name) +VALUES ('STARTERS'), + ('DISHES'), + ('DESSERTS'), + ('PIZZAS'), + ('RED_WINES'), + ('WHITE_WINES'); + +INSERT INTO product (name, description, price, category_id) +VALUES ('Coconut Cake', 'Fresh Coconut', 4.90, 3), + ('Pasta', 'Fresh Pasta', 12.79, 2), + ('Foie Gras', 'Fresh Pasta', 12.79, 1); + +INSERT INTO selected_day (date) +VALUES ('2035-01-01'), + ('2035-03-02'), + ('2035-03-10'); \ No newline at end of file diff --git a/bootstrap/src/main/resources/language/i18n_en.properties b/bootstrap/src/main/resources/language/i18n_en.properties new file mode 100644 index 0000000..97d5203 --- /dev/null +++ b/bootstrap/src/main/resources/language/i18n_en.properties @@ -0,0 +1,7 @@ +internal.error=Unable to perform action +red.wines.name=RED WINES +white.wines.name=WHITE WINES +starters.name=STARTERS +dishes.name=DISHES +desserts.name=DESSERTS +pizzas.name=PIZZAS \ No newline at end of file diff --git a/bootstrap/src/main/resources/language/i18n_fr.properties b/bootstrap/src/main/resources/language/i18n_fr.properties new file mode 100644 index 0000000..fd05ad6 --- /dev/null +++ b/bootstrap/src/main/resources/language/i18n_fr.properties @@ -0,0 +1,7 @@ +internal.error=Impossible de réaliser l'action +red.wines.name=VINS ROUGES +white.wines.name=VINS BLANCS +starters.name=ENTREES +dishes.name=PLATS +desserts.name=DESSERTS +pizzas.name=PIZZAS \ No newline at end of file diff --git a/bootstrap/src/main/resources/logback-spring.xml b/bootstrap/src/main/resources/logback-spring.xml new file mode 100755 index 0000000..a406c5d --- /dev/null +++ b/bootstrap/src/main/resources/logback-spring.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + %d{dd-MM-yyyy HH:mm:ss.SSS} [r:%X{request_id:--}] %magenta([%thread]) %contextName %highlight(%-5level) %yellow(%logger{36}.%M) - %msg%n + + + + + + + + + + + + + + + + + + + ${XPEDITIS_LOGS_PROD}/everything.log + true + + + %d{dd-MM-yyyy HH:mm:ss.SSS} [r:%X{request_id:--}] [%thread] %contextName %-5level %logger{36}.%M - %msg%n + + + + + + + + ${XPEDITIS_LOGS_PROD}/archived/everything_%d{dd-MM-yyyy}_%i.log + + 1MB + 10 + 10MB + + + + + + + + + + + diff --git a/bootstrap/src/main/resources/templates/addEvent.html b/bootstrap/src/main/resources/templates/addEvent.html new file mode 100644 index 0000000..c110e4c --- /dev/null +++ b/bootstrap/src/main/resources/templates/addEvent.html @@ -0,0 +1,74 @@ + +
+
+
+
+

Upload Image Example

+

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/bootstrap/src/main/resources/templates/addImages.html b/bootstrap/src/main/resources/templates/addImages.html new file mode 100644 index 0000000..9b2cb8b --- /dev/null +++ b/bootstrap/src/main/resources/templates/addImages.html @@ -0,0 +1,42 @@ + +
+
+
+
+

Upload Image Example

+

+
+
+ + +
+
+ +
+ + +
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/bootstrap/src/test/java/com/dh7789dev/xpeditis/LeBlrApplicationTests.java b/bootstrap/src/test/java/com/dh7789dev/xpeditis/LeBlrApplicationTests.java new file mode 100755 index 0000000..0d8751c --- /dev/null +++ b/bootstrap/src/test/java/com/dh7789dev/xpeditis/LeBlrApplicationTests.java @@ -0,0 +1,14 @@ +package com.dh7789dev.xpeditis; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles("dev") +class LeBlrApplicationTests { + + @Test + void contextLoads() { + } +} diff --git a/common/pom.xml b/common/pom.xml new file mode 100755 index 0000000..6f40493 --- /dev/null +++ b/common/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + + + com.dh7789dev + xpeditis + 0.0.1-SNAPSHOT + + + common + jar + + diff --git a/common/src/main/java/com/dh7789dev/xpeditis/CommonUtil.java b/common/src/main/java/com/dh7789dev/xpeditis/CommonUtil.java new file mode 100644 index 0000000..8ee036a --- /dev/null +++ b/common/src/main/java/com/dh7789dev/xpeditis/CommonUtil.java @@ -0,0 +1,8 @@ +package com.dh7789dev.xpeditis; + +public class CommonUtil { + + public void sayHello() { + System.out.println("Hello!"); + } +} diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..51c261e --- /dev/null +++ b/compose.yml @@ -0,0 +1,79 @@ +services: + + db: + image: mysql:latest + restart: always + ports: + - "3306:3306" + networks: + - xpeditis + environment: + MYSQL_DATABASE_FILE: /run/secrets/mysql-database + MYSQL_USER_FILE: /run/secrets/mysql-user + MYSQL_PASSWORD_FILE: /run/secrets/mysql-password + MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql-root-password + # MYSQL_RANDOM_ROOT_PASSWORD: '1' + secrets: + - mysql-database + - mysql-user + - mysql-password + - mysql-root-password + volumes: + - type: bind + source: ../database/mysql + target: /var/lib/mysql + - type: bind + source: ../database/config/docker-fixes.cnf + target: /etc/mysql/conf.d/docker-fixes.cnf + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "10" + container_name: db + + back: + image: leblr-backend + restart: always + build: + context: . + dockerfile: Dockerfile + args: + XPEDITIS_PROFILE: prod + secrets: + - mysql-user + - mysql-password + target: production + ports: + - "8081:8080" + networks: + - xpeditis + depends_on: + - db + environment: + SPRING_DATASOURCE_URL: + SPRING_DATASOURCE_USERNAME: + SPRING_DATASOURCE_PASSWORD: + volumes: + - ./logs/prod:/opt/app/logs/prod:rw + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "10" + container_name: leblr_backend + +secrets: + mysql-user: + file: ../database/secrets/mysql-user.txt + mysql-database: + file: ../database/secrets/mysql-database.txt + mysql-password: + file: ../database/secrets/mysql-password.txt + mysql-root-password: + file: ../database/secrets/mysql-root-password.txt + +networks: + leblr: + name: leblr + external: true \ No newline at end of file diff --git a/domain/api/pom.xml b/domain/api/pom.xml new file mode 100755 index 0000000..0fb873b --- /dev/null +++ b/domain/api/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + com.dh7789dev + domain + 0.0.1-SNAPSHOT + + + api + jar + + + 23 + 23 + UTF-8 + + + + + com.dh7789dev + data + ${project.version} + + + org.springframework + spring-web + + + \ No newline at end of file diff --git a/domain/api/src/main/java/com/dh7789dev/xpeditis/AuthenticationService.java b/domain/api/src/main/java/com/dh7789dev/xpeditis/AuthenticationService.java new file mode 100644 index 0000000..471d8bc --- /dev/null +++ b/domain/api/src/main/java/com/dh7789dev/xpeditis/AuthenticationService.java @@ -0,0 +1,9 @@ +package com.dh7789dev.xpeditis; + +import com.dh7789dev.xpeditis.dto.AuthenticationRequest; +import com.dh7789dev.xpeditis.dto.AuthenticationResponse; + +public interface AuthenticationService { + + AuthenticationResponse authenticate(AuthenticationRequest request); +} diff --git a/domain/api/src/main/java/com/dh7789dev/xpeditis/EmailService.java b/domain/api/src/main/java/com/dh7789dev/xpeditis/EmailService.java new file mode 100644 index 0000000..7d3217c --- /dev/null +++ b/domain/api/src/main/java/com/dh7789dev/xpeditis/EmailService.java @@ -0,0 +1,8 @@ +package com.dh7789dev.xpeditis; + +public interface EmailService { + + void sendCustomerReservationEmail(Reservation reservation); + + void sendAdminReservationEmail(Reservation reservation); +} diff --git a/domain/api/src/main/java/com/dh7789dev/xpeditis/NlsService.java b/domain/api/src/main/java/com/dh7789dev/xpeditis/NlsService.java new file mode 100644 index 0000000..51d398b --- /dev/null +++ b/domain/api/src/main/java/com/dh7789dev/xpeditis/NlsService.java @@ -0,0 +1,17 @@ +package com.dh7789dev.xpeditis; + +public interface NlsService { + + /** + * @param key nls message key + * @return the message corresponding to the key translated into the desired language + */ + String getMessage(String key); + + /** + * @param key nls message key + * @param parameters parameters to use in the message + * @return the message corresponding to the key translated into the desired language + */ + String getMessage(String key, Object[] parameters); +} diff --git a/domain/api/src/main/java/com/dh7789dev/xpeditis/StorageService.java b/domain/api/src/main/java/com/dh7789dev/xpeditis/StorageService.java new file mode 100644 index 0000000..3088145 --- /dev/null +++ b/domain/api/src/main/java/com/dh7789dev/xpeditis/StorageService.java @@ -0,0 +1,15 @@ +package com.dh7789dev.xpeditis; + +import org.springframework.core.io.Resource; +import org.springframework.web.multipart.MultipartFile; + +public interface StorageService { + + void init(); + + String upload(MultipartFile file, String newFileName); + + Resource download(String fileName); + + void delete(String fileName); +} diff --git a/domain/api/src/main/java/com/dh7789dev/xpeditis/UserService.java b/domain/api/src/main/java/com/dh7789dev/xpeditis/UserService.java new file mode 100644 index 0000000..aa94e7e --- /dev/null +++ b/domain/api/src/main/java/com/dh7789dev/xpeditis/UserService.java @@ -0,0 +1,10 @@ +package com.dh7789dev.xpeditis; + +import com.dh7789dev.xpeditis.dto.ChangePasswordRequest; + +import java.security.Principal; + +public interface UserService { + + void changePassword(ChangePasswordRequest request, Principal connectedUser); +} diff --git a/domain/data/pom.xml b/domain/data/pom.xml new file mode 100755 index 0000000..34f2108 --- /dev/null +++ b/domain/data/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + com.dh7789dev + domain + 0.0.1-SNAPSHOT + + + data + jar + + + 23 + 23 + UTF-8 + + + + + org.projectlombok + lombok + provided + + + jakarta.validation + jakarta.validation-api + + + + com.fasterxml.jackson.core + jackson-annotations + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + + org.projectlombok + lombok + ${org.projectlombok.version} + + + + + + + \ No newline at end of file diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/AuthenticationRequest.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/AuthenticationRequest.java new file mode 100644 index 0000000..0fecaa1 --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/AuthenticationRequest.java @@ -0,0 +1,21 @@ +package com.dh7789dev.xpeditis.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +@Data +@AllArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +@Accessors(chain = true) +public class AuthenticationRequest { + + @NotBlank + final String username; + + @NotBlank + final String password; +} diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/AuthenticationResponse.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/AuthenticationResponse.java new file mode 100644 index 0000000..97c3509 --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/AuthenticationResponse.java @@ -0,0 +1,31 @@ +package com.dh7789dev.xpeditis.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +import java.util.Date; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +@Accessors(chain = true) +public class AuthenticationResponse { + + @JsonProperty("access_token") + String accessToken; + + @JsonProperty("refresh_token") + String refreshToken; + + @JsonProperty("created_at") + Date createdAt; + + @JsonProperty("expires_at") + Date expiresAt; +} diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/ChangePasswordRequest.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/ChangePasswordRequest.java new file mode 100644 index 0000000..bf8c41b --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/ChangePasswordRequest.java @@ -0,0 +1,18 @@ +package com.dh7789dev.xpeditis.dto; + +import lombok.AccessLevel; +import lombok.Data; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +@Accessors(chain = true) +public class ChangePasswordRequest { + + String currentPassword; + + String newPassword; + + String confirmationPassword; +} diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/CustomInternalExceptionResponse.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/CustomInternalExceptionResponse.java new file mode 100644 index 0000000..9b158e8 --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/CustomInternalExceptionResponse.java @@ -0,0 +1,36 @@ +package com.dh7789dev.xpeditis.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AccessLevel; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +import java.time.Instant; + +@Data +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +@Accessors(chain = true) +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class CustomInternalExceptionResponse { + + String id; + + Instant timestamp; + + String errorCode; + + String errorMessage; + + String requestURL; + + public CustomInternalExceptionResponse(String id, String errorCode, String errorMessage, String requestURL) { + this.id = id; + this.errorCode = errorCode; + this.errorMessage = errorMessage; + this.requestURL = requestURL; + this.timestamp = Instant.now(); + } +} diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/Gallery.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/Gallery.java new file mode 100644 index 0000000..9949ae6 --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/Gallery.java @@ -0,0 +1,35 @@ +package com.dh7789dev.xpeditis.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import jakarta.validation.constraints.NotBlank; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +@Accessors(chain = true) +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class Gallery { + + Long id; + + @NotBlank(message = "name cannot be empty") + String name; + + String description; + + List images; + + @Override + public String toString() { + return "Gallery(" + String.format("name=%s, description=%s)", name, description); + } +} diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/Image.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/Image.java new file mode 100644 index 0000000..0a5c10d --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/Image.java @@ -0,0 +1,26 @@ +package com.dh7789dev.xpeditis.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AccessLevel; +import lombok.Data; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +@Accessors(chain = true) +public class Image { + + Long id; + + String name; + + @JsonIgnore + String path; + + String uri; + + public Image(String name) { + this.name = name; + } +} diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/CustomInternalException.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/CustomInternalException.java new file mode 100644 index 0000000..29cdd54 --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/CustomInternalException.java @@ -0,0 +1,43 @@ +package com.dh7789dev.xpeditis.exception; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +@Getter +@Setter +@FieldDefaults(level = AccessLevel.PRIVATE) +@Accessors(chain = true) +public class CustomInternalException extends RuntimeException { + + private static final String INTERNAL_ERROR = "internal.error"; + + final String errorCode; + + final String nlsKey; + + final String[] nlsParameters; + + public CustomInternalException(String message) { + super(message); + errorCode = "InternalError"; + nlsKey = INTERNAL_ERROR; + nlsParameters = new String[]{}; + } + + public CustomInternalException(String errorCode, String message, String nlsKey) { + super(message); + this.errorCode = errorCode; + this.nlsKey = nlsKey; + this.nlsParameters= new String[]{}; + } + + public CustomInternalException(String errorCode, String message, String nlsKey, String[] nlsParameters) { + super(message); + this.errorCode = errorCode; + this.nlsKey = nlsKey; + this.nlsParameters = nlsParameters; + } +} diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/GlobalNotFoundException.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/GlobalNotFoundException.java new file mode 100755 index 0000000..fff71c2 --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/GlobalNotFoundException.java @@ -0,0 +1,21 @@ +package com.dh7789dev.xpeditis.exception; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +@Getter +@Setter +@FieldDefaults(level = AccessLevel.PRIVATE) +@Accessors(chain = true) +public class GlobalNotFoundException extends RuntimeException { + + final String errorCode; + + public GlobalNotFoundException(String subject) { + super((subject + " Not Found")); + this.errorCode = subject + "NotFound"; + } +} diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/NlsNotFoundException.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/NlsNotFoundException.java new file mode 100644 index 0000000..3d36393 --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/NlsNotFoundException.java @@ -0,0 +1,8 @@ +package com.dh7789dev.xpeditis.exception; + +public class NlsNotFoundException extends RuntimeException { + + public NlsNotFoundException(String nlsKey, String locale) { + super("Nls message not found for nlsKey " + nlsKey + " and locale " + locale); + } +} diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/StorageException.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/StorageException.java new file mode 100644 index 0000000..73e2c26 --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/StorageException.java @@ -0,0 +1,12 @@ +package com.dh7789dev.xpeditis.exception; + +public class StorageException extends RuntimeException { + + public StorageException(String message) { + super(message); + } + + public StorageException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/domain/pom.xml b/domain/pom.xml new file mode 100755 index 0000000..6b21f0a --- /dev/null +++ b/domain/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + com.dh7789dev + xpeditis + 0.0.1-SNAPSHOT + + + domain + pom + + + 23 + 23 + UTF-8 + + + + data + api + spi + service + + \ No newline at end of file diff --git a/domain/service/pom.xml b/domain/service/pom.xml new file mode 100755 index 0000000..29a66b1 --- /dev/null +++ b/domain/service/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + com.dh7789dev + domain + 0.0.1-SNAPSHOT + + + service + jar + + + 23 + 23 + UTF-8 + 5.9.2 + + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-test + test + + + com.dh7789dev + api + ${project.version} + + + com.dh7789dev + spi + ${project.version} + + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + org.projectlombok + lombok + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + + org.projectlombok + lombok + ${org.projectlombok.version} + + + + + + + \ No newline at end of file diff --git a/domain/service/src/main/java/com/dh7789dev/xpeditis/AuthenticationServiceImpl.java b/domain/service/src/main/java/com/dh7789dev/xpeditis/AuthenticationServiceImpl.java new file mode 100644 index 0000000..d9988ed --- /dev/null +++ b/domain/service/src/main/java/com/dh7789dev/xpeditis/AuthenticationServiceImpl.java @@ -0,0 +1,20 @@ +package com.dh7789dev.xpeditis; + +import com.dh7789dev.xpeditis.dto.AuthenticationRequest; +import com.dh7789dev.xpeditis.dto.AuthenticationResponse; +import org.springframework.stereotype.Service; + +@Service +public class AuthenticationServiceImpl implements AuthenticationService { + + private final AuthenticationRepository authenticationRepository; + + public AuthenticationServiceImpl(AuthenticationRepository authenticationRepository) { + this.authenticationRepository = authenticationRepository; + } + + @Override + public AuthenticationResponse authenticate(AuthenticationRequest request) { + return authenticationRepository.authenticate(request); + } +} diff --git a/domain/service/src/main/java/com/dh7789dev/xpeditis/EmailServiceImpl.java b/domain/service/src/main/java/com/dh7789dev/xpeditis/EmailServiceImpl.java new file mode 100644 index 0000000..5c67b5b --- /dev/null +++ b/domain/service/src/main/java/com/dh7789dev/xpeditis/EmailServiceImpl.java @@ -0,0 +1,59 @@ +package com.dh7789dev.xpeditis; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.time.format.DateTimeFormatter; + +@Service +public class EmailServiceImpl implements EmailService { + + @Value("${application.email.from}") + String adminEmail; + + private final EmailSenderRepository emailSenderRepository; + + private final DateTimeFormatter outputFormatter; + + @Autowired + public EmailServiceImpl(EmailSenderRepository emailSenderRepository) { + this.emailSenderRepository = emailSenderRepository; + this.outputFormatter = DateTimeFormatter.ofPattern("dd-MM-yyyy 'à' HH:mm"); + } + + protected String buildCustomerReservationEmail(String name, String date, int nbPerson) { + return "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + "

Bienvenue au Restaurant le BLR!

\n" + + " \"Restaurant\n" + + "
\n" + + "

Bonjour " + name + ",

\n" + + "

\n" + + " Merci beaucoup pour votre réservation chez Le BLR.
\n" + + " Nous sommes ravis de vous avoir parmi nous !
\n" + + " Vous avez effectué une réservation pour " + nbPerson + " " + (nbPerson > 1 ? "personnes" : "personne") + ".
\n" + + " Nous avons hâte de vous accueillir dans notre restaurant le " + date + " et de vous offrir une expérience inoubliable !
\n" + + " À très bientôt,
\n" + + " L'équipe du Le BLR\n" + + "

\n" + + "
\n" + + "
"; + } + + @Override + public void sendCustomerReservationEmail(Reservation reservation) { + emailSenderRepository.send(reservation.getEmail(), + buildCustomerReservationEmail(reservation.getName(), reservation.getDate().format(outputFormatter), reservation.getNbPerson())); + } + + @Override + public void sendAdminReservationEmail(Reservation reservation) { + emailSenderRepository.send(adminEmail, + buildCustomerReservationEmail(reservation.getName(), reservation.getDate().format(outputFormatter), reservation.getNbPerson())); + } +} diff --git a/domain/service/src/main/java/com/dh7789dev/xpeditis/FileSystemStorageService.java b/domain/service/src/main/java/com/dh7789dev/xpeditis/FileSystemStorageService.java new file mode 100644 index 0000000..9f7cafb --- /dev/null +++ b/domain/service/src/main/java/com/dh7789dev/xpeditis/FileSystemStorageService.java @@ -0,0 +1,86 @@ +package com.dh7789dev.xpeditis; + +import com.dh7789dev.xpeditis.exception.StorageException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +@Slf4j +@Service +public class FileSystemStorageService implements StorageService { + + @Value("${file.upload-dir}") + String uploadDir; + + @Override + public void init() { + Path directory = Paths.get(uploadDir); + if (!Files.exists(directory)) { + try { + log.info("Create directory {} to store images", directory); + Files.createDirectories(directory); + } catch (IOException e) { + throw new StorageException("Failed to create directory " + uploadDir, e); + } + } + } + + @Override + public String upload(MultipartFile file, String newFileName) { + + if (!file.isEmpty()) { + + Path filePath = Paths.get(uploadDir, newFileName); + log.info("File path: {}", filePath); + try { + Files.write(filePath, file.getBytes()); + } catch (IOException e) { + throw new StorageException("Failed to store file", e); + } + log.info("save {} to {}", file.getOriginalFilename(), newFileName); + return filePath.toString(); + } else { + throw new StorageException("Failed to store file"); + } + } + + @Override + public Resource download(String fileName) { + + try { + Path filePath = Paths.get(uploadDir).resolve(fileName).normalize(); + log.info("Download file path: {}", fileName); + Resource resource = new UrlResource(filePath.toUri()); + if (!resource.exists() || !resource.isReadable()) { + throw new StorageException("Could not read file: " + fileName); + } + return resource; + } catch (IOException e) { + throw new StorageException("Failed to download file " + fileName, e); + } + } + + @Override + public void delete(String fileName) { + + try { + Path filePath = Paths.get(uploadDir).resolve(fileName).normalize(); + log.info("Delete file path: {}", fileName); + if (Files.exists(filePath)) { + Files.delete(filePath); + } else { + throw new StorageException("File does not exist: " + fileName); + } + } catch (IOException e) { + throw new StorageException("Failed to delete file " + fileName, e); + } + } +} diff --git a/domain/service/src/main/java/com/dh7789dev/xpeditis/NlsServiceImpl.java b/domain/service/src/main/java/com/dh7789dev/xpeditis/NlsServiceImpl.java new file mode 100644 index 0000000..89b6bec --- /dev/null +++ b/domain/service/src/main/java/com/dh7789dev/xpeditis/NlsServiceImpl.java @@ -0,0 +1,56 @@ +package com.dh7789dev.xpeditis; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.text.MessageFormat; +import java.util.Locale; + +@Service +public class NlsServiceImpl implements NlsService { + + private final NlsRepository nlsRepository; + + @Autowired + public NlsServiceImpl(NlsRepository nlsRepository) { + this.nlsRepository = nlsRepository; + } + + /** + * @param key nls message key + * @return the message corresponding to the key translated into the desired language + */ + @Override + public String getMessage(String key) { + return nlsRepository.getMessage(key); + } + + /** + * @param key nls message key + * @param parameters parameters to use in the message + * @return the message corresponding to the key translated into the desired language + */ + @Override + public String getMessage(String key, Object[] parameters) { + String message = nlsRepository.getMessage(key, parameters); + // escape simple quotes: double them + message = message.replace("'", "''").replace("{", "'{"); + // format message + return MessageFormat.format(message, parameters); + } + + private Locale getLocale(final Locale locale) { + Locale locale1; + if (locale != null) { + // hack for simplified chinese + if (locale.getLanguage().equals("zh") && locale.getCountry().isEmpty()) { + locale1 = Locale.SIMPLIFIED_CHINESE; + } else { + locale1 = locale; + } + } else { + locale1 = Locale.getDefault(); + } + return locale1; + } +} diff --git a/domain/service/src/main/java/com/dh7789dev/xpeditis/UserServiceImpl.java b/domain/service/src/main/java/com/dh7789dev/xpeditis/UserServiceImpl.java new file mode 100644 index 0000000..bbc05a0 --- /dev/null +++ b/domain/service/src/main/java/com/dh7789dev/xpeditis/UserServiceImpl.java @@ -0,0 +1,21 @@ +package com.dh7789dev.xpeditis; + +import com.dh7789dev.xpeditis.dto.ChangePasswordRequest; +import org.springframework.stereotype.Service; + +import java.security.Principal; + +@Service +public class UserServiceImpl implements UserService { + + private final UserRepository userRepository; + + public UserServiceImpl(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public void changePassword(ChangePasswordRequest request, Principal connectedUser) { + userRepository.changePassword(request, connectedUser); + } +} diff --git a/domain/service/src/test/java/com/dh7789dev/xpeditis/MenuServiceImplTest.java b/domain/service/src/test/java/com/dh7789dev/xpeditis/MenuServiceImplTest.java new file mode 100644 index 0000000..707a260 --- /dev/null +++ b/domain/service/src/test/java/com/dh7789dev/xpeditis/MenuServiceImplTest.java @@ -0,0 +1,35 @@ +package com.dh7789dev.xpeditis; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class MenuServiceImplTest { + + @Mock + private MenuRepository menuRepository; + + @InjectMocks + private MenuServiceImpl menuService; + + @Test + void given_whenGetCategories_then() { + + // given + when(menuRepository.findAll()).thenReturn(List.of()); + + // when + List categories = menuService.getCategories(); + + // then + assertThat(categories).isEmpty(); + } +} diff --git a/domain/spi/pom.xml b/domain/spi/pom.xml new file mode 100755 index 0000000..ad1d866 --- /dev/null +++ b/domain/spi/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + com.dh7789dev + domain + 0.0.1-SNAPSHOT + + + spi + jar + + + 23 + 23 + UTF-8 + + + + + com.dh7789dev + data + ${project.version} + + + \ No newline at end of file diff --git a/domain/spi/src/main/java/com/dh7789dev/xpeditis/AuthenticationRepository.java b/domain/spi/src/main/java/com/dh7789dev/xpeditis/AuthenticationRepository.java new file mode 100644 index 0000000..db7d130 --- /dev/null +++ b/domain/spi/src/main/java/com/dh7789dev/xpeditis/AuthenticationRepository.java @@ -0,0 +1,9 @@ +package com.dh7789dev.xpeditis; + +import com.dh7789dev.xpeditis.dto.AuthenticationRequest; +import com.dh7789dev.xpeditis.dto.AuthenticationResponse; + +public interface AuthenticationRepository { + + AuthenticationResponse authenticate(AuthenticationRequest request); +} diff --git a/domain/spi/src/main/java/com/dh7789dev/xpeditis/EmailSenderRepository.java b/domain/spi/src/main/java/com/dh7789dev/xpeditis/EmailSenderRepository.java new file mode 100644 index 0000000..0164537 --- /dev/null +++ b/domain/spi/src/main/java/com/dh7789dev/xpeditis/EmailSenderRepository.java @@ -0,0 +1,6 @@ +package com.dh7789dev.xpeditis; + +public interface EmailSenderRepository { + + void send(String emailAddressTo, String emailContent); +} diff --git a/domain/spi/src/main/java/com/dh7789dev/xpeditis/NlsRepository.java b/domain/spi/src/main/java/com/dh7789dev/xpeditis/NlsRepository.java new file mode 100644 index 0000000..4f963d2 --- /dev/null +++ b/domain/spi/src/main/java/com/dh7789dev/xpeditis/NlsRepository.java @@ -0,0 +1,17 @@ +package com.dh7789dev.xpeditis; + +public interface NlsRepository { + + /** + * @param key nls message key + * @return the message corresponding to the key translated into the desired language + */ + String getMessage(String key); + + /** + * @param key nls message key + * @param parameters parameters to use in the message + * @return the message corresponding to the key translated into the desired language + */ + String getMessage(String key, Object[] parameters); +} diff --git a/domain/spi/src/main/java/com/dh7789dev/xpeditis/UserRepository.java b/domain/spi/src/main/java/com/dh7789dev/xpeditis/UserRepository.java new file mode 100644 index 0000000..9e3d29f --- /dev/null +++ b/domain/spi/src/main/java/com/dh7789dev/xpeditis/UserRepository.java @@ -0,0 +1,10 @@ +package com.dh7789dev.xpeditis; + +import com.dh7789dev.xpeditis.dto.ChangePasswordRequest; + +import java.security.Principal; + +public interface UserRepository { + + void changePassword(ChangePasswordRequest request, Principal connectedUser); +} diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..bf9b867 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +# ./wait-for-it.sh db:3306 --timeout=60 --strict -- +java -jar -Dserver.port=8080 xpeditis.jar diff --git a/infrastructure/flyway.conf b/infrastructure/flyway.conf new file mode 100644 index 0000000..e489898 --- /dev/null +++ b/infrastructure/flyway.conf @@ -0,0 +1,5 @@ +flyway.user=user +flyway.password=LEBLR_p4ssw0rd_971 +flyway.schemas=leblr +flyway.url=jdbc:mysql://localhost:3306/leblr +flyway.locations=classpath:db/migration/structure,classpath:db/migration/data \ No newline at end of file diff --git a/infrastructure/pom.xml b/infrastructure/pom.xml new file mode 100755 index 0000000..d6ac811 --- /dev/null +++ b/infrastructure/pom.xml @@ -0,0 +1,151 @@ + + + 4.0.0 + + + com.dh7789dev + xpeditis + 0.0.1-SNAPSHOT + + + infrastructure + jar + + + 23 + 23 + UTF-8 + 11.3.1 + 0.12.6 + + + + + com.dh7789dev + spi + ${project.version} + + + org.projectlombok + lombok + provided + + + org.mapstruct + mapstruct + + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-mail + + + org.springframework.boot + spring-boot-starter-test + test + + + + + io.jsonwebtoken + jjwt-api + ${io.jsonwebtoken.version} + compile + + + io.jsonwebtoken + jjwt-impl + ${io.jsonwebtoken.version} + + + io.jsonwebtoken + jjwt-jackson + ${io.jsonwebtoken.version} + + + + + + dev + + true + + + + com.h2database + h2 + 2.3.232 + runtime + + + + + prod + + + com.mysql + mysql-connector-j + 9.2.0 + runtime + + + + org.flywaydb + flyway-core + ${org.flywaydb.version} + + + org.flywaydb + flyway-mysql + ${org.flywaydb.version} + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + + org.projectlombok + lombok + ${org.projectlombok.version} + + + org.mapstruct + mapstruct-processor + ${org.mapstruct.version} + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + + + + org.flywaydb + flyway-maven-plugin + ${org.flywaydb.version} + + + + \ No newline at end of file diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/configuration/AuditorAwareImpl.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/configuration/AuditorAwareImpl.java new file mode 100644 index 0000000..6fe55c1 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/configuration/AuditorAwareImpl.java @@ -0,0 +1,33 @@ +package com.dh7789dev.xpeditis.configuration; + +import com.dh7789dev.xpeditis.entity.UserEntity; +import org.springframework.data.domain.AuditorAware; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component(value = "auditorProvider") +public class AuditorAwareImpl implements AuditorAware { + + private static final String DEFAULT_AUDITOR = "SYSTEM"; + + @Override + public Optional getCurrentAuditor() { + + Authentication authentication = SecurityContextHolder + .getContext() + .getAuthentication(); + + if (authentication == null || !authentication.isAuthenticated()) { + return Optional.empty(); + } else if (authentication instanceof AnonymousAuthenticationToken) { + return Optional.of(DEFAULT_AUDITOR); + } + + UserEntity userPrincipal = (UserEntity) authentication.getPrincipal(); + return Optional.ofNullable(userPrincipal.getUsername()); + } +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/EventDao.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/EventDao.java new file mode 100755 index 0000000..f90ab29 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/EventDao.java @@ -0,0 +1,17 @@ +package com.dh7789dev.xpeditis.dao; + +import com.dh7789dev.xpeditis.entity.EventEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +public interface EventDao extends JpaRepository { + + Optional findByName(String name); + + List findAllByDate(Instant date); + + List findAllByOrderByDateDescNameAsc(); +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/GalleryDao.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/GalleryDao.java new file mode 100644 index 0000000..2dc055a --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/GalleryDao.java @@ -0,0 +1,7 @@ +package com.dh7789dev.xpeditis.dao; + +import com.dh7789dev.xpeditis.entity.GalleryEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface GalleryDao extends JpaRepository { +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/ImageDao.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/ImageDao.java new file mode 100644 index 0000000..3c3c066 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/ImageDao.java @@ -0,0 +1,7 @@ +package com.dh7789dev.xpeditis.dao; + +import com.dh7789dev.xpeditis.entity.ImageEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ImageDao extends JpaRepository { +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/MenuDao.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/MenuDao.java new file mode 100644 index 0000000..fe807c3 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/MenuDao.java @@ -0,0 +1,8 @@ +package com.dh7789dev.xpeditis.dao; + +import com.dh7789dev.xpeditis.entity.MenuCategoryEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MenuDao extends JpaRepository { + +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/ProductDao.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/ProductDao.java new file mode 100644 index 0000000..88ec680 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/ProductDao.java @@ -0,0 +1,8 @@ +package com.dh7789dev.xpeditis.dao; + +import com.dh7789dev.xpeditis.entity.ProductEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ProductDao extends JpaRepository { + +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/ReservationDao.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/ReservationDao.java new file mode 100644 index 0000000..9a98db8 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/ReservationDao.java @@ -0,0 +1,18 @@ +package com.dh7789dev.xpeditis.dao; + +import com.dh7789dev.xpeditis.entity.ReservationEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; + +public interface ReservationDao extends JpaRepository { + + List findByName(String name); + + @Query("SELECT r FROM ReservationEntity r WHERE r.date >= :startOfDay AND r.date < :endOfDay") + List findReservationsByDates(@Param("startOfDay") LocalDateTime startOfDay, + @Param("endOfDay") LocalDateTime endOfDay); +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/SelectedDayDao.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/SelectedDayDao.java new file mode 100644 index 0000000..d138c3a --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/SelectedDayDao.java @@ -0,0 +1,14 @@ +package com.dh7789dev.xpeditis.dao; + +import com.dh7789dev.xpeditis.entity.SelectedDayEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.time.LocalDate; +import java.util.List; + +public interface SelectedDayDao extends JpaRepository { + + @Query("SELECT day FROM SelectedDayEntity day WHERE day.date >= :date") + List findAllFromDate(LocalDate date); +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/TokenDao.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/TokenDao.java new file mode 100644 index 0000000..fcaada5 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/TokenDao.java @@ -0,0 +1,20 @@ +package com.dh7789dev.xpeditis.dao; + +import com.dh7789dev.xpeditis.entity.TokenEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; +import java.util.Optional; + +public interface TokenDao extends JpaRepository { + + Optional findByToken(String token); + + @Query(value = """ + select t from TokenEntity t inner join UserEntity u\s + on t.user.id = u.id\s + where u.id = :userId and (t.expired = false or t.revoked = false)\s + """) + List findAllValidTokenByUserId(Long userId); +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/UserDao.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/UserDao.java new file mode 100755 index 0000000..f7aba3f --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/UserDao.java @@ -0,0 +1,11 @@ +package com.dh7789dev.xpeditis.dao; + +import com.dh7789dev.xpeditis.entity.UserEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserDao extends JpaRepository { + + Optional findByUsername(String username); +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/BaseEntity.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/BaseEntity.java new file mode 100755 index 0000000..dd2f565 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/BaseEntity.java @@ -0,0 +1,48 @@ +package com.dh7789dev.xpeditis.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.FieldDefaults; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.io.Serializable; +import java.time.Instant; + +@EntityListeners(AuditingEntityListener.class) +@Getter +@Setter +@MappedSuperclass +@FieldDefaults(level = AccessLevel.PRIVATE) +public abstract class BaseEntity implements Serializable { + + @Id + @Column(name = "id", updatable = false, nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + +// @Version +// @Column(name = "version") +// int version; + + @CreatedDate + @Column(name = "created_date", updatable = false, nullable = false, columnDefinition = "TIMESTAMP default CURRENT_TIMESTAMP") + Instant createdDate; + + @LastModifiedDate + @Column(name = "modified_date", updatable = false, nullable = false, columnDefinition = "TIMESTAMP default CURRENT_TIMESTAMP") + Instant modifiedDate; + + @CreatedBy + @Column(name = "created_by", updatable = false, nullable = false, columnDefinition = "varchar(255) default 'SYSTEM'") + String createdBy; + + @LastModifiedBy + @Column(name = "modified_by") + String modifiedBy; +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/EventEntity.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/EventEntity.java new file mode 100755 index 0000000..7271cb1 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/EventEntity.java @@ -0,0 +1,34 @@ +package com.dh7789dev.xpeditis.entity; + +import jakarta.persistence.*; +import lombok.*; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +import java.time.LocalDate; +import java.util.List; + +@Entity +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +@Accessors(chain = true) +@Table(name = "event", uniqueConstraints = { + @UniqueConstraint(columnNames = {"name"}) +}) +public class EventEntity extends BaseEntity { + + @Column(nullable = false, unique = true) + String name; + + @Column + String description; + + @Column(nullable = false) + LocalDate date; + + @OneToMany(mappedBy = "event", cascade = CascadeType.ALL, orphanRemoval = true) + private List images; +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/GalleryEntity.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/GalleryEntity.java new file mode 100644 index 0000000..0f7b106 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/GalleryEntity.java @@ -0,0 +1,30 @@ +package com.dh7789dev.xpeditis.entity; + +import jakarta.persistence.*; +import lombok.*; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +import java.util.List; + +@Entity +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +@Accessors(chain = true) +@Table(name = "gallery", uniqueConstraints = { + @UniqueConstraint(columnNames = {"name"}) +}) +public class GalleryEntity extends BaseEntity { + + @Column(unique = true) + String name; + + @Column + String description; + + @OneToMany(mappedBy = "gallery", cascade = CascadeType.ALL, orphanRemoval = true) + private List images; +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/ImageEntity.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/ImageEntity.java new file mode 100644 index 0000000..1e53ca9 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/ImageEntity.java @@ -0,0 +1,33 @@ +package com.dh7789dev.xpeditis.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +@Entity +@Getter +@Setter +@FieldDefaults(level = AccessLevel.PRIVATE) +@Accessors(chain = true) +@Table(name = "image", uniqueConstraints = { + @UniqueConstraint(columnNames = {"name", "path"}) +}) +public class ImageEntity extends BaseEntity { + + @Column(unique = true) + String name; + + @Column(unique = true) + String path; + + @ManyToOne + @JoinColumn(name = "event_id") + EventEntity event; + + @ManyToOne + @JoinColumn(name = "gallery_id") + GalleryEntity gallery; +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/MenuCategoryEntity.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/MenuCategoryEntity.java new file mode 100644 index 0000000..7f89200 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/MenuCategoryEntity.java @@ -0,0 +1,29 @@ +package com.dh7789dev.xpeditis.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.List; + +@Entity +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Accessors(chain = true) +@Table(name = "menu_category", uniqueConstraints = { + @UniqueConstraint(columnNames = {"name"}) +}) +public class MenuCategoryEntity extends BaseEntity { + + @Enumerated(EnumType.STRING) + @Column(name = "name", nullable = false, unique = true) + private MenuCategory.Name name; + + @OneToMany(mappedBy = "category") + private List products; +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/Permission.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/Permission.java new file mode 100644 index 0000000..b37462d --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/Permission.java @@ -0,0 +1,20 @@ +package com.dh7789dev.xpeditis.entity; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum Permission { + + ADMIN_READ("admin:read"), + ADMIN_UPDATE("admin:update"), + ADMIN_CREATE("admin:create"), + ADMIN_DELETE("admin:delete"), + MANAGER_READ("management:read"), + MANAGER_UPDATE("management:update"), + MANAGER_CREATE("management:create"), + MANAGER_DELETE("management:delete"); + + private final String permission; +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/ProductEntity.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/ProductEntity.java new file mode 100644 index 0000000..5549fbe --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/ProductEntity.java @@ -0,0 +1,34 @@ +package com.dh7789dev.xpeditis.entity; + +import jakarta.persistence.*; +import lombok.*; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +import java.math.BigDecimal; + +@Entity +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +@Accessors(chain = true) +@Table(name = "product", uniqueConstraints = { + @UniqueConstraint(columnNames = {"name"}) +}) +public class ProductEntity extends BaseEntity { + + @Column(nullable = false, unique = true) + String name; + + @Column + String description; + + @Column(nullable = false) + BigDecimal price; + + @ManyToOne + @JoinColumn(name = "category_id") + private MenuCategoryEntity category; +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/ReservationEntity.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/ReservationEntity.java new file mode 100644 index 0000000..e3a55ee --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/ReservationEntity.java @@ -0,0 +1,35 @@ +package com.dh7789dev.xpeditis.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Table(name = "reservation") +public class ReservationEntity extends BaseEntity { + + @Column(nullable = false) + String name; + + @Column(nullable = false) + String email; + + @Column + String phone; + + @Column(nullable = false) + int nbPerson; + + @Column(nullable = false) + LocalDateTime date; +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/Role.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/Role.java new file mode 100644 index 0000000..d3c97ed --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/Role.java @@ -0,0 +1,45 @@ +package com.dh7789dev.xpeditis.entity; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.dh7789dev.xpeditis.entity.Permission.*; + +@Getter +@RequiredArgsConstructor +public enum Role { + + USER(Collections.emptySet()), + ADMIN(Set.of( + ADMIN_READ, + ADMIN_UPDATE, + ADMIN_DELETE, + ADMIN_CREATE, + MANAGER_READ, + MANAGER_UPDATE, + MANAGER_DELETE, + MANAGER_CREATE) + ), + MANAGER(Set.of( + MANAGER_READ, + MANAGER_UPDATE, + MANAGER_DELETE, + MANAGER_CREATE)); + + private final Set permissions; + + public List getAuthorities() { + var authorities = getPermissions() + .stream() + .map(permission -> new SimpleGrantedAuthority(permission.getPermission())) + .collect(Collectors.toList()); + authorities.add(new SimpleGrantedAuthority("ROLE_" + this.name())); + return authorities; + } +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/SelectedDayEntity.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/SelectedDayEntity.java new file mode 100644 index 0000000..152b18a --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/SelectedDayEntity.java @@ -0,0 +1,22 @@ +package com.dh7789dev.xpeditis.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.*; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +import java.time.LocalDate; + +@Entity +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +@Accessors(chain = true) +@Table(name = "selected_day") +public class SelectedDayEntity extends BaseEntity { + + LocalDate date; +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/TokenEntity.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/TokenEntity.java new file mode 100644 index 0000000..34869f7 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/TokenEntity.java @@ -0,0 +1,38 @@ +package com.dh7789dev.xpeditis.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; + +@Entity +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Accessors(chain = true) +@Table(name = "token", uniqueConstraints = { + @UniqueConstraint(columnNames = {"token"}) +}) +public class TokenEntity extends BaseEntity { + + public enum Type { + BEARER + } + + @Column(unique = true) + private String token; + + @Enumerated(EnumType.STRING) + private Type tokenType = Type.BEARER; + + private boolean revoked; + + private boolean expired; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private UserEntity user; +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/UserEntity.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/UserEntity.java new file mode 100755 index 0000000..7132ca7 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/UserEntity.java @@ -0,0 +1,87 @@ +package com.dh7789dev.xpeditis.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.NaturalId; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +@Entity +@Getter +@Setter +@Table(name = "users", uniqueConstraints = { + @UniqueConstraint(columnNames = {"username", "email"}) +}) +public class UserEntity extends BaseEntity implements UserDetails { + + @NaturalId + @Column(nullable = false, unique = true, length = 50) + private String username; + + @Column(name = "first_name", length = 50) + private String firstName; + + @Column(name = "last_name", length = 50) + private String lastName; + + @Column(nullable = false, unique = true, length = 50) + private String email; + + @Column(nullable = false) + private String password; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Role role; + + @Column(name = "enabled", nullable = false, columnDefinition = "BOOLEAN DEFAULT TRUE NOT NULL") + private boolean enabled; + + @OneToMany(mappedBy = "user") + private List tokens; + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public Collection getAuthorities() { + return role.getAuthorities(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public String toString() { + return "UserEntity(" + super.toString() + String.format("username=%s, firstName=%s, lastName=%s, email=%s, role=%s)", + username, firstName, lastName, email, role.name()); + } +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/EventMapper.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/EventMapper.java new file mode 100755 index 0000000..b1c820d --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/EventMapper.java @@ -0,0 +1,25 @@ +package com.dh7789dev.xpeditis.mapper; + +import com.dh7789dev.xpeditis.entity.EventEntity; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper(/*componentModel = MappingConstants.ComponentModel.SPRING,*/ + uses = {ImageMapper.class}) +public interface EventMapper { + + EventMapper INSTANCE = Mappers.getMapper(EventMapper.class); + + @Mapping(target = "createdDate", ignore = true) + @Mapping(target = "modifiedDate", ignore = true) + @Mapping(target = "createdBy", ignore = true) + @Mapping(target = "modifiedBy", ignore = true) + EventEntity eventToEventEntity(Event event); + + Event eventEntityToEvent(EventEntity eventEntity); + + List eventEntitiesToEvents(List eventEntities); +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/GalleryMapper.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/GalleryMapper.java new file mode 100644 index 0000000..ebc63fc --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/GalleryMapper.java @@ -0,0 +1,22 @@ +package com.dh7789dev.xpeditis.mapper; + +import com.dh7789dev.xpeditis.dto.Gallery; +import com.dh7789dev.xpeditis.entity.GalleryEntity; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper(/*componentModel = MappingConstants.ComponentModel.SPRING,*/ + uses = {ImageMapper.class}) +public interface GalleryMapper { + + GalleryMapper INSTANCE = Mappers.getMapper(GalleryMapper.class); + + @Mapping(target = "createdDate", ignore = true) + @Mapping(target = "modifiedDate", ignore = true) + @Mapping(target = "createdBy", ignore = true) + @Mapping(target = "modifiedBy", ignore = true) + GalleryEntity galleryToGalleryEntity(Gallery gallery); + + Gallery galleryEntityToGallery(GalleryEntity galleryEntity); +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/ImageMapper.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/ImageMapper.java new file mode 100644 index 0000000..80b8c56 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/ImageMapper.java @@ -0,0 +1,25 @@ +package com.dh7789dev.xpeditis.mapper; + +import com.dh7789dev.xpeditis.dto.Image; +import com.dh7789dev.xpeditis.entity.ImageEntity; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import org.mapstruct.factory.Mappers; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface ImageMapper { + + ImageMapper INSTANCE = Mappers.getMapper(ImageMapper.class); + + @Mapping(target = "createdDate", ignore = true) + @Mapping(target = "modifiedDate", ignore = true) + @Mapping(target = "createdBy", ignore = true) + @Mapping(target = "modifiedBy", ignore = true) + @Mapping(target = "event", ignore = true) + ImageEntity imageToImageEntity(Image image); + + @Mapping(target = "path", ignore = true) + @Mapping(target = "uri", expression = "java(\"/api/v1/images/\" + imageEntity.getId())") + Image imageEntityToImage(ImageEntity imageEntity); +} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/MenuCategoryMapper.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/MenuCategoryMapper.java new file mode 100644 index 0000000..234b167 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/MenuCategoryMapper.java @@ -0,0 +1,27 @@ +package com.dh7789dev.xpeditis.mapper; + +import com.dh7789dev.xpeditis.entity.MenuCategoryEntity; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper(/*componentModel = MappingConstants.ComponentModel.SPRING,*/ + uses = {ProductMapper.class}) +public interface MenuCategoryMapper { + + MenuCategoryMapper INSTANCE = Mappers.getMapper(MenuCategoryMapper.class); + + @Mapping(target = "createdDate", ignore = true) + @Mapping(target = "modifiedDate", ignore = true) + @Mapping(target = "createdBy", ignore = true) + @Mapping(target = "modifiedBy", ignore = true) + @Mapping(source = "menuItems", target = "products") + MenuCategoryEntity menuCategoryToMenuCategoryEntity(MenuCategory menuCategory); + + @Mapping(source = "products", target = "menuItems") + MenuCategory menuCategoryEntityToMenuCategory(MenuCategoryEntity menuCategoryEntity); + + List menuCategoryEntitiesToMenuCategories(List menuCategoryEntities); +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/ProductMapper.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/ProductMapper.java new file mode 100644 index 0000000..fbe0ab8 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/ProductMapper.java @@ -0,0 +1,26 @@ +package com.dh7789dev.xpeditis.mapper; + +import com.dh7789dev.xpeditis.entity.ProductEntity; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface ProductMapper { + + ProductMapper INSTANCE = Mappers.getMapper(ProductMapper.class); + + @Mapping(target = "createdDate", ignore = true) + @Mapping(target = "modifiedDate", ignore = true) + @Mapping(target = "createdBy", ignore = true) + @Mapping(target = "modifiedBy", ignore = true) + @Mapping(target = "category", ignore = true) + ProductEntity menuItemToProductEntity(MenuItem menuItem); + + MenuItem productEntityToMenuItem(ProductEntity productEntity); + + List productEntitiesToMenuItems(List productEntities); +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/ReservationMapper.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/ReservationMapper.java new file mode 100644 index 0000000..db3d284 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/ReservationMapper.java @@ -0,0 +1,25 @@ +package com.dh7789dev.xpeditis.mapper; + +import com.dh7789dev.xpeditis.entity.ReservationEntity; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface ReservationMapper { + + ReservationMapper INSTANCE = Mappers.getMapper(ReservationMapper.class); + + @Mapping(target = "createdDate", ignore = true) + @Mapping(target = "modifiedDate", ignore = true) + @Mapping(target = "createdBy", ignore = true) + @Mapping(target = "modifiedBy", ignore = true) + ReservationEntity reservationToReservationEntity(Reservation reservation); + + Reservation reservationEntityToReservation(ReservationEntity reservationEntity); + + List reservationEntitiesToReservation(List reservationEntities); +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/SelectedDayMapper.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/SelectedDayMapper.java new file mode 100644 index 0000000..2542a65 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/SelectedDayMapper.java @@ -0,0 +1,25 @@ +package com.dh7789dev.xpeditis.mapper; + +import com.dh7789dev.xpeditis.entity.SelectedDayEntity; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface SelectedDayMapper { + + SelectedDayMapper INSTANCE = Mappers.getMapper(SelectedDayMapper.class); + + @Mapping(target = "createdDate", ignore = true) + @Mapping(target = "modifiedDate", ignore = true) + @Mapping(target = "createdBy", ignore = true) + @Mapping(target = "modifiedBy", ignore = true) + SelectedDayEntity selectedDayToSelectedDayEntity(SelectedDay selectedDay); + + SelectedDay selectedDayEntityToSelectedDay(SelectedDayEntity selectedDayEntity); + + List selectedDayEntitiesToSelectedDays(List selectedDayEntities); +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/AuthenticationJwtRepository.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/AuthenticationJwtRepository.java new file mode 100644 index 0000000..f936652 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/AuthenticationJwtRepository.java @@ -0,0 +1,75 @@ +package com.dh7789dev.xpeditis.repository; + +import com.dh7789dev.xpeditis.AuthenticationRepository; +import com.dh7789dev.xpeditis.dao.TokenDao; +import com.dh7789dev.xpeditis.dao.UserDao; +import com.dh7789dev.xpeditis.dto.AuthenticationRequest; +import com.dh7789dev.xpeditis.dto.AuthenticationResponse; +import com.dh7789dev.xpeditis.entity.TokenEntity; +import com.dh7789dev.xpeditis.entity.UserEntity; +import com.dh7789dev.xpeditis.util.JwtUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AuthenticationJwtRepository implements AuthenticationRepository { + + private final AuthenticationManager authenticationManager; + private final UserDao userDao; + private final TokenDao tokenDao; + private final JwtUtil jwtUtil; + + @Override + public AuthenticationResponse authenticate(AuthenticationRequest request) { + + log.info("username: {}, password: {}", request.getUsername(), request.getPassword()); + UsernamePasswordAuthenticationToken authToken = UsernamePasswordAuthenticationToken + .unauthenticated(request.getUsername(), request.getPassword()); + Authentication authentication = authenticationManager.authenticate(authToken); + + if (!authentication.isAuthenticated()) { + throw new UsernameNotFoundException("Failed to authenticate"); + } + + var userEntity = userDao.findByUsername(request.getUsername()).orElseThrow(); + + var jwtToken = jwtUtil.generateToken(userEntity); + var refreshToken = jwtUtil.generateRefreshToken(userEntity); + + revokeAllUserTokens(userEntity); + saveUserToken(userEntity, jwtToken); + + return new AuthenticationResponse() + .setAccessToken(jwtToken) + .setRefreshToken(refreshToken) + .setCreatedAt(jwtUtil.extractCreatedAt(jwtToken)) + .setExpiresAt(jwtUtil.extractExpiration(jwtToken)); + } + + private void saveUserToken(UserEntity user, String jwtToken) { + var tokenEntity = new TokenEntity() + .setUser(user) + .setToken(jwtToken) + .setTokenType(TokenEntity.Type.BEARER) + .setExpired(false) + .setRevoked(false); + tokenDao.save(tokenEntity); + } + + private void revokeAllUserTokens(UserEntity userEntity) { + var validUserTokens = tokenDao.findAllValidTokenByUserId(userEntity.getId()); + if (validUserTokens.isEmpty()) return; + validUserTokens.forEach(token -> { + token.setExpired(true); + token.setRevoked(true); + }); + tokenDao.saveAll(validUserTokens); + } +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/JavaMailSenderRepository.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/JavaMailSenderRepository.java new file mode 100644 index 0000000..ea2a997 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/JavaMailSenderRepository.java @@ -0,0 +1,44 @@ +package com.dh7789dev.xpeditis.repository; + +import com.dh7789dev.xpeditis.EmailSenderRepository; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Repository; + +@Slf4j +@Repository +public class JavaMailSenderRepository implements EmailSenderRepository { + + @Value("${application.email.from}") + String emailAddressFrom; + + private final JavaMailSender emailSender; + + @Autowired + public JavaMailSenderRepository(JavaMailSender emailSender) { + this.emailSender = emailSender; + } + + @Async + @Override + public void send(String emailAddressTo, String emailContent) { + try { + MimeMessage mimeMessage = emailSender.createMimeMessage(); + MimeMessageHelper helperMessage = new MimeMessageHelper(mimeMessage, "utf-8"); + helperMessage.setText(emailContent, true); + helperMessage.setTo(emailAddressTo); + helperMessage.setSubject("Reservation Confirmation"); + helperMessage.setFrom(emailAddressFrom); + emailSender.send(mimeMessage); + } catch (MessagingException e) { + log.error("failed to send email", e); + throw new IllegalStateException("failed to send email"); + } + } +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/NlsCatalogRepository.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/NlsCatalogRepository.java new file mode 100644 index 0000000..fcf9054 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/NlsCatalogRepository.java @@ -0,0 +1,46 @@ +package com.dh7789dev.xpeditis.repository; + +import com.dh7789dev.xpeditis.NlsRepository; +import com.dh7789dev.xpeditis.exception.NlsNotFoundException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.NoSuchMessageException; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Repository; + +@Slf4j +@Repository +public class NlsCatalogRepository implements NlsRepository { + + private final MessageSource messageSource; + + @Autowired + public NlsCatalogRepository(MessageSource messageSource) { + this.messageSource = messageSource; + } + + /** + * @param key nls message key + * @return the message corresponding to the key translated into the desired language + */ + @Override + public String getMessage(String key) { + return this.getMessage(key, null); + } + + /** + * @param key nls message key + * @param parameters parameters to use in the message + * @return the message corresponding to the key translated into the desired language + */ + @Override + public String getMessage(String key, Object[] parameters) { + try { + return messageSource.getMessage(key, parameters, LocaleContextHolder.getLocale()); + } catch (NoSuchMessageException e) { + log.warn(e.getMessage()); + throw new NlsNotFoundException(key, LocaleContextHolder.getLocale().toString()); + } + } +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/UserJpaRepository.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/UserJpaRepository.java new file mode 100644 index 0000000..5140f25 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/UserJpaRepository.java @@ -0,0 +1,45 @@ +package com.dh7789dev.xpeditis.repository; + +import com.dh7789dev.xpeditis.UserRepository; +import com.dh7789dev.xpeditis.dao.UserDao; +import com.dh7789dev.xpeditis.dto.ChangePasswordRequest; +import com.dh7789dev.xpeditis.entity.UserEntity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Repository; + +import java.security.Principal; + +@Repository +public class UserJpaRepository implements UserRepository { + + private final UserDao userDao; + + private final PasswordEncoder passwordEncoder; + + @Autowired + public UserJpaRepository(UserDao userDao, PasswordEncoder passwordEncoder) { + this.userDao = userDao; + this.passwordEncoder = passwordEncoder; + } + + @Override + public void changePassword(ChangePasswordRequest request, Principal connectedUser) { + + var userEntity = (UserEntity) ((UsernamePasswordAuthenticationToken) connectedUser).getPrincipal(); + + // check if the current password is correct + if (!passwordEncoder.matches(request.getCurrentPassword(), userEntity.getPassword())) { + throw new IllegalStateException("Wrong password"); + } + // check if the two new passwords are the same + if (!request.getNewPassword().equals(request.getConfirmationPassword())) { + throw new IllegalStateException("Password are not the same"); + } + + // update the password + userEntity.setPassword(passwordEncoder.encode(request.getNewPassword())); + userDao.save(userEntity); + } +} diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/util/JwtUtil.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/util/JwtUtil.java new file mode 100644 index 0000000..3afa46a --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/util/JwtUtil.java @@ -0,0 +1,96 @@ +package com.dh7789dev.xpeditis.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +@Component +public class JwtUtil { + + @Value("${application.security.jwt.secret-key}") + private String secretKey; + + @Value("${application.security.jwt.expiration}") + private long jwtExpiration; + + @Value("${application.security.jwt.refresh-token.expiration}") + private long refreshExpiration; + + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + private Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith(getSignInKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + public String generateToken(UserDetails userDetails) { + return generateToken(new HashMap<>(), userDetails); + } + + public String generateToken( + Map extraClaims, + UserDetails userDetails) { + return buildToken(extraClaims, userDetails, jwtExpiration); + } + + public String generateRefreshToken(UserDetails userDetails) { + return buildToken(new HashMap<>(), userDetails, refreshExpiration); + } + + private String buildToken( + Map extraClaims, + UserDetails userDetails, + long expiration) { + + return Jwts.builder() + .claims(extraClaims) + .subject(userDetails.getUsername()) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSignInKey()) + .compact(); + } + + public boolean isTokenValid(String token, UserDetails userDetails) { + final String username = extractUsername(token); + return (username.equals(userDetails.getUsername())) && !isTokenExpired(token); + } + + private boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + public Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + public Date extractCreatedAt(String token) throws JwtException { + return extractClaim(token, Claims::getIssuedAt); + } + + private SecretKey getSignInKey() { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + return Keys.hmacShaKeyFor(keyBytes); + } +} diff --git a/infrastructure/src/main/java/lombok.config b/infrastructure/src/main/java/lombok.config new file mode 100755 index 0000000..8f7e8aa --- /dev/null +++ b/infrastructure/src/main/java/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true \ No newline at end of file diff --git a/infrastructure/src/main/resources/db/migration/data/V1.1__INIT_DB_DATA.sql b/infrastructure/src/main/resources/db/migration/data/V1.1__INIT_DB_DATA.sql new file mode 100644 index 0000000..051d672 --- /dev/null +++ b/infrastructure/src/main/resources/db/migration/data/V1.1__INIT_DB_DATA.sql @@ -0,0 +1,19 @@ +INSERT INTO users (username, email, password, role) +VALUES ('leblr_user', 'contact@leblr.fr', '{bcrypt}$2y$10$.qkbukzzX21D.bqbI.B2R.tvWP90o/Y16QRWVLodw51BHft7ZWbc.', 'USER'), + ('leblr_admin', 'leblr.77590@gmail.com', '{bcrypt}$2y$10$kp1V7UYDEWn17WSK16UcmOnFd1mPFVF6UkLrOOCGtf24HOYt8p1iC', 'ADMIN'); + +INSERT INTO gallery (name, description) +VALUES ('gallery', ''); + +INSERT INTO menu_category (name) +VALUES ('STARTERS'), + ('DISHES'), + ('DESSERTS'), + ('PIZZAS'), + ('RED_WINES'), + ('WHITE_WINES'); + +INSERT INTO product (name, description, price, category_id) +VALUES ('Coconut Cake', 'Fresh Coconut', 4.90, 3), + ('Pasta', 'Fresh Pasta', 12.79, 2), + ('Foie Gras', 'Fresh Pasta', 12.79, 1); \ No newline at end of file diff --git a/infrastructure/src/main/resources/db/migration/structure/V1__INIT_DB_SCHEMA.sql b/infrastructure/src/main/resources/db/migration/structure/V1__INIT_DB_SCHEMA.sql new file mode 100644 index 0000000..bf176d0 --- /dev/null +++ b/infrastructure/src/main/resources/db/migration/structure/V1__INIT_DB_SCHEMA.sql @@ -0,0 +1,126 @@ +CREATE TABLE IF NOT EXISTS users ( + id BIGINT AUTO_INCREMENT NOT NULL, + created_date timestamp DEFAULT NOW() NOT NULL, + modified_date timestamp DEFAULT NOW() NOT NULL, + created_by VARCHAR(255) DEFAULT 'SYSTEM' NOT NULL, + modified_by VARCHAR(255) NULL, + username VARCHAR(50) NOT NULL, + first_name VARCHAR(50) NULL, + last_name VARCHAR(50) NULL, + email VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + role VARCHAR(255) NOT NULL, + enabled BOOLEAN DEFAULT TRUE NOT NULL, + CONSTRAINT pk_users PRIMARY KEY (id) +)ENGINE=InnoDB DEFAULT CHARSET=UTF8; +ALTER TABLE users ADD CONSTRAINT uc_users_username UNIQUE (username); +ALTER TABLE users ADD CONSTRAINT uc_users_email UNIQUE (email); + +CREATE TABLE IF NOT EXISTS event ( + id BIGINT AUTO_INCREMENT NOT NULL, + created_date timestamp DEFAULT NOW() NOT NULL, + modified_date timestamp DEFAULT NOW() NOT NULL, + created_by VARCHAR(255) DEFAULT 'SYSTEM' NOT NULL, + modified_by VARCHAR(255) NULL, + name VARCHAR(50) NOT NULL, + description VARCHAR(255) NULL, + date date NOT NULL, + CONSTRAINT pk_event PRIMARY KEY (id) +); +ALTER TABLE event ADD CONSTRAINT uc_event_name UNIQUE (name); + +CREATE TABLE IF NOT EXISTS gallery ( + id BIGINT AUTO_INCREMENT NOT NULL, + created_date timestamp DEFAULT NOW() NOT NULL, + modified_date timestamp DEFAULT NOW() NOT NULL, + created_by VARCHAR(255) DEFAULT 'SYSTEM' NOT NULL, + modified_by VARCHAR(255) NULL, + name VARCHAR(50) NOT NULL, + description VARCHAR(255) NULL, + CONSTRAINT pk_gallery PRIMARY KEY (id) +); +ALTER TABLE gallery ADD CONSTRAINT uc_gallery_name UNIQUE (name); + +CREATE TABLE IF NOT EXISTS image ( + id BIGINT AUTO_INCREMENT NOT NULL, + created_date timestamp DEFAULT NOW() NOT NULL, + modified_date timestamp DEFAULT NOW() NOT NULL, + created_by VARCHAR(255) DEFAULT 'SYSTEM' NOT NULL, + modified_by VARCHAR(255) NULL, + name VARCHAR(255) NOT NULL, + path VARCHAR(255) NOT NULL, + event_id BIGINT NULL, + gallery_id BIGINT NULL, + CONSTRAINT pk_image PRIMARY KEY (id) +); +ALTER TABLE image ADD CONSTRAINT uc_image_name UNIQUE (name); +ALTER TABLE image ADD CONSTRAINT uc_image_path UNIQUE (path); +ALTER TABLE image ADD CONSTRAINT FK_IMAGE_ON_EVENT FOREIGN KEY (event_id) REFERENCES event (id); +ALTER TABLE image ADD CONSTRAINT FK_IMAGE_ON_GALLERY FOREIGN KEY (gallery_id) REFERENCES gallery (id); +ALTER TABLE image ADD CONSTRAINT CHK_ONE_NOT_NULL CHECK (event_id IS NOT NULL OR gallery_id IS NOT NULL); + +CREATE TABLE IF NOT EXISTS menu_category ( + id BIGINT AUTO_INCREMENT NOT NULL, + created_date timestamp DEFAULT NOW() NOT NULL, + modified_date timestamp DEFAULT NOW() NOT NULL, + created_by VARCHAR(255) DEFAULT 'SYSTEM' NOT NULL, + modified_by VARCHAR(255) NULL, + name VARCHAR(255) NOT NULL, + CONSTRAINT pk_menu_category PRIMARY KEY (id) +); +ALTER TABLE menu_category ADD CONSTRAINT uc_menu_category_name UNIQUE (name); + +CREATE TABLE IF NOT EXISTS product ( + id BIGINT AUTO_INCREMENT NOT NULL, + created_date timestamp DEFAULT NOW() NOT NULL, + modified_date timestamp DEFAULT NOW() NOT NULL, + created_by VARCHAR(255) DEFAULT 'SYSTEM' NOT NULL, + modified_by VARCHAR(255) NULL, + name VARCHAR(255) NOT NULL, + description VARCHAR(255) NULL, + price DECIMAL(5, 2) NOT NULL, + category_id BIGINT NOT NULL, + CONSTRAINT pk_product PRIMARY KEY (id) +); +ALTER TABLE product ADD CONSTRAINT uc_product_name UNIQUE (name); +ALTER TABLE product ADD CONSTRAINT FK_PRODUCT_ON_CATEGORY FOREIGN KEY (category_id) REFERENCES menu_category (id); + +CREATE TABLE IF NOT EXISTS token ( + id BIGINT AUTO_INCREMENT NOT NULL, + created_date timestamp DEFAULT NOW() NOT NULL, + modified_date timestamp DEFAULT NOW() NOT NULL, + created_by VARCHAR(255) DEFAULT 'SYSTEM' NOT NULL, + modified_by VARCHAR(255) NULL, + token VARCHAR(255) NOT NULL, + token_type VARCHAR(50) NOT NULL, + revoked BOOLEAN DEFAULT TRUE NOT NULL, + expired BOOLEAN DEFAULT TRUE NOT NULL, + user_id BIGINT NOT NULL, + CONSTRAINT pk_token PRIMARY KEY (id) +); +ALTER TABLE token ADD CONSTRAINT uc_token_token UNIQUE (token); +ALTER TABLE token ADD CONSTRAINT FK_TOKEN_ON_USER FOREIGN KEY (user_id) REFERENCES users (id); + +CREATE TABLE IF NOT EXISTS reservation ( + id BIGINT AUTO_INCREMENT NOT NULL, + created_date timestamp DEFAULT NOW() NOT NULL, + modified_date timestamp DEFAULT NOW() NOT NULL, + created_by VARCHAR(255) DEFAULT 'SYSTEM' NOT NULL, + modified_by VARCHAR(255) NULL, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + phone VARCHAR(255) NULL, + nb_person INT NULL, + date timestamp NOT NULL, + CONSTRAINT pk_reservation PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS selected_day ( + id BIGINT AUTO_INCREMENT NOT NULL, + created_date timestamp DEFAULT NOW() NOT NULL, + modified_date timestamp DEFAULT NOW() NOT NULL, + created_by VARCHAR(255) DEFAULT 'SYSTEM' NOT NULL, + modified_by VARCHAR(255) NULL, + date date NOT NULL, + CONSTRAINT pk_selected_day PRIMARY KEY (id) +); \ No newline at end of file diff --git a/infrastructure/src/test/java/com/dh7789dev/xpeditis/dao/ReservationDaoTest.java b/infrastructure/src/test/java/com/dh7789dev/xpeditis/dao/ReservationDaoTest.java new file mode 100644 index 0000000..6034620 --- /dev/null +++ b/infrastructure/src/test/java/com/dh7789dev/xpeditis/dao/ReservationDaoTest.java @@ -0,0 +1,97 @@ +package com.dh7789dev.xpeditis.dao; + +import jakarta.persistence.EntityManager; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.ContextConfiguration; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@Disabled +@DataJpaTest +@ContextConfiguration(classes = ReservationDao.class) +class ReservationDaoTest { + + @Autowired + private ReservationJpaRepository reservationJpaRepository; + + @Autowired + private EntityManager entityManager; + + private LocalDateTime parseDate(String date) { + return LocalDateTime.parse(date); + } + + private Reservation getReservation( + Long id, + String customerName, + LocalDateTime date, + String email) { + + Reservation reservation = new Reservation(); + reservation.setId(id); + reservation.setName(customerName); + reservation.setDate(date); + reservation.setEmail(email); + reservation.setPhone("0123456"); + reservation.setNbPerson(2); + + return reservation; + } + + @BeforeEach + void setUp() { + LocalDateTime date = LocalDateTime.parse("2024-01-29T10:00:00"); + Reservation res1 = getReservation(1L, "Mathis", date, "user@gmail.com"); + reservationJpaRepository.save(res1); + entityManager.flush(); + entityManager.clear(); + + LocalDateTime date2 = LocalDateTime.parse("2024-01-30T15:30:00"); + Reservation res2 = getReservation(2L, "Xavier", date2, "user2@gmail.com"); + reservationJpaRepository.save(res2); + entityManager.flush(); + entityManager.clear(); + + LocalDateTime date3 = LocalDateTime.parse("2024-02-02T12:00:00"); + Reservation res3 = getReservation(3L, "David", date3, "elPatrone@gmail.com"); + reservationJpaRepository.save(res3); //out of date range + entityManager.flush(); + entityManager.clear(); + + // Verify entities were saved + assertNotNull(reservationJpaRepository.findById(1L).orElse(null), "Res1 should be saved"); + assertNotNull(reservationJpaRepository.findById(2L).orElse(null), "Res2 should be saved"); + assertNotNull(reservationJpaRepository.findById(3L).orElse(null), "Res3 should be saved"); + } + + @AfterEach + void tearDown() { + reservationJpaRepository.delete(reservationJpaRepository.findAll().get(0)); + reservationJpaRepository.delete(reservationJpaRepository.findAll().get(1)); + reservationJpaRepository.delete(reservationJpaRepository.findAll().get(2)); + } + + @Test + void givenIntervalDates_whenFindReservationByDates_ThenReturnReservation() { + + // ------ given --------- + LocalDateTime startDate = parseDate("2024-01-29T00:00:00"); + LocalDateTime endDate = parseDate("2024-02-01T00:00:00"); + + // ------ when --------- + List reservations = reservationJpaRepository.findReservationsByDates(startDate, endDate); + + // ------ then --------- + assertNotNull(reservations); + assertEquals(2, reservations.size(), "There should be 2 reservations"); + } +} \ No newline at end of file diff --git a/infrastructure/src/test/java/com/dh7789dev/xpeditis/repository/ReservationJpaRepositoryTest.java b/infrastructure/src/test/java/com/dh7789dev/xpeditis/repository/ReservationJpaRepositoryTest.java new file mode 100644 index 0000000..3e56db1 --- /dev/null +++ b/infrastructure/src/test/java/com/dh7789dev/xpeditis/repository/ReservationJpaRepositoryTest.java @@ -0,0 +1,67 @@ +package com.dh7789dev.xpeditis.repository; + +import com.dh7789dev.xpeditis.dao.ReservationDao; +import com.dh7789dev.xpeditis.entity.ReservationEntity; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ReservationJpaRepositoryTest { + + @Mock + ReservationDao reservationDao; + + @InjectMocks + ReservationJpaRepository reservationJpaRepository; + + private ReservationEntity getReservation( + Long id, + String customerName, + LocalDateTime date, + String email, + String phone, + int nbPerson) { + ReservationEntity reservation = new ReservationEntity(); + reservation.setId(id); + reservation.setName(customerName); + reservation.setDate(date); + reservation.setEmail(email); + reservation.setPhone(phone); + reservation.setNbPerson(nbPerson); + + return reservation; + } + + @Test + void givenCustomerName_whenFindByCustomerName_thenReturnCustomer() { + //-------- given ---------- + String customerName = "John Doe"; + + LocalDateTime date = LocalDateTime.parse("2024-01-29T10:00:00"); + System.out.println(date); + ReservationEntity reservation1 = getReservation(1L, customerName, date, "user@gmail.com", "0123456", 2); + + when(reservationDao.findByName(customerName)).thenReturn(List.of(reservation1)); + + //-------- when ------------ + List result = reservationJpaRepository.findByName(customerName); + + //-------- then ------------- + verify(reservationDao, times(1)).findByName(anyString()); + verify(reservationDao, times(1)).findByName(customerName); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals(reservation1.getId(), result.getFirst().getId()); + } +} \ No newline at end of file diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..19529dd --- /dev/null +++ b/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100755 index 0000000..249bdf3 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml new file mode 100755 index 0000000..d3d49fa --- /dev/null +++ b/pom.xml @@ -0,0 +1,107 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.4.1 + + + + com.dh7789dev + xpeditis + 0.0.1-SNAPSHOT + pom + xpeditis + Xpeditis project for Spring Boot + + + bootstrap + common + application + domain + infrastructure + + + + + + + + + + + + + + + + + + 23 + 23 + 23 + UTF-8 + UTF-8 + 1.18.36 + 1.6.3 + + + + + + org.projectlombok + lombok + ${org.projectlombok.version} + + + org.mapstruct + mapstruct + ${org.mapstruct.version} + + + org.assertj + assertj-core + 3.26.3 + test + + + jakarta.mail + jakarta.mail-api + 2.1.2 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + + + + + + src/main/resources + true + + + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + org.projectlombok + lombok + + + + + + + diff --git a/wait-for-it.sh b/wait-for-it.sh new file mode 100644 index 0000000..d990e0d --- /dev/null +++ b/wait-for-it.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi