From 66b37ec20d7ced29834453ea52978a66f73b8b3e Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 28 May 2026 13:51:24 +0800 Subject: [PATCH 01/25] fix: add equals and hashCode to Accessory and Tuning entities --- .../domain/entity/Accessory.java | 19 +++++++++++++++++++ .../deltaforceguide/domain/entity/Tuning.java | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/Accessory.java b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/Accessory.java index 8c4b6b9..b76b622 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/Accessory.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/Accessory.java @@ -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); + } } diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/Tuning.java b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/Tuning.java index 4ab9879..83eb1b8 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/Tuning.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/Tuning.java @@ -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); + } } From f866d93fb47bb3a8a68affea532b0231f1e28dc9 Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 28 May 2026 13:54:30 +0800 Subject: [PATCH 02/25] feat: add gitlab webhook verification --- .../deltaforceguide/config/AppConfig.java | 21 +++- .../deltaforceguide/config/FilterConfig.java | 21 ++++ .../deltaforceguide/config/WebhookConfig.java | 10 ++ .../deltaforceguide/filter/WebhookFilter.java | 22 ++++ .../interceptor/GitLabWebhookInterceptor.java | 109 ++++++++++++++++++ .../manager/WebhookManager.java | 21 ++++ .../properties/GitLabWebhookProperties.java | 6 + .../properties/WebhookProperties.java | 9 ++ .../service/WebhookService.java | 14 +++ .../wrapper/RepeatedlyReadRequestWrapper.java | 56 +++++++++ 10 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/onixbyte/deltaforceguide/config/FilterConfig.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/config/WebhookConfig.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/filter/WebhookFilter.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/interceptor/GitLabWebhookInterceptor.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/manager/WebhookManager.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/properties/GitLabWebhookProperties.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/properties/WebhookProperties.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/wrapper/RepeatedlyReadRequestWrapper.java diff --git a/src/main/java/com/onixbyte/deltaforceguide/config/AppConfig.java b/src/main/java/com/onixbyte/deltaforceguide/config/AppConfig.java index 32ec283..8d7eb6c 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/config/AppConfig.java +++ b/src/main/java/com/onixbyte/deltaforceguide/config/AppConfig.java @@ -1,10 +1,29 @@ package com.onixbyte.deltaforceguide.config; +import com.onixbyte.deltaforceguide.interceptor.GitLabWebhookInterceptor; 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 GitLabWebhookInterceptor gitLabWebhookInterceptor; + + @Autowired + public AppConfig( + GitLabWebhookInterceptor gitLabWebhookInterceptor + ) { + this.gitLabWebhookInterceptor = gitLabWebhookInterceptor; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(gitLabWebhookInterceptor) + .addPathPatterns("/webhook/gitlab"); + } } diff --git a/src/main/java/com/onixbyte/deltaforceguide/config/FilterConfig.java b/src/main/java/com/onixbyte/deltaforceguide/config/FilterConfig.java new file mode 100644 index 0000000..a80bcfc --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/config/FilterConfig.java @@ -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 loggingFilter(WebhookFilter webhookFilter) { + var registrationBean = new FilterRegistrationBean(); + + registrationBean.setFilter(webhookFilter); + registrationBean.addUrlPatterns("/webhook/*"); + registrationBean.setOrder(1); + + return registrationBean; + } +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/config/WebhookConfig.java b/src/main/java/com/onixbyte/deltaforceguide/config/WebhookConfig.java new file mode 100644 index 0000000..1cb1a6c --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/config/WebhookConfig.java @@ -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 { +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/filter/WebhookFilter.java b/src/main/java/com/onixbyte/deltaforceguide/filter/WebhookFilter.java new file mode 100644 index 0000000..ddfd071 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/filter/WebhookFilter.java @@ -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); + } +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitLabWebhookInterceptor.java b/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitLabWebhookInterceptor.java new file mode 100644 index 0000000..34eb9fd --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitLabWebhookInterceptor.java @@ -0,0 +1,109 @@ +package com.onixbyte.deltaforceguide.interceptor; + +import com.onixbyte.deltaforceguide.exeption.BizException; +import com.onixbyte.deltaforceguide.properties.WebhookProperties; +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.MessageDigest; +import java.util.Base64; + +@Component +public class GitLabWebhookInterceptor implements HandlerInterceptor { + + private static final Logger log = LoggerFactory.getLogger(GitLabWebhookInterceptor.class); + private static final String TOKEN_PREFIX = "whsec_"; + + private final WebhookProperties webhookProperties; + + public GitLabWebhookInterceptor(WebhookProperties webhookProperties) { + this.webhookProperties = webhookProperties; + } + + @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 webhookId = request.getHeader("webhook-id"); + var webhookTimestamp = request.getHeader("webhook-timestamp"); + var webhookSignature = request.getHeader("webhook-signature"); + + if (webhookId == null || webhookTimestamp == null || webhookSignature == null) { + log.warn("Missing webhook headers from ip={}", request.getRemoteAddr()); + throw new BizException(HttpStatus.UNAUTHORIZED, + "Missing webhook verification headers"); + } + + var signingToken = webhookProperties.gitlab().signingToken(); + if (signingToken == null || signingToken.isBlank()) { + log.debug("No GitLab signing token configured, skipping signature verification"); + return true; + } + + var body = req.getBodyString(); + var signedContent = "%s.%s.%s".formatted(webhookId, webhookTimestamp, body); + + try { + var decodedKey = decodeSigningToken(signingToken); + var computedDigest = computeHmacSha256(decodedKey, signedContent); + var computedSignature = "v1,%s".formatted(computedDigest); + + var signatures = webhookSignature.split(" "); + var matched = false; + for (var sig : signatures) { + if (MessageDigest.isEqual(computedSignature.getBytes(StandardCharsets.UTF_8), + sig.trim().getBytes(StandardCharsets.UTF_8))) { + matched = true; + break; + } + } + + if (!matched) { + log.warn("Invalid webhook signature from ip={}", request.getRemoteAddr()); + throw new BizException(HttpStatus.UNAUTHORIZED, "Invalid webhook signature"); + } + } catch (BizException e) { + throw e; + } catch (Exception e) { + log.error("Failed to verify webhook signature", e); + throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR, + "Failed to verify webhook signature"); + } + + return true; + } + + private byte[] decodeSigningToken(String token) { + if (!token.startsWith(TOKEN_PREFIX)) { + throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR, + "Signing token must start with " + TOKEN_PREFIX); + } + var encoded = token.substring(TOKEN_PREFIX.length()); + return Base64.getDecoder().decode(encoded); + } + + private String computeHmacSha256(byte[] key, String data) throws Exception { + var mac = Mac.getInstance("HmacSHA256"); + var secretKey = new SecretKeySpec(key, "HmacSHA256"); + mac.init(secretKey); + var hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(hash); + } +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/manager/WebhookManager.java b/src/main/java/com/onixbyte/deltaforceguide/manager/WebhookManager.java new file mode 100644 index 0000000..a4512ba --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/manager/WebhookManager.java @@ -0,0 +1,21 @@ +package com.onixbyte.deltaforceguide.manager; + +import com.onixbyte.deltaforceguide.properties.GitLabWebhookProperties; +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 GitLabWebhookProperties getGitLabWebhookProperties() { + return webhookProperties.gitlab(); + } +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/properties/GitLabWebhookProperties.java b/src/main/java/com/onixbyte/deltaforceguide/properties/GitLabWebhookProperties.java new file mode 100644 index 0000000..318830e --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/properties/GitLabWebhookProperties.java @@ -0,0 +1,6 @@ +package com.onixbyte.deltaforceguide.properties; + +public record GitLabWebhookProperties( + String signingToken +) { +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/properties/WebhookProperties.java b/src/main/java/com/onixbyte/deltaforceguide/properties/WebhookProperties.java new file mode 100644 index 0000000..bcd439c --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/properties/WebhookProperties.java @@ -0,0 +1,9 @@ +package com.onixbyte.deltaforceguide.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "app.webhook") +public record WebhookProperties( + GitLabWebhookProperties gitlab +) { +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java b/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java new file mode 100644 index 0000000..4a1f50d --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java @@ -0,0 +1,14 @@ +package com.onixbyte.deltaforceguide.service; + +import com.onixbyte.deltaforceguide.manager.WebhookManager; +import org.springframework.stereotype.Service; + +@Service +public class WebhookService { + + private final WebhookManager webhookManager; + + public WebhookService(WebhookManager webhookManager) { + this.webhookManager = webhookManager; + } +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/wrapper/RepeatedlyReadRequestWrapper.java b/src/main/java/com/onixbyte/deltaforceguide/wrapper/RepeatedlyReadRequestWrapper.java new file mode 100644 index 0000000..ee31c22 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/wrapper/RepeatedlyReadRequestWrapper.java @@ -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); + } +} From d44f5f74fee421dd8917e77d19b56c52f2ab328b Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 28 May 2026 13:55:22 +0800 Subject: [PATCH 03/25] chore: ignore frp client config --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b734735..e026c93 100644 --- a/.gitignore +++ b/.gitignore @@ -157,3 +157,6 @@ gradle-app.setting .classpath gradle.properties + +# frp config +frpc.toml From 8d24b6082da8012ebe1589ad9b2789338a5678d8 Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 28 May 2026 15:18:25 +0800 Subject: [PATCH 04/25] feat: add gitlab webhook http entrypoint --- .../controller/WebhookController.java | 20 +++++++++ .../domain/dto/GitLabWebhookRequest.java | 45 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 src/main/java/com/onixbyte/deltaforceguide/controller/WebhookController.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitLabWebhookRequest.java diff --git a/src/main/java/com/onixbyte/deltaforceguide/controller/WebhookController.java b/src/main/java/com/onixbyte/deltaforceguide/controller/WebhookController.java new file mode 100644 index 0000000..4ca9a66 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/controller/WebhookController.java @@ -0,0 +1,20 @@ +package com.onixbyte.deltaforceguide.controller; + +import com.onixbyte.deltaforceguide.domain.dto.GitLabWebhookRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/webhook") +public class WebhookController { + + private static final Logger log = LoggerFactory.getLogger(WebhookController.class); + + @PostMapping("/gitlab") + public void gitlabWebhook( + @RequestBody GitLabWebhookRequest request + ) { + log.info("request={}", request); + } +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitLabWebhookRequest.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitLabWebhookRequest.java new file mode 100644 index 0000000..b48ec75 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitLabWebhookRequest.java @@ -0,0 +1,45 @@ +package com.onixbyte.deltaforceguide.domain.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import java.time.OffsetDateTime; +import java.util.List; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public record GitLabWebhookRequest( + String objectKind, + String eventType, + GitLabWebhookObjectAttributes objectAttributes +) { + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + @JsonIgnoreProperties(ignoreUnknown = true) + public record GitLabWebhookLabel( + Long id, + String title, + @JsonProperty("color") + String colour, + Long projectId, + String createdAt, + String updatedAt, + Boolean template, + String description, + String type, + Long groupId + ) {} + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + @JsonIgnoreProperties(ignoreUnknown = true) + public record GitLabWebhookObjectAttributes( + Long id, + String title, + String description, + List labels + ) {} + + +} From 72ec8758027072cfa176a853a0bc82aa2e9bfc08 Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 28 May 2026 15:22:36 +0800 Subject: [PATCH 05/25] docs: add Javadoc for GitLabWebhookInterceptor --- .../interceptor/GitLabWebhookInterceptor.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitLabWebhookInterceptor.java b/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitLabWebhookInterceptor.java index 34eb9fd..b200963 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitLabWebhookInterceptor.java +++ b/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitLabWebhookInterceptor.java @@ -18,6 +18,14 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.Base64; +/** + * Verifies GitLab webhook requests by validating the {@code webhook-id}, + * {@code webhook-timestamp}, and {@code webhook-signature} headers against + * a configured signing token using HMAC-SHA256. + * + *

Supports GitLab's v1 signature scheme. Verification is skipped when no + * signing token is configured. + */ @Component public class GitLabWebhookInterceptor implements HandlerInterceptor { @@ -26,10 +34,28 @@ public class GitLabWebhookInterceptor implements HandlerInterceptor { private final WebhookProperties webhookProperties; + /** + * Creates a new interceptor with the given webhook configuration. + * + * @param webhookProperties the webhook configuration properties + */ public GitLabWebhookInterceptor(WebhookProperties webhookProperties) { this.webhookProperties = webhookProperties; } + /** + * Validates the GitLab webhook signature headers on the incoming request. + * Reads {@code webhook-id}, {@code webhook-timestamp}, and + * {@code webhook-signature} headers and verifies the signature against the + * configured signing token. If no token is configured, verification is skipped. + * + * @param request the incoming HTTP request (must be a {@link RepeatedlyReadRequestWrapper}) + * @param response the HTTP response + * @param handler the chosen handler to execute + * @return {@code true} if the request is authentic + * @throws BizException with {@code 401} if headers are missing or the signature is invalid, + * with {@code 500} if verification fails unexpectedly + */ @Override public boolean preHandle( @NonNull HttpServletRequest request, @@ -90,6 +116,14 @@ public class GitLabWebhookInterceptor implements HandlerInterceptor { return true; } + /** + * Decodes a GitLab-format signing token by stripping the {@code whsec_} prefix + * and Base64-decoding the remainder. + * + * @param token the prefixed signing token + * @return the raw key bytes + * @throws BizException if the token does not start with {@code whsec_} + */ private byte[] decodeSigningToken(String token) { if (!token.startsWith(TOKEN_PREFIX)) { throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR, @@ -99,6 +133,14 @@ public class GitLabWebhookInterceptor implements HandlerInterceptor { return Base64.getDecoder().decode(encoded); } + /** + * Computes the Base64-encoded HMAC-SHA256 digest for the given data. + * + * @param key the secret key bytes + * @param data the content to sign + * @return Base64-encoded HMAC-SHA256 digest + * @throws Exception if the HMAC algorithm is unavailable + */ private String computeHmacSha256(byte[] key, String data) throws Exception { var mac = Mac.getInstance("HmacSHA256"); var secretKey = new SecretKeySpec(key, "HmacSHA256"); From 4810ef2b1f54cac8bce73b1d6444a29bc02fd942 Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 28 May 2026 15:24:42 +0800 Subject: [PATCH 06/25] refactor: migrate properties accessing to access via manager --- .../interceptor/GitLabWebhookInterceptor.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitLabWebhookInterceptor.java b/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitLabWebhookInterceptor.java index b200963..68030fc 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitLabWebhookInterceptor.java +++ b/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitLabWebhookInterceptor.java @@ -1,7 +1,7 @@ package com.onixbyte.deltaforceguide.interceptor; import com.onixbyte.deltaforceguide.exeption.BizException; -import com.onixbyte.deltaforceguide.properties.WebhookProperties; +import com.onixbyte.deltaforceguide.manager.WebhookManager; import com.onixbyte.deltaforceguide.wrapper.RepeatedlyReadRequestWrapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -32,15 +32,15 @@ public class GitLabWebhookInterceptor implements HandlerInterceptor { private static final Logger log = LoggerFactory.getLogger(GitLabWebhookInterceptor.class); private static final String TOKEN_PREFIX = "whsec_"; - private final WebhookProperties webhookProperties; + private final WebhookManager webhookManager; /** * Creates a new interceptor with the given webhook configuration. * - * @param webhookProperties the webhook configuration properties + * @param webhookManager the webhook manager */ - public GitLabWebhookInterceptor(WebhookProperties webhookProperties) { - this.webhookProperties = webhookProperties; + public GitLabWebhookInterceptor(WebhookManager webhookManager) { + this.webhookManager = webhookManager; } /** @@ -77,7 +77,7 @@ public class GitLabWebhookInterceptor implements HandlerInterceptor { "Missing webhook verification headers"); } - var signingToken = webhookProperties.gitlab().signingToken(); + var signingToken = webhookManager.getGitLabWebhookProperties().signingToken(); if (signingToken == null || signingToken.isBlank()) { log.debug("No GitLab signing token configured, skipping signature verification"); return true; From 243283b78878e01d61403c52037c28db4fc2a610 Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 28 May 2026 15:28:53 +0800 Subject: [PATCH 07/25] docs: reformatted javadocs --- .../deltaforceguide/interceptor/GitLabWebhookInterceptor.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitLabWebhookInterceptor.java b/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitLabWebhookInterceptor.java index 68030fc..f933928 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitLabWebhookInterceptor.java +++ b/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitLabWebhookInterceptor.java @@ -45,6 +45,7 @@ public class GitLabWebhookInterceptor implements HandlerInterceptor { /** * Validates the GitLab webhook signature headers on the incoming request. + *

* Reads {@code webhook-id}, {@code webhook-timestamp}, and * {@code webhook-signature} headers and verifies the signature against the * configured signing token. If no token is configured, verification is skipped. From 70323434873fdeca864bce1c75b8a657f5f64c79 Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 28 May 2026 15:35:22 +0800 Subject: [PATCH 08/25] feat: update URI to match standard in GitLab issues --- .../onixbyte/deltaforceguide/controller/WebhookController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/onixbyte/deltaforceguide/controller/WebhookController.java b/src/main/java/com/onixbyte/deltaforceguide/controller/WebhookController.java index 4ca9a66..b882977 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/controller/WebhookController.java +++ b/src/main/java/com/onixbyte/deltaforceguide/controller/WebhookController.java @@ -6,7 +6,7 @@ import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/webhook") +@RequestMapping("/webhooks") public class WebhookController { private static final Logger log = LoggerFactory.getLogger(WebhookController.class); From ce330bca871011d1d22307a9130cbdcd7926b465 Mon Sep 17 00:00:00 2001 From: siujamo Date: Fri, 29 May 2026 15:10:48 +0800 Subject: [PATCH 09/25] feat: create GitHub Webhook request object --- .../deltaforceguide/config/FilterConfig.java | 2 +- .../dto/GitHubIssueLabeledWebhookRequest.java | 7 +++++++ .../domain/dto/GitHubWebhookIssue.java | 13 +++++++++++++ .../domain/dto/GitHubWebhookLabel.java | 6 ++++++ 4 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubIssueLabeledWebhookRequest.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookIssue.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookLabel.java diff --git a/src/main/java/com/onixbyte/deltaforceguide/config/FilterConfig.java b/src/main/java/com/onixbyte/deltaforceguide/config/FilterConfig.java index a80bcfc..40442ab 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/config/FilterConfig.java +++ b/src/main/java/com/onixbyte/deltaforceguide/config/FilterConfig.java @@ -13,7 +13,7 @@ public class FilterConfig { var registrationBean = new FilterRegistrationBean(); registrationBean.setFilter(webhookFilter); - registrationBean.addUrlPatterns("/webhook/*"); + registrationBean.addUrlPatterns("/webhooks/*"); registrationBean.setOrder(1); return registrationBean; diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubIssueLabeledWebhookRequest.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubIssueLabeledWebhookRequest.java new file mode 100644 index 0000000..b7384ef --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubIssueLabeledWebhookRequest.java @@ -0,0 +1,7 @@ +package com.onixbyte.deltaforceguide.domain.dto; + +public record GitHubIssueLabeledWebhookRequest( + String action, + GitHubWebhookIssue issue +) { +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookIssue.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookIssue.java new file mode 100644 index 0000000..abc3655 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookIssue.java @@ -0,0 +1,13 @@ +package com.onixbyte.deltaforceguide.domain.dto; + +import java.util.List; + +public record GitHubWebhookIssue( + String url, + Long id, + String title, + String body, + List labels +) { + +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookLabel.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookLabel.java new file mode 100644 index 0000000..fb3407a --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookLabel.java @@ -0,0 +1,6 @@ +package com.onixbyte.deltaforceguide.domain.dto; + +public record GitHubWebhookLabel( + String name +) { +} From f9c210c8b3429110201874579aaca43914b5458f Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 1 Jun 2026 13:58:03 +0800 Subject: [PATCH 10/25] chore: bump onixbyte toolbox to v3.4.0 --- gradle/libs.versions.toml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 89621f5..811ee52 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" @@ -42,12 +41,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" } From 44271eeec4bae1460228afdf13c62f47f8312e70 Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 1 Jun 2026 15:27:35 +0800 Subject: [PATCH 11/25] feat: add GitHub webhook DTOs with snake_case mapping and header constants Replace GitHubIssueLabeledWebhookRequest with GitHubIssueRequest. Add number and repository fields for dedup key construction. Add Jackson @JsonNaming/@JsonIgnoreProperties for GitHub payload deserialisation. Add GitHubWebhookHeader constants for webhook header names. --- .../dto/GitHubIssueLabeledWebhookRequest.java | 7 --- .../domain/dto/GitHubIssueRequest.java | 14 +++++ .../domain/dto/GitHubWebhookIssue.java | 7 +++ .../domain/dto/GitHubWebhookLabel.java | 6 ++ .../domain/dto/GitHubWebhookRepository.java | 12 ++++ .../shared/GitHubWebhookHeader.java | 56 +++++++++++++++++++ 6 files changed, 95 insertions(+), 7 deletions(-) delete mode 100644 src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubIssueLabeledWebhookRequest.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubIssueRequest.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookRepository.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/shared/GitHubWebhookHeader.java diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubIssueLabeledWebhookRequest.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubIssueLabeledWebhookRequest.java deleted file mode 100644 index b7384ef..0000000 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubIssueLabeledWebhookRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.onixbyte.deltaforceguide.domain.dto; - -public record GitHubIssueLabeledWebhookRequest( - String action, - GitHubWebhookIssue issue -) { -} diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubIssueRequest.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubIssueRequest.java new file mode 100644 index 0000000..449591b --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubIssueRequest.java @@ -0,0 +1,14 @@ +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 +) { +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookIssue.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookIssue.java index abc3655..91263cc 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookIssue.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookIssue.java @@ -1,10 +1,17 @@ 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 labels diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookLabel.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookLabel.java index fb3407a..33954ab 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookLabel.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookLabel.java @@ -1,5 +1,11 @@ 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 ) { diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookRepository.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookRepository.java new file mode 100644 index 0000000..e3858d6 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookRepository.java @@ -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 +) { +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/shared/GitHubWebhookHeader.java b/src/main/java/com/onixbyte/deltaforceguide/shared/GitHubWebhookHeader.java new file mode 100644 index 0000000..05b9d58 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/shared/GitHubWebhookHeader.java @@ -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 Validating webhook deliveries. + */ + 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"; +} From 12469f1b27d99e2a71cc49deb3eda8c96101fd41 Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 1 Jun 2026 15:29:19 +0800 Subject: [PATCH 12/25] feat: replace GitLab webhook properties with GitHub webhook config Replace GitLabWebhookProperties with GitHubWebhookProperties (secret, allowed-users). Update WebhookProperties and WebhookManager to use GitHub-only configuration. Add app.webhook.github defaults to application.yaml. --- .../onixbyte/deltaforceguide/manager/WebhookManager.java | 6 +++--- .../properties/GitHubWebhookProperties.java | 9 +++++++++ .../properties/GitLabWebhookProperties.java | 6 ------ .../deltaforceguide/properties/WebhookProperties.java | 2 +- src/main/resources/application.yaml | 6 ++++++ 5 files changed, 19 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/onixbyte/deltaforceguide/properties/GitHubWebhookProperties.java delete mode 100644 src/main/java/com/onixbyte/deltaforceguide/properties/GitLabWebhookProperties.java diff --git a/src/main/java/com/onixbyte/deltaforceguide/manager/WebhookManager.java b/src/main/java/com/onixbyte/deltaforceguide/manager/WebhookManager.java index a4512ba..1e63f36 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/manager/WebhookManager.java +++ b/src/main/java/com/onixbyte/deltaforceguide/manager/WebhookManager.java @@ -1,6 +1,6 @@ package com.onixbyte.deltaforceguide.manager; -import com.onixbyte.deltaforceguide.properties.GitLabWebhookProperties; +import com.onixbyte.deltaforceguide.properties.GitHubWebhookProperties; import com.onixbyte.deltaforceguide.properties.WebhookProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -15,7 +15,7 @@ public class WebhookManager { this.webhookProperties = webhookProperties; } - public GitLabWebhookProperties getGitLabWebhookProperties() { - return webhookProperties.gitlab(); + public GitHubWebhookProperties github() { + return webhookProperties.github(); } } diff --git a/src/main/java/com/onixbyte/deltaforceguide/properties/GitHubWebhookProperties.java b/src/main/java/com/onixbyte/deltaforceguide/properties/GitHubWebhookProperties.java new file mode 100644 index 0000000..bf91c6a --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/properties/GitHubWebhookProperties.java @@ -0,0 +1,9 @@ +package com.onixbyte.deltaforceguide.properties; + +import java.util.List; + +public record GitHubWebhookProperties( + String secret, + List allowedUsers +) { +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/properties/GitLabWebhookProperties.java b/src/main/java/com/onixbyte/deltaforceguide/properties/GitLabWebhookProperties.java deleted file mode 100644 index 318830e..0000000 --- a/src/main/java/com/onixbyte/deltaforceguide/properties/GitLabWebhookProperties.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.onixbyte.deltaforceguide.properties; - -public record GitLabWebhookProperties( - String signingToken -) { -} diff --git a/src/main/java/com/onixbyte/deltaforceguide/properties/WebhookProperties.java b/src/main/java/com/onixbyte/deltaforceguide/properties/WebhookProperties.java index bcd439c..d420105 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/properties/WebhookProperties.java +++ b/src/main/java/com/onixbyte/deltaforceguide/properties/WebhookProperties.java @@ -4,6 +4,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "app.webhook") public record WebhookProperties( - GitLabWebhookProperties gitlab + GitHubWebhookProperties github ) { } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 66611ee..74de069 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -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: From 8c8ca58b745061d00996f32b2feeeec2d394ac4c Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 1 Jun 2026 15:29:32 +0800 Subject: [PATCH 13/25] feat: implement GitHub webhook HMAC-SHA256 signature verification Verify X-Hub-Signature-256 header using CryptoUtil.hmacSha256 from onixbyte crypto-toolbox. Signature check is skipped when no secret is configured. Uses MessageDigest.isEqual for constant-time comparison. --- build.gradle.kts | 1 + gradle/libs.versions.toml | 1 + .../interceptor/GitHubWebhookInterceptor.java | 85 +++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 src/main/java/com/onixbyte/deltaforceguide/interceptor/GitHubWebhookInterceptor.java diff --git a/build.gradle.kts b/build.gradle.kts index 0ce2b78..878912f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 811ee52..d9650f6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,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" } diff --git a/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitHubWebhookInterceptor.java b/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitHubWebhookInterceptor.java new file mode 100644 index 0000000..2f8e111 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitHubWebhookInterceptor.java @@ -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. + * + *

Verification is skipped when no secret is configured. The signature format is + * {@code sha256=} 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"); + } + } +} From 7fafa0d90356045a4b27fb477290f384d790df6b Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 1 Jun 2026 15:30:14 +0800 Subject: [PATCH 14/25] refactor: extract ModificationManager for modification creation Move create/batchCreate transactional logic from ModificationService into a dedicated ModificationManager. Both ModificationService and WebhookService delegate to the manager, respecting the Controller -> Service -> Manager layering rule. --- .../manager/ModificationManager.java | 115 ++++++++++++++++++ .../service/ModificationService.java | 47 +------ 2 files changed, 121 insertions(+), 41 deletions(-) create mode 100644 src/main/java/com/onixbyte/deltaforceguide/manager/ModificationManager.java diff --git a/src/main/java/com/onixbyte/deltaforceguide/manager/ModificationManager.java b/src/main/java/com/onixbyte/deltaforceguide/manager/ModificationManager.java new file mode 100644 index 0000000..eb0007a --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/manager/ModificationManager.java @@ -0,0 +1,115 @@ +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 batchCreate(List requests) { + var firearmIds = requests.stream() + .map(ModificationRequest::firearmId) + .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)); + + Map 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(); + } + + 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 toAccessories(List 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 toTunings(List 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; + } +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/ModificationService.java b/src/main/java/com/onixbyte/deltaforceguide/service/ModificationService.java index 39e5ec0..a5129af 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/service/ModificationService.java +++ b/src/main/java/com/onixbyte/deltaforceguide/service/ModificationService.java @@ -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 batchCreate(List requests) { - Set firearmIds = requests.stream() - .map(ModificationRequest::firearmId) - .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)); - - Map firearmMap = new HashMap<>(); - firearmRepository.findAllById(firearmIds).forEach(firearm -> firearmMap.put(firearm.getId(), firearm)); - - if (firearmMap.size() != firearmIds.size()) { - List missingFirearmIds = firearmIds.stream() - .filter(id -> !firearmMap.containsKey(id)) - .toList(); - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Firearm not found: " + missingFirearmIds); - } - - List 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 safeTags(List tags) { return tags == null ? new ArrayList<>() : tags; } From c30b5701e4eadbfc5eac77a2dbd0defc921a00c9 Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 1 Jun 2026 15:30:30 +0800 Subject: [PATCH 15/25] feat: implement GitHub webhook controller and processing service WebhookService parses YAML-fenced issue body, filters by "weapon-mod" label, deduplicates via Redis SETNX with 12h TTL, and delegates to ModificationManager for single/batch creation. GitHubWebhookController verifies X-GitHub-Event=issues and action=opened before delegating to the service. Register GitHubWebhookInterceptor for /webhooks/github in AppConfig. --- .../deltaforceguide/config/AppConfig.java | 12 +- .../controller/GitHubWebhookController.java | 43 ++++ .../service/WebhookService.java | 186 +++++++++++++++++- 3 files changed, 231 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/onixbyte/deltaforceguide/controller/GitHubWebhookController.java diff --git a/src/main/java/com/onixbyte/deltaforceguide/config/AppConfig.java b/src/main/java/com/onixbyte/deltaforceguide/config/AppConfig.java index de34b6f..6bba2db 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/config/AppConfig.java +++ b/src/main/java/com/onixbyte/deltaforceguide/config/AppConfig.java @@ -1,7 +1,7 @@ package com.onixbyte.deltaforceguide.config; +import com.onixbyte.deltaforceguide.interceptor.GitHubWebhookInterceptor; import com.onixbyte.deltaforceguide.interceptor.TrafficInterceptor; -import com.onixbyte.deltaforceguide.interceptor.GitLabWebhookInterceptor; import com.onixbyte.deltaforceguide.properties.AppProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -15,21 +15,21 @@ public class AppConfig implements WebMvcConfigurer { private final TrafficInterceptor trafficInterceptor; - private final GitLabWebhookInterceptor gitLabWebhookInterceptor; + private final GitHubWebhookInterceptor gitHubWebhookInterceptor; @Autowired public AppConfig( TrafficInterceptor trafficInterceptor, - GitLabWebhookInterceptor gitLabWebhookInterceptor + GitHubWebhookInterceptor gitHubWebhookInterceptor ) { this.trafficInterceptor = trafficInterceptor; - this.gitLabWebhookInterceptor = gitLabWebhookInterceptor; + this.gitHubWebhookInterceptor = gitHubWebhookInterceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(trafficInterceptor); - registry.addInterceptor(gitLabWebhookInterceptor) - .addPathPatterns("/webhook/gitlab"); + registry.addInterceptor(gitHubWebhookInterceptor) + .addPathPatterns("/webhooks/github"); } } diff --git a/src/main/java/com/onixbyte/deltaforceguide/controller/GitHubWebhookController.java b/src/main/java/com/onixbyte/deltaforceguide/controller/GitHubWebhookController.java new file mode 100644 index 0000000..580e1f9 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/controller/GitHubWebhookController.java @@ -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 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(); + } +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java b/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java index 4a1f50d..3489c2c 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java +++ b/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java @@ -1,14 +1,192 @@ package com.onixbyte.deltaforceguide.service; -import com.onixbyte.deltaforceguide.manager.WebhookManager; +import com.fasterxml.jackson.databind.ObjectMapper; +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 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 final WebhookManager webhookManager; + 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); - public WebhookService(WebhookManager webhookManager) { - this.webhookManager = webhookManager; + private final ModificationManager modificationManager; + private final RedisTemplate redisTemplate; + private final Yaml yaml; + + public WebhookService( + ModificationManager modificationManager, + RedisTemplate redisTemplate + ) { + this.modificationManager = modificationManager; + this.redisTemplate = redisTemplate; + this.yaml = new Yaml(); + } + + public void processIssueEvent(GitHubIssueRequest request) { + var issue = request.issue(); + var repository = request.repository(); + + 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) 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 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 data) { + var list = (List>) 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 data) { + Long firearmId = toLong(data.get("firearmId")); + String name = (String) data.get("name"); + String code = (String) data.get("code"); + List tags = toStringList(data.get("tags")); + List 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 mapAccessories(Object raw) { + if (!(raw instanceof List list)) { + return new ArrayList<>(); + } + var result = new ArrayList(); + 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 mapTunings(Object raw) { + if (!(raw instanceof List list)) { + return new ArrayList<>(); + } + var result = new ArrayList(); + 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 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 hasTriggerLabel(List 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; } } From 8a9cf110af841d658760af393d8cec4d6c7db954 Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 1 Jun 2026 15:37:06 +0800 Subject: [PATCH 16/25] chore: remove deprecated GitLab webhook code GitLab webhook has been superseded by the GitHub webhook implementation. Remove WebhookController (formerly GitLabWebhookController), GitLabWebhookRequest DTO, and GitLabWebhookInterceptor. --- .../controller/WebhookController.java | 20 --- .../domain/dto/GitLabWebhookRequest.java | 45 ------ .../interceptor/GitLabWebhookInterceptor.java | 152 ------------------ 3 files changed, 217 deletions(-) delete mode 100644 src/main/java/com/onixbyte/deltaforceguide/controller/WebhookController.java delete mode 100644 src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitLabWebhookRequest.java delete mode 100644 src/main/java/com/onixbyte/deltaforceguide/interceptor/GitLabWebhookInterceptor.java diff --git a/src/main/java/com/onixbyte/deltaforceguide/controller/WebhookController.java b/src/main/java/com/onixbyte/deltaforceguide/controller/WebhookController.java deleted file mode 100644 index b882977..0000000 --- a/src/main/java/com/onixbyte/deltaforceguide/controller/WebhookController.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.onixbyte.deltaforceguide.controller; - -import com.onixbyte.deltaforceguide.domain.dto.GitLabWebhookRequest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/webhooks") -public class WebhookController { - - private static final Logger log = LoggerFactory.getLogger(WebhookController.class); - - @PostMapping("/gitlab") - public void gitlabWebhook( - @RequestBody GitLabWebhookRequest request - ) { - log.info("request={}", request); - } -} diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitLabWebhookRequest.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitLabWebhookRequest.java deleted file mode 100644 index b48ec75..0000000 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitLabWebhookRequest.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.onixbyte.deltaforceguide.domain.dto; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.annotation.JsonNaming; - -import java.time.OffsetDateTime; -import java.util.List; - -@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) -@JsonIgnoreProperties(ignoreUnknown = true) -public record GitLabWebhookRequest( - String objectKind, - String eventType, - GitLabWebhookObjectAttributes objectAttributes -) { - - @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) - @JsonIgnoreProperties(ignoreUnknown = true) - public record GitLabWebhookLabel( - Long id, - String title, - @JsonProperty("color") - String colour, - Long projectId, - String createdAt, - String updatedAt, - Boolean template, - String description, - String type, - Long groupId - ) {} - - @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) - @JsonIgnoreProperties(ignoreUnknown = true) - public record GitLabWebhookObjectAttributes( - Long id, - String title, - String description, - List labels - ) {} - - -} diff --git a/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitLabWebhookInterceptor.java b/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitLabWebhookInterceptor.java deleted file mode 100644 index f933928..0000000 --- a/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitLabWebhookInterceptor.java +++ /dev/null @@ -1,152 +0,0 @@ -package com.onixbyte.deltaforceguide.interceptor; - -import com.onixbyte.deltaforceguide.exeption.BizException; -import com.onixbyte.deltaforceguide.manager.WebhookManager; -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.MessageDigest; -import java.util.Base64; - -/** - * Verifies GitLab webhook requests by validating the {@code webhook-id}, - * {@code webhook-timestamp}, and {@code webhook-signature} headers against - * a configured signing token using HMAC-SHA256. - * - *

Supports GitLab's v1 signature scheme. Verification is skipped when no - * signing token is configured. - */ -@Component -public class GitLabWebhookInterceptor implements HandlerInterceptor { - - private static final Logger log = LoggerFactory.getLogger(GitLabWebhookInterceptor.class); - private static final String TOKEN_PREFIX = "whsec_"; - - private final WebhookManager webhookManager; - - /** - * Creates a new interceptor with the given webhook configuration. - * - * @param webhookManager the webhook manager - */ - public GitLabWebhookInterceptor(WebhookManager webhookManager) { - this.webhookManager = webhookManager; - } - - /** - * Validates the GitLab webhook signature headers on the incoming request. - *

- * Reads {@code webhook-id}, {@code webhook-timestamp}, and - * {@code webhook-signature} headers and verifies the signature against the - * configured signing token. If no token is configured, verification is skipped. - * - * @param request the incoming HTTP request (must be a {@link RepeatedlyReadRequestWrapper}) - * @param response the HTTP response - * @param handler the chosen handler to execute - * @return {@code true} if the request is authentic - * @throws BizException with {@code 401} if headers are missing or the signature is invalid, - * with {@code 500} if verification fails unexpectedly - */ - @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 webhookId = request.getHeader("webhook-id"); - var webhookTimestamp = request.getHeader("webhook-timestamp"); - var webhookSignature = request.getHeader("webhook-signature"); - - if (webhookId == null || webhookTimestamp == null || webhookSignature == null) { - log.warn("Missing webhook headers from ip={}", request.getRemoteAddr()); - throw new BizException(HttpStatus.UNAUTHORIZED, - "Missing webhook verification headers"); - } - - var signingToken = webhookManager.getGitLabWebhookProperties().signingToken(); - if (signingToken == null || signingToken.isBlank()) { - log.debug("No GitLab signing token configured, skipping signature verification"); - return true; - } - - var body = req.getBodyString(); - var signedContent = "%s.%s.%s".formatted(webhookId, webhookTimestamp, body); - - try { - var decodedKey = decodeSigningToken(signingToken); - var computedDigest = computeHmacSha256(decodedKey, signedContent); - var computedSignature = "v1,%s".formatted(computedDigest); - - var signatures = webhookSignature.split(" "); - var matched = false; - for (var sig : signatures) { - if (MessageDigest.isEqual(computedSignature.getBytes(StandardCharsets.UTF_8), - sig.trim().getBytes(StandardCharsets.UTF_8))) { - matched = true; - break; - } - } - - if (!matched) { - log.warn("Invalid webhook signature from ip={}", request.getRemoteAddr()); - throw new BizException(HttpStatus.UNAUTHORIZED, "Invalid webhook signature"); - } - } catch (BizException e) { - throw e; - } catch (Exception e) { - log.error("Failed to verify webhook signature", e); - throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR, - "Failed to verify webhook signature"); - } - - return true; - } - - /** - * Decodes a GitLab-format signing token by stripping the {@code whsec_} prefix - * and Base64-decoding the remainder. - * - * @param token the prefixed signing token - * @return the raw key bytes - * @throws BizException if the token does not start with {@code whsec_} - */ - private byte[] decodeSigningToken(String token) { - if (!token.startsWith(TOKEN_PREFIX)) { - throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR, - "Signing token must start with " + TOKEN_PREFIX); - } - var encoded = token.substring(TOKEN_PREFIX.length()); - return Base64.getDecoder().decode(encoded); - } - - /** - * Computes the Base64-encoded HMAC-SHA256 digest for the given data. - * - * @param key the secret key bytes - * @param data the content to sign - * @return Base64-encoded HMAC-SHA256 digest - * @throws Exception if the HMAC algorithm is unavailable - */ - private String computeHmacSha256(byte[] key, String data) throws Exception { - var mac = Mac.getInstance("HmacSHA256"); - var secretKey = new SecretKeySpec(key, "HmacSHA256"); - mac.init(secretKey); - var hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); - return Base64.getEncoder().encodeToString(hash); - } -} From 0530c1f633feaca21635a18cbb1f8f9986433f1c Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 1 Jun 2026 16:30:46 +0800 Subject: [PATCH 17/25] feat: add allowed-users sender filtering for GitHub webhook Only issues submitted by users in the allowed-users list are processed. An empty or null list allows all senders (no filtering). Checks sender.login from the webhook payload against the configured list. --- .../domain/dto/GitHubIssueRequest.java | 3 ++- .../domain/dto/GitHubWebhookSender.java | 12 +++++++++ .../service/WebhookService.java | 27 +++++++++++++++++-- 3 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookSender.java diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubIssueRequest.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubIssueRequest.java index 449591b..8c4e1a6 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubIssueRequest.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubIssueRequest.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming; public record GitHubIssueRequest( String action, GitHubWebhookIssue issue, - GitHubWebhookRepository repository + GitHubWebhookRepository repository, + GitHubWebhookSender sender ) { } diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookSender.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookSender.java new file mode 100644 index 0000000..416891e --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/GitHubWebhookSender.java @@ -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 +) { +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java b/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java index 3489c2c..f54f39c 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java +++ b/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java @@ -1,11 +1,11 @@ package com.onixbyte.deltaforceguide.service; -import com.fasterxml.jackson.databind.ObjectMapper; 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; @@ -29,20 +29,30 @@ public class WebhookService { private final ModificationManager modificationManager; private final RedisTemplate redisTemplate; + private final WebhookManager webhookManager; private final Yaml yaml; public WebhookService( ModificationManager modificationManager, - RedisTemplate redisTemplate + RedisTemplate 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()); @@ -155,6 +165,19 @@ public class WebhookService { 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 labels) { if (labels == null) { return false; From 4ee741d307a233851f0bdc05af6d2445c74b9615 Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 1 Jun 2026 16:34:37 +0800 Subject: [PATCH 18/25] feat: add firearm name lookup for webhook YAML parsing When firearmId is absent from the YAML block, resolveFirearmId falls back to firearmName lookup via FirearmRepository.findByName(). If both are present, firearmId takes precedence. --- .../manager/ModificationManager.java | 15 +++++++++++++++ .../repository/FirearmRepository.java | 4 ++++ .../deltaforceguide/service/WebhookService.java | 4 +++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/onixbyte/deltaforceguide/manager/ModificationManager.java b/src/main/java/com/onixbyte/deltaforceguide/manager/ModificationManager.java index eb0007a..f7defab 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/manager/ModificationManager.java +++ b/src/main/java/com/onixbyte/deltaforceguide/manager/ModificationManager.java @@ -71,6 +71,21 @@ public class ModificationManager { .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) diff --git a/src/main/java/com/onixbyte/deltaforceguide/repository/FirearmRepository.java b/src/main/java/com/onixbyte/deltaforceguide/repository/FirearmRepository.java index dcf85f8..b486703 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/repository/FirearmRepository.java +++ b/src/main/java/com/onixbyte/deltaforceguide/repository/FirearmRepository.java @@ -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 { Page findAllByType(FirearmType type, Pageable pageable); + + List findByName(String name); } diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java b/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java index f54f39c..91f454b 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java +++ b/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java @@ -109,7 +109,9 @@ public class WebhookService { } private ModificationRequest mapToRequest(Map data) { - Long firearmId = toLong(data.get("firearmId")); + 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 tags = toStringList(data.get("tags")); From 20bc18d416cb393542a951321d7e1b66a9169ae5 Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 1 Jun 2026 16:59:05 +0800 Subject: [PATCH 19/25] fix: rename misnamed loggingFilter bean to webhookFilter --- .../java/com/onixbyte/deltaforceguide/config/FilterConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/onixbyte/deltaforceguide/config/FilterConfig.java b/src/main/java/com/onixbyte/deltaforceguide/config/FilterConfig.java index 40442ab..b0c3e0f 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/config/FilterConfig.java +++ b/src/main/java/com/onixbyte/deltaforceguide/config/FilterConfig.java @@ -9,7 +9,7 @@ import org.springframework.context.annotation.Configuration; public class FilterConfig { @Bean - public FilterRegistrationBean loggingFilter(WebhookFilter webhookFilter) { + public FilterRegistrationBean webhookFilter(WebhookFilter webhookFilter) { var registrationBean = new FilterRegistrationBean(); registrationBean.setFilter(webhookFilter); From 3616ad9eab05a297b9bee99e8d0faa9812b1e0c5 Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 1 Jun 2026 16:59:08 +0800 Subject: [PATCH 20/25] chore: remove unused imports in GitHubWebhookInterceptor --- .../deltaforceguide/interceptor/GitHubWebhookInterceptor.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitHubWebhookInterceptor.java b/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitHubWebhookInterceptor.java index 2f8e111..9aacbe7 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitHubWebhookInterceptor.java +++ b/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitHubWebhookInterceptor.java @@ -14,8 +14,6 @@ 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; From de61e1feb7bfb025cb3d3eba06748f18b2cb6dba Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 1 Jun 2026 16:59:12 +0800 Subject: [PATCH 21/25] refactor: remove redundant @Transactional from delegated service methods --- .../onixbyte/deltaforceguide/service/ModificationService.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/ModificationService.java b/src/main/java/com/onixbyte/deltaforceguide/service/ModificationService.java index a5129af..37aa1d4 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/service/ModificationService.java +++ b/src/main/java/com/onixbyte/deltaforceguide/service/ModificationService.java @@ -79,12 +79,10 @@ public class ModificationService { return modificationRepository.findAllTags(firearmId); } - @Transactional public ModificationResponse create(ModificationRequest request) { return modificationManager.create(request); } - @Transactional public List batchCreate(List requests) { return modificationManager.batchCreate(requests); } From ed2a0f4ae0ad1bbf909a50bd8c83340f30c62079 Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 1 Jun 2026 16:59:40 +0800 Subject: [PATCH 22/25] style: replace fully qualified type names and clean up imports --- .../onixbyte/deltaforceguide/service/WebhookService.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java b/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java index 91f454b..a0ffed4 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java +++ b/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java @@ -1,9 +1,6 @@ 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.domain.dto.*; import com.onixbyte.deltaforceguide.manager.ModificationManager; import com.onixbyte.deltaforceguide.manager.WebhookManager; import org.slf4j.Logger; @@ -168,7 +165,7 @@ public class WebhookService { } private boolean isAllowedSender( - com.onixbyte.deltaforceguide.domain.dto.GitHubWebhookSender sender + GitHubWebhookSender sender ) { var allowedUsers = webhookManager.github().allowedUsers(); if (allowedUsers == null || allowedUsers.isEmpty()) { @@ -180,7 +177,7 @@ public class WebhookService { return allowedUsers.contains(sender.login()); } - private boolean hasTriggerLabel(List labels) { + private boolean hasTriggerLabel(List labels) { if (labels == null) { return false; } From b0c41e08ea8cba3f421eda5121f1bbea2972a000 Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 1 Jun 2026 16:59:47 +0800 Subject: [PATCH 23/25] fix: use \R instead of \n in YAML fence regex for cross-platform linebreaks --- .../com/onixbyte/deltaforceguide/service/WebhookService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java b/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java index a0ffed4..b9b394c 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java +++ b/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java @@ -22,7 +22,7 @@ public class WebhookService { 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); + Pattern.compile("```ya?ml\\s*\\R(.*?)```", Pattern.DOTALL); private final ModificationManager modificationManager; private final RedisTemplate redisTemplate; From 5b5062aae9dbca391a7d506260fdb19479ae0a43 Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 1 Jun 2026 17:00:03 +0800 Subject: [PATCH 24/25] fix: validate firearmId is present before constructing ModificationRequest --- .../onixbyte/deltaforceguide/service/WebhookService.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java b/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java index b9b394c..44f57db 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java +++ b/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java @@ -6,7 +6,9 @@ import com.onixbyte.deltaforceguide.manager.WebhookManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; import org.yaml.snakeyaml.Yaml; import java.time.Duration; @@ -109,6 +111,10 @@ public class WebhookService { Long firearmId = modificationManager.resolveFirearmId( toLong(data.get("firearmId")), (String) data.get("firearmName")); + if (firearmId == null) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, + "YAML must contain firearmId or firearmName"); + } String name = (String) data.get("name"); String code = (String) data.get("code"); List tags = toStringList(data.get("tags")); From 9594efe716d94b0bf4da86ef8fa067783a67953d Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 1 Jun 2026 17:01:50 +0800 Subject: [PATCH 25/25] fix: use BizException instead of ResponseStatusException for firearmId validation --- .../com/onixbyte/deltaforceguide/service/WebhookService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java b/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java index 44f57db..3476958 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java +++ b/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java @@ -8,7 +8,7 @@ import org.slf4j.LoggerFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; -import org.springframework.web.server.ResponseStatusException; +import com.onixbyte.deltaforceguide.exeption.BizException; import org.yaml.snakeyaml.Yaml; import java.time.Duration; @@ -112,7 +112,7 @@ public class WebhookService { toLong(data.get("firearmId")), (String) data.get("firearmName")); if (firearmId == null) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, + throw new BizException(HttpStatus.BAD_REQUEST, "YAML must contain firearmId or firearmName"); } String name = (String) data.get("name");