From 46855f100b2cd3f1776d11f918e9d364b69d8898 Mon Sep 17 00:00:00 2001 From: Hoan Nguyen <83745591+nqhoan-axonivy@users.noreply.github.com> Date: Mon, 29 Jul 2024 14:30:13 +0700 Subject: [PATCH 1/3] Feature/MARP-700 authenticate for sync products api (#68) --- .github/workflows/docker-build.yml | 1 + marketplace-build/.env | 3 +- .../com/axonivy/market/config/WebConfig.java | 10 +++- .../constants/RequestMappingConstants.java | 9 +++ .../constants/RequestParamConstants.java | 20 +++++++ .../market/controller/AppController.java | 12 ++-- .../market/controller/FeedbackController.java | 56 ++++++++++-------- .../market/controller/OAuth2Controller.java | 55 ++++++++++-------- .../market/controller/ProductController.java | 46 +++++++++------ .../controller/ProductDetailsController.java | 51 ++++++++++------- .../market/github/model/GitHubProperty.java | 3 + .../market/github/service/GitHubService.java | 17 ++++-- .../service/impl/GitHubServiceImpl.java | 57 ++++++++++++------- .../src/main/resources/application.properties | 6 +- .../controller/OAuth2ControllerTest.java | 27 +++++---- .../market/service/SchedulingTasksTest.java | 10 ++-- 16 files changed, 240 insertions(+), 143 deletions(-) create mode 100644 marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 347360989..dbdb3aaf1 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -67,6 +67,7 @@ jobs: sed -i "s/^MARKET_GITHUB_OAUTH_APP_CLIENT_ID=.*$/MARKET_GITHUB_OAUTH_APP_CLIENT_ID=$OAUTH_APP_CLIENT_ID/" $ENV_FILE sed -i "s/^MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=.*$/MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=$OAUTH_APP_CLIENT_SECRET/" $ENV_FILE sed -i "s/^MARKET_JWT_SECRET_KEY=.*$/MARKET_JWT_SECRET_KEY=$MARKET_JWT_SECRET_KEY/" $ENV_FILE + sed -i "s/^MARKET_CORS_ALLOWED_ORIGIN=.*$/MARKET_CORS_ALLOWED_ORIGIN=$MARKET_CORS_ALLOWED_ORIGIN/" $ENV_FILE - name: Build and bring up containers without cache working-directory: ./marketplace-build diff --git a/marketplace-build/.env b/marketplace-build/.env index c08dfe92a..a6dee6c27 100644 --- a/marketplace-build/.env +++ b/marketplace-build/.env @@ -8,4 +8,5 @@ MARKET_GITHUB_TOKEN= MARKETPLACE_INSTALLATION_URL= MARKET_GITHUB_OAUTH_APP_CLIENT_ID= MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET= -MARKET_JWT_SECRET_KEY= \ No newline at end of file +MARKET_JWT_SECRET_KEY= +MARKET_CORS_ALLOWED_ORIGIN= \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/WebConfig.java b/marketplace-service/src/main/java/com/axonivy/market/config/WebConfig.java index 0ff332d8c..833903bb5 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/config/WebConfig.java +++ b/marketplace-service/src/main/java/com/axonivy/market/config/WebConfig.java @@ -9,13 +9,17 @@ @Configuration public class WebConfig implements WebMvcConfigurer { + private static final String ALL_MAPPINGS = "/**"; private static final String[] EXCLUDE_PATHS = { "/", "/swagger-ui/**", "/api-docs/**" }; private static final String[] ALLOWED_HEADERS = { "Accept-Language", "Content-Type", "Authorization", "X-Requested-By", "x-requested-with", "X-Forwarded-Host", "x-xsrf-token" }; - private static final String[] ALLOWED_METHODS = { "GET", "OPTIONS" }; + private static final String[] ALLOWED_METHODS = { "GET", "POST", "PUT", "DELETE", "OPTIONS" }; private final MarketHeaderInterceptor headerInterceptor; + @Value("${market.cors.allowed.origin.patterns}") + private String marketCorsAllowedOriginPatterns; + @Value("${market.cors.allowed.origin.maxAge}") private int marketCorsAllowedOriginMaxAge; @@ -30,7 +34,7 @@ public void addInterceptors(InterceptorRegistry registry) { @Override public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**").allowedOrigins("*").allowedMethods(ALLOWED_METHODS).allowedHeaders(ALLOWED_HEADERS) - .maxAge(marketCorsAllowedOriginMaxAge); + registry.addMapping(ALL_MAPPINGS).allowedOriginPatterns(marketCorsAllowedOriginPatterns) + .allowedMethods(ALLOWED_METHODS).allowedHeaders(ALLOWED_HEADERS).maxAge(marketCorsAllowedOriginMaxAge); } } \ No newline at end of file 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 1ea85be50..617cb4c4e 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 @@ -5,6 +5,7 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class RequestMappingConstants { + public static final String ALL = "*"; public static final String ROOT = "/"; public static final String API = ROOT + "api"; public static final String SYNC = ROOT + "sync"; @@ -12,4 +13,12 @@ public class RequestMappingConstants { 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"; + public static final String GIT_HUB_LOGIN = "/github/login"; + public static final String AUTH = "/auth"; + public static final String BY_ID = "/{id}"; + public static final String BY_ID_AND_TAG = "/{id}/{tag}"; + public static final String VERSIONS_BY_ID = "/{id}/versions"; + public static final String PRODUCT_BY_ID = "/product/{id}"; + public static final String PRODUCT_RATING_BY_ID = "/product/{id}/rating"; + public static final String INSTALLATION_COUNT_BY_ID = "/installationcount/{id}"; } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java new file mode 100644 index 000000000..69366096b --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java @@ -0,0 +1,20 @@ +package com.axonivy.market.constants; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class RequestParamConstants { + public static final String ID = "id"; + public static final String KEY = "key"; + public static final String TAG = "tag"; + public static final String TYPE = "type"; + public static final String KEYWORD = "keyword"; + public static final String LANGUAGE = "language"; + public static final String USER_ID = "userId"; + public static final String AUTHORIZATION = "Authorization"; + public static final String RESET_SYNC = "resetSync"; + public static final String PRODUCT_ID = "productId"; + public static final String SHOW_DEV_VERSION = "isShowDevVersion"; + public static final String DESIGNER_VERSION = "designerVersion"; +} 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 536b348cf..2323878c9 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 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; + import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -10,8 +10,10 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -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; @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 index ce7a8e10a..74ebf6d0a 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java @@ -1,15 +1,16 @@ package com.axonivy.market.controller; -import com.axonivy.market.assembler.FeedbackModelAssembler; -import com.axonivy.market.constants.CommonConstants; -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 static com.axonivy.market.constants.RequestMappingConstants.BY_ID; +import static com.axonivy.market.constants.RequestMappingConstants.FEEDBACK; +import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_BY_ID; +import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_RATING_BY_ID; +import static com.axonivy.market.constants.RequestParamConstants.AUTHORIZATION; +import static com.axonivy.market.constants.RequestParamConstants.ID; +import static com.axonivy.market.constants.RequestParamConstants.USER_ID; + +import java.net.URI; +import java.util.List; + import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; @@ -17,7 +18,6 @@ import org.springframework.hateoas.PagedModel; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -28,10 +28,17 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import java.net.URI; -import java.util.List; +import com.axonivy.market.assembler.FeedbackModelAssembler; +import com.axonivy.market.constants.CommonConstants; +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 static com.axonivy.market.constants.RequestMappingConstants.FEEDBACK; +import io.jsonwebtoken.Claims; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; @RestController @RequestMapping(FEEDBACK) @@ -52,8 +59,8 @@ public FeedbackController(FeedbackService feedbackService, JwtService jwtService } @Operation(summary = "Find all feedbacks by product id") - @GetMapping("/product/{productId}") - public ResponseEntity> findFeedbacks(@PathVariable("productId") String productId, + @GetMapping(PRODUCT_BY_ID) + public ResponseEntity> findFeedbacks(@PathVariable(ID) String productId, Pageable pageable) { Page results = feedbackService.findFeedbacks(productId, pageable); if (results.isEmpty()) { @@ -64,24 +71,23 @@ public ResponseEntity> findFeedbacks(@PathVariable("pr return new ResponseEntity<>(pageResources, HttpStatus.OK); } - @GetMapping("/{id}") - public ResponseEntity findFeedback(@PathVariable("id") String id) { + @GetMapping(BY_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("userId") String userId, + public ResponseEntity findFeedbackByUserIdAndProductId(@RequestParam(USER_ID) String userId, @RequestParam("productId") String productId) { Feedback feedback = feedbackService.findFeedbackByUserIdAndProductId(userId, productId); return ResponseEntity.ok(feedbackModelAssembler.toModel(feedback)); } - @CrossOrigin("*") @PostMapping public ResponseEntity createFeedback(@RequestBody @Valid FeedbackModel feedback, - @RequestHeader(value = "Authorization") String authorizationHeader) { + @RequestHeader(value = AUTHORIZATION) String authorizationHeader) { String token = null; if (authorizationHeader != null && authorizationHeader.startsWith(CommonConstants.BEARER)) { token = authorizationHeader.substring(CommonConstants.BEARER.length()).trim(); // Remove "Bearer " prefix @@ -89,22 +95,22 @@ public ResponseEntity createFeedback(@RequestBody @Valid FeedbackModel fee // Validate the token if (token == null || !jwtService.validateToken(token)) { - return ResponseEntity.status(401).build(); // Unauthorized if token is missing or invalid + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).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()) + URI location = ServletUriComponentsBuilder.fromCurrentRequest().path(BY_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) { + @GetMapping(PRODUCT_RATING_BY_ID) + public ResponseEntity> getProductRating(@PathVariable(ID) String productId) { return ResponseEntity.ok(feedbackService.getProductRatingById(productId)); } 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 index 377aeed5c..968c81e2b 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java @@ -1,51 +1,56 @@ package com.axonivy.market.controller; -import com.axonivy.market.constants.GitHubConstants; -import com.axonivy.market.entity.User; -import com.axonivy.market.github.model.GitHubAccessTokenResponse; -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 static com.axonivy.market.constants.RequestMappingConstants.AUTH; +import static com.axonivy.market.constants.RequestMappingConstants.GIT_HUB_LOGIN; +import static org.apache.commons.lang3.StringUtils.EMPTY; + +import java.util.Collections; +import java.util.Map; + +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CrossOrigin; 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 com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.entity.User; +import com.axonivy.market.github.model.GitHubAccessTokenResponse; +import com.axonivy.market.github.model.GitHubProperty; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.model.Oauth2AuthorizationCode; +import com.axonivy.market.service.JwtService; @RestController -@RequestMapping("/auth") +@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 GitHubProperty gitHubProperty; private final GitHubService gitHubService; private final JwtService jwtService; - public OAuth2Controller(GitHubService gitHubService, JwtService jwtService) { + public OAuth2Controller(GitHubService gitHubService, JwtService jwtService, GitHubProperty gitHubProperty) { this.gitHubService = gitHubService; this.jwtService = jwtService; + this.gitHubProperty = gitHubProperty; } - @CrossOrigin("*") - @PostMapping("/github/login") - public ResponseEntity gitHubLogin(@RequestBody Oauth2AuthorizationCode oauth2AuthorizationCode) { - GitHubAccessTokenResponse tokenResponse = gitHubService.getAccessToken(oauth2AuthorizationCode.getCode(), clientId, - clientSecret); - String accessToken = tokenResponse.getAccessToken(); + @PostMapping(GIT_HUB_LOGIN) + public ResponseEntity> gitHubLogin(@RequestBody Oauth2AuthorizationCode oauth2AuthorizationCode) { + String accessToken = EMPTY; + try { + GitHubAccessTokenResponse tokenResponse = gitHubService.getAccessToken(oauth2AuthorizationCode.getCode(), + gitHubProperty); + accessToken = tokenResponse.getAccessToken(); + } catch (Exception e) { + return new ResponseEntity<>(Map.of(e.getClass().getName(), e.getMessage()), HttpStatus.BAD_REQUEST); + } User user = gitHubService.getAndUpdateUser(accessToken); - String jwtToken = jwtService.generateToken(user); - - return ResponseEntity.ok().body(Collections.singletonMap(GitHubConstants.Json.TOKEN, jwtToken)); + return new ResponseEntity<>(Collections.singletonMap(GitHubConstants.Json.TOKEN, jwtToken), HttpStatus.OK); } } \ 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 2d4980d08..4cfe168b2 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,5 +1,28 @@ package com.axonivy.market.controller; +import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT; +import static com.axonivy.market.constants.RequestMappingConstants.SYNC; +import static com.axonivy.market.constants.RequestParamConstants.AUTHORIZATION; +import static com.axonivy.market.constants.RequestParamConstants.KEYWORD; +import static com.axonivy.market.constants.RequestParamConstants.LANGUAGE; +import static com.axonivy.market.constants.RequestParamConstants.RESET_SYNC; +import static com.axonivy.market.constants.RequestParamConstants.TYPE; + +import org.apache.commons.lang3.time.StopWatch; +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.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + import com.axonivy.market.assembler.ProductModelAssembler; import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.constants.GitHubConstants; @@ -9,19 +32,8 @@ 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; -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 static com.axonivy.market.constants.RequestMappingConstants.PRODUCT; -import static com.axonivy.market.constants.RequestMappingConstants.SYNC; +import io.swagger.v3.oas.annotations.Operation; @RestController @RequestMapping(PRODUCT) @@ -42,9 +54,9 @@ public ProductController(ProductService productService, GitHubService gitHubServ @Operation(summary = "Find all products", description = "Be default system will finds product by type as 'all'") @GetMapping() - public ResponseEntity> findProducts(@RequestParam(name = "type") String type, - @RequestParam(required = false, name = "keyword") String keyword, - @RequestParam(name = "language") String language, Pageable pageable) { + public ResponseEntity> findProducts(@RequestParam(name = TYPE) String type, + @RequestParam(required = false, name = KEYWORD) String keyword, + @RequestParam(name = LANGUAGE) String language, Pageable pageable) { Page results = productService.findProducts(type, keyword, language, pageable); if (results.isEmpty()) { return generateEmptyPagedModel(); @@ -55,8 +67,8 @@ public ResponseEntity> findProducts(@RequestParam(name } @PutMapping(SYNC) - public ResponseEntity syncProducts(@RequestHeader(value = "Authorization") String authorizationHeader, - @RequestParam(value = "resetSync", required = false) Boolean resetSync) { + public ResponseEntity syncProducts(@RequestHeader(value = AUTHORIZATION) String authorizationHeader, + @RequestParam(value = RESET_SYNC, required = false) Boolean resetSync) { String token = null; if (authorizationHeader.startsWith(CommonConstants.BEARER)) { token = authorizationHeader.substring(CommonConstants.BEARER.length()).trim(); // Remove "Bearer " prefix diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java index 2d89ffd0c..b09611dcb 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java @@ -1,23 +1,34 @@ package com.axonivy.market.controller; -import com.axonivy.market.assembler.ProductDetailModelAssembler; -import com.axonivy.market.model.MavenArtifactVersionModel; -import com.axonivy.market.model.ProductDetailModel; -import com.axonivy.market.service.ProductService; -import com.axonivy.market.service.VersionService; -import io.swagger.v3.oas.annotations.Operation; +import static com.axonivy.market.constants.RequestMappingConstants.BY_ID; +import static com.axonivy.market.constants.RequestMappingConstants.BY_ID_AND_TAG; +import static com.axonivy.market.constants.RequestMappingConstants.INSTALLATION_COUNT_BY_ID; +import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_DETAILS; +import static com.axonivy.market.constants.RequestMappingConstants.VERSIONS_BY_ID; +import static com.axonivy.market.constants.RequestParamConstants.DESIGNER_VERSION; +import static com.axonivy.market.constants.RequestParamConstants.ID; +import static com.axonivy.market.constants.RequestParamConstants.SHOW_DEV_VERSION; +import static com.axonivy.market.constants.RequestParamConstants.TAG; + +import java.util.List; + import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; 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.CrossOrigin; -import java.util.List; -import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_DETAILS; +import com.axonivy.market.assembler.ProductDetailModelAssembler; +import com.axonivy.market.model.MavenArtifactVersionModel; +import com.axonivy.market.model.ProductDetailModel; +import com.axonivy.market.service.ProductService; +import com.axonivy.market.service.VersionService; + +import io.swagger.v3.oas.annotations.Operation; @RestController @RequestMapping(PRODUCT_DETAILS) @@ -33,31 +44,31 @@ public ProductDetailsController(VersionService versionService, ProductService pr this.detailModelAssembler = detailModelAssembler; } - @GetMapping("/{id}/{tag}") - public ResponseEntity findProductDetailsByVersion(@PathVariable("id") String id, - @PathVariable("tag") String tag) { + @GetMapping(BY_ID_AND_TAG) + public ResponseEntity findProductDetailsByVersion(@PathVariable(ID) String id, + @PathVariable(TAG) String tag) { var productDetail = productService.fetchProductDetail(id); return new ResponseEntity<>(detailModelAssembler.toModel(productDetail, tag), HttpStatus.OK); } @Operation(summary = "increase installation count by 1", description = "update installation count when click download product files by users") @CrossOrigin(originPatterns = "*") - @PutMapping("/installationcount/{key}") - public ResponseEntity syncInstallationCount(@PathVariable("key") String key) { + @PutMapping(INSTALLATION_COUNT_BY_ID) + public ResponseEntity syncInstallationCount(@PathVariable(ID) String key) { int result = productService.updateInstallationCountForProduct(key); return new ResponseEntity<>(result, HttpStatus.OK); } - @GetMapping("/{id}") - public ResponseEntity findProductDetails(@PathVariable("id") String id) { + @GetMapping(BY_ID) + public ResponseEntity findProductDetails(@PathVariable(ID) String id) { var productDetail = productService.fetchProductDetail(id); return new ResponseEntity<>(detailModelAssembler.toModel(productDetail, null), HttpStatus.OK); } - @GetMapping("/{id}/versions") - public ResponseEntity> findProductVersionsById(@PathVariable("id") String id, - @RequestParam(name = "isShowDevVersion") boolean isShowDevVersion, - @RequestParam(name = "designerVersion", required = false) String designerVersion) { + @GetMapping(VERSIONS_BY_ID) + public ResponseEntity> findProductVersionsById(@PathVariable(ID) String id, + @RequestParam(SHOW_DEV_VERSION) boolean isShowDevVersion, + @RequestParam(name = DESIGNER_VERSION, required = false) String designerVersion) { List models = versionService.getArtifactsAndVersionToDisplay(id, isShowDevVersion, designerVersion); return new ResponseEntity<>(models, HttpStatus.OK); diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/model/GitHubProperty.java b/marketplace-service/src/main/java/com/axonivy/market/github/model/GitHubProperty.java index e0e30f669..3c9c52c59 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/model/GitHubProperty.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/model/GitHubProperty.java @@ -17,4 +17,7 @@ public class GitHubProperty { private String token; + private String oauth2ClientId; + private String oauth2ClientSecret; + } diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java index ae4753315..23bc9640f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java @@ -1,15 +1,19 @@ package com.axonivy.market.github.service; -import com.axonivy.market.entity.User; -import com.axonivy.market.exceptions.model.UnauthorizedException; -import com.axonivy.market.github.model.GitHubAccessTokenResponse; +import java.io.IOException; +import java.util.List; + import org.kohsuke.github.GHContent; import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GitHub; -import java.io.IOException; -import java.util.List; +import com.axonivy.market.entity.User; +import com.axonivy.market.exceptions.model.MissingHeaderException; +import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException; +import com.axonivy.market.exceptions.model.UnauthorizedException; +import com.axonivy.market.github.model.GitHubAccessTokenResponse; +import com.axonivy.market.github.model.GitHubProperty; public interface GitHubService { @@ -23,7 +27,8 @@ public interface GitHubService { GHContent getGHContent(GHRepository ghRepository, String path, String ref) throws IOException; - GitHubAccessTokenResponse getAccessToken(String code, String clientId, String clientSecret); + GitHubAccessTokenResponse getAccessToken(String code, GitHubProperty gitHubProperty) + throws Oauth2ExchangeCodeException, MissingHeaderException; User getAndUpdateUser(String accessToken); diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java index 46c5e8c03..8b8486fa2 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java @@ -1,8 +1,36 @@ package com.axonivy.market.github.service.impl; +import static org.apache.commons.lang3.StringUtils.EMPTY; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHOrganization; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GitHub; +import org.kohsuke.github.GitHubBuilder; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.entity.User; import com.axonivy.market.enums.ErrorCode; +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.exceptions.model.UnauthorizedException; @@ -11,24 +39,6 @@ import com.axonivy.market.github.service.GitHubService; import com.axonivy.market.github.util.GitHubUtils; import com.axonivy.market.repository.UserRepository; -import org.kohsuke.github.*; -import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.*; -import org.springframework.stereotype.Service; -import org.springframework.util.Assert; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.RestTemplate; - -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import static org.apache.commons.lang3.StringUtils.EMPTY; @Service public class GitHubServiceImpl implements GitHubService { @@ -74,11 +84,14 @@ public GHContent getGHContent(GHRepository ghRepository, String path, String ref } @Override - public GitHubAccessTokenResponse getAccessToken(String code, String clientId, String clientSecret) - throws Oauth2ExchangeCodeException { + public GitHubAccessTokenResponse getAccessToken(String code, GitHubProperty gitHubProperty) + throws Oauth2ExchangeCodeException, MissingHeaderException { + if (gitHubProperty == null) { + throw new MissingHeaderException(); + } MultiValueMap params = new LinkedMultiValueMap<>(); - params.add(GitHubConstants.Json.CLIENT_ID, clientId); - params.add(GitHubConstants.Json.CLIENT_SECRET, clientSecret); + params.add(GitHubConstants.Json.CLIENT_ID, gitHubProperty.getOauth2ClientId()); + params.add(GitHubConstants.Json.CLIENT_SECRET, gitHubProperty.getOauth2ClientSecret()); params.add(GitHubConstants.Json.CODE, code); HttpHeaders headers = new HttpHeaders(); diff --git a/marketplace-service/src/main/resources/application.properties b/marketplace-service/src/main/resources/application.properties index 04676038e..6020ca4b5 100644 --- a/marketplace-service/src/main/resources/application.properties +++ b/marketplace-service/src/main/resources/application.properties @@ -8,10 +8,10 @@ server.forward-headers-strategy=framework springdoc.api-docs.path=/api-docs springdoc.swagger-ui.path=/swagger-ui.html market.cors.allowed.origin.maxAge=3600 +market.cors.allowed.origin.patterns=${MARKET_CORS_ALLOWED_ORIGIN} synchronized.installation.counts.path=/home/data/market-installation.json market.github.token=${MARKET_GITHUB_TOKEN} -logging.level.org.springframework.security=DEBUG -spring.security.oauth2.client.registration.github.client-id=${MARKET_GITHUB_OAUTH_APP_CLIENT_ID} -spring.security.oauth2.client.registration.github.client-secret=${MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET} +market.github.oauth2-clientId=${MARKET_GITHUB_OAUTH_APP_CLIENT_ID} +market.github.oauth2-clientSecret=${MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET} jwt.secret=${MARKET_JWT_SECRET_KEY} jwt.expiration=365 diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java index e53c4fd6d..cc4ae1bf8 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java @@ -1,10 +1,11 @@ package com.axonivy.market.controller; -import com.axonivy.market.entity.User; -import com.axonivy.market.github.service.GitHubService; -import com.axonivy.market.github.model.GitHubAccessTokenResponse; -import com.axonivy.market.model.Oauth2AuthorizationCode; -import com.axonivy.market.service.JwtService; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.util.Map; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -13,11 +14,13 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.ResponseEntity; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; +import com.axonivy.market.entity.User; +import com.axonivy.market.exceptions.model.MissingHeaderException; +import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException; +import com.axonivy.market.github.model.GitHubAccessTokenResponse; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.model.Oauth2AuthorizationCode; +import com.axonivy.market.service.JwtService; @ExtendWith(MockitoExtension.class) class OAuth2ControllerTest { @@ -40,12 +43,12 @@ void setup() { } @Test - void testGitHubLogin() { + void testGitHubLogin() throws Oauth2ExchangeCodeException, MissingHeaderException { String accessToken = "sampleAccessToken"; User user = createUserMock(); String jwtToken = "sampleJwtToken"; - when(gitHubService.getAccessToken(any(), any(), any())).thenReturn(createGitHubAccessTokenResponseMock()); + when(gitHubService.getAccessToken(any(), any())).thenReturn(createGitHubAccessTokenResponseMock()); when(gitHubService.getAndUpdateUser(accessToken)).thenReturn(user); when(jwtService.generateToken(user)).thenReturn(jwtToken); diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java index 5d7571a98..6e3523138 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java @@ -1,18 +1,20 @@ package com.axonivy.market.service; -import com.axonivy.market.schedulingtask.ScheduledTasks; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.verify; + import org.awaitility.Awaitility; import org.awaitility.Durations; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.SpyBean; -import static org.mockito.Mockito.atLeast; -import static org.mockito.Mockito.verify; +import com.axonivy.market.schedulingtask.ScheduledTasks; @SpringBootTest(properties = { "MONGODB_USERNAME=user", "MONGODB_PASSWORD=password", "MONGODB_HOST=mongoHost", "MONGODB_DATABASE=product", "MARKET_GITHUB_OAUTH_APP_CLIENT_ID=clientId", - "MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=clientSecret", "MARKET_JWT_SECRET_KEY=jwtSecret" }) + "MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=clientSecret", "MARKET_JWT_SECRET_KEY=jwtSecret", + "MARKET_CORS_ALLOWED_ORIGIN=*" }) class SchedulingTasksTest { @SpyBean From 8bfacf275d4217be86492f9dd3ae914ad539863e Mon Sep 17 00:00:00 2001 From: Hoan Nguyen <83745591+nqhoan-axonivy@users.noreply.github.com> Date: Mon, 29 Jul 2024 14:52:46 +0700 Subject: [PATCH 2/3] Feature/marp 700 authenticate for sync products api (#69) --- .github/workflows/docker-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index dbdb3aaf1..85d374815 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -48,6 +48,7 @@ jobs: MONGODB_DATABASE: ${{ secrets.MONGODB_DATABASE }} GH_TOKEN: ${{ secrets.GH_TOKEN }} MARKET_JWT_SECRET_KEY: ${{ secrets.MARKET_JWT_SECRET_KEY }} + MARKET_CORS_ALLOWED_ORIGIN: ${{ secrets.MARKET_CORS_ALLOWED_ORIGIN }} run: | if [ "${{ inputs.build_env }}" == "production" ]; then OAUTH_APP_CLIENT_ID=${{ secrets.OAUTH_APP_CLIENT_ID }} From c73c4e6e17b62c1d2b8104387eda5dd9d85f7cf1 Mon Sep 17 00:00:00 2001 From: Hoan Nguyen <83745591+nqhoan-axonivy@users.noreply.github.com> Date: Tue, 30 Jul 2024 09:05:23 +0700 Subject: [PATCH 3/3] MARP-700-Authenticate-for-SYNC-products-api (#71) --- marketplace-build/docker-compose.yml | 1 + marketplace-build/release/docker-compose.yml | 1 + marketplace-build/release/sprint-compose.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/marketplace-build/docker-compose.yml b/marketplace-build/docker-compose.yml index 46023528c..5ba3d718d 100644 --- a/marketplace-build/docker-compose.yml +++ b/marketplace-build/docker-compose.yml @@ -47,6 +47,7 @@ services: - MARKET_GITHUB_OAUTH_APP_CLIENT_ID=${MARKET_GITHUB_OAUTH_APP_CLIENT_ID} - MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=${MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET} - MARKET_JWT_SECRET_KEY=${MARKET_JWT_SECRET_KEY} + - MARKET_CORS_ALLOWED_ORIGIN=${MARKET_CORS_ALLOWED_ORIGIN} build: context: ../marketplace-service dockerfile: Dockerfile diff --git a/marketplace-build/release/docker-compose.yml b/marketplace-build/release/docker-compose.yml index 53a422276..7238c46c1 100644 --- a/marketplace-build/release/docker-compose.yml +++ b/marketplace-build/release/docker-compose.yml @@ -38,5 +38,6 @@ services: - MARKET_GITHUB_OAUTH_APP_CLIENT_ID=${MARKET_GITHUB_OAUTH_APP_CLIENT_ID} - MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=${MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET} - MARKET_JWT_SECRET_KEY=${MARKET_JWT_SECRET_KEY} + - MARKET_CORS_ALLOWED_ORIGIN=${MARKET_CORS_ALLOWED_ORIGIN} volumes: mongodata: \ No newline at end of file diff --git a/marketplace-build/release/sprint-compose.yml b/marketplace-build/release/sprint-compose.yml index 0b3a43ccc..408caa96b 100644 --- a/marketplace-build/release/sprint-compose.yml +++ b/marketplace-build/release/sprint-compose.yml @@ -38,6 +38,7 @@ services: - MARKET_GITHUB_OAUTH_APP_CLIENT_ID=${MARKET_GITHUB_OAUTH_APP_CLIENT_ID} - MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=${MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET} - MARKET_JWT_SECRET_KEY=${MARKET_JWT_SECRET_KEY} + - MARKET_CORS_ALLOWED_ORIGIN=${MARKET_CORS_ALLOWED_ORIGIN} volumes: mongodata: \ No newline at end of file