From bf05c58b65b94b857033a1087da4cc6bc2b310ec Mon Sep 17 00:00:00 2001
From: Khanh Nguyen <119989010+ndkhanh-axonivy@users.noreply.github.com>
Date: Fri, 12 Jul 2024 13:38:34 +0700
Subject: [PATCH 1/3] Feature/MARP 357 create detail pages for new market
website rating (#28)
---
.github/workflows/service-dev-build.yml | 2 +-
marketplace-service/pom.xml | 10 +
.../market/MarketplaceServiceApplication.java | 6 +-
.../assembler/FeedbackModelAssembler.java | 57 ++++++
.../assembler/ProductModelAssembler.java | 11 +-
.../config/MarketApiDocumentConfig.java | 10 +-
.../config/MarketHeaderInterceptor.java | 8 +-
.../axonivy/market/config/MongoConfig.java | 11 +-
.../market/constants/EntityConstants.java | 1 +
.../market/constants/GitHubConstants.java | 17 ++
.../constants/RequestMappingConstants.java | 1 +
.../market/controller/AppController.java | 12 +-
.../market/controller/FeedbackController.java | 107 ++++++++++
.../market/controller/OAuth2Controller.java | 48 +++++
.../market/controller/ProductController.java | 44 ++--
.../market/controller/UserController.java | 27 ---
.../com/axonivy/market/entity/Feedback.java | 55 +++++
.../axonivy/market/entity/GitHubRepoMeta.java | 7 +-
.../market/entity/MavenArtifactModel.java | 9 +-
.../java/com/axonivy/market/entity/User.java | 47 ++++-
.../com/axonivy/market/enums/ErrorCode.java | 6 +-
.../com/axonivy/market/enums/FileStatus.java | 4 +-
.../com/axonivy/market/enums/FileType.java | 4 +-
.../com/axonivy/market/enums/SortOption.java | 4 +-
.../com/axonivy/market/enums/TypeOption.java | 4 +-
.../market/exceptions/ExceptionHandlers.java | 44 +++-
.../model/InvalidParamException.java | 1 -
.../model/MissingHeaderException.java | 3 +
.../exceptions/model/NotFoundException.java | 4 +-
.../model/Oauth2ExchangeCodeException.java | 19 ++
.../market/github/model/ArchivedArtifact.java | 1 -
.../market/github/model/GitHubFile.java | 5 +-
.../com/axonivy/market/github/model/Meta.java | 5 +-
.../service/GHAxonIvyMarketRepoService.java | 7 +-
.../market/github/service/GitHubService.java | 22 +-
.../impl/GHAxonIvyMarketRepoServiceImpl.java | 27 +--
.../service/impl/GitHubServiceImpl.java | 89 ++++++++-
.../axonivy/market/model/DisplayValue.java | 6 +-
.../axonivy/market/model/FeedbackModel.java | 42 ++++
.../market/model/MultilingualismValue.java | 4 +-
.../market/model/Oauth2AuthorizationCode.java | 12 ++
.../axonivy/market/model/ProductModel.java | 14 +-
.../axonivy/market/model/ProductRating.java | 16 ++
.../market/repository/FeedbackRepository.java | 21 ++
.../repository/GitHubRepoMetaRepository.java | 3 +-
.../market/repository/ProductRepository.java | 3 +-
.../market/repository/UserRepository.java | 1 +
.../market/schedulingtask/ScheduledTasks.java | 6 +-
.../market/service/FeedbackService.java | 17 ++
.../axonivy/market/service/JwtService.java | 10 +
.../market/service/ProductService.java | 3 +-
.../axonivy/market/service/UserService.java | 7 +-
.../service/impl/FeedbackServiceImpl.java | 102 ++++++++++
.../market/service/impl/JwtServiceImpl.java | 54 +++++
.../market/service/impl/UserServiceImpl.java | 18 +-
.../src/main/resources/application.properties | 4 +
.../market/controller/AppControllerTest.java | 6 +-
.../controller/FeedbackControllerTest.java | 161 +++++++++++++++
.../controller/OAuth2ControllerTest.java | 66 ++++++
.../controller/ProductControllerTest.java | 39 ++--
.../market/controller/UserControllerTest.java | 27 ---
.../market/handler/ExceptionHandlersTest.java | 17 +-
.../service/FeedbackServiceImplTest.java | 189 ++++++++++++++++++
.../GHAxonIvyMarketRepoServiceImplTest.java | 31 ++-
.../market/service/GitHubServiceImplTest.java | 35 +++-
.../market/service/JwtServiceImplTest.java | 94 +++++++++
.../market/service/SchedulingTasksTest.java | 7 +-
.../market/service/UserServiceImplTest.java | 27 ++-
68 files changed, 1490 insertions(+), 291 deletions(-)
create mode 100644 marketplace-service/src/main/java/com/axonivy/market/assembler/FeedbackModelAssembler.java
create mode 100644 marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java
create mode 100644 marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java
delete mode 100644 marketplace-service/src/main/java/com/axonivy/market/controller/UserController.java
create mode 100644 marketplace-service/src/main/java/com/axonivy/market/entity/Feedback.java
create mode 100644 marketplace-service/src/main/java/com/axonivy/market/exceptions/model/Oauth2ExchangeCodeException.java
create mode 100644 marketplace-service/src/main/java/com/axonivy/market/model/FeedbackModel.java
create mode 100644 marketplace-service/src/main/java/com/axonivy/market/model/Oauth2AuthorizationCode.java
create mode 100644 marketplace-service/src/main/java/com/axonivy/market/model/ProductRating.java
create mode 100644 marketplace-service/src/main/java/com/axonivy/market/repository/FeedbackRepository.java
create mode 100644 marketplace-service/src/main/java/com/axonivy/market/service/FeedbackService.java
create mode 100644 marketplace-service/src/main/java/com/axonivy/market/service/JwtService.java
create mode 100644 marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java
create mode 100644 marketplace-service/src/main/java/com/axonivy/market/service/impl/JwtServiceImpl.java
create mode 100644 marketplace-service/src/test/java/com/axonivy/market/controller/FeedbackControllerTest.java
create mode 100644 marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java
delete mode 100644 marketplace-service/src/test/java/com/axonivy/market/controller/UserControllerTest.java
create mode 100644 marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java
create mode 100644 marketplace-service/src/test/java/com/axonivy/market/service/JwtServiceImplTest.java
diff --git a/.github/workflows/service-dev-build.yml b/.github/workflows/service-dev-build.yml
index e310a9378..3700e5322 100644
--- a/.github/workflows/service-dev-build.yml
+++ b/.github/workflows/service-dev-build.yml
@@ -39,4 +39,4 @@ jobs:
- name: Restart Tomcat server
run: |
sudo systemctl stop tomcat
- sudo systemctl start tomcat
+ sudo systemctl start tomcat
\ No newline at end of file
diff --git a/marketplace-service/pom.xml b/marketplace-service/pom.xml
index 947e700d7..d6a0d9a67 100644
--- a/marketplace-service/pom.xml
+++ b/marketplace-service/pom.xml
@@ -64,6 +64,16 @@
github-api
1.321
+
+ io.jsonwebtoken
+ jjwt
+ 0.9.1
+
+
+ javax.xml.bind
+ jaxb-api
+ 2.3.1
+
diff --git a/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java b/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java
index 06660037c..52cdb27d9 100644
--- a/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java
+++ b/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java
@@ -1,5 +1,7 @@
package com.axonivy.market;
+import com.axonivy.market.service.ProductService;
+import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.time.StopWatch;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@@ -9,10 +11,6 @@
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
-import com.axonivy.market.service.ProductService;
-
-import lombok.extern.log4j.Log4j2;
-
@Log4j2
@EnableAsync
@EnableScheduling
diff --git a/marketplace-service/src/main/java/com/axonivy/market/assembler/FeedbackModelAssembler.java b/marketplace-service/src/main/java/com/axonivy/market/assembler/FeedbackModelAssembler.java
new file mode 100644
index 000000000..a981b099a
--- /dev/null
+++ b/marketplace-service/src/main/java/com/axonivy/market/assembler/FeedbackModelAssembler.java
@@ -0,0 +1,57 @@
+package com.axonivy.market.assembler;
+
+import com.axonivy.market.controller.FeedbackController;
+import com.axonivy.market.entity.Feedback;
+import com.axonivy.market.entity.User;
+import com.axonivy.market.exceptions.model.NotFoundException;
+import com.axonivy.market.model.FeedbackModel;
+import com.axonivy.market.service.UserService;
+import lombok.extern.log4j.Log4j2;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport;
+import org.springframework.stereotype.Component;
+
+import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
+import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
+
+@Log4j2
+@Component
+public class FeedbackModelAssembler extends RepresentationModelAssemblerSupport {
+
+ private final UserService userService;
+
+ public FeedbackModelAssembler(UserService userService) {
+ super(Feedback.class, FeedbackModel.class);
+ this.userService = userService;
+ }
+
+ @Override
+ public FeedbackModel toModel(Feedback feedback) {
+ FeedbackModel resource = new FeedbackModel();
+ resource.add(linkTo(methodOn(FeedbackController.class).findFeedback(feedback.getId()))
+ .withSelfRel());
+ return createResource(resource, feedback);
+ }
+
+ private FeedbackModel createResource(FeedbackModel model, Feedback feedback) {
+ User user;
+ try {
+ user = userService.findUser(feedback.getUserId());
+ }
+ catch (NotFoundException e) {
+ log.warn(e.getMessage());
+ user = new User();
+ }
+ model.setId(feedback.getId());
+ model.setUsername(StringUtils.isBlank(user.getName()) ? user.getUsername() : user.getName());
+ model.setUserAvatarUrl(user.getAvatarUrl());
+ model.setUserProvider(user.getProvider());
+ model.setProductId(feedback.getProductId());
+ model.setContent(feedback.getContent());
+ model.setRating(feedback.getRating());
+ model.setCreatedAt(feedback.getCreatedAt());
+ model.setUpdatedAt(feedback.getUpdatedAt());
+ return model;
+ }
+
+}
diff --git a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java
index 00b94a52f..a50d82d1a 100644
--- a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java
+++ b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java
@@ -1,14 +1,13 @@
package com.axonivy.market.assembler;
-import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
-import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
-
-import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport;
-import org.springframework.stereotype.Component;
-
import com.axonivy.market.controller.ProductDetailsController;
import com.axonivy.market.entity.Product;
import com.axonivy.market.model.ProductModel;
+import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport;
+import org.springframework.stereotype.Component;
+
+import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
+import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
@Component
public class ProductModelAssembler extends RepresentationModelAssemblerSupport {
diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java b/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java
index 61b6a0773..805f30120 100644
--- a/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java
+++ b/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java
@@ -1,15 +1,15 @@
package com.axonivy.market.config;
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.PathItem;
+import io.swagger.v3.oas.models.media.StringSchema;
+import io.swagger.v3.oas.models.parameters.Parameter;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import io.swagger.v3.oas.models.Operation;
-import io.swagger.v3.oas.models.PathItem;
-import io.swagger.v3.oas.models.media.StringSchema;
-import io.swagger.v3.oas.models.parameters.Parameter;
-import static com.axonivy.market.constants.CommonConstants.*;
+import static com.axonivy.market.constants.CommonConstants.REQUESTED_BY;
@Configuration
public class MarketApiDocumentConfig {
diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java b/marketplace-service/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java
index 963706069..83b281062 100644
--- a/marketplace-service/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java
+++ b/marketplace-service/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java
@@ -1,15 +1,13 @@
package com.axonivy.market.config;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Component;
-import org.springframework.web.servlet.HandlerInterceptor;
-
import com.axonivy.market.constants.CommonConstants;
import com.axonivy.market.exceptions.model.MissingHeaderException;
-
import io.swagger.v3.oas.models.PathItem.HttpMethod;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class MarketHeaderInterceptor implements HandlerInterceptor {
diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java b/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java
index a6cd2bc05..7f558f1cc 100644
--- a/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java
+++ b/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java
@@ -1,10 +1,15 @@
package com.axonivy.market.config;
+import com.mongodb.ConnectionString;
+import com.mongodb.MongoClientSettings;
+import com.mongodb.client.MongoClient;
+import com.mongodb.client.MongoClients;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
+import org.springframework.data.mongodb.config.EnableMongoAuditing;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
@@ -12,13 +17,9 @@
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
-import com.mongodb.ConnectionString;
-import com.mongodb.MongoClientSettings;
-import com.mongodb.client.MongoClient;
-import com.mongodb.client.MongoClients;
-
@Configuration
@EnableMongoRepositories(basePackages = "com.axonivy.market.repository")
+@EnableMongoAuditing
public class MongoConfig extends AbstractMongoClientConfiguration {
@Value("${spring.data.mongodb.host}")
diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java
index 76c1c45ab..0d9752cb9 100644
--- a/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java
+++ b/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java
@@ -9,4 +9,5 @@ public class EntityConstants {
public static final String PRODUCT = "Product";
public static final String MAVEN_ARTIFACT_VERSION = "MavenArtifactVersion";
public static final String GH_REPO_META = "GitHubRepoMeta";
+ public static final String FEEDBACK = "Feedback";
}
diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java
index 33c84df88..39d35a77c 100644
--- a/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java
+++ b/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java
@@ -10,4 +10,21 @@ public class GitHubConstants {
public static final String AXONIVY_MARKETPLACE_PATH = "market";
public static final String DEFAULT_BRANCH = "feature/MARP-463-Multilingualism-for-Website";
public static final String PRODUCT_JSON_FILE_PATH_FORMAT = "%s/product.json";
+ public static final String GITHUB_PROVIDER_NAME = "GitHub";
+ public static final String GITHUB_GET_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
+
+ @NoArgsConstructor(access = AccessLevel.PRIVATE)
+ public static class Json {
+ public static final String ACCESS_TOKEN = "access_token";
+ public static final String TOKEN = "token";
+ public static final String CLIENT_ID = "client_id";
+ public static final String CLIENT_SECRET = "client_secret";
+ public static final String CODE = "code";
+ public static final String ERROR = "error";
+ public static final String ERROR_DESCRIPTION = "error";
+ public static final String USER_ID = "id";
+ public static final String USER_NAME = "name";
+ public static final String USER_AVATAR_URL = "avatar_url";
+ public static final String USER_LOGIN_NAME = "login";
+ }
}
diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java
index f4687a442..5efdc47e6 100644
--- a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java
+++ b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java
@@ -11,5 +11,6 @@ public class RequestMappingConstants {
public static final String USER_MAPPING = "/user";
public static final String PRODUCT = API + "/product";
public static final String PRODUCT_DETAILS = API + "/product-details";
+ public static final String FEEDBACK = API + "/feedback";
public static final String SWAGGER_URL = "/swagger-ui/index.html";
}
\ No newline at end of file
diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java
index 60523b72a..45f712d2d 100644
--- a/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java
+++ b/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java
@@ -1,8 +1,8 @@
package com.axonivy.market.controller;
-import static com.axonivy.market.constants.RequestMappingConstants.ROOT;
-import static com.axonivy.market.constants.RequestMappingConstants.SWAGGER_URL;
-
+import com.axonivy.market.enums.ErrorCode;
+import com.axonivy.market.model.Message;
+import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
@@ -10,10 +10,8 @@
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
-import com.axonivy.market.enums.ErrorCode;
-import com.axonivy.market.model.Message;
-
-import lombok.extern.log4j.Log4j2;
+import static com.axonivy.market.constants.RequestMappingConstants.ROOT;
+import static com.axonivy.market.constants.RequestMappingConstants.SWAGGER_URL;
@Log4j2
@RestController
diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java
new file mode 100644
index 000000000..3232f42be
--- /dev/null
+++ b/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java
@@ -0,0 +1,107 @@
+package com.axonivy.market.controller;
+
+import com.axonivy.market.assembler.FeedbackModelAssembler;
+import com.axonivy.market.entity.Feedback;
+import com.axonivy.market.model.FeedbackModel;
+import com.axonivy.market.model.ProductRating;
+import com.axonivy.market.service.FeedbackService;
+import com.axonivy.market.service.JwtService;
+import io.jsonwebtoken.Claims;
+import io.swagger.v3.oas.annotations.Operation;
+import jakarta.validation.Valid;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PagedResourcesAssembler;
+import org.springframework.hateoas.PagedModel;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
+
+import java.net.URI;
+import java.util.List;
+
+import static com.axonivy.market.constants.RequestMappingConstants.FEEDBACK;
+
+@RestController
+@RequestMapping(FEEDBACK)
+public class FeedbackController {
+
+ private final FeedbackService feedbackService;
+ private final JwtService jwtService;
+ private final FeedbackModelAssembler feedbackModelAssembler;
+
+ private final PagedResourcesAssembler pagedResourcesAssembler;
+
+ public FeedbackController(FeedbackService feedbackService, JwtService jwtService, FeedbackModelAssembler feedbackModelAssembler, PagedResourcesAssembler pagedResourcesAssembler) {
+ this.feedbackService = feedbackService;
+ this.jwtService = jwtService;
+ this.feedbackModelAssembler = feedbackModelAssembler;
+ this.pagedResourcesAssembler = pagedResourcesAssembler;
+ }
+
+ @Operation(summary = "Find all feedbacks by product id")
+ @GetMapping("/product/{productId}")
+ public ResponseEntity> findFeedbacks(@PathVariable String productId, Pageable pageable) {
+ Page results = feedbackService.findFeedbacks(productId, pageable);
+ if (results.isEmpty()) {
+ return generateEmptyPagedModel();
+ }
+ var responseContent = new PageImpl<>(results.getContent(), pageable, results.getTotalElements());
+ var pageResources = pagedResourcesAssembler.toModel(responseContent, feedbackModelAssembler);
+ return new ResponseEntity<>(pageResources, HttpStatus.OK);
+ }
+
+ @GetMapping("/{id}")
+ public ResponseEntity findFeedback(@PathVariable("id") String id) {
+ Feedback feedback = feedbackService.findFeedback(id);
+ return ResponseEntity.ok(feedbackModelAssembler.toModel(feedback));
+ }
+
+ @Operation(summary = "Find all feedbacks by user id and product id")
+ @GetMapping()
+ public ResponseEntity findFeedbackByUserIdAndProductId(
+ @RequestParam String userId,
+ @RequestParam String productId) {
+ Feedback feedback = feedbackService.findFeedbackByUserIdAndProductId(userId, productId);
+ return ResponseEntity.ok(feedbackModelAssembler.toModel(feedback));
+ }
+
+ @PostMapping
+ public ResponseEntity createFeedback(@RequestBody @Valid Feedback feedback, @RequestHeader(value = "Authorization", required = false) String authorizationHeader) {
+ String token = null;
+ if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
+ token = authorizationHeader.substring(7); // Remove "Bearer " prefix
+ }
+
+ // Validate the token
+ if (token == null || !jwtService.validateToken(token)) {
+ return ResponseEntity.status(401).build(); // Unauthorized if token is missing or invalid
+ }
+
+ Claims claims = jwtService.getClaimsFromToken(token);
+ feedback.setUserId(claims.getSubject());
+ Feedback newFeedback = feedbackService.upsertFeedback(feedback);
+
+ URI location = ServletUriComponentsBuilder.fromCurrentRequest()
+ .path("/{id}")
+ .buildAndExpand(newFeedback.getId())
+ .toUri();
+
+ return ResponseEntity.created(location).build();
+ }
+
+ @Operation(summary = "Find rating information of product by id")
+ @GetMapping("/product/{productId}/rating")
+ public ResponseEntity> getProductRating(@PathVariable("productId") String productId) {
+ return ResponseEntity.ok(feedbackService.getProductRatingById(productId));
+ }
+
+ @SuppressWarnings("unchecked")
+ private ResponseEntity> generateEmptyPagedModel() {
+ var emptyPagedModel = (PagedModel) pagedResourcesAssembler
+ .toEmptyModel(Page.empty(), FeedbackModel.class);
+ return new ResponseEntity<>(emptyPagedModel, HttpStatus.OK);
+ }
+}
diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java b/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java
new file mode 100644
index 000000000..a3ebbca64
--- /dev/null
+++ b/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java
@@ -0,0 +1,48 @@
+package com.axonivy.market.controller;
+
+import com.axonivy.market.constants.GitHubConstants;
+import com.axonivy.market.entity.User;
+import com.axonivy.market.github.service.GitHubService;
+import com.axonivy.market.model.Oauth2AuthorizationCode;
+import com.axonivy.market.service.JwtService;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Collections;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/auth")
+public class OAuth2Controller {
+
+ @Value("${spring.security.oauth2.client.registration.github.client-id}")
+ private String clientId;
+
+ @Value("${spring.security.oauth2.client.registration.github.client-secret}")
+ private String clientSecret;
+
+ private final GitHubService gitHubService;
+
+ private final JwtService jwtService;
+
+ public OAuth2Controller(GitHubService gitHubService, JwtService jwtService) {
+ this.gitHubService = gitHubService;
+ this.jwtService = jwtService;
+ }
+
+ @PostMapping("/github/login")
+ public ResponseEntity> gitHubLogin(@RequestBody Oauth2AuthorizationCode oauth2AuthorizationCode) {
+ Map tokenResponse = gitHubService.getAccessToken(oauth2AuthorizationCode.getCode(), clientId, clientSecret);
+ String accessToken = (String) tokenResponse.get(GitHubConstants.Json.ACCESS_TOKEN);
+
+ User user = gitHubService.getAndUpdateUser(accessToken);
+
+ String jwtToken = jwtService.generateToken(user);
+
+ return ResponseEntity.ok().body(Collections.singletonMap(GitHubConstants.Json.TOKEN, jwtToken));
+ }
+}
\ No newline at end of file
diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java
index ce273cd33..6dbd73c4d 100644
--- a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java
+++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java
@@ -1,8 +1,12 @@
package com.axonivy.market.controller;
-import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT;
-import static com.axonivy.market.constants.RequestMappingConstants.SYNC;
-
+import com.axonivy.market.assembler.ProductModelAssembler;
+import com.axonivy.market.entity.Product;
+import com.axonivy.market.enums.ErrorCode;
+import com.axonivy.market.model.Message;
+import com.axonivy.market.model.ProductModel;
+import com.axonivy.market.service.ProductService;
+import io.swagger.v3.oas.annotations.Operation;
import org.apache.commons.lang3.time.StopWatch;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
@@ -11,32 +15,22 @@
import org.springframework.hateoas.PagedModel;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
-import com.axonivy.market.assembler.ProductModelAssembler;
-import com.axonivy.market.entity.Product;
-import com.axonivy.market.enums.ErrorCode;
-import com.axonivy.market.model.Message;
-import com.axonivy.market.model.ProductModel;
-import com.axonivy.market.service.ProductService;
-
-import io.swagger.v3.oas.annotations.Operation;
+import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT;
+import static com.axonivy.market.constants.RequestMappingConstants.SYNC;
@RestController
@RequestMapping(PRODUCT)
public class ProductController {
- private final ProductService service;
+ private final ProductService productService;
private final ProductModelAssembler assembler;
private final PagedResourcesAssembler pagedResourcesAssembler;
- public ProductController(ProductService service, ProductModelAssembler assembler,
- PagedResourcesAssembler pagedResourcesAssembler) {
- this.service = service;
+ public ProductController(ProductService productService, ProductModelAssembler assembler,
+ PagedResourcesAssembler pagedResourcesAssembler) {
+ this.productService = productService;
this.assembler = assembler;
this.pagedResourcesAssembler = pagedResourcesAssembler;
}
@@ -44,14 +38,14 @@ public ProductController(ProductService service, ProductModelAssembler assembler
@Operation(summary = "Find all products", description = "Be default system will finds product by type as 'all'")
@GetMapping()
public ResponseEntity> findProducts(
- @RequestParam(required = true, name = "type") String type,
+ @RequestParam(name = "type") String type,
@RequestParam(required = false, name = "keyword") String keyword,
- @RequestParam(required = true, name = "language") String language, Pageable pageable) {
- Page results = service.findProducts(type, keyword, language, pageable);
+ @RequestParam(name = "language") String language, Pageable pageable) {
+ Page results = productService.findProducts(type, keyword, language, pageable);
if (results.isEmpty()) {
return generateEmptyPagedModel();
}
- var responseContent = new PageImpl(results.getContent(), pageable, results.getTotalElements());
+ var responseContent = new PageImpl<>(results.getContent(), pageable, results.getTotalElements());
var pageResources = pagedResourcesAssembler.toModel(responseContent, assembler);
return new ResponseEntity<>(pageResources, HttpStatus.OK);
}
@@ -60,7 +54,7 @@ public ResponseEntity> findProducts(
public ResponseEntity syncProducts() {
var stopWatch = new StopWatch();
stopWatch.start();
- var isAlreadyUpToDate = service.syncLatestDataFromMarketRepo();
+ var isAlreadyUpToDate = productService.syncLatestDataFromMarketRepo();
var message = new Message();
message.setHelpCode(ErrorCode.SUCCESSFUL.getCode());
message.setHelpText(ErrorCode.SUCCESSFUL.getHelpText());
diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/UserController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/UserController.java
deleted file mode 100644
index c83c7cc2b..000000000
--- a/marketplace-service/src/main/java/com/axonivy/market/controller/UserController.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.axonivy.market.controller;
-
-import com.axonivy.market.entity.User;
-import com.axonivy.market.service.UserService;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-import java.util.List;
-
-import static com.axonivy.market.constants.RequestMappingConstants.USER_MAPPING;
-
-@RestController
-@RequestMapping(USER_MAPPING)
-public class UserController {
- private final UserService userService;
-
- public UserController(UserService userService) {
- this.userService = userService;
- }
-
- @GetMapping
- public ResponseEntity> getAllUser() {
- return ResponseEntity.ok(userService.getAllUsers());
- }
-}
\ No newline at end of file
diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/Feedback.java b/marketplace-service/src/main/java/com/axonivy/market/entity/Feedback.java
new file mode 100644
index 000000000..166da0c23
--- /dev/null
+++ b/marketplace-service/src/main/java/com/axonivy/market/entity/Feedback.java
@@ -0,0 +1,55 @@
+package com.axonivy.market.entity;
+
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+import static com.axonivy.market.constants.EntityConstants.FEEDBACK;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@Document(FEEDBACK)
+public class Feedback implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 29519800556564714L;
+
+ @Id
+ private String id;
+
+ private String userId;
+
+ @NotBlank(message = "Product id cannot be blank")
+ private String productId;
+
+ @NotBlank(message = "Content cannot be blank")
+ @Size(max = 5, message = "Content length must be up to 250 characters")
+ private String content;
+
+ @Min(value = 1, message = "Rating should not be less than 1")
+ @Max(value = 5, message = "Rating should not be greater than 5")
+ private Integer rating;
+
+ @CreatedDate
+ private Date createdAt;
+
+ @LastModifiedDate
+ private Date updatedAt;
+
+ public void setContent(String content) {
+ this.content = content != null ? content.trim() : null;
+ }
+}
diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java b/marketplace-service/src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java
index 2e0770816..d2ef46fbf 100644
--- a/marketplace-service/src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java
+++ b/marketplace-service/src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java
@@ -1,12 +1,11 @@
package com.axonivy.market.entity;
-import static com.axonivy.market.constants.EntityConstants.GH_REPO_META;
-
+import lombok.Getter;
+import lombok.Setter;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
-import lombok.Getter;
-import lombok.Setter;
+import static com.axonivy.market.constants.EntityConstants.GH_REPO_META;
@Getter
@Setter
diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java b/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java
index 6b5328e26..2d48d4c6a 100644
--- a/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java
+++ b/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java
@@ -1,14 +1,13 @@
package com.axonivy.market.entity;
-import java.io.Serializable;
-import java.util.Objects;
-
-import org.springframework.data.annotation.Transient;
-
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
+import org.springframework.data.annotation.Transient;
+
+import java.io.Serializable;
+import java.util.Objects;
@AllArgsConstructor
@NoArgsConstructor
diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/User.java b/marketplace-service/src/main/java/com/axonivy/market/entity/User.java
index 1b88f1095..0f8e7b612 100644
--- a/marketplace-service/src/main/java/com/axonivy/market/entity/User.java
+++ b/marketplace-service/src/main/java/com/axonivy/market/entity/User.java
@@ -1,21 +1,48 @@
package com.axonivy.market.entity;
-import static com.axonivy.market.constants.EntityConstants.USER;
-
-import org.springframework.data.annotation.Id;
-import org.springframework.data.mongodb.core.mapping.Document;
-
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.index.Indexed;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+import static com.axonivy.market.constants.EntityConstants.USER;
@Getter
@Setter
@NoArgsConstructor
@Document(USER)
-public class User {
- @Id
- private String id;
- private String username;
- private String password;
+public class User implements Serializable {
+ @Serial
+ private static final long serialVersionUID = -1244486023332931059L;
+
+ @Id
+ private String id;
+
+ @Indexed(unique = true)
+ private String gitHubId;
+
+ private String provider;
+ private String username;
+ private String name;
+ private String avatarUrl;
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder().append(id).hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || this.getClass() != obj.getClass()) {
+ return false;
+ }
+ return new EqualsBuilder().append(id, ((User) obj).getId()).isEquals();
+ }
}
diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java b/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java
index de9d54c28..7aef2b47c 100644
--- a/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java
+++ b/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java
@@ -18,8 +18,12 @@
public enum ErrorCode {
SUCCESSFUL("0000", "SUCCESSFUL"), PRODUCT_FILTER_INVALID("1101", "PRODUCT_FILTER_INVALID"),
PRODUCT_SORT_INVALID("1102", "PRODUCT_SORT_INVALID"),
+ PRODUCT_NOT_FOUND("1103", "PRODUCT_NOT_FOUND"),
GH_FILE_STATUS_INVALID("0201", "GIT_HUB_FILE_STATUS_INVALID"),
- GH_FILE_TYPE_INVALID("0202", "GIT_HUB_FILE_TYPE_INVALID");
+ GH_FILE_TYPE_INVALID("0202", "GIT_HUB_FILE_TYPE_INVALID"),
+ USER_NOT_FOUND("2103", "USER_NOT_FOUND"),
+ FEEDBACK_NOT_FOUND("3103", "FEEDBACK_NOT_FOUND"),
+ ARGUMENT_BAD_REQUEST("4000", "ARGUMENT_BAD_REQUEST");
String code;
String helpText;
diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/FileStatus.java b/marketplace-service/src/main/java/com/axonivy/market/enums/FileStatus.java
index d75ca9f54..eb155031f 100644
--- a/marketplace-service/src/main/java/com/axonivy/market/enums/FileStatus.java
+++ b/marketplace-service/src/main/java/com/axonivy/market/enums/FileStatus.java
@@ -1,11 +1,9 @@
package com.axonivy.market.enums;
-import org.apache.commons.lang3.StringUtils;
-
import com.axonivy.market.exceptions.model.NotFoundException;
-
import lombok.AllArgsConstructor;
import lombok.Getter;
+import org.apache.commons.lang3.StringUtils;
@Getter
@AllArgsConstructor
diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/FileType.java b/marketplace-service/src/main/java/com/axonivy/market/enums/FileType.java
index 75bb5beb9..7703e3cbd 100644
--- a/marketplace-service/src/main/java/com/axonivy/market/enums/FileType.java
+++ b/marketplace-service/src/main/java/com/axonivy/market/enums/FileType.java
@@ -1,11 +1,9 @@
package com.axonivy.market.enums;
-import org.apache.commons.lang3.StringUtils;
-
import com.axonivy.market.exceptions.model.NotFoundException;
-
import lombok.AllArgsConstructor;
import lombok.Getter;
+import org.apache.commons.lang3.StringUtils;
@Getter
@AllArgsConstructor
diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java b/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java
index 59914ab90..c3e9714e3 100644
--- a/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java
+++ b/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java
@@ -1,11 +1,9 @@
package com.axonivy.market.enums;
-import org.apache.commons.lang3.StringUtils;
-
import com.axonivy.market.exceptions.model.InvalidParamException;
-
import lombok.AllArgsConstructor;
import lombok.Getter;
+import org.apache.commons.lang3.StringUtils;
@Getter
@AllArgsConstructor
diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java b/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java
index 3b513ea4a..1c30aca92 100644
--- a/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java
+++ b/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java
@@ -1,10 +1,8 @@
package com.axonivy.market.enums;
-import org.apache.commons.lang3.StringUtils;
-
import com.axonivy.market.exceptions.model.InvalidParamException;
-
import lombok.Getter;
+import org.apache.commons.lang3.StringUtils;
@Getter
public enum TypeOption {
diff --git a/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java
index 3dc315608..d9b1ab725 100644
--- a/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java
+++ b/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java
@@ -1,19 +1,47 @@
package com.axonivy.market.exceptions;
+import com.axonivy.market.enums.ErrorCode;
+import com.axonivy.market.exceptions.model.InvalidParamException;
+import com.axonivy.market.exceptions.model.MissingHeaderException;
+import com.axonivy.market.exceptions.model.NotFoundException;
+import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException;
+import com.axonivy.market.model.Message;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
+import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
-import com.axonivy.market.exceptions.model.InvalidParamException;
-import com.axonivy.market.exceptions.model.MissingHeaderException;
-import com.axonivy.market.exceptions.model.NotFoundException;
-import com.axonivy.market.model.Message;
+import java.util.ArrayList;
+import java.util.List;
@ControllerAdvice
public class ExceptionHandlers extends ResponseEntityExceptionHandler {
+ @Override
+ protected ResponseEntity