Skip to content

Commit

Permalink
Feature/MARP-700 authenticate for sync products api (#68)
Browse files Browse the repository at this point in the history
  • Loading branch information
nqhoan-axonivy authored Jul 29, 2024
1 parent f764ae4 commit 46855f1
Show file tree
Hide file tree
Showing 16 changed files with 240 additions and 143 deletions.
1 change: 1 addition & 0 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion marketplace-build/.env
Original file line number Diff line number Diff line change
Expand Up @@ -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=
MARKET_JWT_SECRET_KEY=
MARKET_CORS_ALLOWED_ORIGIN=
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,20 @@

@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";
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";
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}";
}
Original file line number Diff line number Diff line change
@@ -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";
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
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;
import org.springframework.web.bind.annotation.RequestMapping;
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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
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;
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.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
Expand All @@ -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)
Expand All @@ -52,8 +59,8 @@ public FeedbackController(FeedbackService feedbackService, JwtService jwtService
}

@Operation(summary = "Find all feedbacks by product id")
@GetMapping("/product/{productId}")
public ResponseEntity<PagedModel<FeedbackModel>> findFeedbacks(@PathVariable("productId") String productId,
@GetMapping(PRODUCT_BY_ID)
public ResponseEntity<PagedModel<FeedbackModel>> findFeedbacks(@PathVariable(ID) String productId,
Pageable pageable) {
Page<Feedback> results = feedbackService.findFeedbacks(productId, pageable);
if (results.isEmpty()) {
Expand All @@ -64,47 +71,46 @@ public ResponseEntity<PagedModel<FeedbackModel>> findFeedbacks(@PathVariable("pr
return new ResponseEntity<>(pageResources, HttpStatus.OK);
}

@GetMapping("/{id}")
public ResponseEntity<FeedbackModel> findFeedback(@PathVariable("id") String id) {
@GetMapping(BY_ID)
public ResponseEntity<FeedbackModel> 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<FeedbackModel> findFeedbackByUserIdAndProductId(@RequestParam("userId") String userId,
public ResponseEntity<FeedbackModel> 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<Void> 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
}

// 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<List<ProductRating>> getProductRating(@PathVariable("productId") String productId) {
@GetMapping(PRODUCT_RATING_BY_ID)
public ResponseEntity<List<ProductRating>> getProductRating(@PathVariable(ID) String productId) {
return ResponseEntity.ok(feedbackService.getProductRatingById(productId));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Object> gitHubLogin(@RequestBody Oauth2AuthorizationCode oauth2AuthorizationCode) {
GitHubAccessTokenResponse tokenResponse = gitHubService.getAccessToken(oauth2AuthorizationCode.getCode(), clientId,
clientSecret);
String accessToken = tokenResponse.getAccessToken();
@PostMapping(GIT_HUB_LOGIN)
public ResponseEntity<Map<String, String>> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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)
Expand All @@ -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<PagedModel<ProductModel>> findProducts(@RequestParam(name = "type") String type,
@RequestParam(required = false, name = "keyword") String keyword,
@RequestParam(name = "language") String language, Pageable pageable) {
public ResponseEntity<PagedModel<ProductModel>> findProducts(@RequestParam(name = TYPE) String type,
@RequestParam(required = false, name = KEYWORD) String keyword,
@RequestParam(name = LANGUAGE) String language, Pageable pageable) {
Page<Product> results = productService.findProducts(type, keyword, language, pageable);
if (results.isEmpty()) {
return generateEmptyPagedModel();
Expand All @@ -55,8 +67,8 @@ public ResponseEntity<PagedModel<ProductModel>> findProducts(@RequestParam(name
}

@PutMapping(SYNC)
public ResponseEntity<Message> syncProducts(@RequestHeader(value = "Authorization") String authorizationHeader,
@RequestParam(value = "resetSync", required = false) Boolean resetSync) {
public ResponseEntity<Message> 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
Expand Down
Loading

0 comments on commit 46855f1

Please sign in to comment.