Compare commits
27 Commits
1.3.3
...
20bc18d416
| Author | SHA1 | Date | |
|---|---|---|---|
|
20bc18d416
|
|||
|
4ee741d307
|
|||
|
0530c1f633
|
|||
|
8a9cf110af
|
|||
|
c30b5701e4
|
|||
|
7fafa0d903
|
|||
|
8c8ca58b74
|
|||
|
12469f1b27
|
|||
|
44271eeec4
|
|||
|
f9c210c8b3
|
|||
|
ce330bca87
|
|||
|
7032343487
|
|||
|
243283b788
|
|||
|
4810ef2b1f
|
|||
|
72ec875802
|
|||
|
6240ec1016
|
|||
|
8d24b6082d
|
|||
|
9bc70d5370
|
|||
|
d44f5f74fe
|
|||
|
f866d93fb4
|
|||
|
66b37ec20d
|
|||
|
0d70b27653
|
|||
|
673ba03f2b
|
|||
| f6255d396c | |||
|
26cea1db82
|
|||
| 8f102f54c7 | |||
| d3681916b2 |
@@ -157,3 +157,6 @@ gradle-app.setting
|
||||
.classpath
|
||||
|
||||
gradle.properties
|
||||
|
||||
# frp config
|
||||
frpc.toml
|
||||
|
||||
@@ -36,6 +36,7 @@ dependencies {
|
||||
implementation(libs.onixbyte.identityGenerator)
|
||||
implementation(libs.onixbyte.captcha)
|
||||
implementation(libs.onixbyte.regions)
|
||||
implementation(libs.onixbyte.cryptoToolbox)
|
||||
implementation(libs.jwt.core)
|
||||
implementation(libs.spring.boot.configurationProcessor)
|
||||
implementation(libs.spring.boot.actuator)
|
||||
|
||||
@@ -4,10 +4,9 @@ javaJwtVersion = "4.5.1"
|
||||
postgresDriverVersion = "42.7.9"
|
||||
h2Version = "2.2.224"
|
||||
springBootVersion = "3.5.13"
|
||||
springSecurityVersion = "6.5.2"
|
||||
reactorVersion = "3.7.8"
|
||||
junitPlatformVersion = "1.12.2"
|
||||
onixbyteVersion = "3.3.0"
|
||||
onixbyteVersion = "3.4.0"
|
||||
onixbyteCaptcha = "1.1.0"
|
||||
onixbyteRegions = "2025.12.0"
|
||||
awsSdkVersion = "2.25.48"
|
||||
@@ -32,6 +31,7 @@ jackson-jsr310 = { group = "com.fasterxml.jackson.datatype", name = "jackson-dat
|
||||
onixbyte-versionCatalogue = { group = "com.onixbyte", name = "version-catalogue", version.ref = "onixbyteVersion" }
|
||||
onixbyte-tuple = { group = "com.onixbyte", name = "tuple" }
|
||||
onixbyte-commonToolbox = { group = "com.onixbyte", name = "common-toolbox" }
|
||||
onixbyte-cryptoToolbox = { group = "com.onixbyte", name = "crypto-toolbox", version.ref = "onixbyteVersion" }
|
||||
onixbyte-mathToolbox = { group = "com.onixbyte", name = "math-toolbox" }
|
||||
onixbyte-identityGenerator = { group = "com.onixbyte", name = "identity-generator" }
|
||||
onixbyte-captcha = { group = "com.onixbyte", name = "captcha", version.ref = "onixbyteCaptcha" }
|
||||
@@ -42,12 +42,10 @@ mybatis-starter-core = { group = "org.mybatis.spring.boot", name = "mybatis-spri
|
||||
spring-boot-starter-jpa = { group = "org.springframework.boot", name = "spring-boot-starter-data-jpa" }
|
||||
hypersistence-core = { group = "io.hypersistence", name = "hypersistence-utils-hibernate-63", version.ref = "hypersistenceVersion" }
|
||||
postgres-driver = { group = "org.postgresql", name = "postgresql", version.ref = "postgresDriverVersion" }
|
||||
mysql-driver = { group = "com.mysql", name = "mysql-connector-j" }
|
||||
h2-database = { group = "com.h2database", name = "h2", version.ref = "h2Version" }
|
||||
spring-boot-starter-redis = { group = "org.springframework.boot", name = "spring-boot-starter-data-redis", version.ref = "springBootVersion" }
|
||||
flyway-core = { group = "org.flywaydb", name = "flyway-core" }
|
||||
flyway-postgresql = { group = "org.flywaydb", name = "flyway-database-postgresql" }
|
||||
flyway-mysql = { group = "org.flywaydb", name = "flyway-mysql" }
|
||||
|
||||
# Spring Boot Core & Web
|
||||
spring-boot-starter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" }
|
||||
|
||||
@@ -1,10 +1,35 @@
|
||||
package com.onixbyte.deltaforceguide.config;
|
||||
|
||||
import com.onixbyte.deltaforceguide.interceptor.GitHubWebhookInterceptor;
|
||||
import com.onixbyte.deltaforceguide.interceptor.TrafficInterceptor;
|
||||
import com.onixbyte.deltaforceguide.properties.AppProperties;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(AppProperties.class)
|
||||
public class AppConfig {
|
||||
public class AppConfig implements WebMvcConfigurer {
|
||||
|
||||
private final TrafficInterceptor trafficInterceptor;
|
||||
|
||||
private final GitHubWebhookInterceptor gitHubWebhookInterceptor;
|
||||
|
||||
@Autowired
|
||||
public AppConfig(
|
||||
TrafficInterceptor trafficInterceptor,
|
||||
GitHubWebhookInterceptor gitHubWebhookInterceptor
|
||||
) {
|
||||
this.trafficInterceptor = trafficInterceptor;
|
||||
this.gitHubWebhookInterceptor = gitHubWebhookInterceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(trafficInterceptor);
|
||||
registry.addInterceptor(gitHubWebhookInterceptor)
|
||||
.addPathPatterns("/webhooks/github");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.onixbyte.deltaforceguide.config;
|
||||
|
||||
import com.onixbyte.deltaforceguide.filter.WebhookFilter;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class FilterConfig {
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean<WebhookFilter> webhookFilter(WebhookFilter webhookFilter) {
|
||||
var registrationBean = new FilterRegistrationBean<WebhookFilter>();
|
||||
|
||||
registrationBean.setFilter(webhookFilter);
|
||||
registrationBean.addUrlPatterns("/webhooks/*");
|
||||
registrationBean.setOrder(1);
|
||||
|
||||
return registrationBean;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.onixbyte.deltaforceguide.config;
|
||||
|
||||
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
||||
import io.swagger.v3.oas.annotations.info.Contact;
|
||||
import io.swagger.v3.oas.annotations.info.Info;
|
||||
import io.swagger.v3.oas.annotations.info.License;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@OpenAPIDefinition(
|
||||
info = @Info(
|
||||
title = "Delta Force Guide Server",
|
||||
description = "API for managing Delta Force game firearm builds",
|
||||
version = "1.3.4",
|
||||
contact = @Contact(
|
||||
name = "Zihlu Wang",
|
||||
email = "zihlu.wang@onixbyte.com"
|
||||
),
|
||||
license = @License(
|
||||
name = "MIT",
|
||||
url = "https://git.onixbyte.cn/onixbyte/delta-force-guide-server/-/raw/main/LICENCE"
|
||||
)
|
||||
)
|
||||
)
|
||||
@Configuration
|
||||
public class OpenApiConfiguration {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.onixbyte.deltaforceguide.config;
|
||||
|
||||
import com.onixbyte.deltaforceguide.properties.WebhookProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(WebhookProperties.class)
|
||||
public class WebhookConfig {
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.onixbyte.deltaforceguide.controller;
|
||||
|
||||
import com.onixbyte.deltaforceguide.domain.dto.GitHubIssueRequest;
|
||||
import com.onixbyte.deltaforceguide.service.WebhookService;
|
||||
import com.onixbyte.deltaforceguide.shared.GitHubWebhookHeader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/webhooks/github")
|
||||
public class GitHubWebhookController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GitHubWebhookController.class);
|
||||
|
||||
private final WebhookService webhookService;
|
||||
|
||||
public GitHubWebhookController(WebhookService webhookService) {
|
||||
this.webhookService = webhookService;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Void> handleWebhook(
|
||||
@RequestHeader(GitHubWebhookHeader.EVENT) String event,
|
||||
@RequestBody GitHubIssueRequest request
|
||||
) {
|
||||
if (!"issues".equals(event)) {
|
||||
log.debug("Ignoring non-issue event: {}", event);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
if (!"opened".equals(request.action())) {
|
||||
log.debug("Ignoring issue action: {}", request.action());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
webhookService.processIssueEvent(request);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,12 @@ package com.onixbyte.deltaforceguide.controller;
|
||||
|
||||
import com.onixbyte.deltaforceguide.service.AppService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "版本信息")
|
||||
@RestController
|
||||
@RequestMapping("/versions")
|
||||
public class VersionController {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.onixbyte.deltaforceguide.domain.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record GitHubIssueRequest(
|
||||
String action,
|
||||
GitHubWebhookIssue issue,
|
||||
GitHubWebhookRepository repository,
|
||||
GitHubWebhookSender sender
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.onixbyte.deltaforceguide.domain.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record GitHubWebhookIssue(
|
||||
String url,
|
||||
Long id,
|
||||
Long number,
|
||||
String title,
|
||||
String body,
|
||||
List<GitHubWebhookLabel> labels
|
||||
) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.onixbyte.deltaforceguide.domain.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record GitHubWebhookLabel(
|
||||
String name
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.onixbyte.deltaforceguide.domain.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record GitHubWebhookRepository(
|
||||
String fullName
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.onixbyte.deltaforceguide.domain.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record GitHubWebhookSender(
|
||||
String login
|
||||
) {
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.onixbyte.deltaforceguide.domain.entity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class Accessory {
|
||||
|
||||
@@ -45,4 +46,22 @@ public class Accessory {
|
||||
public void removeTuning(Tuning tuning) {
|
||||
this.tunings.remove(tuning);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof Accessory accessory)) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(slotName, accessory.slotName)
|
||||
&& Objects.equals(accessoryName, accessory.accessoryName)
|
||||
&& Objects.equals(tunings, accessory.tunings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(slotName, accessoryName, tunings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.onixbyte.deltaforceguide.domain.entity;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class Tuning {
|
||||
|
||||
private String tuningName;
|
||||
@@ -23,4 +25,21 @@ public class Tuning {
|
||||
public void setTuningValue(Double tuningValue) {
|
||||
this.tuningValue = tuningValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof Tuning tuning)) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(tuningName, tuning.tuningName)
|
||||
&& Objects.equals(tuningValue, tuning.tuningValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(tuningName, tuningValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.onixbyte.deltaforceguide.filter;
|
||||
|
||||
import com.onixbyte.deltaforceguide.wrapper.RepeatedlyReadRequestWrapper;
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Component
|
||||
public class WebhookFilter implements Filter {
|
||||
@Override
|
||||
public void doFilter(
|
||||
ServletRequest request,
|
||||
ServletResponse response,
|
||||
FilterChain chain
|
||||
) throws IOException, ServletException {
|
||||
var httpRequest = (HttpServletRequest) request;
|
||||
var wrappedRequest = new RepeatedlyReadRequestWrapper(httpRequest);
|
||||
chain.doFilter(wrappedRequest, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.onixbyte.deltaforceguide.interceptor;
|
||||
|
||||
import com.onixbyte.crypto.util.CryptoUtil;
|
||||
import com.onixbyte.deltaforceguide.exeption.BizException;
|
||||
import com.onixbyte.deltaforceguide.manager.WebhookManager;
|
||||
import com.onixbyte.deltaforceguide.shared.GitHubWebhookHeader;
|
||||
import com.onixbyte.deltaforceguide.wrapper.RepeatedlyReadRequestWrapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* Verifies GitHub webhook requests by validating the {@code X-Hub-Signature-256}
|
||||
* header against the configured secret using HMAC-SHA256.
|
||||
*
|
||||
* <p>Verification is skipped when no secret is configured. The signature format is
|
||||
* {@code sha256=<hex-digest>} as documented by GitHub.
|
||||
*/
|
||||
@Component
|
||||
public class GitHubWebhookInterceptor implements HandlerInterceptor {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GitHubWebhookInterceptor.class);
|
||||
|
||||
private final WebhookManager webhookManager;
|
||||
|
||||
public GitHubWebhookInterceptor(WebhookManager webhookManager) {
|
||||
this.webhookManager = webhookManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean preHandle(
|
||||
@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull Object handler
|
||||
) {
|
||||
if (!(request instanceof RepeatedlyReadRequestWrapper req)) {
|
||||
throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"Request body is not readable");
|
||||
}
|
||||
|
||||
var secret = webhookManager.github().secret();
|
||||
if (secret == null || secret.isBlank()) {
|
||||
log.debug("No GitHub webhook secret configured, skipping signature verification");
|
||||
return true;
|
||||
}
|
||||
|
||||
var signatureHeader = req.getHeader(GitHubWebhookHeader.SIGNATURE_256);
|
||||
if (signatureHeader == null || signatureHeader.isBlank()) {
|
||||
log.warn("Missing {} header from ip={}",
|
||||
GitHubWebhookHeader.SIGNATURE_256, request.getRemoteAddr());
|
||||
throw new BizException(HttpStatus.UNAUTHORIZED,
|
||||
"Missing webhook signature header");
|
||||
}
|
||||
|
||||
var body = req.getBodyString();
|
||||
try {
|
||||
var computed = "sha256=" + CryptoUtil.hmacSha256(secret, body);
|
||||
|
||||
if (!MessageDigest.isEqual(
|
||||
computed.getBytes(StandardCharsets.UTF_8),
|
||||
signatureHeader.getBytes(StandardCharsets.UTF_8))) {
|
||||
log.warn("Invalid webhook signature from ip={}", request.getRemoteAddr());
|
||||
throw new BizException(HttpStatus.UNAUTHORIZED, "Invalid webhook signature");
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
log.error("Failed to compute HMAC-SHA256", e);
|
||||
throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"Failed to verify webhook signature");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.onixbyte.deltaforceguide.interceptor;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
@Component
|
||||
public class TrafficInterceptor implements HandlerInterceptor {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TrafficInterceptor.class);
|
||||
|
||||
@Override
|
||||
public boolean preHandle(
|
||||
@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull Object handler
|
||||
) {
|
||||
var ip = resolveClientIp(request);
|
||||
var method = request.getMethod();
|
||||
var uri = request.getRequestURI();
|
||||
var query = request.getQueryString();
|
||||
var contentType = request.getContentType();
|
||||
var contentLength = request.getContentLength();
|
||||
var userAgent = request.getHeader("User-Agent");
|
||||
|
||||
log.info("Request method={}, uri={}, query={}, ip={}, content-type={}, content-length={}, user-agent={}",
|
||||
method, uri, query, ip, contentType, contentLength, userAgent);
|
||||
return true;
|
||||
}
|
||||
|
||||
private String resolveClientIp(HttpServletRequest request) {
|
||||
var xForwardedFor = request.getHeader("X-Forwarded-For");
|
||||
if (xForwardedFor != null && !xForwardedFor.isBlank()) {
|
||||
return xForwardedFor.split(",")[0].trim();
|
||||
}
|
||||
var xRealIp = request.getHeader("X-Real-IP");
|
||||
if (xRealIp != null && !xRealIp.isBlank()) {
|
||||
return xRealIp.trim();
|
||||
}
|
||||
return request.getRemoteAddr();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.onixbyte.deltaforceguide.manager;
|
||||
|
||||
import com.onixbyte.deltaforceguide.domain.entity.Accessory;
|
||||
import com.onixbyte.deltaforceguide.domain.entity.Firearm;
|
||||
import com.onixbyte.deltaforceguide.domain.entity.Modification;
|
||||
import com.onixbyte.deltaforceguide.domain.entity.Tuning;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.AccessoryRequest;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.ModificationRequest;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.ModificationResponse;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.TuningRequest;
|
||||
import com.onixbyte.deltaforceguide.repository.FirearmRepository;
|
||||
import com.onixbyte.deltaforceguide.repository.ModificationRepository;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class ModificationManager {
|
||||
|
||||
private final ModificationRepository modificationRepository;
|
||||
private final FirearmRepository firearmRepository;
|
||||
|
||||
public ModificationManager(
|
||||
ModificationRepository modificationRepository,
|
||||
FirearmRepository firearmRepository
|
||||
) {
|
||||
this.modificationRepository = modificationRepository;
|
||||
this.firearmRepository = firearmRepository;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ModificationResponse create(ModificationRequest request) {
|
||||
var firearm = firearmRepository.findById(request.firearmId())
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
|
||||
"Firearm not found: " + request.firearmId()));
|
||||
var modification = toEntity(request, firearm);
|
||||
return ModificationResponse.from(modificationRepository.save(modification));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<ModificationResponse> batchCreate(List<ModificationRequest> requests) {
|
||||
var firearmIds = requests.stream()
|
||||
.map(ModificationRequest::firearmId)
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||
|
||||
Map<Long, Firearm> firearmMap = new HashMap<>();
|
||||
firearmRepository.findAllById(firearmIds)
|
||||
.forEach(firearm -> firearmMap.put(firearm.getId(), firearm));
|
||||
|
||||
if (firearmMap.size() != firearmIds.size()) {
|
||||
var missing = firearmIds.stream()
|
||||
.filter((id) -> !firearmMap.containsKey(id))
|
||||
.toList();
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
|
||||
"Firearm not found: " + missing);
|
||||
}
|
||||
|
||||
var modifications = requests.stream()
|
||||
.map(req -> toEntity(req, firearmMap.get(req.firearmId())))
|
||||
.toList();
|
||||
return modificationRepository.saveAll(modifications)
|
||||
.stream()
|
||||
.map(ModificationResponse::from)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public Long resolveFirearmId(Long firearmId, String firearmName) {
|
||||
if (firearmId != null) {
|
||||
return firearmId;
|
||||
}
|
||||
if (firearmName == null || firearmName.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
var matches = firearmRepository.findByName(firearmName);
|
||||
if (matches.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
|
||||
"Firearm not found by name: " + firearmName);
|
||||
}
|
||||
return matches.getFirst().getId();
|
||||
}
|
||||
|
||||
private Modification toEntity(ModificationRequest request, Firearm firearm) {
|
||||
return Modification.builder()
|
||||
.firearm(firearm)
|
||||
.name(request.name())
|
||||
.code(request.code())
|
||||
.tags(request.tags())
|
||||
.accessories(toAccessories(request.accessories()))
|
||||
.note(request.note())
|
||||
.author(request.author())
|
||||
.videoUrl(request.videoUrl())
|
||||
.build();
|
||||
}
|
||||
|
||||
private List<Accessory> toAccessories(List<AccessoryRequest> requests) {
|
||||
if (requests == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return requests.stream().map(this::toAccessory).toList();
|
||||
}
|
||||
|
||||
private Accessory toAccessory(AccessoryRequest request) {
|
||||
var accessory = new Accessory();
|
||||
accessory.setSlotName(request.slotName());
|
||||
accessory.setAccessoryName(request.accessoryName());
|
||||
accessory.setTunings(toTunings(request.tunings()));
|
||||
return accessory;
|
||||
}
|
||||
|
||||
private List<Tuning> toTunings(List<TuningRequest> requests) {
|
||||
if (requests == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return requests.stream().map(this::toTuning).toList();
|
||||
}
|
||||
|
||||
private Tuning toTuning(TuningRequest request) {
|
||||
var tuning = new Tuning();
|
||||
tuning.setTuningName(request.tuningName());
|
||||
tuning.setTuningValue(request.tuningValue());
|
||||
return tuning;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.onixbyte.deltaforceguide.manager;
|
||||
|
||||
import com.onixbyte.deltaforceguide.properties.GitHubWebhookProperties;
|
||||
import com.onixbyte.deltaforceguide.properties.WebhookProperties;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class WebhookManager {
|
||||
|
||||
private final WebhookProperties webhookProperties;
|
||||
|
||||
@Autowired
|
||||
public WebhookManager(WebhookProperties webhookProperties) {
|
||||
this.webhookProperties = webhookProperties;
|
||||
}
|
||||
|
||||
public GitHubWebhookProperties github() {
|
||||
return webhookProperties.github();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.onixbyte.deltaforceguide.properties;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record GitHubWebhookProperties(
|
||||
String secret,
|
||||
List<String> allowedUsers
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.onixbyte.deltaforceguide.properties;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(prefix = "app.webhook")
|
||||
public record WebhookProperties(
|
||||
GitHubWebhookProperties github
|
||||
) {
|
||||
}
|
||||
@@ -7,9 +7,13 @@ import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface FirearmRepository extends JpaRepository<Firearm, Long> {
|
||||
|
||||
Page<Firearm> findAllByType(FirearmType type, Pageable pageable);
|
||||
|
||||
List<Firearm> findByName(String name);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.onixbyte.deltaforceguide.domain.entity.Accessory;
|
||||
import com.onixbyte.deltaforceguide.domain.entity.Firearm;
|
||||
import com.onixbyte.deltaforceguide.domain.entity.Modification;
|
||||
import com.onixbyte.deltaforceguide.domain.entity.Tuning;
|
||||
import com.onixbyte.deltaforceguide.manager.ModificationManager;
|
||||
import com.onixbyte.deltaforceguide.repository.FirearmRepository;
|
||||
import com.onixbyte.deltaforceguide.repository.ModificationRepository;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
@@ -21,10 +22,8 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
@@ -32,15 +31,18 @@ public class ModificationService {
|
||||
|
||||
private final ModificationRepository modificationRepository;
|
||||
private final FirearmRepository firearmRepository;
|
||||
private final ModificationManager modificationManager;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public ModificationService(
|
||||
ModificationRepository modificationRepository,
|
||||
FirearmRepository firearmRepository,
|
||||
ModificationManager modificationManager,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this.modificationRepository = modificationRepository;
|
||||
this.firearmRepository = firearmRepository;
|
||||
this.modificationManager = modificationManager;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@@ -79,36 +81,12 @@ public class ModificationService {
|
||||
|
||||
@Transactional
|
||||
public ModificationResponse create(ModificationRequest request) {
|
||||
Firearm firearm = firearmRepository.findById(request.firearmId())
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Firearm not found: " + request.firearmId()));
|
||||
|
||||
Modification modification = toEntity(request, firearm);
|
||||
return ModificationResponse.from(modificationRepository.save(modification));
|
||||
return modificationManager.create(request);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<ModificationResponse> batchCreate(List<ModificationRequest> requests) {
|
||||
Set<Long> firearmIds = requests.stream()
|
||||
.map(ModificationRequest::firearmId)
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||
|
||||
Map<Long, Firearm> firearmMap = new HashMap<>();
|
||||
firearmRepository.findAllById(firearmIds).forEach(firearm -> firearmMap.put(firearm.getId(), firearm));
|
||||
|
||||
if (firearmMap.size() != firearmIds.size()) {
|
||||
List<Long> missingFirearmIds = firearmIds.stream()
|
||||
.filter(id -> !firearmMap.containsKey(id))
|
||||
.toList();
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Firearm not found: " + missingFirearmIds);
|
||||
}
|
||||
|
||||
List<Modification> modifications = requests.stream()
|
||||
.map(request -> toEntity(request, firearmMap.get(request.firearmId())))
|
||||
.toList();
|
||||
return modificationRepository.saveAll(modifications)
|
||||
.stream()
|
||||
.map(ModificationResponse::from)
|
||||
.toList();
|
||||
return modificationManager.batchCreate(requests);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -155,19 +133,6 @@ public class ModificationService {
|
||||
modificationRepository.deleteAllInBatch(modifications);
|
||||
}
|
||||
|
||||
private Modification toEntity(ModificationRequest request, Firearm firearm) {
|
||||
return Modification.builder()
|
||||
.firearm(firearm)
|
||||
.name(request.name())
|
||||
.code(request.code())
|
||||
.tags(safeTags(request.tags()))
|
||||
.accessories(toAccessories(request.accessories()))
|
||||
.note(request.note())
|
||||
.author(request.author())
|
||||
.videoUrl(request.videoUrl())
|
||||
.build();
|
||||
}
|
||||
|
||||
private List<String> safeTags(List<String> tags) {
|
||||
return tags == null ? new ArrayList<>() : tags;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
package com.onixbyte.deltaforceguide.service;
|
||||
|
||||
import com.onixbyte.deltaforceguide.domain.dto.AccessoryRequest;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.GitHubIssueRequest;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.ModificationRequest;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.TuningRequest;
|
||||
import com.onixbyte.deltaforceguide.manager.ModificationManager;
|
||||
import com.onixbyte.deltaforceguide.manager.WebhookManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Service
|
||||
public class WebhookService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(WebhookService.class);
|
||||
private static final String TRIGGER_LABEL = "weapon-mod";
|
||||
private static final Duration DEDUP_TTL = Duration.ofHours(12);
|
||||
private static final Pattern YAML_FENCE =
|
||||
Pattern.compile("```ya?ml\\s*\\n(.*?)```", Pattern.DOTALL);
|
||||
|
||||
private final ModificationManager modificationManager;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final WebhookManager webhookManager;
|
||||
private final Yaml yaml;
|
||||
|
||||
public WebhookService(
|
||||
ModificationManager modificationManager,
|
||||
RedisTemplate<String, Object> redisTemplate,
|
||||
WebhookManager webhookManager
|
||||
) {
|
||||
this.modificationManager = modificationManager;
|
||||
this.redisTemplate = redisTemplate;
|
||||
this.webhookManager = webhookManager;
|
||||
this.yaml = new Yaml();
|
||||
}
|
||||
|
||||
public void processIssueEvent(GitHubIssueRequest request) {
|
||||
var issue = request.issue();
|
||||
var repository = request.repository();
|
||||
var sender = request.sender();
|
||||
|
||||
if (!isAllowedSender(sender)) {
|
||||
log.info("Issue #{} sender={} not in allowed-users, skipping",
|
||||
issue.number(), sender != null ? sender.login() : "null");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasTriggerLabel(issue.labels())) {
|
||||
log.debug("Issue #{} lacks trigger label, skipping", issue.number());
|
||||
return;
|
||||
}
|
||||
|
||||
var dedupKey = "github:webhook:processed:%s:%d"
|
||||
.formatted(repository.fullName(), issue.number());
|
||||
var acquired = redisTemplate.opsForValue()
|
||||
.setIfAbsent(dedupKey, "1", DEDUP_TTL);
|
||||
if (acquired == null || !acquired) {
|
||||
log.info("Issue #{} already processed, skipping", issue.number());
|
||||
return;
|
||||
}
|
||||
|
||||
var parsedYaml = extractYaml(issue.body());
|
||||
if (parsedYaml == null) {
|
||||
log.warn("No YAML block found in issue #{} body", issue.number());
|
||||
return;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
var data = (Map<String, Object>) yaml.load(parsedYaml);
|
||||
if (data == null) {
|
||||
log.warn("Empty YAML block in issue #{}", issue.number());
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.containsKey("modifications")) {
|
||||
processBatch(issue.number(), data);
|
||||
} else {
|
||||
processSingle(issue.number(), data);
|
||||
}
|
||||
}
|
||||
|
||||
private void processSingle(Long issueNumber, Map<String, Object> data) {
|
||||
var request = mapToRequest(data);
|
||||
log.info("Creating modification from issue #{}: name={}", issueNumber, request.name());
|
||||
modificationManager.create(request);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void processBatch(Long issueNumber, Map<String, Object> data) {
|
||||
var list = (List<Map<String, Object>>) data.get("modifications");
|
||||
if (list == null || list.isEmpty()) {
|
||||
log.warn("Empty modifications list in issue #{}", issueNumber);
|
||||
return;
|
||||
}
|
||||
var requests = list.stream()
|
||||
.map(this::mapToRequest)
|
||||
.toList();
|
||||
log.info("Batch creating {} modifications from issue #{}", requests.size(), issueNumber);
|
||||
modificationManager.batchCreate(requests);
|
||||
}
|
||||
|
||||
private ModificationRequest mapToRequest(Map<String, Object> data) {
|
||||
Long firearmId = modificationManager.resolveFirearmId(
|
||||
toLong(data.get("firearmId")),
|
||||
(String) data.get("firearmName"));
|
||||
String name = (String) data.get("name");
|
||||
String code = (String) data.get("code");
|
||||
List<String> tags = toStringList(data.get("tags"));
|
||||
List<AccessoryRequest> accessories = mapAccessories(data.get("accessories"));
|
||||
String note = (String) data.get("note");
|
||||
String author = (String) data.get("author");
|
||||
String videoUrl = (String) data.get("videoUrl");
|
||||
|
||||
return new ModificationRequest(firearmId, name, code, tags, accessories,
|
||||
note, author, videoUrl);
|
||||
}
|
||||
|
||||
private List<AccessoryRequest> mapAccessories(Object raw) {
|
||||
if (!(raw instanceof List<?> list)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
var result = new ArrayList<AccessoryRequest>();
|
||||
for (var item : list) {
|
||||
if (item instanceof Map<?, ?> map) {
|
||||
result.add(new AccessoryRequest(
|
||||
(String) map.get("slotName"),
|
||||
(String) map.get("accessoryName"),
|
||||
mapTunings(map.get("tunings"))
|
||||
));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<TuningRequest> mapTunings(Object raw) {
|
||||
if (!(raw instanceof List<?> list)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
var result = new ArrayList<TuningRequest>();
|
||||
for (var item : list) {
|
||||
if (item instanceof Map<?, ?> map) {
|
||||
result.add(new TuningRequest(
|
||||
(String) map.get("tuningName"),
|
||||
toDouble(map.get("tuningValue"))
|
||||
));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<String> toStringList(Object raw) {
|
||||
if (raw instanceof List<?> list) {
|
||||
return list.stream()
|
||||
.filter(String.class::isInstance)
|
||||
.map(String.class::cast)
|
||||
.toList();
|
||||
}
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
private boolean isAllowedSender(
|
||||
com.onixbyte.deltaforceguide.domain.dto.GitHubWebhookSender sender
|
||||
) {
|
||||
var allowedUsers = webhookManager.github().allowedUsers();
|
||||
if (allowedUsers == null || allowedUsers.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (sender == null || sender.login() == null) {
|
||||
return false;
|
||||
}
|
||||
return allowedUsers.contains(sender.login());
|
||||
}
|
||||
|
||||
private boolean hasTriggerLabel(List<com.onixbyte.deltaforceguide.domain.dto.GitHubWebhookLabel> labels) {
|
||||
if (labels == null) {
|
||||
return false;
|
||||
}
|
||||
return labels.stream().anyMatch(label -> TRIGGER_LABEL.equals(label.name()));
|
||||
}
|
||||
|
||||
private String extractYaml(String body) {
|
||||
if (body == null) {
|
||||
return null;
|
||||
}
|
||||
var matcher = YAML_FENCE.matcher(body);
|
||||
return matcher.find() ? matcher.group(1) : null;
|
||||
}
|
||||
|
||||
private Long toLong(Object value) {
|
||||
if (value instanceof Number num) {
|
||||
return num.longValue();
|
||||
}
|
||||
if (value instanceof String s) {
|
||||
return Long.parseLong(s);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Double toDouble(Object value) {
|
||||
if (value instanceof Number num) {
|
||||
return num.doubleValue();
|
||||
}
|
||||
if (value instanceof String s) {
|
||||
return Double.parseDouble(s);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.onixbyte.deltaforceguide.shared;
|
||||
|
||||
/**
|
||||
* This class lists the header names that GitHub sends in webhook requests.
|
||||
*
|
||||
* @author siujamo
|
||||
*/
|
||||
public class GitHubWebhookHeader {
|
||||
|
||||
/**
|
||||
* The unique identifier of the webhook.
|
||||
*/
|
||||
public static final String HOOK_ID = "X-GitHub-Hook-ID";
|
||||
|
||||
/**
|
||||
* The name of the event that triggered the delivery.
|
||||
*/
|
||||
public static final String EVENT = "X-GitHub-Event";
|
||||
|
||||
/**
|
||||
* A globally unique identifier (GUID) to identify the event.
|
||||
*/
|
||||
public static final String DELIVERY = "X-GitHub-Delivery";
|
||||
|
||||
/**
|
||||
* This header is sent if the webhook is configured with a {@code secret}. This is the HMAC hex
|
||||
* digest of the request body, and is generated using the SHA-1 hash function and the secret as
|
||||
* the HMAC {@code key}. {@code X-Hub-Signature} is provided for compatibility with
|
||||
* existing integrations. We recommend that you use the more secure
|
||||
* {@code X-Hub-Signature-256} instead.
|
||||
*/
|
||||
public static final String SIGNATURE = "X-Hub-Signature";
|
||||
|
||||
/**
|
||||
* This header is sent if the webhook is configured with a {@code secret}. This is the HMAC hex
|
||||
* digest of the request body, and is generated using the SHA-256 hash function and the
|
||||
* {@code secret} as the HMAC key. For more information, see <a href="https://docs.github.com/en/webhooks/using-webhooks/securing-your-webhooks"
|
||||
* >Validating webhook deliveries</a>.
|
||||
*/
|
||||
public static final String SIGNATURE_256 = "X-Hub-Signature-256";
|
||||
|
||||
/**
|
||||
* This header will always have the prefix {@code GitHub-Hookshot/}.
|
||||
*/
|
||||
public static final String USER_AGENT = "User-Agent";
|
||||
|
||||
/**
|
||||
* The type of resource where the webhook was created.
|
||||
*/
|
||||
public static final String INSTALLATION_TARGET_TYPE = "X-GitHub-Hook-Installation-Target-Type";
|
||||
|
||||
/**
|
||||
* The unique identifier of the resource where the webhook was created.
|
||||
*/
|
||||
public static final String INSTALLATION_TARGET_ID = "X-GitHub-Hook-Installation-Target-ID";
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.onixbyte.deltaforceguide.wrapper;
|
||||
|
||||
import jakarta.servlet.ReadListener;
|
||||
import jakarta.servlet.ServletInputStream;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class RepeatedlyReadRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private final byte[] body;
|
||||
|
||||
public RepeatedlyReadRequestWrapper(HttpServletRequest request) throws IOException {
|
||||
super(request);
|
||||
this.body = StreamUtils.copyToByteArray(request.getInputStream());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() {
|
||||
final var byteArrayInputStream = new ByteArrayInputStream(body);
|
||||
return new ServletInputStream() {
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return byteArrayInputStream.available() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return byteArrayInputStream.read();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() throws IOException {
|
||||
return new BufferedReader(new InputStreamReader(this.getInputStream(), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public String getBodyString() {
|
||||
return new String(body, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,12 @@ mybatis:
|
||||
type-handlers-package: com.onixbyte.deltaforceguide.mapper.handler
|
||||
mapper-locations: classpath:/mapper/*.xml
|
||||
|
||||
app:
|
||||
webhook:
|
||||
github:
|
||||
secret: ${GITHUB_WEBHOOK_SECRET:}
|
||||
allowed-users: []
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.hibernate:
|
||||
|
||||
Reference in New Issue
Block a user