diff --git a/api-gateway/src/main/java/com/hoangtien2k3/apigateway/ApiGatewayApplication.java b/api-gateway/src/main/java/com/hoangtien2k3/apigateway/ApiGatewayApplication.java index f16633ef..cac267bc 100644 --- a/api-gateway/src/main/java/com/hoangtien2k3/apigateway/ApiGatewayApplication.java +++ b/api-gateway/src/main/java/com/hoangtien2k3/apigateway/ApiGatewayApplication.java @@ -2,10 +2,8 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.netflix.eureka.EnableEurekaClient; @SpringBootApplication -@EnableEurekaClient public class ApiGatewayApplication { public static void main(String[] args) { diff --git a/build_output.txt b/build_output.txt new file mode 100644 index 00000000..4a801f4d Binary files /dev/null and b/build_output.txt differ diff --git a/common-lib/pom.xml b/common-lib/pom.xml index c5363121..57322a1f 100644 --- a/common-lib/pom.xml +++ b/common-lib/pom.xml @@ -46,11 +46,11 @@ org.springframework.kafka spring-kafka - - - - - + + com.opencsv + opencsv + 5.9 + diff --git a/discovery-service/src/main/java/com/hoangtien2k3/discoveryserver/DiscoveryServerApplication.java b/discovery-service/src/main/java/com/hoangtien2k3/discoveryserver/DiscoveryServerApplication.java index 621606ce..2592cf6d 100644 --- a/discovery-service/src/main/java/com/hoangtien2k3/discoveryserver/DiscoveryServerApplication.java +++ b/discovery-service/src/main/java/com/hoangtien2k3/discoveryserver/DiscoveryServerApplication.java @@ -2,12 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; import java.util.TimeZone; @SpringBootApplication -@EnableEurekaServer public class DiscoveryServerApplication { public static void main(String[] args) { diff --git a/discovery-service/src/main/resources/application.properties b/discovery-service/src/main/resources/application.properties index 134aab63..23df2c97 100644 --- a/discovery-service/src/main/resources/application.properties +++ b/discovery-service/src/main/resources/application.properties @@ -1,11 +1,10 @@ -# default port for eureka server -server.port=8761 +# default port for consul server +server.port=8500 spring.application.name=DISCOVERY-SERVICE -eureka.instance.hostname=localhost -eureka.client.register-with-eureka=false -eureka.client.fetch-registry=false +spring.cloud.consul.host=localhost +spring.cloud.consul.port=8500 -# Eureka server url -eureka.client.serviceUrl.defaultZone=${EUREKA_URI:http://localhost:8761/eureka} +spring.cloud.consul.discovery.register=true +spring.cloud.consul.discovery.health-check-interval=10s diff --git a/favourite-service/src/main/java/com/hoangtien2k3qx1/favouriteservice/FavouriteServiceApplication.java b/favourite-service/src/main/java/com/hoangtien2k3qx1/favouriteservice/FavouriteServiceApplication.java index 24ab1e61..d2ea11c6 100644 --- a/favourite-service/src/main/java/com/hoangtien2k3qx1/favouriteservice/FavouriteServiceApplication.java +++ b/favourite-service/src/main/java/com/hoangtien2k3qx1/favouriteservice/FavouriteServiceApplication.java @@ -2,12 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import java.util.TimeZone; @SpringBootApplication -@EnableEurekaClient public class FavouriteServiceApplication { public static void main(String[] args) { diff --git a/favourite-service/src/main/java/com/hoangtien2k3qx1/favouriteservice/config/Swagger2Config.java b/favourite-service/src/main/java/com/hoangtien2k3qx1/favouriteservice/config/Swagger2Config.java index 68a8dad3..bdea5265 100644 --- a/favourite-service/src/main/java/com/hoangtien2k3qx1/favouriteservice/config/Swagger2Config.java +++ b/favourite-service/src/main/java/com/hoangtien2k3qx1/favouriteservice/config/Swagger2Config.java @@ -1,36 +1,69 @@ package com.hoangtien2k3qx1.favouriteservice.config; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import springfox.documentation.builders.ApiInfoBuilder; -import springfox.documentation.builders.PathSelectors; -import springfox.documentation.builders.RequestHandlerSelectors; -import springfox.documentation.service.ApiInfo; -import springfox.documentation.service.Contact; -import springfox.documentation.spi.DocumentationType; -import springfox.documentation.spring.web.plugins.Docket; + +import java.util.List; @Configuration -@EnableWebMvc public class Swagger2Config { + + @Value("${server.port:8080}") + private String serverPort; + + @Value("${spring.application.name:favourite-service}") + private String applicationName; + @Bean - public Docket api() { - return new Docket(DocumentationType.SWAGGER_2).select() - .apis(RequestHandlerSelectors.any()) - .paths(PathSelectors.any()) - .build() - .apiInfo(apiEndPointsInfo()); + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(apiInfo()) + .servers(getServers()); } - private ApiInfo apiEndPointsInfo() { - return new ApiInfoBuilder() - .title("FAVORITE-SERVICE API") - .description("API Documentation's FAVORITE-SERVICE") - .contact(new Contact("hoangtien2k3", "https://hoangtien2k3qx1.github.io/", "hoangtien2k3qx1@gmail.com")) - .license("Apache 2.0") - .licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html") + private Info apiInfo() { + return new Info() + .title("FAVORITE SERVICE API") + .description(""" + ## Favorite Service API Documentation + + This service handles all favorite-related operations including: + - Managing user favorites + - Tracking favorite items + - Favorite lists and collections + + ### Features + - Add/remove items to favorites + - Organize favorites into lists + - Share favorite collections + - Integration with product and user services + """) .version("1.0.0") - .build(); + .contact(new Contact() + .name("hoangtien2k3") + .url("https://hoangtien2k3qx1.github.io/") + .email("hoangtien2k3qx1@gmail.com")) + .license(new License() + .name("Apache 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0.html")); + } + + private List getServers() { + Server localServer = new Server() + .url("http://localhost:" + serverPort) + .description("Local development server"); + + Server productionServer = new Server() + .url("https://api.yourdomain.com/favourite-service") + .description("Production server"); + + return List.of(localServer, productionServer); } -} +} \ No newline at end of file diff --git a/inventory-service/src/main/java/com/hoangtien2k3/inventoryservice/InventoryServiceApplication.java b/inventory-service/src/main/java/com/hoangtien2k3/inventoryservice/InventoryServiceApplication.java index d02f755f..4cbc8e09 100644 --- a/inventory-service/src/main/java/com/hoangtien2k3/inventoryservice/InventoryServiceApplication.java +++ b/inventory-service/src/main/java/com/hoangtien2k3/inventoryservice/InventoryServiceApplication.java @@ -2,10 +2,8 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.netflix.eureka.EnableEurekaClient; @SpringBootApplication -@EnableEurekaClient public class InventoryServiceApplication { public static void main(String[] args) { diff --git a/inventory-service/src/main/java/com/hoangtien2k3/inventoryservice/config/OpenApiConfig.java b/inventory-service/src/main/java/com/hoangtien2k3/inventoryservice/config/OpenApiConfig.java new file mode 100644 index 00000000..3c4ca291 --- /dev/null +++ b/inventory-service/src/main/java/com/hoangtien2k3/inventoryservice/config/OpenApiConfig.java @@ -0,0 +1,70 @@ +package com.hoangtien2k3.inventoryservice.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +public class OpenApiConfig { + + @Value("${server.port:8080}") + private String serverPort; + + @Value("${spring.application.name:inventory-service}") + private String applicationName; + + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(apiInfo()) + .servers(getServers()); + } + + private Info apiInfo() { + return new Info() + .title("INVENTORY SERVICE API") + .description(""" + ## Inventory Service API Documentation + + This service handles all inventory-related operations including: + - Product inventory management + - Stock level tracking + - Inventory updates and synchronization + - Product availability checks + + ### Features + - Real-time inventory tracking + - Multi-warehouse support + - Stock alerts and notifications + - Integration with order and product services + """) + .version("1.0.0") + .contact(new Contact() + .name("hoangtien2k3") + .url("https://hoangtien2k3qx1.github.io/") + .email("hoangtien2k3qx1@gmail.com")) + .license(new License() + .name("Apache 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0.html")); + } + + private List getServers() { + Server localServer = new Server() + .url("http://localhost:" + serverPort) + .description("Local development server"); + + Server productionServer = new Server() + .url("https://api.yourdomain.com/inventory-service") + .description("Production server"); + + return List.of(localServer, productionServer); + } +} \ No newline at end of file diff --git a/inventory-service/src/main/java/com/hoangtien2k3/inventoryservice/config/Swagger2Config.java b/inventory-service/src/main/java/com/hoangtien2k3/inventoryservice/config/Swagger2Config.java deleted file mode 100644 index 7b3b5d3c..00000000 --- a/inventory-service/src/main/java/com/hoangtien2k3/inventoryservice/config/Swagger2Config.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.hoangtien2k3.inventoryservice.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import springfox.documentation.builders.ApiInfoBuilder; -import springfox.documentation.builders.PathSelectors; -import springfox.documentation.builders.RequestHandlerSelectors; -import springfox.documentation.service.ApiInfo; -import springfox.documentation.service.Contact; -import springfox.documentation.spi.DocumentationType; -import springfox.documentation.spring.web.plugins.Docket; - -@Configuration -@EnableWebMvc -public class Swagger2Config { - @Bean - public Docket api() { - return new Docket(DocumentationType.SWAGGER_2).select() - .apis(RequestHandlerSelectors.any()) - .paths(PathSelectors.any()) - .build() - .apiInfo(apiEndPointsInfo()); - } - - private ApiInfo apiEndPointsInfo() { - return new ApiInfoBuilder() - .title("INVENTORY-SERVICE API") - .description("API Documentation's INVENTORY-SERVICE") - .contact(new Contact("hoangtien2k3", "https://hoangtien2k3qx1.github.io/", "hoangtien2k3qx1@gmail.com")) - .license("Apache 2.0") - .licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html") - .version("1.0.0") - .build(); - } -} diff --git a/media-service/src/main/java/com/hoangtien2k3/media/config/Hoangtien2k3Config.java b/media-service/src/main/java/com/hoangtien2k3/media/config/Hoangtien2k3Config.java new file mode 100644 index 00000000..b42cb9d1 --- /dev/null +++ b/media-service/src/main/java/com/hoangtien2k3/media/config/Hoangtien2k3Config.java @@ -0,0 +1,15 @@ +package com.hoangtien2k3.media.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class Hoangtien2k3Config { + + @Value("${app.public-url:http://localhost:8080}") + private String publicUrl; + + public String publicUrl() { + return publicUrl; + } +} \ No newline at end of file diff --git a/media-service/src/main/java/com/hoangtien2k3/media/service/MediaServiceImpl.java b/media-service/src/main/java/com/hoangtien2k3/media/service/MediaServiceImpl.java index d5414209..0c9b1b4e 100644 --- a/media-service/src/main/java/com/hoangtien2k3/media/service/MediaServiceImpl.java +++ b/media-service/src/main/java/com/hoangtien2k3/media/service/MediaServiceImpl.java @@ -16,8 +16,10 @@ import lombok.SneakyThrows; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; +import com.hoangtien2k3.media.config.Hoangtien2k3Config; import org.springframework.web.util.UriComponentsBuilder; + @RequiredArgsConstructor @Service public class MediaServiceImpl implements MediaService { @@ -25,6 +27,7 @@ public class MediaServiceImpl implements MediaService { private final MediaVmMapper mediaVmMapper; private final MediaRepository mediaRepository; private final FileSystemRepository fileSystemRepository; + private final Hoangtien2k3Config hoangtien2k3Config; // Added this @Override @SneakyThrows diff --git a/notification-service/pom.xml b/notification-service/pom.xml index 21b47180..d1f7bc75 100644 --- a/notification-service/pom.xml +++ b/notification-service/pom.xml @@ -30,6 +30,15 @@ springfox-swagger2 3.0.0 + + org.springframework.boot + spring-boot-starter-mail + + + com.sun.mail + jakarta.mail + 2.0.1 + io.springfox springfox-swagger-ui @@ -64,6 +73,11 @@ spring-boot-starter-mail 2.5.6 + + org.eclipse.angus + angus-mail + 2.0.2 + org.springframework spring-core diff --git a/notification-service/src/main/java/com/hoangtien2k3/notificationservice/service/impl/EmailServiceImpl.java b/notification-service/src/main/java/com/hoangtien2k3/notificationservice/service/impl/EmailServiceImpl.java index 8fc84139..23e8d097 100644 --- a/notification-service/src/main/java/com/hoangtien2k3/notificationservice/service/impl/EmailServiceImpl.java +++ b/notification-service/src/main/java/com/hoangtien2k3/notificationservice/service/impl/EmailServiceImpl.java @@ -2,6 +2,8 @@ import com.hoangtien2k3.notificationservice.dto.EmailDetails; import com.hoangtien2k3.notificationservice.service.EmailService; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -14,8 +16,6 @@ import org.springframework.web.multipart.MultipartFile; import reactor.core.publisher.Mono; -import javax.mail.MessagingException; -import javax.mail.internet.MimeMessage; import java.io.File; import java.util.Arrays; import java.util.Objects; @@ -65,7 +65,7 @@ public Mono sendMailWithAttachment(EmailDetails details) { javaMailSender.send(mimeMessage); return "Mail Sent Successfully"; }) - .onErrorResume(MessagingException.class, ex -> { + .onErrorResume(ex -> { log.error("Error while sending mail with attachment", ex); return Mono.just("Error while Sending Mail with Attachment"); }); @@ -96,5 +96,4 @@ public Mono sendMail(MultipartFile[] files, String to, String[] cc, Stri return Mono.just("Error while Sending Email"); }); } - -} +} \ No newline at end of file diff --git a/order-service/src/main/java/com/hoangtien2k3/orderservice/OrderServiceApplication.java b/order-service/src/main/java/com/hoangtien2k3/orderservice/OrderServiceApplication.java index 7bf0f550..e2e2cc84 100644 --- a/order-service/src/main/java/com/hoangtien2k3/orderservice/OrderServiceApplication.java +++ b/order-service/src/main/java/com/hoangtien2k3/orderservice/OrderServiceApplication.java @@ -2,10 +2,8 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.netflix.eureka.EnableEurekaClient; @SpringBootApplication -@EnableEurekaClient public class OrderServiceApplication { public static void main(String[] args) { diff --git a/order-service/src/main/java/com/hoangtien2k3/orderservice/api/CartController.java b/order-service/src/main/java/com/hoangtien2k3/orderservice/api/CartController.java index f5962dc2..d1aebb99 100644 --- a/order-service/src/main/java/com/hoangtien2k3/orderservice/api/CartController.java +++ b/order-service/src/main/java/com/hoangtien2k3/orderservice/api/CartController.java @@ -3,10 +3,12 @@ import com.hoangtien2k3.orderservice.dto.order.CartDto; import com.hoangtien2k3.orderservice.service.CartService; import com.hoangtien2k3.orderservice.service.CallAPI; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; @@ -18,7 +20,7 @@ import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Mono; -import javax.validation.Valid; +import jakarta.validation.Valid; import java.util.Collections; import java.util.List; @@ -26,16 +28,16 @@ @RequiredArgsConstructor @RestController @RequestMapping("/api/carts") -@Tag(name = "CartController", description = "Operations related to carts") +@Tag(name = "Cart Controller", description = "Cart management APIs") public class CartController { private final CartService cartService; private final CallAPI testCallApi; - @ApiOperation(value = "Get all carts", notes = "Retrieve a list of all carts.") + @Operation(summary = "Get all carts", description = "Retrieve a list of all carts.") @ApiResponses({ - @ApiResponse(code = 200, message = "Carts retrieved successfully", response = List.class), - @ApiResponse(code = 204, message = "No content", response = ResponseEntity.class) + @ApiResponse(responseCode = "200", description = "Carts retrieved successfully"), + @ApiResponse(responseCode = "204", description = "No content") }) @GetMapping @PreAuthorize("hasAuthority('ADMIN') or hasAuthority('USER')") @@ -46,10 +48,10 @@ public Mono>> findAll() { .defaultIfEmpty(ResponseEntity.ok(Collections.emptyList())); } - @ApiOperation(value = "Get all carts with paging", notes = "Retrieve a paginated list of all carts.") + @Operation(summary = "Get all carts with paging", description = "Retrieve a paginated list of all carts.") @ApiResponses({ - @ApiResponse(code = 200, message = "Carts retrieved successfully", response = Page.class), - @ApiResponse(code = 204, message = "No content", response = ResponseEntity.class) + @ApiResponse(responseCode = "200", description = "Carts retrieved successfully"), + @ApiResponse(responseCode = "204", description = "No content") }) @GetMapping("/all") @PreAuthorize("hasAuthority('ADMIN') or hasAuthority('USER')") @@ -62,10 +64,10 @@ public Mono>> findAll(@RequestParam(defaultValue = .defaultIfEmpty(ResponseEntity.noContent().build()); } - @ApiOperation(value = "Get cart by ID", notes = "Retrieve cart information based on the provided ID.") + @Operation(summary = "Get cart by ID", description = "Retrieve cart information based on the provided ID.") @ApiResponses({ - @ApiResponse(code = 200, message = "Cart retrieved successfully", response = CartDto.class), - @ApiResponse(code = 404, message = "Cart not found", response = ResponseEntity.class) + @ApiResponse(responseCode = "200", description = "Cart retrieved successfully"), + @ApiResponse(responseCode = "404", description = "Cart not found") }) @GetMapping("/{cartId}") @PreAuthorize("hasAuthority('USER') or hasAuthority('ADMIN')") @@ -76,11 +78,11 @@ public ResponseEntity> findById(@PathVariable("cartId") return ResponseEntity.ok(this.cartService.findById(Integer.parseInt(cartId))); } - - @ApiOperation(value = "Save cart", notes = "Save a new cart.") + @Operation(summary = "Save cart", description = "Save a new cart.") @ApiResponses({ - @ApiResponse(code = 200, message = "Cart saved successfully", response = CartDto.class), - @ApiResponse(code = 500, message = "Internal Server Error", response = ResponseEntity.class) + @ApiResponse(responseCode = "201", description = "Cart created successfully"), + @ApiResponse(responseCode = "400", description = "Bad request"), + @ApiResponse(responseCode = "500", description = "Internal Server Error") }) @PostMapping @PreAuthorize("hasAuthority('USER')") @@ -89,14 +91,15 @@ public Mono> save(@RequestBody @Valid final CartDto cartDto) { log.info("*** CartDto, resource; save cart *"); return cartService.save(cartDto) - .map(ResponseEntity::ok) + .map(cart -> ResponseEntity.status(HttpStatus.CREATED).body(cart)) .defaultIfEmpty(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()); } - @ApiOperation(value = "Update cart", notes = "Update an existing cart.") + @Operation(summary = "Update cart", description = "Update an existing cart.") @ApiResponses({ - @ApiResponse(code = 200, message = "Cart updated successfully", response = CartDto.class), - @ApiResponse(code = 404, message = "Cart not found", response = ResponseEntity.class) + @ApiResponse(responseCode = "200", description = "Cart updated successfully"), + @ApiResponse(responseCode = "404", description = "Cart not found"), + @ApiResponse(responseCode = "400", description = "Bad request") }) @PutMapping @PreAuthorize("hasAuthority('ADMIN')") @@ -109,10 +112,11 @@ public Mono> update(@RequestBody .defaultIfEmpty(ResponseEntity.notFound().build()); } - @ApiOperation(value = "Update cart by ID", notes = "Update an existing cart based on the provided ID.") + @Operation(summary = "Update cart by ID", description = "Update an existing cart based on the provided ID.") @ApiResponses({ - @ApiResponse(code = 200, message = "Cart updated successfully", response = CartDto.class), - @ApiResponse(code = 404, message = "Cart not found", response = ResponseEntity.class) + @ApiResponse(responseCode = "200", description = "Cart updated successfully"), + @ApiResponse(responseCode = "404", description = "Cart not found"), + @ApiResponse(responseCode = "400", description = "Bad request") }) @PutMapping("/{cartId}") @PreAuthorize("hasAuthority('USER')") @@ -128,10 +132,10 @@ public Mono> update(@PathVariable("cartId") .defaultIfEmpty(ResponseEntity.notFound().build()); } - @ApiOperation(value = "Delete cart by ID", notes = "Delete a cart based on the provided ID.") + @Operation(summary = "Delete cart by ID", description = "Delete a cart based on the provided ID.") @ApiResponses({ - @ApiResponse(code = 200, message = "Cart deleted successfully", response = Boolean.class), - @ApiResponse(code = 404, message = "Cart not found", response = ResponseEntity.class) + @ApiResponse(responseCode = "200", description = "Cart deleted successfully"), + @ApiResponse(responseCode = "404", description = "Cart not found") }) @DeleteMapping("/{cartId}") @PreAuthorize("hasAuthority('USER')") @@ -141,5 +145,4 @@ public Mono> deleteById(@PathVariable("cartId") final St .thenReturn(ResponseEntity.ok(true)) .defaultIfEmpty(ResponseEntity.status(HttpStatus.NOT_FOUND).body(false)); } - -} +} \ No newline at end of file diff --git a/order-service/src/main/java/com/hoangtien2k3/orderservice/api/OrderController.java b/order-service/src/main/java/com/hoangtien2k3/orderservice/api/OrderController.java index 790e5bd1..ade69880 100644 --- a/order-service/src/main/java/com/hoangtien2k3/orderservice/api/OrderController.java +++ b/order-service/src/main/java/com/hoangtien2k3/orderservice/api/OrderController.java @@ -2,12 +2,12 @@ import com.hoangtien2k3.orderservice.dto.order.OrderDto; import com.hoangtien2k3.orderservice.service.OrderService; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -15,9 +15,9 @@ import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Mono; -import javax.validation.Valid; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.util.Collections; import java.util.List; @@ -25,15 +25,15 @@ @RequiredArgsConstructor @RestController @RequestMapping("/api/orders") -@Tag(name = "OrderController", description = "Operations related to orders") +@Tag(name = "Order Controller", description = "Order management APIs") public class OrderController { private final OrderService orderService; - @ApiOperation(value = "Get all orders", notes = "Retrieve a list of all orders.") + @Operation(summary = "Get all orders", description = "Retrieve a list of all orders.") @ApiResponses({ - @ApiResponse(code = 200, message = "Orders retrieved successfully", response = List.class), - @ApiResponse(code = 204, message = "No content", response = ResponseEntity.class) + @ApiResponse(responseCode = "200", description = "Orders retrieved successfully"), + @ApiResponse(responseCode = "204", description = "No content") }) @GetMapping @PreAuthorize("hasAuthority('ADMIN') or hasAuthority('USER')") @@ -44,10 +44,10 @@ public Mono>> findAll() { .defaultIfEmpty(ResponseEntity.ok(Collections.emptyList())); } - @ApiOperation(value = "Get all orders with paging", notes = "Retrieve a paginated list of all orders.") + @Operation(summary = "Get all orders with paging", description = "Retrieve a paginated list of all orders.") @ApiResponses({ - @ApiResponse(code = 200, message = "Orders retrieved successfully", response = Page.class), - @ApiResponse(code = 204, message = "No content", response = ResponseEntity.class) + @ApiResponse(responseCode = "200", description = "Orders retrieved successfully"), + @ApiResponse(responseCode = "204", description = "No content") }) @GetMapping("/all") @PreAuthorize("hasAuthority('ADMIN') or hasAuthority('USER')") @@ -60,10 +60,10 @@ public Mono>> findAll(@RequestParam(defaultValue = .defaultIfEmpty(ResponseEntity.noContent().build()); } - @ApiOperation(value = "Get order by ID", notes = "Retrieve order information based on the provided ID.") + @Operation(summary = "Get order by ID", description = "Retrieve order information based on the provided ID.") @ApiResponses({ - @ApiResponse(code = 200, message = "Order retrieved successfully", response = OrderDto.class), - @ApiResponse(code = 404, message = "Order not found", response = ResponseEntity.class) + @ApiResponse(responseCode = "200", description = "Order retrieved successfully"), + @ApiResponse(responseCode = "404", description = "Order not found") }) @GetMapping("/{orderId}") @PreAuthorize("hasAuthority('ADMIN') or hasAuthority('USER')") @@ -74,10 +74,11 @@ public ResponseEntity> findById(@PathVariable("orderId") return ResponseEntity.ok(orderService.findById(Integer.parseInt(orderId))); } - @ApiOperation(value = "Save order", notes = "Save a new order.") + @Operation(summary = "Save order", description = "Save a new order.") @ApiResponses({ - @ApiResponse(code = 200, message = "Order saved successfully", response = OrderDto.class), - @ApiResponse(code = 500, message = "Internal Server Error", response = ResponseEntity.class) + @ApiResponse(responseCode = "201", description = "Order created successfully"), + @ApiResponse(responseCode = "400", description = "Bad request"), + @ApiResponse(responseCode = "500", description = "Internal Server Error") }) @PostMapping @PreAuthorize("hasAuthority('USER')") @@ -86,14 +87,15 @@ public Mono> save(@RequestBody @Valid final OrderDto orderDto) { log.info("*** OrderDto, resource; save order *"); return orderService.save(orderDto) - .map(ResponseEntity::ok) + .map(order -> ResponseEntity.status(HttpStatus.CREATED).body(order)) .defaultIfEmpty(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()); } - @ApiOperation(value = "Update order", notes = "Update an existing order.") + @Operation(summary = "Update order", description = "Update an existing order.") @ApiResponses({ - @ApiResponse(code = 200, message = "Order updated successfully", response = OrderDto.class), - @ApiResponse(code = 404, message = "Order not found", response = ResponseEntity.class) + @ApiResponse(responseCode = "200", description = "Order updated successfully"), + @ApiResponse(responseCode = "404", description = "Order not found"), + @ApiResponse(responseCode = "400", description = "Bad request") }) @PutMapping @PreAuthorize("hasAuthority('ADMIN')") @@ -106,10 +108,11 @@ public Mono> update(@RequestBody .defaultIfEmpty(ResponseEntity.notFound().build()); } - @ApiOperation(value = "Update order by ID", notes = "Update an existing order based on the provided ID.") + @Operation(summary = "Update order by ID", description = "Update an existing order based on the provided ID.") @ApiResponses({ - @ApiResponse(code = 200, message = "Order updated successfully", response = OrderDto.class), - @ApiResponse(code = 404, message = "Order not found", response = ResponseEntity.class) + @ApiResponse(responseCode = "200", description = "Order updated successfully"), + @ApiResponse(responseCode = "404", description = "Order not found"), + @ApiResponse(responseCode = "400", description = "Bad request") }) @PutMapping("/{orderId}") @PreAuthorize("hasAuthority('USER')") @@ -125,25 +128,23 @@ public Mono> update(@PathVariable("orderId") .defaultIfEmpty(ResponseEntity.notFound().build()); } - @ApiOperation(value = "Delete order by ID", notes = "Delete an order based on the provided ID.") + @Operation(summary = "Delete order by ID", description = "Delete an order based on the provided ID.") @ApiResponses({ - @ApiResponse(code = 200, message = "Order deleted successfully", response = Boolean.class), - @ApiResponse(code = 404, message = "Order not found", response = ResponseEntity.class) + @ApiResponse(responseCode = "200", description = "Order deleted successfully"), + @ApiResponse(responseCode = "404", description = "Order not found") }) @DeleteMapping("/{orderId}") @PreAuthorize("hasAuthority('USER') or hasAuthority('ADMIN')") public Mono> deleteById(@PathVariable("orderId") final String orderId) { log.info("*** Boolean, resource; delete order by id *"); - this.orderService.deleteById(Integer.parseInt(orderId)); return orderService.deleteById(Integer.parseInt(orderId)) .thenReturn(ResponseEntity.ok(true)) .defaultIfEmpty(ResponseEntity.status(HttpStatus.NOT_FOUND).body(false)); } - + @Operation(summary = "Check if order exists", description = "Check if an order exists by ID.") @GetMapping("/existOrderId") - public Boolean existsByOrderId(Integer orderId) { - return orderService.existsByOrderId(orderId); + public Mono existsByOrderId(@RequestParam Integer orderId) { + return Mono.just(orderService.existsByOrderId(orderId)); } - -} +} \ No newline at end of file diff --git a/order-service/src/main/java/com/hoangtien2k3/orderservice/config/swagger/OpenApiConfig.java b/order-service/src/main/java/com/hoangtien2k3/orderservice/config/swagger/OpenApiConfig.java new file mode 100644 index 00000000..ccdbddf1 --- /dev/null +++ b/order-service/src/main/java/com/hoangtien2k3/orderservice/config/swagger/OpenApiConfig.java @@ -0,0 +1,84 @@ +package com.hoangtien2k3.orderservice.config.swagger; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +public class OpenApiConfig { + + @Value("${server.port:8080}") + private String serverPort; + + @Value("${spring.application.name:order-service}") + private String applicationName; + + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(apiInfo()) + .addSecurityItem(new SecurityRequirement().addList("Bearer Authentication")) + .components(new Components() + .addSecuritySchemes("Bearer Authentication", createBearerSecurityScheme())) + .servers(getServers()); + } + + private Info apiInfo() { + return new Info() + .title("ORDER SERVICE API") + .description(""" + ## Order Service API Documentation + + This service handles all order-related operations including: + - Order creation and management + - Order tracking and status updates + - Integration with cart and payment services + - Order history and reporting + + ### Authentication + All endpoints require JWT Bearer token authentication. + Include the token in the Authorization header as: `Bearer {token}` + """) + .version("1.0.0") + .contact(new Contact() + .name("hoangtien2k3") + .url("https://hoangtien2k3qx1.github.io/") + .email("hoangtien2k3qx1@gmail.com")) + .license(new License() + .name("MIT License") + .url("https://mit-license.org/")) + .termsOfService("https://hoangtien2k3qx1.github.io/terms"); + } + + private SecurityScheme createBearerSecurityScheme() { + return new SecurityScheme() + .name("Bearer Authentication") + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .description("Enter your JWT token in the format: Bearer {token}") + .in(SecurityScheme.In.HEADER); + } + + private List getServers() { + Server localServer = new Server() + .url("http://localhost:" + serverPort) + .description("Local development server"); + + Server productionServer = new Server() + .url("https://api.yourdomain.com/order-service") + .description("Production server"); + + return List.of(localServer, productionServer); + } +} \ No newline at end of file diff --git a/order-service/src/main/java/com/hoangtien2k3/orderservice/config/swagger/Swagger2Config.java b/order-service/src/main/java/com/hoangtien2k3/orderservice/config/swagger/Swagger2Config.java deleted file mode 100644 index 23e9732e..00000000 --- a/order-service/src/main/java/com/hoangtien2k3/orderservice/config/swagger/Swagger2Config.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.hoangtien2k3.orderservice.config.swagger; - -import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; -import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; -import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType; -import org.springframework.boot.actuate.endpoint.ExposableEndpoint; -import org.springframework.boot.actuate.endpoint.web.*; -import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier; -import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier; -import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.env.Environment; -import org.springframework.util.StringUtils; -import springfox.documentation.builders.ApiInfoBuilder; -import springfox.documentation.builders.PathSelectors; -import springfox.documentation.builders.RequestHandlerSelectors; -import springfox.documentation.service.*; -import springfox.documentation.spi.DocumentationType; -import springfox.documentation.spi.service.contexts.SecurityContext; -import springfox.documentation.spring.web.plugins.Docket; -import springfox.documentation.swagger2.annotations.EnableSwagger2; - -import java.util.*; - -@Configuration -@EnableSwagger2 -public class Swagger2Config { - @Bean - public Docket api() { - return new Docket(DocumentationType.SWAGGER_2) - .select() - .apis(RequestHandlerSelectors.any()) - .paths(PathSelectors.any()) - .build() - .apiInfo(apiEndPointsInfo()) - .securitySchemes(List.of(apiKey())) - .securityContexts(Collections.singletonList(securityContext())); - } - - private ApiInfo apiEndPointsInfo() { - return new ApiInfoBuilder() - .title("ORDER-SERVICE API") - .description("API Documentation's ORDER-SERVICE") - .contact(new Contact("hoangtien2k3", "https://hoangtien2k3qx1.github.io/", "hoangtien2k3qx1@gmail.com")) - .license("MIT") - .licenseUrl("https://mit-license.org/") - .version("1.0.0") - .build(); - } - - private ApiKey apiKey() { - return new ApiKey("JWT", "Authorization", "header"); - } - - private SecurityContext securityContext() { - return SecurityContext.builder() - .securityReferences(defaultAuth()) - .build(); - } - - private List defaultAuth() { - AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything"); - SecurityReference reference = new SecurityReference("JWT", new AuthorizationScope[]{authorizationScope}); - return List.of(reference); - } - - @Bean - public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier, - ServletEndpointsSupplier servletEndpointsSupplier, ControllerEndpointsSupplier controllerEndpointsSupplier, - EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties, - WebEndpointProperties webEndpointProperties, Environment environment) { - List> allEndpoints = new ArrayList<>(); - Collection webEndpoints = webEndpointsSupplier.getEndpoints(); - allEndpoints.addAll(webEndpoints); - allEndpoints.addAll(servletEndpointsSupplier.getEndpoints()); - allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints()); - String basePath = webEndpointProperties.getBasePath(); - EndpointMapping endpointMapping = new EndpointMapping(basePath); - boolean shouldRegisterLinksMapping = this.shouldRegisterLinksMapping(webEndpointProperties, environment, - basePath); - return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes, - corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath), - shouldRegisterLinksMapping, null); - } - - private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment, - String basePath) { - return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath) - || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT)); - } - -} \ No newline at end of file diff --git a/order-service/src/main/java/com/hoangtien2k3/orderservice/config/web/WebSecurityConfig.java b/order-service/src/main/java/com/hoangtien2k3/orderservice/config/web/WebSecurityConfig.java index 277bebc7..3812e2c5 100644 --- a/order-service/src/main/java/com/hoangtien2k3/orderservice/config/web/WebSecurityConfig.java +++ b/order-service/src/main/java/com/hoangtien2k3/orderservice/config/web/WebSecurityConfig.java @@ -6,18 +6,18 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 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.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity -@EnableGlobalMethodSecurity(prePostEnabled = true) -public class WebSecurityConfig extends WebSecurityConfigurerAdapter { +@EnableMethodSecurity(prePostEnabled = true) +public class WebSecurityConfig { private final JwtEntryPoint jwtEntryPoint; @@ -32,27 +32,38 @@ public JwtTokenFilter jwtTokenFilter() { } @Bean - @Override - public AuthenticationManager authenticationManager() throws Exception { - return super.authenticationManager(); + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); } - @Override - protected void configure(HttpSecurity httpSecurity) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { httpSecurity - .csrf(AbstractHttpConfigurer::disable) - .authorizeRequests() - .antMatchers("/api/orders/**").authenticated() - .antMatchers("/api/carts/**").authenticated() - .antMatchers("/v2/api-docs", "/swagger-ui/**", "/swagger-resources/**", "/webjars/**").permitAll() - .anyRequest().permitAll() - .and() - .sessionManagement() - .sessionCreationPolicy(SessionCreationPolicy.STATELESS) - .and() - .exceptionHandling().authenticationEntryPoint(jwtEntryPoint) - .and() + .csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/orders/**").authenticated() + .requestMatchers("/api/carts/**").authenticated() + // SpringDoc OpenAPI endpoints + .requestMatchers( + "/v3/api-docs/**", + "/swagger-ui/**", + "/swagger-ui.html", + "/swagger-resources/**", + "/webjars/**", + "/api-docs/**" + ).permitAll() + // Actuator endpoints (optional) + .requestMatchers("/actuator/**").permitAll() + .anyRequest().permitAll() + ) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .exceptionHandling(exception -> exception + .authenticationEntryPoint(jwtEntryPoint) + ) .addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); - } -} + return httpSecurity.build(); + } +} \ No newline at end of file diff --git a/order-service/src/main/java/com/hoangtien2k3/orderservice/entity/AbstractMappedEntity.java b/order-service/src/main/java/com/hoangtien2k3/orderservice/entity/AbstractMappedEntity.java index 45b49bca..485e6525 100644 --- a/order-service/src/main/java/com/hoangtien2k3/orderservice/entity/AbstractMappedEntity.java +++ b/order-service/src/main/java/com/hoangtien2k3/orderservice/entity/AbstractMappedEntity.java @@ -9,8 +9,8 @@ import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; -import javax.persistence.Column; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; import java.io.Serial; import java.io.Serializable; import java.time.Instant; diff --git a/order-service/src/main/java/com/hoangtien2k3/orderservice/entity/Cart.java b/order-service/src/main/java/com/hoangtien2k3/orderservice/entity/Cart.java index 117838ea..77f43021 100644 --- a/order-service/src/main/java/com/hoangtien2k3/orderservice/entity/Cart.java +++ b/order-service/src/main/java/com/hoangtien2k3/orderservice/entity/Cart.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.*; -import javax.persistence.*; +import jakarta.persistence.*; import java.io.Serial; import java.util.Set; diff --git a/order-service/src/main/java/com/hoangtien2k3/orderservice/entity/Order.java b/order-service/src/main/java/com/hoangtien2k3/orderservice/entity/Order.java index 68db17d5..69c6365d 100644 --- a/order-service/src/main/java/com/hoangtien2k3/orderservice/entity/Order.java +++ b/order-service/src/main/java/com/hoangtien2k3/orderservice/entity/Order.java @@ -13,7 +13,7 @@ import lombok.*; import org.springframework.format.annotation.DateTimeFormat; -import javax.persistence.*; +import jakarta.persistence.*; import java.io.Serial; import java.time.LocalDateTime; diff --git a/order-service/src/main/java/com/hoangtien2k3/orderservice/security/JwtEntryPoint.java b/order-service/src/main/java/com/hoangtien2k3/orderservice/security/JwtEntryPoint.java index ce32605c..b529d4c6 100644 --- a/order-service/src/main/java/com/hoangtien2k3/orderservice/security/JwtEntryPoint.java +++ b/order-service/src/main/java/com/hoangtien2k3/orderservice/security/JwtEntryPoint.java @@ -6,9 +6,9 @@ import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; @Component diff --git a/order-service/src/main/java/com/hoangtien2k3/orderservice/security/JwtTokenFilter.java b/order-service/src/main/java/com/hoangtien2k3/orderservice/security/JwtTokenFilter.java index bccb7bf1..27ee56ee 100644 --- a/order-service/src/main/java/com/hoangtien2k3/orderservice/security/JwtTokenFilter.java +++ b/order-service/src/main/java/com/hoangtien2k3/orderservice/security/JwtTokenFilter.java @@ -14,10 +14,10 @@ import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.filter.OncePerRequestFilter; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; public class JwtTokenFilter extends OncePerRequestFilter { diff --git a/payment-service/pom.xml b/payment-service/pom.xml index 34e5d596..234dd2d3 100644 --- a/payment-service/pom.xml +++ b/payment-service/pom.xml @@ -131,6 +131,11 @@ org.apache.kafka kafka-clients + + org.springdoc + springdoc-openapi-ui + 1.7.0 + diff --git a/payment-service/src/main/java/com/hoangtien2k3/paymentservice/PaymentServiceApplication.java b/payment-service/src/main/java/com/hoangtien2k3/paymentservice/PaymentServiceApplication.java index 5d5c0806..0c91800b 100644 --- a/payment-service/src/main/java/com/hoangtien2k3/paymentservice/PaymentServiceApplication.java +++ b/payment-service/src/main/java/com/hoangtien2k3/paymentservice/PaymentServiceApplication.java @@ -2,10 +2,8 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.netflix.eureka.EnableEurekaClient; @SpringBootApplication -@EnableEurekaClient public class PaymentServiceApplication { public static void main(String[] args) { diff --git a/payment-service/src/main/java/com/hoangtien2k3/paymentservice/api/PaymentController.java b/payment-service/src/main/java/com/hoangtien2k3/paymentservice/api/PaymentController.java index 38731f72..f5228817 100644 --- a/payment-service/src/main/java/com/hoangtien2k3/paymentservice/api/PaymentController.java +++ b/payment-service/src/main/java/com/hoangtien2k3/paymentservice/api/PaymentController.java @@ -5,9 +5,11 @@ import com.hoangtien2k3.paymentservice.http.HeaderGenerator; import com.hoangtien2k3.paymentservice.service.PaymentService; import com.hoangtien2k3.paymentservice.service.impl.PaymentServiceImpl; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -18,9 +20,9 @@ import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Mono; -import javax.validation.Valid; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.util.Collections; import java.util.List; @@ -39,10 +41,10 @@ public class PaymentController { @Autowired private final HeaderGenerator headerGenerator; - @ApiOperation(value = "Get all payment", notes = "Retrieve a list of all payment.") + @Operation(summary = "Get all payments", description = "Retrieve a list of all payments.") @ApiResponses({ - @ApiResponse(code = 200, message = "Payments retrieved successfully", response = List.class), - @ApiResponse(code = 204, message = "No content", response = ResponseEntity.class) + @ApiResponse(responseCode = "200", description = "Payments retrieved successfully", content = @Content(schema = @Schema(implementation = List.class))), + @ApiResponse(responseCode = "204", description = "No content") }) @GetMapping @PreAuthorize("hasAuthority('ADMIN')") @@ -53,10 +55,10 @@ public Mono>> findAll() { .defaultIfEmpty(ResponseEntity.ok(Collections.emptyList())); } - @ApiOperation(value = "Get all payments with paging", notes = "Retrieve a paginated list of all payments.") + @Operation(summary = "Get all payments with paging", description = "Retrieve a paginated list of all payments.") @ApiResponses({ - @ApiResponse(code = 200, message = "Payments retrieved successfully", response = Page.class), - @ApiResponse(code = 204, message = "No content", response = ResponseEntity.class) + @ApiResponse(responseCode = "200", description = "Payments retrieved successfully", content = @Content(schema = @Schema(implementation = Page.class))), + @ApiResponse(responseCode = "204", description = "No content") }) @GetMapping("/all") @PreAuthorize("hasAuthority('ADMIN')") @@ -69,10 +71,10 @@ public Mono>> findAll(@RequestParam(defaultValue .defaultIfEmpty(ResponseEntity.notFound().build()); } - @ApiOperation(value = "Get payment by ID", notes = "Retrieve cart information based on the provided ID.") + @Operation(summary = "Get payment by ID", description = "Retrieve payment information based on the provided ID.") @ApiResponses({ - @ApiResponse(code = 200, message = "Payment retrieved successfully", response = PaymentDto.class), - @ApiResponse(code = 404, message = "Payment not found", response = ResponseEntity.class) + @ApiResponse(responseCode = "200", description = "Payment retrieved successfully", content = @Content(schema = @Schema(implementation = PaymentDto.class))), + @ApiResponse(responseCode = "404", description = "Payment not found") }) @GetMapping("/{paymentId}") @PreAuthorize("hasAuthority('USER') or hasAuthority('ADMIN')") @@ -83,17 +85,22 @@ public ResponseEntity> findById(@PathVariable("paymentId") return ResponseEntity.ok(paymentService.findById(Integer.parseInt(paymentId))); } - + @Operation(summary = "Get order by ID", description = "Retrieve order information by order ID.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Order retrieved successfully", content = @Content(schema = @Schema(implementation = OrderDto.class))), + @ApiResponse(responseCode = "404", description = "Order not found") + }) @GetMapping("/getOrder/{orderId}") + @PreAuthorize("hasAuthority('USER') or hasAuthority('ADMIN')") public ResponseEntity> getOrderDto(@PathVariable("orderId") final Integer orderId) { return ResponseEntity.ok(paymentServiceImpl.getOrderDto(orderId)); } - - @ApiOperation(value = "Save payment", notes = "Save a new payment.") + @Operation(summary = "Save payment", description = "Save a new payment.") @ApiResponses({ - @ApiResponse(code = 200, message = "Payment saved successfully", response = PaymentDto.class), - @ApiResponse(code = 500, message = "Internal Server Error", response = ResponseEntity.class) + @ApiResponse(responseCode = "201", description = "Payment created successfully", content = @Content(schema = @Schema(implementation = PaymentDto.class))), + @ApiResponse(responseCode = "400", description = "Bad request"), + @ApiResponse(responseCode = "500", description = "Internal Server Error") }) @PostMapping @PreAuthorize("hasAuthority('USER')") @@ -102,14 +109,15 @@ public Mono> save(@RequestBody @Valid final PaymentDto paymentDto) { log.info("*** PaymentDto, resource; save payments *"); return paymentService.save(paymentDto) - .map(ResponseEntity::ok) + .map(payment -> ResponseEntity.status(HttpStatus.CREATED).body(payment)) .defaultIfEmpty(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()); } - @ApiOperation(value = "Update payment", notes = "Update an existing payment.") + @Operation(summary = "Update payment", description = "Update an existing payment.") @ApiResponses({ - @ApiResponse(code = 200, message = "Payment updated successfully", response = PaymentDto.class), - @ApiResponse(code = 404, message = "Payment not found", response = ResponseEntity.class) + @ApiResponse(responseCode = "200", description = "Payment updated successfully", content = @Content(schema = @Schema(implementation = PaymentDto.class))), + @ApiResponse(responseCode = "404", description = "Payment not found"), + @ApiResponse(responseCode = "400", description = "Bad request") }) @PutMapping @PreAuthorize("hasAuthority('ADMIN')") @@ -122,11 +130,11 @@ public Mono> update(@RequestBody .defaultIfEmpty(ResponseEntity.notFound().build()); } - - @ApiOperation(value = "Update payment by ID", notes = "Update an existing cart based on the provided ID.") + @Operation(summary = "Update payment by ID", description = "Update an existing payment based on the provided ID.") @ApiResponses({ - @ApiResponse(code = 200, message = "Payment updated successfully", response = PaymentDto.class), - @ApiResponse(code = 404, message = "Payment not found", response = ResponseEntity.class) + @ApiResponse(responseCode = "200", description = "Payment updated successfully", content = @Content(schema = @Schema(implementation = PaymentDto.class))), + @ApiResponse(responseCode = "404", description = "Payment not found"), + @ApiResponse(responseCode = "400", description = "Bad request") }) @PutMapping("/{paymentId}") @PreAuthorize("hasAuthority('USER')") @@ -142,10 +150,10 @@ public Mono> update(@PathVariable("paymentId") .defaultIfEmpty(ResponseEntity.notFound().build()); } - @ApiOperation(value = "Delete payment by ID", notes = "Delete a cart based on the provided ID.") + @Operation(summary = "Delete payment by ID", description = "Delete a payment based on the provided ID.") @ApiResponses({ - @ApiResponse(code = 200, message = "Payment deleted successfully", response = Boolean.class), - @ApiResponse(code = 404, message = "Payment not found", response = ResponseEntity.class) + @ApiResponse(responseCode = "200", description = "Payment deleted successfully", content = @Content(schema = @Schema(implementation = Boolean.class))), + @ApiResponse(responseCode = "404", description = "Payment not found") }) @DeleteMapping("/{paymentId}") @PreAuthorize("hasAuthority('USER')") @@ -155,5 +163,4 @@ public Mono> deleteById(@PathVariable("paymentId") final .thenReturn(ResponseEntity.ok(true)) .defaultIfEmpty(ResponseEntity.status(HttpStatus.NOT_FOUND).body(false)); } - } \ No newline at end of file diff --git a/payment-service/src/main/java/com/hoangtien2k3/paymentservice/config/swagger/Swagger2Config.java b/payment-service/src/main/java/com/hoangtien2k3/paymentservice/config/swagger/Swagger2Config.java deleted file mode 100644 index edcd5372..00000000 --- a/payment-service/src/main/java/com/hoangtien2k3/paymentservice/config/swagger/Swagger2Config.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.hoangtien2k3.paymentservice.config.swagger; - -import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; -import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; -import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType; -import org.springframework.boot.actuate.endpoint.ExposableEndpoint; -import org.springframework.boot.actuate.endpoint.web.*; -import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier; -import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier; -import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.env.Environment; -import org.springframework.util.StringUtils; -import springfox.documentation.builders.ApiInfoBuilder; -import springfox.documentation.builders.PathSelectors; -import springfox.documentation.builders.RequestHandlerSelectors; -import springfox.documentation.service.*; -import springfox.documentation.spi.DocumentationType; -import springfox.documentation.spi.service.contexts.SecurityContext; -import springfox.documentation.spring.web.plugins.Docket; -import springfox.documentation.swagger2.annotations.EnableSwagger2; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -@Configuration -@EnableSwagger2 -public class Swagger2Config { - - @Bean - public Docket api() { - return new Docket(DocumentationType.SWAGGER_2) - .select() - .apis(RequestHandlerSelectors.any()) - .paths(PathSelectors.any()) - .build() - .apiInfo(apiEndPointsInfo()) - .securitySchemes(List.of(apiKey())) - .securityContexts(Collections.singletonList(securityContext())); - } - - private ApiInfo apiEndPointsInfo() { - return new ApiInfoBuilder() - .title("ORDER-SERVICE API") - .description("API Documentation's ORDER-SERVICE") - .contact(new Contact("hoangtien2k3", "https://hoangtien2k3qx1.github.io/", "hoangtien2k3qx1@gmail.com")) - .license("MIT") - .licenseUrl("https://mit-license.org/") - .version("1.0.0") - .build(); - } - - private ApiKey apiKey() { - return new ApiKey("JWT", "Authorization", "header"); - } - - private SecurityContext securityContext() { - return SecurityContext.builder() - .securityReferences(defaultAuth()) - .build(); - } - - private List defaultAuth() { - AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything"); - SecurityReference reference = new SecurityReference("JWT", new AuthorizationScope[]{authorizationScope}); - return List.of(reference); - } - - @Bean - public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier, - ServletEndpointsSupplier servletEndpointsSupplier, ControllerEndpointsSupplier controllerEndpointsSupplier, - EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties, - WebEndpointProperties webEndpointProperties, Environment environment) { - List> allEndpoints = new ArrayList<>(); - Collection webEndpoints = webEndpointsSupplier.getEndpoints(); - allEndpoints.addAll(webEndpoints); - allEndpoints.addAll(servletEndpointsSupplier.getEndpoints()); - allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints()); - String basePath = webEndpointProperties.getBasePath(); - EndpointMapping endpointMapping = new EndpointMapping(basePath); - boolean shouldRegisterLinksMapping = this.shouldRegisterLinksMapping(webEndpointProperties, environment, - basePath); - return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes, - corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath), - shouldRegisterLinksMapping); - } - - private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment, - String basePath) { - return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath) - || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT)); - } - -} \ No newline at end of file diff --git a/payment-service/src/main/java/com/hoangtien2k3/paymentservice/config/swagger/SwaggerConfig.java b/payment-service/src/main/java/com/hoangtien2k3/paymentservice/config/swagger/SwaggerConfig.java new file mode 100644 index 00000000..1a4de7bb --- /dev/null +++ b/payment-service/src/main/java/com/hoangtien2k3/paymentservice/config/swagger/SwaggerConfig.java @@ -0,0 +1,84 @@ +package com.hoangtien2k3.paymentservice.config.swagger; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +public class SwaggerConfig { + + @Value("${server.port:8080}") + private String serverPort; + + @Value("${spring.application.name:payment-service}") + private String applicationName; + + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(apiInfo()) + .addSecurityItem(new SecurityRequirement().addList("Bearer Authentication")) + .components(new Components() + .addSecuritySchemes("Bearer Authentication", createBearerSecurityScheme())) + .servers(getServers()); + } + + private Info apiInfo() { + return new Info() + .title("PAYMENT SERVICE API") + .description(""" + ## Payment Service API Documentation + + This service handles all payment-related operations including: + - Payment processing and management + - Payment status tracking + - Integration with order service + - Transaction history + + ### Authentication + All endpoints require JWT Bearer token authentication. + Include the token in the Authorization header as: `Bearer {token}` + """) + .version("1.0.0") + .contact(new Contact() + .name("hoangtien2k3") + .url("https://hoangtien2k3qx1.github.io/") + .email("hoangtien2k3qx1@gmail.com")) + .license(new License() + .name("MIT License") + .url("https://mit-license.org/")) + .termsOfService("https://hoangtien2k3qx1.github.io/terms"); + } + + private SecurityScheme createBearerSecurityScheme() { + return new SecurityScheme() + .name("Bearer Authentication") + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .description("Enter your JWT token in the format: Bearer {token}") + .in(SecurityScheme.In.HEADER); + } + + private List getServers() { + Server localServer = new Server() + .url("http://localhost:" + serverPort) + .description("Local development server"); + + Server productionServer = new Server() + .url("https://api.yourdomain.com/payment-service") + .description("Production server"); + + return List.of(localServer, productionServer); + } +} \ No newline at end of file diff --git a/payment-service/src/main/java/com/hoangtien2k3/paymentservice/config/web/WebSecurityConfig.java b/payment-service/src/main/java/com/hoangtien2k3/paymentservice/config/web/WebSecurityConfig.java index 58176b7e..3b31ecc7 100644 --- a/payment-service/src/main/java/com/hoangtien2k3/paymentservice/config/web/WebSecurityConfig.java +++ b/payment-service/src/main/java/com/hoangtien2k3/paymentservice/config/web/WebSecurityConfig.java @@ -6,17 +6,18 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 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.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity -@EnableGlobalMethodSecurity(prePostEnabled = true) -public class WebSecurityConfig extends WebSecurityConfigurerAdapter { +@EnableMethodSecurity(prePostEnabled = true) +public class WebSecurityConfig { private final JwtEntryPoint jwtEntryPoint; @@ -31,26 +32,38 @@ public JwtTokenFilter jwtTokenFilter() { } @Bean - @Override - public AuthenticationManager authenticationManager() throws Exception { - return super.authenticationManager(); + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); } - @Override - protected void configure(HttpSecurity httpSecurity) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { httpSecurity - .csrf(AbstractHttpConfigurer::disable) - .authorizeRequests() - .antMatchers("/api/orders/**").authenticated() - .antMatchers("/api/carts/**").authenticated() - .antMatchers("/v2/api-docs", "/swagger-ui/**", "/swagger-resources/**", "/webjars/**").permitAll() - .anyRequest().permitAll() - .and() - .sessionManagement() - .sessionCreationPolicy(SessionCreationPolicy.STATELESS) - .and() - .exceptionHandling().authenticationEntryPoint(jwtEntryPoint) - .and() + .csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/orders/**").authenticated() + .requestMatchers("/api/carts/**").authenticated() + .requestMatchers("/api/payments/**").authenticated() + // SpringDoc OpenAPI endpoints + .requestMatchers( + "/v3/api-docs/**", + "/swagger-ui/**", + "/swagger-ui.html", + "/swagger-resources/**", + "/webjars/**" + ).permitAll() + // Actuator endpoints (optional) + .requestMatchers("/actuator/**").permitAll() + .anyRequest().permitAll() + ) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .exceptionHandling(exception -> exception + .authenticationEntryPoint(jwtEntryPoint) + ) .addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); + + return httpSecurity.build(); } -} +} \ No newline at end of file diff --git a/payment-service/src/main/java/com/hoangtien2k3/paymentservice/entity/AbstractMappedEntity.java b/payment-service/src/main/java/com/hoangtien2k3/paymentservice/entity/AbstractMappedEntity.java index a63e24ba..59a39175 100644 --- a/payment-service/src/main/java/com/hoangtien2k3/paymentservice/entity/AbstractMappedEntity.java +++ b/payment-service/src/main/java/com/hoangtien2k3/paymentservice/entity/AbstractMappedEntity.java @@ -4,7 +4,7 @@ import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; -import javax.persistence.Column; +import jakarta.persistence.*; import java.io.Serial; import java.io.Serializable; import java.time.Instant; diff --git a/payment-service/src/main/java/com/hoangtien2k3/paymentservice/entity/Payment.java b/payment-service/src/main/java/com/hoangtien2k3/paymentservice/entity/Payment.java index 8cf3f037..88eba359 100644 --- a/payment-service/src/main/java/com/hoangtien2k3/paymentservice/entity/Payment.java +++ b/payment-service/src/main/java/com/hoangtien2k3/paymentservice/entity/Payment.java @@ -3,7 +3,7 @@ import lombok.*; -import javax.persistence.*; +import jakarta.persistence.*; import java.io.Serial; import java.io.Serializable; diff --git a/payment-service/src/main/java/com/hoangtien2k3/paymentservice/http/HeaderGenerator.java b/payment-service/src/main/java/com/hoangtien2k3/paymentservice/http/HeaderGenerator.java index 1fa7f9d1..bd04b546 100644 --- a/payment-service/src/main/java/com/hoangtien2k3/paymentservice/http/HeaderGenerator.java +++ b/payment-service/src/main/java/com/hoangtien2k3/paymentservice/http/HeaderGenerator.java @@ -3,7 +3,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Service; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequest; import java.net.URI; import java.net.URISyntaxException; diff --git a/payment-service/src/main/java/com/hoangtien2k3/paymentservice/security/JwtEntryPoint.java b/payment-service/src/main/java/com/hoangtien2k3/paymentservice/security/JwtEntryPoint.java index 6ed7ee4b..cfa4227a 100644 --- a/payment-service/src/main/java/com/hoangtien2k3/paymentservice/security/JwtEntryPoint.java +++ b/payment-service/src/main/java/com/hoangtien2k3/paymentservice/security/JwtEntryPoint.java @@ -6,9 +6,9 @@ import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; @Component diff --git a/payment-service/src/main/java/com/hoangtien2k3/paymentservice/security/JwtTokenFilter.java b/payment-service/src/main/java/com/hoangtien2k3/paymentservice/security/JwtTokenFilter.java index 6d358ba9..a02121d5 100644 --- a/payment-service/src/main/java/com/hoangtien2k3/paymentservice/security/JwtTokenFilter.java +++ b/payment-service/src/main/java/com/hoangtien2k3/paymentservice/security/JwtTokenFilter.java @@ -15,10 +15,10 @@ import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.filter.OncePerRequestFilter; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; public class JwtTokenFilter extends OncePerRequestFilter { diff --git a/payment-service/src/main/java/com/hoangtien2k3/paymentservice/service/impl/PaymentServiceImpl.java b/payment-service/src/main/java/com/hoangtien2k3/paymentservice/service/impl/PaymentServiceImpl.java index f41b3792..f5037d8a 100644 --- a/payment-service/src/main/java/com/hoangtien2k3/paymentservice/service/impl/PaymentServiceImpl.java +++ b/payment-service/src/main/java/com/hoangtien2k3/paymentservice/service/impl/PaymentServiceImpl.java @@ -24,7 +24,7 @@ import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; -import javax.transaction.Transactional; +import jakarta.transaction.Transactional; import java.util.List; @Service diff --git a/product-service/src/main/java/com/hoangtien2k3/productservice/ProductServiceApplication.java b/product-service/src/main/java/com/hoangtien2k3/productservice/ProductServiceApplication.java index 4e7943fb..b5b2fcf7 100644 --- a/product-service/src/main/java/com/hoangtien2k3/productservice/ProductServiceApplication.java +++ b/product-service/src/main/java/com/hoangtien2k3/productservice/ProductServiceApplication.java @@ -2,10 +2,8 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.netflix.eureka.EnableEurekaClient; @SpringBootApplication -@EnableEurekaClient public class ProductServiceApplication { public static void main(String[] args) { diff --git a/product-service/src/main/java/com/hoangtien2k3/productservice/api/CategoryController.java b/product-service/src/main/java/com/hoangtien2k3/productservice/api/CategoryController.java index 8db217e8..a9a6d256 100644 --- a/product-service/src/main/java/com/hoangtien2k3/productservice/api/CategoryController.java +++ b/product-service/src/main/java/com/hoangtien2k3/productservice/api/CategoryController.java @@ -14,9 +14,9 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import javax.validation.Valid; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.util.List; @Slf4j diff --git a/product-service/src/main/java/com/hoangtien2k3/productservice/api/ProductController.java b/product-service/src/main/java/com/hoangtien2k3/productservice/api/ProductController.java index 3e4a2a0c..e50a82cb 100644 --- a/product-service/src/main/java/com/hoangtien2k3/productservice/api/ProductController.java +++ b/product-service/src/main/java/com/hoangtien2k3/productservice/api/ProductController.java @@ -10,9 +10,9 @@ import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; -import javax.validation.Valid; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.util.List; diff --git a/product-service/src/main/java/com/hoangtien2k3/productservice/entity/AbstractMappedEntity.java b/product-service/src/main/java/com/hoangtien2k3/productservice/entity/AbstractMappedEntity.java index abdecba2..b9078720 100644 --- a/product-service/src/main/java/com/hoangtien2k3/productservice/entity/AbstractMappedEntity.java +++ b/product-service/src/main/java/com/hoangtien2k3/productservice/entity/AbstractMappedEntity.java @@ -9,7 +9,7 @@ import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; -import javax.persistence.*; +import jakarta.persistence.*; import java.io.Serial; import java.io.Serializable; import java.time.Instant; diff --git a/product-service/src/main/java/com/hoangtien2k3/productservice/entity/Category.java b/product-service/src/main/java/com/hoangtien2k3/productservice/entity/Category.java index f7a92692..b072c5ef 100644 --- a/product-service/src/main/java/com/hoangtien2k3/productservice/entity/Category.java +++ b/product-service/src/main/java/com/hoangtien2k3/productservice/entity/Category.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.*; -import javax.persistence.*; +import jakarta.persistence.*; import java.io.Serial; import java.io.Serializable; import java.util.Set; diff --git a/product-service/src/main/java/com/hoangtien2k3/productservice/entity/Product.java b/product-service/src/main/java/com/hoangtien2k3/productservice/entity/Product.java index d46de04c..e623529d 100644 --- a/product-service/src/main/java/com/hoangtien2k3/productservice/entity/Product.java +++ b/product-service/src/main/java/com/hoangtien2k3/productservice/entity/Product.java @@ -1,7 +1,7 @@ package com.hoangtien2k3.productservice.entity; import lombok.*; -import javax.persistence.*; +import jakarta.persistence.*; import java.io.Serial; import java.io.Serializable; diff --git a/promotion-service/pom.xml b/promotion-service/pom.xml index a4caa042..6cf38c09 100644 --- a/promotion-service/pom.xml +++ b/promotion-service/pom.xml @@ -50,6 +50,15 @@ h2 test + + io.github.resilience4j + resilience4j-spring-boot3 + 2.2.0 + + + org.springframework.boot + spring-boot-starter-aop + diff --git a/promotion-service/src/main/java/com/hoangtien2k3/promotion/config/SecurityConfig.java b/promotion-service/src/main/java/com/hoangtien2k3/promotion/config/SecurityConfig.java index a51da1d9..fbd7c103 100644 --- a/promotion-service/src/main/java/com/hoangtien2k3/promotion/config/SecurityConfig.java +++ b/promotion-service/src/main/java/com/hoangtien2k3/promotion/config/SecurityConfig.java @@ -7,7 +7,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.jwt.Jwt; @@ -15,18 +15,35 @@ import org.springframework.security.web.SecurityFilterChain; @Configuration +@EnableWebSecurity public class SecurityConfig { + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .authorizeRequests(auth -> auth - .antMatchers("/actuator/prometheus", "/actuator/health/**", - "/swagger-ui", "/swagger-ui/**", "/error", "/v3/api-docs/**").permitAll() - .antMatchers("/storefront/**").permitAll() - .antMatchers("/backoffice/**").hasRole("ADMIN") + .authorizeHttpRequests(auth -> auth + .requestMatchers( + "/actuator/prometheus", + "/actuator/health/**", + "/swagger-ui", + "/swagger-ui/**", + "/swagger-ui.html", + "/error", + "/v3/api-docs/**", + "/api-docs/**", + "/webjars/**", + "/swagger-resources/**" + ).permitAll() + .requestMatchers("/storefront/**").permitAll() + .requestMatchers("/backoffice/**").hasRole("ADMIN") .anyRequest().authenticated()) - .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) - .csrf().disable(); + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt + .jwtAuthenticationConverter(jwtAuthenticationConverterForKeycloak()) + ) + ) + .csrf(csrf -> csrf.disable()); + return http.build(); } @@ -34,7 +51,15 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public JwtAuthenticationConverter jwtAuthenticationConverterForKeycloak() { Converter> jwtGrantedAuthoritiesConverter = jwt -> { Map> realmAccess = jwt.getClaim("realm_access"); + if (realmAccess == null) { + return java.util.Collections.emptyList(); + } + Collection roles = realmAccess.get("roles"); + if (roles == null) { + return java.util.Collections.emptyList(); + } + return roles.stream() .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) .collect(Collectors.toList()); @@ -45,4 +70,4 @@ public JwtAuthenticationConverter jwtAuthenticationConverterForKeycloak() { return jwtAuthenticationConverter; } -} +} \ No newline at end of file diff --git a/promotion-service/src/main/java/com/hoangtien2k3/promotion/listener/CustomAuditingEntityListener.java b/promotion-service/src/main/java/com/hoangtien2k3/promotion/listener/CustomAuditingEntityListener.java index 06d5f456..193d4a0f 100644 --- a/promotion-service/src/main/java/com/hoangtien2k3/promotion/listener/CustomAuditingEntityListener.java +++ b/promotion-service/src/main/java/com/hoangtien2k3/promotion/listener/CustomAuditingEntityListener.java @@ -6,8 +6,8 @@ import org.springframework.data.auditing.AuditingHandler; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import javax.persistence.PrePersist; -import javax.persistence.PreUpdate; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; @Configurable public class CustomAuditingEntityListener extends AuditingEntityListener { diff --git a/promotion-service/src/main/java/com/hoangtien2k3/promotion/model/AbstractAuditEntity.java b/promotion-service/src/main/java/com/hoangtien2k3/promotion/model/AbstractAuditEntity.java index 47cc1429..a9c131a1 100644 --- a/promotion-service/src/main/java/com/hoangtien2k3/promotion/model/AbstractAuditEntity.java +++ b/promotion-service/src/main/java/com/hoangtien2k3/promotion/model/AbstractAuditEntity.java @@ -9,8 +9,8 @@ import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.LastModifiedBy; -import javax.persistence.EntityListeners; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; @MappedSuperclass @Getter diff --git a/promotion-service/src/main/java/com/hoangtien2k3/promotion/model/Promotion.java b/promotion-service/src/main/java/com/hoangtien2k3/promotion/model/Promotion.java index f45b93dc..8aaf5e63 100644 --- a/promotion-service/src/main/java/com/hoangtien2k3/promotion/model/Promotion.java +++ b/promotion-service/src/main/java/com/hoangtien2k3/promotion/model/Promotion.java @@ -13,7 +13,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; -import javax.persistence.*; +import jakarta.persistence.*; @Entity @Table(name = "promotion") diff --git a/promotion-service/src/main/java/com/hoangtien2k3/promotion/model/PromotionApply.java b/promotion-service/src/main/java/com/hoangtien2k3/promotion/model/PromotionApply.java index 1e61abe5..6f2167d0 100644 --- a/promotion-service/src/main/java/com/hoangtien2k3/promotion/model/PromotionApply.java +++ b/promotion-service/src/main/java/com/hoangtien2k3/promotion/model/PromotionApply.java @@ -7,7 +7,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; -import javax.persistence.*; +import jakarta.persistence.*; @Entity @Table(name = "promotion_apply") diff --git a/promotion-service/src/main/java/com/hoangtien2k3/promotion/model/PromotionUsage.java b/promotion-service/src/main/java/com/hoangtien2k3/promotion/model/PromotionUsage.java index 52b1692b..60d7478e 100644 --- a/promotion-service/src/main/java/com/hoangtien2k3/promotion/model/PromotionUsage.java +++ b/promotion-service/src/main/java/com/hoangtien2k3/promotion/model/PromotionUsage.java @@ -7,7 +7,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; -import javax.persistence.*; +import jakarta.persistence.*; @Entity @Table(name = "promotion_usage") diff --git a/promotion-service/src/main/java/com/hoangtien2k3/promotion/validation/PromotionConstraint.java b/promotion-service/src/main/java/com/hoangtien2k3/promotion/validation/PromotionConstraint.java index 72088bae..1e2c2fe4 100644 --- a/promotion-service/src/main/java/com/hoangtien2k3/promotion/validation/PromotionConstraint.java +++ b/promotion-service/src/main/java/com/hoangtien2k3/promotion/validation/PromotionConstraint.java @@ -1,7 +1,7 @@ package com.hoangtien2k3.promotion.validation; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/promotion-service/src/main/java/com/hoangtien2k3/promotion/viewmodel/PromotionPostVm.java b/promotion-service/src/main/java/com/hoangtien2k3/promotion/viewmodel/PromotionPostVm.java index 9c5f9293..8121c7d0 100644 --- a/promotion-service/src/main/java/com/hoangtien2k3/promotion/viewmodel/PromotionPostVm.java +++ b/promotion-service/src/main/java/com/hoangtien2k3/promotion/viewmodel/PromotionPostVm.java @@ -14,9 +14,9 @@ import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; @EqualsAndHashCode(callSuper = true) @SuperBuilder diff --git a/promotion-service/src/main/java/com/hoangtien2k3/promotion/viewmodel/PromotionPutVm.java b/promotion-service/src/main/java/com/hoangtien2k3/promotion/viewmodel/PromotionPutVm.java index fca627e1..88523a82 100644 --- a/promotion-service/src/main/java/com/hoangtien2k3/promotion/viewmodel/PromotionPutVm.java +++ b/promotion-service/src/main/java/com/hoangtien2k3/promotion/viewmodel/PromotionPutVm.java @@ -13,9 +13,9 @@ import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; @EqualsAndHashCode(callSuper = true) @SuperBuilder diff --git a/rating-service/src/main/java/com/hoangtien2k3/rating/config/SecurityConfig.java b/rating-service/src/main/java/com/hoangtien2k3/rating/config/SecurityConfig.java index 02d6f8a8..cffd65f5 100644 --- a/rating-service/src/main/java/com/hoangtien2k3/rating/config/SecurityConfig.java +++ b/rating-service/src/main/java/com/hoangtien2k3/rating/config/SecurityConfig.java @@ -1,48 +1,45 @@ package com.hoangtien2k3.rating.config; -import java.util.Collection; -import java.util.Map; -import java.util.stream.Collectors; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.convert.converter.Converter; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.web.SecurityFilterChain; @Configuration +@EnableWebSecurity public class SecurityConfig { + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .authorizeRequests(auth -> auth - .antMatchers("/actuator/prometheus", "/actuator/health/**", - "/swagger-ui", "/swagger-ui/**", "/error", "/v3/api-docs/**").permitAll() - .antMatchers("/storefront/**").permitAll() - .antMatchers("/backoffice/**").hasRole("ADMIN") - .anyRequest().authenticated()) - .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) - .csrf().disable(); + .authorizeHttpRequests(auth -> auth + .requestMatchers( + "/actuator/prometheus", + "/actuator/health/**", + "/swagger-ui", + "/swagger-ui/**", + "/swagger-ui.html", + "/error", + "/v3/api-docs/**" + ).permitAll() + .requestMatchers("/storefront/**").permitAll() + .requestMatchers("/backoffice/**").hasRole("ADMIN") + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt + .jwtAuthenticationConverter(jwtAuthenticationConverterForKeycloak()) + ) + ) + .csrf(csrf -> csrf.disable()); + return http.build(); } @Bean public JwtAuthenticationConverter jwtAuthenticationConverterForKeycloak() { - Converter> jwtGrantedAuthoritiesConverter = jwt -> { - Map> realmAccess = jwt.getClaim("realm_access"); - Collection roles = realmAccess.get("roles"); - return roles.stream() - .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) - .collect(Collectors.toList()); - }; - - var jwtAuthenticationConverter = new JwtAuthenticationConverter(); - jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); - - return jwtAuthenticationConverter; + return new JwtAuthenticationConverter(); } -} +} \ No newline at end of file diff --git a/search-service/src/main/java/com/hoangtien2k3/search/config/SecurityConfig.java b/search-service/src/main/java/com/hoangtien2k3/search/config/SecurityConfig.java index 80e32cf1..8ba0862a 100644 --- a/search-service/src/main/java/com/hoangtien2k3/search/config/SecurityConfig.java +++ b/search-service/src/main/java/com/hoangtien2k3/search/config/SecurityConfig.java @@ -8,7 +8,6 @@ import org.springframework.core.convert.converter.Converter; 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.oauth2.server.resource.OAuth2ResourceServerConfigurer; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.jwt.Jwt; @@ -18,17 +17,35 @@ @Configuration @EnableWebSecurity public class SecurityConfig { + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .authorizeRequests(auth -> auth - .antMatchers("/actuator/prometheus", "/actuator/health/**", - "/swagger-ui", "/swagger-ui/**", "/error", "/v3/api-docs/**").permitAll() - .antMatchers("/storefront/**").permitAll() - .antMatchers("/backoffice/**").hasRole("ADMIN") - .anyRequest().authenticated()) - .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) - .csrf().disable(); + .authorizeHttpRequests(auth -> auth + .requestMatchers( + "/actuator/prometheus", + "/actuator/health/**", + "/actuator/info", + "/swagger-ui", + "/swagger-ui/**", + "/swagger-ui.html", + "/error", + "/v3/api-docs/**", + "/api-docs/**", + "/webjars/**", + "/swagger-resources/**" + ).permitAll() + .requestMatchers("/storefront/**").permitAll() + .requestMatchers("/backoffice/**").hasRole("ADMIN") + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt + .jwtAuthenticationConverter(jwtAuthenticationConverterForKeycloak()) + ) + ) + .csrf(csrf -> csrf.disable()); + return http.build(); } @@ -36,15 +53,24 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public JwtAuthenticationConverter jwtAuthenticationConverterForKeycloak() { Converter> jwtGrantedAuthoritiesConverter = jwt -> { Map> realmAccess = jwt.getClaim("realm_access"); + + if (realmAccess == null) { + return java.util.Collections.emptyList(); + } + Collection roles = realmAccess.get("roles"); + if (roles == null) { + return java.util.Collections.emptyList(); + } + return roles.stream() .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) .collect(Collectors.toList()); }; - var jwtAuthenticationConverter = new JwtAuthenticationConverter(); + JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); return jwtAuthenticationConverter; } -} +} \ No newline at end of file diff --git a/search-service/src/test/java/com/hoangtien2k3/search/service/ProductServiceTest.java b/search-service/src/test/java/com/hoangtien2k3/search/service/ProductServiceTest.java index e74bf18f..bd1fd859 100644 --- a/search-service/src/test/java/com/hoangtien2k3/search/service/ProductServiceTest.java +++ b/search-service/src/test/java/com/hoangtien2k3/search/service/ProductServiceTest.java @@ -33,7 +33,7 @@ import org.springframework.data.elasticsearch.core.TotalHitsRelation; import org.springframework.data.elasticsearch.core.suggest.response.Suggest; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; class ProductServiceTest { diff --git a/shipping-service/src/main/java/com/hoangtien2k3/shippingservice/ShippingServiceApplication.java b/shipping-service/src/main/java/com/hoangtien2k3/shippingservice/ShippingServiceApplication.java index 150f76ca..ead06396 100644 --- a/shipping-service/src/main/java/com/hoangtien2k3/shippingservice/ShippingServiceApplication.java +++ b/shipping-service/src/main/java/com/hoangtien2k3/shippingservice/ShippingServiceApplication.java @@ -2,11 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.netflix.eureka.EnableEurekaClient; @SpringBootApplication -@EnableEurekaClient -public class ShippingServiceApplication { +class ShippingServiceApplication { public static void main(String[] args) { SpringApplication.run(ShippingServiceApplication.class, args); diff --git a/tax-service/pom.xml b/tax-service/pom.xml index e11fe267..4c528bad 100644 --- a/tax-service/pom.xml +++ b/tax-service/pom.xml @@ -52,6 +52,11 @@ org.liquibase liquibase-core + + io.github.resilience4j + resilience4j-spring-boot3 + 2.2.0 + io.projectreactor.addons reactor-extra diff --git a/tax-service/src/main/java/com/hoangtien2k3/tax/config/SecurityConfig.java b/tax-service/src/main/java/com/hoangtien2k3/tax/config/SecurityConfig.java index 0788cfc3..da29593f 100644 --- a/tax-service/src/main/java/com/hoangtien2k3/tax/config/SecurityConfig.java +++ b/tax-service/src/main/java/com/hoangtien2k3/tax/config/SecurityConfig.java @@ -6,9 +6,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; -import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.jwt.Jwt; @@ -16,19 +15,34 @@ import org.springframework.security.web.SecurityFilterChain; @Configuration +@EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .authorizeRequests(auth -> auth - .antMatchers("/actuator/prometheus", "/actuator/health/**", - "/swagger-ui", "/swagger-ui/**", "/error", "/v3/api-docs/**").permitAll() - .antMatchers("/storefront/**").permitAll() - .antMatchers("/backoffice/**").hasRole("ADMIN") - .anyRequest().authenticated()) - .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) - .csrf().disable(); + .authorizeHttpRequests(auth -> auth + .requestMatchers( + "/actuator/prometheus", + "/actuator/health/**", + "/swagger-ui", + "/swagger-ui/**", + "/swagger-ui.html", + "/error", + "/v3/api-docs/**", + "/api-docs/**" + ).permitAll() + .requestMatchers("/storefront/**").permitAll() + .requestMatchers("/backoffice/**").hasRole("ADMIN") + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt + .jwtAuthenticationConverter(jwtAuthenticationConverterForKeycloak()) + ) + ) + .csrf(csrf -> csrf.disable()); + return http.build(); } @@ -36,7 +50,16 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public JwtAuthenticationConverter jwtAuthenticationConverterForKeycloak() { Converter> jwtGrantedAuthoritiesConverter = jwt -> { Map> realmAccess = jwt.getClaim("realm_access"); + + if (realmAccess == null) { + return java.util.Collections.emptyList(); + } + Collection roles = realmAccess.get("roles"); + if (roles == null) { + return java.util.Collections.emptyList(); + } + return roles.stream() .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) .collect(Collectors.toList()); @@ -47,4 +70,4 @@ public JwtAuthenticationConverter jwtAuthenticationConverterForKeycloak() { return jwtAuthenticationConverter; } -} +} \ No newline at end of file diff --git a/tax-service/src/main/java/com/hoangtien2k3/tax/controller/TaxClassController.java b/tax-service/src/main/java/com/hoangtien2k3/tax/controller/TaxClassController.java index 70af1e6e..238cee90 100644 --- a/tax-service/src/main/java/com/hoangtien2k3/tax/controller/TaxClassController.java +++ b/tax-service/src/main/java/com/hoangtien2k3/tax/controller/TaxClassController.java @@ -25,7 +25,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.UriComponentsBuilder; -import javax.validation.Valid; +import jakarta.validation.Valid; @RestController @RequestMapping(ApiConstant.TAX_CLASS_URL) diff --git a/tax-service/src/main/java/com/hoangtien2k3/tax/controller/TaxRateController.java b/tax-service/src/main/java/com/hoangtien2k3/tax/controller/TaxRateController.java index baf6eb0f..4636b1ec 100644 --- a/tax-service/src/main/java/com/hoangtien2k3/tax/controller/TaxRateController.java +++ b/tax-service/src/main/java/com/hoangtien2k3/tax/controller/TaxRateController.java @@ -25,7 +25,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.UriComponentsBuilder; -import javax.validation.Valid; +import jakarta.validation.Valid; @RestController @RequestMapping(ApiConstant.TAX_RATE_URL) diff --git a/tax-service/src/main/java/com/hoangtien2k3/tax/exception/ApiExceptionHandler.java b/tax-service/src/main/java/com/hoangtien2k3/tax/exception/ApiExceptionHandler.java index 4b570d9d..df4cdf2d 100644 --- a/tax-service/src/main/java/com/hoangtien2k3/tax/exception/ApiExceptionHandler.java +++ b/tax-service/src/main/java/com/hoangtien2k3/tax/exception/ApiExceptionHandler.java @@ -14,7 +14,7 @@ import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.context.request.WebRequest; -import javax.validation.ConstraintViolationException; +import jakarta.validation.ConstraintViolationException; @ControllerAdvice @Slf4j diff --git a/tax-service/src/main/java/com/hoangtien2k3/tax/listener/CustomAuditingEntityListener.java b/tax-service/src/main/java/com/hoangtien2k3/tax/listener/CustomAuditingEntityListener.java index ec99bbd4..f55a4038 100644 --- a/tax-service/src/main/java/com/hoangtien2k3/tax/listener/CustomAuditingEntityListener.java +++ b/tax-service/src/main/java/com/hoangtien2k3/tax/listener/CustomAuditingEntityListener.java @@ -6,8 +6,8 @@ import org.springframework.data.auditing.AuditingHandler; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import javax.persistence.PrePersist; -import javax.persistence.PreUpdate; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; @Configurable public class CustomAuditingEntityListener extends AuditingEntityListener { diff --git a/tax-service/src/main/java/com/hoangtien2k3/tax/model/AbstractAuditEntity.java b/tax-service/src/main/java/com/hoangtien2k3/tax/model/AbstractAuditEntity.java index f63dece0..a0aaf118 100644 --- a/tax-service/src/main/java/com/hoangtien2k3/tax/model/AbstractAuditEntity.java +++ b/tax-service/src/main/java/com/hoangtien2k3/tax/model/AbstractAuditEntity.java @@ -9,8 +9,8 @@ import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.LastModifiedBy; -import javax.persistence.EntityListeners; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; @MappedSuperclass @Getter diff --git a/tax-service/src/main/java/com/hoangtien2k3/tax/model/TaxClass.java b/tax-service/src/main/java/com/hoangtien2k3/tax/model/TaxClass.java index bbdb35b4..b0d8c372 100644 --- a/tax-service/src/main/java/com/hoangtien2k3/tax/model/TaxClass.java +++ b/tax-service/src/main/java/com/hoangtien2k3/tax/model/TaxClass.java @@ -7,7 +7,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; -import javax.persistence.*; +import jakarta.persistence.*; @Entity @Table(name = "tax_class") diff --git a/tax-service/src/main/java/com/hoangtien2k3/tax/model/TaxRate.java b/tax-service/src/main/java/com/hoangtien2k3/tax/model/TaxRate.java index ebd01ed9..f9b3bf30 100644 --- a/tax-service/src/main/java/com/hoangtien2k3/tax/model/TaxRate.java +++ b/tax-service/src/main/java/com/hoangtien2k3/tax/model/TaxRate.java @@ -7,7 +7,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; -import javax.persistence.*; +import jakarta.persistence.*; @Entity @Table(name = "tax_rate") diff --git a/tax-service/src/main/java/com/hoangtien2k3/tax/service/LocationService.java b/tax-service/src/main/java/com/hoangtien2k3/tax/service/LocationService.java index 9483b557..253d2496 100644 --- a/tax-service/src/main/java/com/hoangtien2k3/tax/service/LocationService.java +++ b/tax-service/src/main/java/com/hoangtien2k3/tax/service/LocationService.java @@ -2,43 +2,59 @@ import com.hoangtien2k3.tax.config.ServiceUrlConfig; import com.hoangtien2k3.tax.viewmodel.location.StateOrProvinceAndCountryGetNameVm; -import java.net.URI; -import java.util.List; - +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; import io.github.resilience4j.retry.annotation.Retry; import lombok.RequiredArgsConstructor; import org.springframework.core.ParameterizedTypeReference; -import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.stereotype.Service; -import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; +import java.net.URI; +import java.util.Collections; +import java.util.List; + @Service @RequiredArgsConstructor -public class LocationService extends AbstractCircuitBreakFallbackHandler { - private final RestClient restClient; +public class LocationService { + private final RestTemplate restTemplate; private final ServiceUrlConfig serviceUrlConfig; @Retry(name = "restApi") @CircuitBreaker(name = "restCircuitBreaker", fallbackMethod = "handleLocationNameListFallback") public List getStateOrProvinceAndCountryNames(List stateOrProvinceIds) { + // Convert List to array for query params + Long[] idsArray = stateOrProvinceIds.toArray(new Long[0]); + final URI url = UriComponentsBuilder.fromHttpUrl(serviceUrlConfig.location()) - .path("/backoffice/state-or-provinces/state-country-names") - .queryParam("stateOrProvinceIds", stateOrProvinceIds).build().toUri(); - final String jwt = - ((Jwt) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getTokenValue(); - return restClient.get() - .uri(url) - .headers(h -> h.setBearerAuth(jwt)) - .retrieve() - .body(new ParameterizedTypeReference>() { - }); + .path("/backoffice/state-or-provinces/state-country-names") + .queryParam("stateOrProvinceIds", (Object[]) idsArray) + .build() + .toUri(); + + final Jwt jwt = (Jwt) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(jwt.getTokenValue()); + HttpEntity entity = new HttpEntity<>(headers); + + return restTemplate.exchange( + url, + HttpMethod.GET, + entity, + new ParameterizedTypeReference>() {} + ).getBody(); } - protected List handleLocationNameListFallback(Throwable throwable) - throws Throwable { - return handleTypedFallback(throwable); + protected List handleLocationNameListFallback(Throwable throwable) { + // Log error (use proper logger in production) + System.err.println("Circuit breaker fallback triggered: " + throwable.getMessage()); + // Return empty list as fallback + return Collections.emptyList(); } -} +} \ No newline at end of file diff --git a/tax-service/src/main/java/com/hoangtien2k3/tax/viewmodel/taxclass/TaxClassPostVm.java b/tax-service/src/main/java/com/hoangtien2k3/tax/viewmodel/taxclass/TaxClassPostVm.java index db7659d8..862828f0 100644 --- a/tax-service/src/main/java/com/hoangtien2k3/tax/viewmodel/taxclass/TaxClassPostVm.java +++ b/tax-service/src/main/java/com/hoangtien2k3/tax/viewmodel/taxclass/TaxClassPostVm.java @@ -2,8 +2,8 @@ import com.hoangtien2k3.tax.model.TaxClass; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.Size; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; public record TaxClassPostVm(@NotBlank String id, @Size(min = 1, max = 450) String name) { diff --git a/tax-service/src/main/java/com/hoangtien2k3/tax/viewmodel/taxrate/TaxRatePostVm.java b/tax-service/src/main/java/com/hoangtien2k3/tax/viewmodel/taxrate/TaxRatePostVm.java index 9c15e187..6b7958eb 100644 --- a/tax-service/src/main/java/com/hoangtien2k3/tax/viewmodel/taxrate/TaxRatePostVm.java +++ b/tax-service/src/main/java/com/hoangtien2k3/tax/viewmodel/taxrate/TaxRatePostVm.java @@ -1,7 +1,7 @@ package com.hoangtien2k3.tax.viewmodel.taxrate; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; public record TaxRatePostVm(@NotNull Double rate, @Size(max = 25) String zipCode, diff --git a/user-service/src/main/java/com/hoangtien2k3/userservice/UserServiceApplication.java b/user-service/src/main/java/com/hoangtien2k3/userservice/UserServiceApplication.java index 34b23a77..19f8ba23 100644 --- a/user-service/src/main/java/com/hoangtien2k3/userservice/UserServiceApplication.java +++ b/user-service/src/main/java/com/hoangtien2k3/userservice/UserServiceApplication.java @@ -2,10 +2,8 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.netflix.eureka.EnableEurekaClient; @SpringBootApplication -@EnableEurekaClient public class UserServiceApplication { public static void main(String[] args) { diff --git a/user-service/src/main/java/com/hoangtien2k3/userservice/api/UserManager.java b/user-service/src/main/java/com/hoangtien2k3/userservice/api/UserManager.java index ec591f5a..bc2fdc57 100644 --- a/user-service/src/main/java/com/hoangtien2k3/userservice/api/UserManager.java +++ b/user-service/src/main/java/com/hoangtien2k3/userservice/api/UserManager.java @@ -8,7 +8,11 @@ import com.hoangtien2k3.userservice.model.dto.response.ResponseMessage; import com.hoangtien2k3.userservice.security.jwt.JwtProvider; import com.hoangtien2k3.userservice.service.UserService; -import io.swagger.annotations.*; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.modelmapper.ModelMapper; import org.springframework.beans.factory.annotation.Autowired; @@ -22,115 +26,108 @@ @Slf4j @RestController @RequestMapping("/api/manager") -@Api(value = "User API", description = "Operations related to users") +@Tag(name = "User Management", description = "User management APIs") public class UserManager { - private final ModelMapper modelMapper; + private final ModelMapper modelMapper; + private final UserService userService; + private final HeaderGenerator headerGenerator; + private final JwtProvider jwtProvider; - private final UserService userService; - private final HeaderGenerator headerGenerator; - private final JwtProvider jwtProvider; + @Autowired + public UserManager(UserService userService, HeaderGenerator headerGenerator, JwtProvider jwtProvider, + ModelMapper modelMapper) { + this.userService = userService; + this.headerGenerator = headerGenerator; + this.jwtProvider = jwtProvider; + this.modelMapper = modelMapper; + } - @Autowired - public UserManager(UserService userService, HeaderGenerator headerGenerator, JwtProvider jwtProvider, - ModelMapper modelMapper) { - this.userService = userService; - this.headerGenerator = headerGenerator; - this.jwtProvider = jwtProvider; - this.modelMapper = modelMapper; - } + @Operation(summary = "Update user information", description = "Update the user information with the provided details.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "User updated successfully"), + @ApiResponse(responseCode = "400", description = "Bad Request") + }) + @PutMapping("update/{id}") + @PreAuthorize("isAuthenticated() and hasAuthority('USER')") + public Mono> update(@PathVariable("id") Long id, + @RequestBody SignUp updateDTO) { + return userService.update(id, updateDTO) + .flatMap(user -> Mono.just(new ResponseEntity<>( + new ResponseMessage("Update user: " + updateDTO.getUsername() + " successfully."), + HttpStatus.OK))) + .onErrorResume(error -> Mono.just(new ResponseEntity<>( + new ResponseMessage("Update user: " + updateDTO.getUsername() + " failed " + error.getMessage()), + HttpStatus.BAD_REQUEST))); + } - @ApiOperation(value = "Update user information", notes = "Update the user information with the provided details.") - @ApiResponses({ - @ApiResponse(code = 200, message = "User updated successfully", response = ResponseMessage.class), - @ApiResponse(code = 400, message = "Bad Request", response = ResponseMessage.class) - }) - @PutMapping("update/{id}") - @PreAuthorize("isAuthenticated() and hasAuthority('USER')") - public Mono> update(@PathVariable("id") Long id, - @RequestBody SignUp updateDTO) { - return userService.update(id, updateDTO) - .flatMap(user -> Mono.just(new ResponseEntity<>( - new ResponseMessage("Update user: " + updateDTO.getUsername() - + " successfully."), - HttpStatus.OK))) - .onErrorResume( - error -> Mono.just(new ResponseEntity<>( - new ResponseMessage("Update user: " - + updateDTO.getUsername() + " failed " - + error.getMessage()), - HttpStatus.BAD_REQUEST))); - } + @Operation(summary = "Change user password", description = "Change the password for the authenticated user.") + @ApiResponse(responseCode = "200", description = "Password changed successfully") + @PutMapping("/change-password") + @PreAuthorize("isAuthenticated() and hasAuthority('USER')") + public Mono changePassword(@RequestBody ChangePasswordRequest request) { + return userService.changePassword(request); + } - @ApiOperation(value = "Change user password", notes = "Change the password for the authenticated user.") - @ApiResponse(code = 200, message = "Password changed successfully", response = String.class) - @PutMapping("/change-password") - @PreAuthorize("isAuthenticated() and hasAuthority('USER')") - public Mono changePassword(@RequestBody ChangePasswordRequest request) { - return userService.changePassword(request); - } + @Operation(summary = "Delete user", description = "Delete a user with the specified ID.") + @DeleteMapping("delete/{id}") + @PreAuthorize("isAuthenticated() and (hasAuthority('USER') or hasAuthority('ADMIN'))") + public Mono delete(@PathVariable("id") Long id) { + return userService.delete(id); + } - @ApiOperation(value = "Delete user", notes = "Delete a user with the specified ID.") - @DeleteMapping("delete/{id}") - @PreAuthorize("isAuthenticated() and (hasAuthority('USER') or hasAuthority('ADMIN'))") - public Mono delete(@PathVariable("id") Long id) { - return userService.delete(id); - } + @Operation(summary = "Get user by username", description = "Retrieve user information based on the provided username.") + @GetMapping("/user") + @PreAuthorize("(isAuthenticated() and (hasAuthority('USER') and principal.username == #username) or hasAuthority('ADMIN'))") + public Mono> getUserByUsername(@RequestParam(value = "username") String username) { + return userService.findByUsername(username) + .map(user -> modelMapper.map(user, UserDto.class)) + .map(userDto -> new ResponseEntity<>(userDto, + headerGenerator.getHeadersForSuccessGetMethod(), HttpStatus.OK)) + .defaultIfEmpty(new ResponseEntity<>(null, headerGenerator.getHeadersForError(), + HttpStatus.NOT_FOUND)); + } - @ApiOperation(value = "Get user by username", notes = "Retrieve user information based on the provided username.") - @GetMapping("/user") - @PreAuthorize("(isAuthenticated() and (hasAuthority('USER') and principal.username == #username) or hasAuthority('ADMIN'))") - public Mono> getUserByUsername(@RequestParam(value = "username") String username) { - return userService.findByUsername(username) - .map(user -> modelMapper.map(user, UserDto.class)) - .map(userDto -> new ResponseEntity<>(userDto, - headerGenerator.getHeadersForSuccessGetMethod(), HttpStatus.OK)) - .defaultIfEmpty(new ResponseEntity<>(null, headerGenerator.getHeadersForError(), - HttpStatus.NOT_FOUND)); - } + @Operation(summary = "Get user by ID", description = "Retrieve user information based on the provided ID.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "User retrieved successfully"), + @ApiResponse(responseCode = "404", description = "User not found") + }) + @GetMapping("/user/{id}") + @PreAuthorize("hasAuthority('ADMIN') or hasAuthority('USER') and principal.id == #id") + public Mono> getUserById(@PathVariable("id") Long id) { + return userService.findById(id) + .map(user -> modelMapper.map(user, UserDto.class)) + .map(userDto -> new ResponseEntity<>(userDto, + headerGenerator.getHeadersForSuccessGetMethod(), HttpStatus.OK)) + .defaultIfEmpty(new ResponseEntity<>(null, headerGenerator.getHeadersForError(), + HttpStatus.NOT_FOUND)); + } - @ApiOperation(value = "Get user by ID", notes = "Retrieve user information based on the provided ID.") - @ApiResponses({ - @ApiResponse(code = 200, message = "User retrieved successfully", response = UserDto.class), - @ApiResponse(code = 404, message = "User not found", response = ResponseEntity.class) - }) - @GetMapping("/user/{id}") - @PreAuthorize("hasAuthority('ADMIN') or hasAuthority('USER') and principal.id == #id") - public Mono> getUserById(@PathVariable("id") Long id) { - return userService.findById(id) - .map(user -> modelMapper.map(user, UserDto.class)) - .map(userDto -> new ResponseEntity<>(userDto, - headerGenerator.getHeadersForSuccessGetMethod(), HttpStatus.OK)) - .defaultIfEmpty(new ResponseEntity<>(null, headerGenerator.getHeadersForError(), - HttpStatus.NOT_FOUND)); - } + @Operation(summary = "Get all users", description = "Retrieve all users with pagination", + security = @SecurityRequirement(name = "Bearer Authentication")) + @GetMapping("/all") + @PreAuthorize("hasAuthority('ADMIN')") + public Mono>> getAllUsers(@RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(defaultValue = "id") String sortBy, + @RequestParam(defaultValue = "ASC") String sortOrder) { + return userService.findAllUsers(page, size, sortBy, sortOrder) + .map(usersPage -> new ResponseEntity<>(usersPage, + headerGenerator.getHeadersForSuccessGetMethod(), HttpStatus.OK)); + } - @ApiOperation(value = "Get a secure user resource", authorizations = { @Authorization(value = "JWT") }) - @GetMapping("/all") - @PreAuthorize("hasAuthority('ADMIN')") - public Mono>> getAllUsers(@RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size, - @RequestParam(defaultValue = "id") String sortBy, - @RequestParam(defaultValue = "ASC") String sortOrder) { - - return userService.findAllUsers(page, size, sortBy, sortOrder) - .map(usersPage -> new ResponseEntity<>(usersPage, - headerGenerator.getHeadersForSuccessGetMethod(), HttpStatus.OK)); - } - - @ApiOperation(value = "Get user information from token", notes = "Retrieve user information based on the provided JWT token.") - @ApiResponses({ - @ApiResponse(code = 200, message = "User information retrieved successfully", response = UserDto.class), - @ApiResponse(code = 404, message = "User not found", response = ResponseEntity.class) - }) - @GetMapping("/info") - public Mono> getUserInfo(@RequestHeader("Authorization") String token) { - String username = jwtProvider.getUserNameFromToken(token); - return userService.findByUsername(username) - .map(user -> modelMapper.map(user, UserDto.class)) - .map(userDto -> new ResponseEntity<>(userDto, - headerGenerator.getHeadersForSuccessGetMethod(), HttpStatus.OK)) - .switchIfEmpty(Mono - .error(new TokenErrorOrAccessTimeOut("Token error or access timeout"))); - } - -} + @Operation(summary = "Get user information from token", description = "Retrieve user information based on the provided JWT token.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "User information retrieved successfully"), + @ApiResponse(responseCode = "404", description = "User not found") + }) + @GetMapping("/info") + public Mono> getUserInfo(@RequestHeader("Authorization") String token) { + String username = jwtProvider.getUserNameFromToken(token); + return userService.findByUsername(username) + .map(user -> modelMapper.map(user, UserDto.class)) + .map(userDto -> new ResponseEntity<>(userDto, + headerGenerator.getHeadersForSuccessGetMethod(), HttpStatus.OK)) + .switchIfEmpty(Mono.error(new TokenErrorOrAccessTimeOut("Token error or access timeout"))); + } +} \ No newline at end of file diff --git a/user-service/src/main/java/com/hoangtien2k3/userservice/config/Swagger2Config.java b/user-service/src/main/java/com/hoangtien2k3/userservice/config/Swagger2Config.java index e327c1d9..967edc9d 100644 --- a/user-service/src/main/java/com/hoangtien2k3/userservice/config/Swagger2Config.java +++ b/user-service/src/main/java/com/hoangtien2k3/userservice/config/Swagger2Config.java @@ -1,94 +1,80 @@ package com.hoangtien2k3.userservice.config; -import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; -import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; -import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType; -import org.springframework.boot.actuate.endpoint.ExposableEndpoint; -import org.springframework.boot.actuate.endpoint.web.*; -import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier; -import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier; -import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.env.Environment; -import org.springframework.util.StringUtils; -import springfox.documentation.builders.ApiInfoBuilder; -import springfox.documentation.builders.PathSelectors; -import springfox.documentation.builders.RequestHandlerSelectors; -import springfox.documentation.service.*; -import springfox.documentation.spi.DocumentationType; -import springfox.documentation.spi.service.contexts.SecurityContext; -import springfox.documentation.spring.web.plugins.Docket; -import springfox.documentation.swagger2.annotations.EnableSwagger2; -import java.util.*; +import java.util.List; @Configuration -@EnableSwagger2 public class Swagger2Config { + @Value("${server.port:8080}") + private String serverPort; + @Bean - public Docket api() { - return new Docket(DocumentationType.SWAGGER_2) - .select() - .apis(RequestHandlerSelectors.any()) - .paths(PathSelectors.any()) - .build() - .apiInfo(apiEndPointsInfo()) - .securitySchemes(List.of(apiKey())) - .securityContexts(Collections.singletonList(securityContext())); + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(apiInfo()) + .addSecurityItem(new SecurityRequirement().addList("Bearer Authentication")) + .components(new Components() + .addSecuritySchemes("Bearer Authentication", createBearerSecurityScheme())) + .servers(getServers()); } - private ApiInfo apiEndPointsInfo() { - return new ApiInfoBuilder() - .title("USER-SERVICE API DOCUMENTATIONS") - .description("API Documentation's USER-SERVICE") - .contact(new Contact("hoangtien2k3", "https://hoangtien2k3qx1.github.io/", "hoangtien2k3qx1@gmail.com")) - .license("MIT") - .licenseUrl("https://mit-license.org/") + private Info apiInfo() { + return new Info() + .title("USER SERVICE API") + .description(""" + ## User Service API Documentation + + This service handles all user-related operations including: + - User registration and authentication + - Profile management + - Role and permission management + + ### Authentication + All endpoints require JWT Bearer token authentication. + Include the token in the Authorization header as: `Bearer {token}` + """) .version("1.0.0") - .build(); + .contact(new Contact() + .name("hoangtien2k3") + .url("https://hoangtien2k3qx1.github.io/") + .email("hoangtien2k3qx1@gmail.com")) + .license(new License() + .name("MIT License") + .url("https://mit-license.org/")) + .termsOfService("https://hoangtien2k3qx1.github.io/terms"); } - private ApiKey apiKey() { - return new ApiKey("JWT", "Authorization", "header"); + private SecurityScheme createBearerSecurityScheme() { + return new SecurityScheme() + .name("Bearer Authentication") + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .description("Enter your JWT token in the format: Bearer {token}") + .in(SecurityScheme.In.HEADER); } - private SecurityContext securityContext() { - return SecurityContext.builder() - .securityReferences(defaultAuth()) - .build(); - } + private List getServers() { + Server localServer = new Server() + .url("http://localhost:" + serverPort) + .description("Local development server"); - private List defaultAuth() { - AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything"); - SecurityReference reference = new SecurityReference("JWT", new AuthorizationScope[]{authorizationScope}); - return List.of(reference); - } + Server productionServer = new Server() + .url("https://api.yourdomain.com/user-service") + .description("Production server"); - @Bean - public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier, - ServletEndpointsSupplier servletEndpointsSupplier, ControllerEndpointsSupplier controllerEndpointsSupplier, - EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties, - WebEndpointProperties webEndpointProperties, Environment environment) { - List> allEndpoints = new ArrayList<>(); - Collection webEndpoints = webEndpointsSupplier.getEndpoints(); - allEndpoints.addAll(webEndpoints); - allEndpoints.addAll(servletEndpointsSupplier.getEndpoints()); - allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints()); - String basePath = webEndpointProperties.getBasePath(); - EndpointMapping endpointMapping = new EndpointMapping(basePath); - boolean shouldRegisterLinksMapping = this.shouldRegisterLinksMapping(webEndpointProperties, environment, - basePath); - return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes, - corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath), - shouldRegisterLinksMapping, null); + return List.of(localServer, productionServer); } - - private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment, - String basePath) { - return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath) - || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT)); - } - } \ No newline at end of file diff --git a/user-service/src/main/java/com/hoangtien2k3/userservice/config/WebSecurityConfig.java b/user-service/src/main/java/com/hoangtien2k3/userservice/config/WebSecurityConfig.java index 41fa850b..118fa17e 100644 --- a/user-service/src/main/java/com/hoangtien2k3/userservice/config/WebSecurityConfig.java +++ b/user-service/src/main/java/com/hoangtien2k3/userservice/config/WebSecurityConfig.java @@ -9,22 +9,21 @@ 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.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 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.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity -@EnableGlobalMethodSecurity(prePostEnabled = true) -public class WebSecurityConfig extends WebSecurityConfigurerAdapter { +@EnableMethodSecurity(prePostEnabled = true) +public class WebSecurityConfig { private final UserDetailService userDetailService; private final JwtEntryPoint jwtEntryPoint; @@ -46,21 +45,13 @@ public PasswordEncoder passwordEncoder() { } @Bean - @Override - public AuthenticationManager authenticationManager() throws Exception { - return super.authenticationManager(); + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); } @Bean public UserDetailsService userDetailsService() { - return new UserDetailService(); - } - - @Override - public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { - authenticationManagerBuilder - .userDetailsService(userDetailService) - .passwordEncoder(passwordEncoder()); + return userDetailService; } @Bean @@ -71,29 +62,40 @@ public AuthenticationProvider authenticationProvider() { return authenticationProvider; } - @Override - protected void configure(HttpSecurity httpSecurity) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { httpSecurity - .csrf(AbstractHttpConfigurer::disable) - .authorizeRequests() - .antMatchers("/api/auth/**").permitAll() - .antMatchers("/api/manager/token").permitAll() - .antMatchers("/api/manager/change-password").authenticated() - .antMatchers("/api/manager/delete/**").authenticated() - .antMatchers("/api/auth/logout").authenticated() - .antMatchers("/api/manager/user/**").permitAll() - .antMatchers("/v2/api-docs", "/swagger-ui/**", "/swagger-resources/**", "/webjars/**").permitAll() - .anyRequest().authenticated() - .and() - .sessionManagement() - .sessionCreationPolicy(SessionCreationPolicy.STATELESS) - .and() - .exceptionHandling().authenticationEntryPoint(jwtEntryPoint) - .and() + .csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(auth -> auth + .requestMatchers( + "/api/auth/**", + "/api/manager/token", + "/api/manager/user/**" + ).permitAll() + .requestMatchers( + "/api/manager/change-password", + "/api/manager/delete/**", + "/api/auth/logout" + ).authenticated() + .requestMatchers( + "/v3/api-docs/**", + "/swagger-ui/**", + "/swagger-ui.html", + "/swagger-resources/**", + "/webjars/**", + "/api-docs/**" + ).permitAll() + .anyRequest().authenticated() + ) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .exceptionHandling(exception -> exception + .authenticationEntryPoint(jwtEntryPoint) + ) .authenticationProvider(authenticationProvider()) .addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); - httpSecurity.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); + return httpSecurity.build(); } - -} +} \ No newline at end of file diff --git a/user-service/src/main/java/com/hoangtien2k3/userservice/http/HeaderGenerator.java b/user-service/src/main/java/com/hoangtien2k3/userservice/http/HeaderGenerator.java index aca8b47a..c4f9d47e 100644 --- a/user-service/src/main/java/com/hoangtien2k3/userservice/http/HeaderGenerator.java +++ b/user-service/src/main/java/com/hoangtien2k3/userservice/http/HeaderGenerator.java @@ -3,7 +3,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Service; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequest; import java.net.URI; import java.net.URISyntaxException; diff --git a/user-service/src/main/java/com/hoangtien2k3/userservice/model/dto/request/SignUp.java b/user-service/src/main/java/com/hoangtien2k3/userservice/model/dto/request/SignUp.java index 12b9ad17..697f6f8f 100644 --- a/user-service/src/main/java/com/hoangtien2k3/userservice/model/dto/request/SignUp.java +++ b/user-service/src/main/java/com/hoangtien2k3/userservice/model/dto/request/SignUp.java @@ -5,9 +5,9 @@ import lombok.RequiredArgsConstructor; import lombok.Setter; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.Pattern; -import javax.validation.constraints.Size; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; import java.util.Set; @Setter diff --git a/user-service/src/main/java/com/hoangtien2k3/userservice/model/dto/response/ResponseMessage.java b/user-service/src/main/java/com/hoangtien2k3/userservice/model/dto/response/ResponseMessage.java index 4a9e74c3..0fa352bc 100644 --- a/user-service/src/main/java/com/hoangtien2k3/userservice/model/dto/response/ResponseMessage.java +++ b/user-service/src/main/java/com/hoangtien2k3/userservice/model/dto/response/ResponseMessage.java @@ -2,7 +2,7 @@ import lombok.*; -import javax.validation.constraints.Size; +import jakarta.validation.constraints.Size; @Getter @Setter diff --git a/user-service/src/main/java/com/hoangtien2k3/userservice/model/entity/Role.java b/user-service/src/main/java/com/hoangtien2k3/userservice/model/entity/Role.java index 495bab55..6a7c3394 100644 --- a/user-service/src/main/java/com/hoangtien2k3/userservice/model/entity/Role.java +++ b/user-service/src/main/java/com/hoangtien2k3/userservice/model/entity/Role.java @@ -4,7 +4,7 @@ import lombok.experimental.Accessors; import org.hibernate.annotations.NaturalId; -import javax.persistence.*; +import jakarta.persistence.*; @Getter @Setter diff --git a/user-service/src/main/java/com/hoangtien2k3/userservice/model/entity/User.java b/user-service/src/main/java/com/hoangtien2k3/userservice/model/entity/User.java index fec8bd8e..51a1a2fd 100644 --- a/user-service/src/main/java/com/hoangtien2k3/userservice/model/entity/User.java +++ b/user-service/src/main/java/com/hoangtien2k3/userservice/model/entity/User.java @@ -4,8 +4,8 @@ import lombok.*; import org.hibernate.annotations.NaturalId; -import javax.persistence.*; -import javax.validation.constraints.*; +import jakarta.persistence.*; +import jakarta.validation.constraints.*; import java.util.HashSet; import java.util.Set; diff --git a/user-service/src/main/java/com/hoangtien2k3/userservice/security/jwt/JwtEntryPoint.java b/user-service/src/main/java/com/hoangtien2k3/userservice/security/jwt/JwtEntryPoint.java index 2066464e..0d89c72d 100644 --- a/user-service/src/main/java/com/hoangtien2k3/userservice/security/jwt/JwtEntryPoint.java +++ b/user-service/src/main/java/com/hoangtien2k3/userservice/security/jwt/JwtEntryPoint.java @@ -6,9 +6,9 @@ import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; @Component diff --git a/user-service/src/main/java/com/hoangtien2k3/userservice/security/jwt/JwtTokenFilter.java b/user-service/src/main/java/com/hoangtien2k3/userservice/security/jwt/JwtTokenFilter.java index 7da5aeb3..66187059 100644 --- a/user-service/src/main/java/com/hoangtien2k3/userservice/security/jwt/JwtTokenFilter.java +++ b/user-service/src/main/java/com/hoangtien2k3/userservice/security/jwt/JwtTokenFilter.java @@ -10,10 +10,10 @@ import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.web.filter.OncePerRequestFilter; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; public class JwtTokenFilter extends OncePerRequestFilter { diff --git a/user-service/src/main/java/com/hoangtien2k3/userservice/security/userprinciple/UserDetailService.java b/user-service/src/main/java/com/hoangtien2k3/userservice/security/userprinciple/UserDetailService.java index e9f93669..c6a31bc7 100644 --- a/user-service/src/main/java/com/hoangtien2k3/userservice/security/userprinciple/UserDetailService.java +++ b/user-service/src/main/java/com/hoangtien2k3/userservice/security/userprinciple/UserDetailService.java @@ -7,8 +7,7 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Service; - -import javax.transaction.Transactional; +import org.springframework.transaction.annotation.Transactional; @Service public class UserDetailService implements UserDetailsService { @@ -40,4 +39,4 @@ public UserDetails loadUserByPhone(String phone) { return UserPrinciple.build(user); } -} +} \ No newline at end of file diff --git a/user-service/src/main/java/com/hoangtien2k3/userservice/service/UserService.java b/user-service/src/main/java/com/hoangtien2k3/userservice/service/UserService.java index 8a965949..39229059 100644 --- a/user-service/src/main/java/com/hoangtien2k3/userservice/service/UserService.java +++ b/user-service/src/main/java/com/hoangtien2k3/userservice/service/UserService.java @@ -11,21 +11,16 @@ public interface UserService { Mono register(SignUp signUp); - Mono login(Login signInForm); - Mono logout(); - Mono update(Long userId, SignUp update); - Mono changePassword(ChangePasswordRequest request); - - // Mono resetPassword(ResetPasswordRequest resetPasswordRequest); Mono delete(Long id); - Mono findById(Long userId); - Mono findByUsername(String userName); - Mono> findAllUsers(int page, int size, String sortBy, String sortOrder); + + boolean existsByUsername(String username); + boolean existsByEmail(String email); + boolean existsByPhoneNumber(String phone); } \ No newline at end of file diff --git a/user-service/src/main/java/com/hoangtien2k3/userservice/service/impl/RoleServiceImpl.java b/user-service/src/main/java/com/hoangtien2k3/userservice/service/impl/RoleServiceImpl.java index 00f5aa4b..0dd9a824 100644 --- a/user-service/src/main/java/com/hoangtien2k3/userservice/service/impl/RoleServiceImpl.java +++ b/user-service/src/main/java/com/hoangtien2k3/userservice/service/impl/RoleServiceImpl.java @@ -11,7 +11,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import javax.transaction.Transactional; +import jakarta.transaction.Transactional; + import java.util.*; @Service diff --git a/user-service/src/main/java/com/hoangtien2k3/userservice/service/impl/UserServiceImpl.java b/user-service/src/main/java/com/hoangtien2k3/userservice/service/impl/UserServiceImpl.java index 6456ef21..1397e514 100644 --- a/user-service/src/main/java/com/hoangtien2k3/userservice/service/impl/UserServiceImpl.java +++ b/user-service/src/main/java/com/hoangtien2k3/userservice/service/impl/UserServiceImpl.java @@ -20,7 +20,6 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.DataAccessException; import org.springframework.data.domain.*; -import org.springframework.http.HttpStatus; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -31,13 +30,14 @@ import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; -import javax.transaction.Transactional; +import jakarta.transaction.Transactional; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; @Service +@Transactional public class UserServiceImpl implements UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; @@ -45,30 +45,31 @@ public class UserServiceImpl implements UserService { private final UserDetailService userDetailsService; private final ModelMapper modelMapper; private final RoleService roleService; - - Gson gson = new Gson(); // google.code.gson - @Autowired - EventProducer eventProducer; - - @Autowired - private WebClient.Builder webClientBuilder; - + private final WebClient.Builder webClientBuilder; + private final EventProducer eventProducer; + + private final Gson gson = new Gson(); + @Value("${refresh.token.url}") private String refreshTokenUrl; @Autowired public UserServiceImpl(UserRepository userRepository, - PasswordEncoder passwordEncoder, - JwtProvider jwtProvider, - UserDetailService userDetailsService, - ModelMapper modelMapper, - RoleService roleService) { + PasswordEncoder passwordEncoder, + JwtProvider jwtProvider, + UserDetailService userDetailsService, + ModelMapper modelMapper, + RoleService roleService, + WebClient.Builder webClientBuilder, + EventProducer eventProducer) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; this.jwtProvider = jwtProvider; this.userDetailsService = userDetailsService; this.modelMapper = modelMapper; this.roleService = roleService; + this.webClientBuilder = webClientBuilder; + this.eventProducer = eventProducer; } @Override @@ -112,7 +113,7 @@ private RoleName mapToRoleName(String roleName) { public Mono login(Login signInForm) { return Mono.fromCallable(() -> { String usernameOrEmail = signInForm.getUsername(); - boolean isEmail = usernameOrEmail.contains("@gmail.com"); + boolean isEmail = usernameOrEmail.contains("@"); UserDetails userDetails; if (isEmail) { @@ -156,7 +157,7 @@ public Mono login(Login signInForm) { .roles(userPrinciple.roles()) .build()) .build(); - }).subscribeOn(Schedulers.boundedElastic()).onErrorResume(Mono::error); + }).subscribeOn(Schedulers.boundedElastic()); } @Override @@ -168,7 +169,7 @@ public Mono logout() { String currentToken = getCurrentToken(); - if (authentication != null && authentication.isAuthenticated()) { + if (authentication != null && authentication.isAuthenticated() && currentToken != null) { // Invalidate the current token by reducing its expiration time String updatedToken = jwtProvider.reduceTokenExpiration(currentToken); } @@ -191,7 +192,6 @@ private String getCurrentToken() { return null; } - @Transactional @Override public Mono update(Long id, SignUp updateDTO) { return Mono.fromCallable(() -> { @@ -205,7 +205,6 @@ public Mono update(Long id, SignUp updateDTO) { }).subscribeOn(Schedulers.boundedElastic()); } - @Transactional @Override public Mono changePassword(ChangePasswordRequest request) { return Mono.fromCallable(() -> { @@ -271,23 +270,19 @@ private boolean validateNewPassword(String newPassword, String confirmPassword) return Objects.equals(newPassword, confirmPassword); } - @Transactional @Override public Mono delete(Long id) { - return Mono.fromRunnable(() -> userRepository.findById(id) - .ifPresentOrElse( - user -> { - try { - userRepository.delete(user); - } catch (DataAccessException e) { - throw new RuntimeException("Error deleting user with userId: " + id, e); - } - }, - () -> { - throw new UserNotFoundException("User not found for userId: " + id); - })) - .subscribeOn(Schedulers.boundedElastic()) - .then(Mono.just("User with id " + id + " deleted successfully.")); + return Mono.fromCallable(() -> { + User user = userRepository.findById(id) + .orElseThrow(() -> new UserNotFoundException("User not found for userId: " + id)); + + try { + userRepository.delete(user); + return "User with id " + id + " deleted successfully."; + } catch (DataAccessException e) { + throw new RuntimeException("Error deleting user with userId: " + id, e); + } + }).subscribeOn(Schedulers.boundedElastic()); } public Mono refreshToken(String refreshToken) { @@ -296,10 +291,12 @@ public Mono refreshToken(String refreshToken) { .uri(refreshTokenUrl) .header("Refresh-Token", refreshToken) .retrieve() - .onStatus(HttpStatus::is4xxClientError, - clientResponse -> Mono.error(new IllegalArgumentException("Refresh token không hợp lệ"))) + .onStatus( + status -> status.value() >= 400 && status.value() < 500, + response -> Mono.error(new IllegalArgumentException("Refresh token không hợp lệ")) + ) .bodyToMono(JwtResponseMessage.class) - .map(JwtResponseMessage::getAccessToken); // Sử dụng getAccessToken để lấy token từ JwtResponse + .map(JwtResponseMessage::getAccessToken); } @Override @@ -326,16 +323,18 @@ public Mono> findAllUsers(int page, int size, String sortBy, Strin }).subscribeOn(Schedulers.boundedElastic()); } + @Override public boolean existsByUsername(String username) { return userRepository.existsByUsername(username); } + @Override public boolean existsByEmail(String email) { return userRepository.existsByEmail(email); } + @Override public boolean existsByPhoneNumber(String phone) { return userRepository.existsByPhoneNumber(phone); } - -} +} \ No newline at end of file