Skip to content

Commit

Permalink
Merge pull request #275 from axonivy-market/develop
Browse files Browse the repository at this point in the history
MARP-1867 Create Marketplace release 1.8.0
  • Loading branch information
nntthuy-axonivy authored Jan 16, 2025
2 parents b8429bd + b701348 commit 157f5d7
Show file tree
Hide file tree
Showing 80 changed files with 1,969 additions and 297 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ jobs:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
MARKET_JWT_SECRET_KEY: ${{ secrets.MARKET_JWT_SECRET_KEY }}
MARKET_CORS_ALLOWED_ORIGIN: ${{ secrets.MARKET_CORS_ALLOWED_ORIGIN }}
MARKET_CLICK_LIMIT: ${{ secrets.MARKET_CLICK_LIMIT }}
MARKET_LIMITED_REQUEST_PATHS: ${{ secrets.MARKET_LIMITED_REQUEST_PATHS }}
run: |
if [ "${{ inputs.build_env }}" == "production" ]; then
OAUTH_APP_CLIENT_ID=${{ secrets.OAUTH_APP_CLIENT_ID }}
Expand All @@ -101,6 +103,8 @@ jobs:
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
sed -i "s/^MARKET_CLICK_LIMIT=.*$/MARKET_CLICK_LIMIT=$MARKET_CLICK_LIMIT/" $ENV_FILE
sed -i "s|^MARKET_LIMITED_REQUEST_PATHS=.*$|MARKET_LIMITED_REQUEST_PATHS=\"$MARKET_LIMITED_REQUEST_PATHS\"|" $ENV_FILE
- name: Update version
if: ${{ inputs.release_version != '' }}
Expand Down
4 changes: 3 additions & 1 deletion marketplace-build/.env
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=
MARKET_JWT_SECRET_KEY=
MARKET_CORS_ALLOWED_ORIGIN=*
MARKET_MONGO_LOG_LEVEL=DEBUG
MARKET_LOG_PATH=logs
MARKET_LOG_PATH=logs
MARKET_CLICK_LIMIT=
MARKET_LIMITED_REQUEST_PATHS=
2 changes: 1 addition & 1 deletion marketplace-build/config/nginx/dev/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ http {
listen 80;
server_name marketplace;
server_tokens off;

client_max_body_size 100M;
root /usr/share/nginx/html;
index index.html;

Expand Down
2 changes: 1 addition & 1 deletion marketplace-build/config/nginx/release/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ http {
listen 80;
server_name marketplace;
server_tokens off;

client_max_body_size 100M;
root /usr/share/nginx/html;
index index.html;

Expand Down
4 changes: 3 additions & 1 deletion marketplace-build/dev/.env
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=
MARKET_JWT_SECRET_KEY=
MARKET_CORS_ALLOWED_ORIGIN=*
MARKET_MONGO_LOG_LEVEL=DEBUG
MARKET_LOG_PATH=logs
MARKET_LOG_PATH=logs
MARKET_CLICK_LIMIT=
MARKET_LIMITED_REQUEST_PATHS=
4 changes: 3 additions & 1 deletion marketplace-build/dev/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ services:
- MARKET_CORS_ALLOWED_ORIGIN=${MARKET_CORS_ALLOWED_ORIGIN}
- MARKET_MONGO_LOG_LEVEL=${MARKET_MONGO_LOG_LEVEL}
- MARKET_LOG_PATH=${MARKET_LOG_PATH}
- MARKET_CLICK_LIMIT=${MARKET_CLICK_LIMIT}
- MARKET_LIMITED_REQUEST_PATHS=${MARKET_LIMITED_REQUEST_PATHS}
build:
context: ../../marketplace-service
dockerfile: Dockerfile
Expand All @@ -53,4 +55,4 @@ volumes:

networks:
marketplace-network:
external: true
external: true
4 changes: 3 additions & 1 deletion marketplace-build/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ services:
- MARKET_CORS_ALLOWED_ORIGIN=${MARKET_CORS_ALLOWED_ORIGIN}
- MARKET_MONGO_LOG_LEVEL=${MARKET_MONGO_LOG_LEVEL}
- MARKET_LOG_PATH=${MARKET_LOG_PATH}
- MARKET_CLICK_LIMIT=${MARKET_CLICK_LIMIT}
- MARKET_LIMITED_REQUEST_PATHS=${MARKET_LIMITED_REQUEST_PATHS}
build:
context: ../marketplace-service
dockerfile: Dockerfile
Expand All @@ -53,4 +55,4 @@ volumes:

networks:
marketplace-network:
external: true
external: true
4 changes: 3 additions & 1 deletion marketplace-build/release/.env
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=
MARKET_JWT_SECRET_KEY=
MARKET_CORS_ALLOWED_ORIGIN=*
MARKET_MONGO_LOG_LEVEL=DEBUG
MARKET_LOG_PATH=logs
MARKET_LOG_PATH=logs
MARKET_CLICK_LIMIT=
MARKET_LIMITED_REQUEST_PATHS=
2 changes: 2 additions & 0 deletions marketplace-build/release/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ services:
- MARKET_CORS_ALLOWED_ORIGIN=${MARKET_CORS_ALLOWED_ORIGIN}
- MARKET_MONGO_LOG_LEVEL=${MARKET_MONGO_LOG_LEVEL}
- MARKET_LOG_PATH=${MARKET_LOG_PATH}
- MARKET_CLICK_LIMIT=${MARKET_CLICK_LIMIT}
- MARKET_LIMITED_REQUEST_PATHS=${MARKET_LIMITED_REQUEST_PATHS}
networks:
- marketplace-network

Expand Down
15 changes: 10 additions & 5 deletions marketplace-service/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,19 @@
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j_jdk17-core</artifactId>
<version>8.14.0</version>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ private void createDetailResource(ProductDetailModel model, Product product) {
model.setContactUs(product.getContactUs());
model.setCost(product.getCost());
model.setInstallationCount(product.getInstallationCount());
model.setCompatibilityRange(product.getCompatibilityRange());
model.setProductModuleContent(ImageUtils.mappingImageForProductModuleContent(product.getProductModuleContent()));
if (StringUtils.isNotBlank(product.getVendorImage())) {
Link vendorLink = linkTo(methodOn(ImageController.class).findImageById(product.getVendorImage())).withSelfRel();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.axonivy.market.config;

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.micrometer.common.util.StringUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Log4j2
@Component
public class LimitCallingConfig extends OncePerRequestFilter {
private static final String REQUEST_HEADER = "X-Forwarded-For";
@Value("${market.allowed.click-capacity}")
private int capacity;

@Value("${market.limited.request-paths}")
private List<String> requestPaths;
private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

String clientIp = getClientIp(request);
String apiPath = request.getRequestURI();

boolean isRequestPathMatched = requestPaths.stream().anyMatch(apiPath::contains);
if (isRequestPathMatched) {
Bucket bucket = buckets.computeIfAbsent(clientIp, this::createNewBucket);

if (bucket.tryConsume(1)) {
log.warn("Request allowed for IP: {}. Remaining tokens: {}", clientIp, bucket.getAvailableTokens());
filterChain.doFilter(request, response);
} else {
response.setStatus(HttpServletResponse.SC_BAD_GATEWAY);
response.getWriter().write("Too many requests. Please try again later.");
}
} else {
filterChain.doFilter(request, response);
}
}

private Bucket createNewBucket(String clientIp) {
Bandwidth limit = Bandwidth.builder()
.capacity(capacity)
.refillGreedy(capacity, Duration.ofMinutes(1))
.build();
return Bucket.builder().addLimit(limit).build();
}

private String getClientIp(HttpServletRequest request) {
String forwardedFor = request.getHeader(REQUEST_HEADER);
if (StringUtils.isNotEmpty(forwardedFor)) {
return forwardedFor.split(",")[0];
}
return request.getRemoteAddr();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ public class CommonConstants {
public static final String ID_WITH_NUMBER_PATTERN = "%s-%s";
public static final String ERROR = "error";
public static final String MESSAGE = "message";
public static final String COMPATIBILITY_RANGE_FORMAT = "%s - %s";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.axonivy.market.constants;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class PreviewConstants {

public static final String PREVIEW_DIR = "data/work/preview";

public static final String IMAGE_DOWNLOAD_URL = "%s/api/image/preview/%s";

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class RequestMappingConstants {
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_FILE_NAME = "/preview/{imageName}";
public static final String BY_ID_AND_VERSION = "/{id}/{version}";
public static final String BEST_MATCH_BY_ID_AND_VERSION = "/{id}/{version}/bestmatch";
public static final String VERSIONS_BY_ID = "/{id}/versions";
Expand All @@ -33,4 +34,5 @@ public class RequestMappingConstants {
public static final String EXTERNAL_DOCUMENT = API + "/externaldocument";
public static final String PRODUCT_MARKETPLACE_DATA = API + "/product-marketplace-data";
public static final String SECURITY_MONITOR = API + "/security-monitor";
public static final String RELEASE_PREVIEW = API + "/release-preview";
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import static com.axonivy.market.constants.RequestMappingConstants.BY_ID;
import static com.axonivy.market.constants.RequestMappingConstants.IMAGE;
import static com.axonivy.market.constants.RequestMappingConstants.BY_FILE_NAME;
import static com.axonivy.market.constants.RequestParamConstants.ID;

@RestController
Expand Down Expand Up @@ -54,4 +55,17 @@ public ResponseEntity<byte[]> findImageById(
}
return new ResponseEntity<>(imageData, headers, HttpStatus.OK);
}

@GetMapping(BY_FILE_NAME)
@Operation(hidden = true)
public ResponseEntity<byte[]> findPreviewImageByName(
@PathVariable("imageName") String imageName) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_PNG);
byte[] imageData = imageService.readPreviewImageByName(imageName);
if (imageData.length == 0) {
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
return new ResponseEntity<>(imageData, headers, HttpStatus.OK);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.axonivy.market.controller;

import com.axonivy.market.logging.Loggable;
import com.axonivy.market.model.DesignerInstallation;
import com.axonivy.market.service.ProductDesignerInstallationService;
import io.swagger.v3.oas.annotations.Operation;
Expand Down Expand Up @@ -30,6 +31,7 @@ public ProductDesignerInstallationController(ProductDesignerInstallationService
this.productDesignerInstallationService = productDesignerInstallationService;
}

@Loggable
@GetMapping(DESIGNER_INSTALLATION_BY_ID)
@Operation(summary = "Get designer installation count by product id.",
description = "get designer installation count by product id")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.axonivy.market.controller;

import com.axonivy.market.model.ReleasePreview;
import com.axonivy.market.service.ReleasePreviewService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
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.multipart.MultipartFile;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import static com.axonivy.market.constants.RequestMappingConstants.RELEASE_PREVIEW;

@Log4j2
@RestController
@RequestMapping(RELEASE_PREVIEW)
@Tag(name = "Release Preview Controller", description = "API to extract zip file and return README data.")
@AllArgsConstructor
public class ReleasePreviewController {

private final ReleasePreviewService previewService;

@PostMapping
@Operation(hidden = true)
public ResponseEntity<Object> extractZipFile(@RequestParam(value = "file") MultipartFile file) {
String baseUrl = ServletUriComponentsBuilder.fromCurrentContextPath().build().toUriString();
ReleasePreview preview = previewService.extract(file, baseUrl);
if (preview == null) {
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
return ResponseEntity.ok(preview);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ public class Product implements Serializable {
private String bestMatchVersion;
@Transient
private boolean isMavenDropins;
@Transient
private String compatibilityRange;

@Override
public int hashCode() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public class ProductDetailModel extends ProductModel {
private int installationCount;
@Schema(description = "The api url to get metadata from product.json")
private String metaProductJsonUrl;
private String compatibilityRange;
private boolean isMavenDropins;

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.axonivy.market.model;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;

import java.util.Map;

import static com.axonivy.market.util.ProductContentUtils.DESCRIPTION;
import static com.axonivy.market.util.ProductContentUtils.DEMO;
import static com.axonivy.market.util.ProductContentUtils.SETUP;
import static com.axonivy.market.util.ProductContentUtils.replaceEmptyContentsWithEnContent;

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ReleasePreview {

@Schema(description = "Product detail description content ",
example = "{ \"de\": \"E-Sign-Konnektor\", \"en\": \"E-sign connector\" }")
private Map<String, String> description;
@Schema(description = "Setup tab content", example = "{ \"de\": \"Setup\", \"en\": \"Setup\" ")
private Map<String, String> setup;
@Schema(description = "Demo tab content", example = "{ \"de\": \"Demo\", \"en\": \"Demo\" ")
private Map<String, String> demo;

public static ReleasePreview from(Map<String, Map<String, String>> moduleContents) {
return ReleasePreview.builder().description(replaceEmptyContentsWithEnContent(moduleContents.get(DESCRIPTION)))
.demo(replaceEmptyContentsWithEnContent(moduleContents.get(DEMO)))
.setup(replaceEmptyContentsWithEnContent(moduleContents.get(SETUP)))
.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ public interface ImageService {
Image mappingImageFromDownloadedFolder(String productId, Path imagePath);

byte[] readImage(String id);

byte[] readPreviewImageByName(String imageName);
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
package com.axonivy.market.service;

import com.axonivy.market.bo.Artifact;
import com.axonivy.market.entity.Product;
import java.util.List;

public interface MetadataService {

int syncAllProductsMetadata();

boolean syncProductMetadata(Product product);

void updateArtifactAndMetadata(String productId , List<String> versions , List<Artifact> artifacts);
}
Loading

0 comments on commit 157f5d7

Please sign in to comment.