From 49f9b59b9936ab8e7232e027d9e7b05ee0edd744 Mon Sep 17 00:00:00 2001 From: siujamo Date: Tue, 26 May 2026 10:58:02 +0800 Subject: [PATCH 01/35] chore: add gradle.properties to .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 0babb7f..b734735 100644 --- a/.gitignore +++ b/.gitignore @@ -155,3 +155,5 @@ gradle-app.setting .project # JDT-specific (Eclipse Java Development Tools) .classpath + +gradle.properties From 0d70b27653a420ac722d13edf06fab10f9a21abe Mon Sep 17 00:00:00 2001 From: siujamo Date: Tue, 26 May 2026 14:24:56 +0800 Subject: [PATCH 02/35] feat: add OpenAPI definition with title, contact, and licence --- .../config/OpenApiConfiguration.java | 26 +++++++++++++++++++ .../controller/VersionController.java | 2 ++ 2 files changed, 28 insertions(+) create mode 100644 src/main/java/com/onixbyte/deltaforceguide/config/OpenApiConfiguration.java diff --git a/src/main/java/com/onixbyte/deltaforceguide/config/OpenApiConfiguration.java b/src/main/java/com/onixbyte/deltaforceguide/config/OpenApiConfiguration.java new file mode 100644 index 0000000..9f47fe1 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/config/OpenApiConfiguration.java @@ -0,0 +1,26 @@ +package com.onixbyte.deltaforceguide.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Contact; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.info.License; +import org.springframework.context.annotation.Configuration; + +@OpenAPIDefinition( + info = @Info( + title = "Delta Force Guide Server", + description = "API for managing Delta Force game firearm builds", + version = "1.3.4", + contact = @Contact( + name = "Zihlu Wang", + email = "zihlu.wang@onixbyte.com" + ), + license = @License( + name = "MIT", + url = "https://git.onixbyte.cn/onixbyte/delta-force-guide-server/-/raw/main/LICENCE" + ) + ) +) +@Configuration +public class OpenApiConfiguration { +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/controller/VersionController.java b/src/main/java/com/onixbyte/deltaforceguide/controller/VersionController.java index a73b43c..67a08ef 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/controller/VersionController.java +++ b/src/main/java/com/onixbyte/deltaforceguide/controller/VersionController.java @@ -2,10 +2,12 @@ package com.onixbyte.deltaforceguide.controller; import com.onixbyte.deltaforceguide.service.AppService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "版本信息") @RestController @RequestMapping("/versions") public class VersionController { From 66b37ec20d7ced29834453ea52978a66f73b8b3e Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 28 May 2026 13:51:24 +0800 Subject: [PATCH 03/35] 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 04/35] 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 05/35] 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 9bc70d5370ad5a47b8ddd03ebd69be427fc116be Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 28 May 2026 15:17:36 +0800 Subject: [PATCH 06/35] feat: add web traffic logger --- .../deltaforceguide/config/AppConfig.java | 18 +++++++- .../interceptor/TrafficInterceptor.java | 46 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/onixbyte/deltaforceguide/interceptor/TrafficInterceptor.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..a29d2b5 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/config/AppConfig.java +++ b/src/main/java/com/onixbyte/deltaforceguide/config/AppConfig.java @@ -1,10 +1,26 @@ package com.onixbyte.deltaforceguide.config; +import com.onixbyte.deltaforceguide.interceptor.TrafficInterceptor; import com.onixbyte.deltaforceguide.properties.AppProperties; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @EnableConfigurationProperties(AppProperties.class) -public class AppConfig { +public class AppConfig implements WebMvcConfigurer { + + private final TrafficInterceptor trafficInterceptor; + + @Autowired + public AppConfig(TrafficInterceptor trafficInterceptor) { + this.trafficInterceptor = trafficInterceptor; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(trafficInterceptor); + } } diff --git a/src/main/java/com/onixbyte/deltaforceguide/interceptor/TrafficInterceptor.java b/src/main/java/com/onixbyte/deltaforceguide/interceptor/TrafficInterceptor.java new file mode 100644 index 0000000..ab3b657 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/interceptor/TrafficInterceptor.java @@ -0,0 +1,46 @@ +package com.onixbyte.deltaforceguide.interceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +public class TrafficInterceptor implements HandlerInterceptor { + + private static final Logger log = LoggerFactory.getLogger(TrafficInterceptor.class); + + @Override + public boolean preHandle( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull Object handler + ) { + var ip = resolveClientIp(request); + var method = request.getMethod(); + var uri = request.getRequestURI(); + var query = request.getQueryString(); + var contentType = request.getContentType(); + var contentLength = request.getContentLength(); + var userAgent = request.getHeader("User-Agent"); + + log.info("Request method={}, uri={}, query={}, ip={}, content-type={}, content-length={}, user-agent={}", + method, uri, query, ip, contentType, contentLength, userAgent); + return true; + } + + private String resolveClientIp(HttpServletRequest request) { + var xForwardedFor = request.getHeader("X-Forwarded-For"); + if (xForwardedFor != null && !xForwardedFor.isBlank()) { + return xForwardedFor.split(",")[0].trim(); + } + var xRealIp = request.getHeader("X-Real-IP"); + if (xRealIp != null && !xRealIp.isBlank()) { + return xRealIp.trim(); + } + return request.getRemoteAddr(); + } +} From 8d24b6082da8012ebe1589ad9b2789338a5678d8 Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 28 May 2026 15:18:25 +0800 Subject: [PATCH 07/35] 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 08/35] 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 09/35] 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 10/35] 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 11/35] 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 eb22b3c4bbbb97f676cd590020a892c894fa1a6b Mon Sep 17 00:00:00 2001 From: zihluwang Date: Fri, 29 May 2026 00:42:27 +0800 Subject: [PATCH 12/35] docs: add Javadocs --- .../DeltaForceGuideApplication.java | 5 ++ .../deltaforceguide/client/TokenClient.java | 5 ++ .../deltaforceguide/config/CorsConfig.java | 11 +++ .../deltaforceguide/config/JacksonConfig.java | 5 ++ .../deltaforceguide/config/MyBatisConfig.java | 5 ++ .../config/SecurityConfig.java | 20 +++++ .../config/SpringDataConfig.java | 5 ++ .../controller/AuthController.java | 5 ++ .../controller/DailyPasswordController.java | 5 ++ .../controller/FirearmController.java | 5 ++ .../controller/GlobalExceptionHandler.java | 5 ++ .../controller/ModificationController.java | 5 ++ .../controller/TagController.java | 5 ++ .../converter/FirearmTypeConverter.java | 5 ++ .../domain/dto/AccessoryRequest.java | 5 ++ .../domain/dto/AccessoryResponse.java | 5 ++ .../domain/dto/DailyPassword.java | 5 ++ .../domain/dto/DailyPasswordData.java | 5 ++ .../domain/dto/DailyPasswordMetadata.java | 5 ++ .../domain/dto/DailyPasswordResponse.java | 5 ++ .../domain/dto/ErrorResponse.java | 5 ++ .../domain/dto/FirearmRequest.java | 5 ++ .../domain/dto/FirearmResponse.java | 5 ++ .../domain/dto/LoginRequest.java | 5 ++ .../dto/ModificationBatchCreateRequest.java | 5 ++ .../dto/ModificationBatchDeleteRequest.java | 5 ++ .../domain/dto/ModificationRequest.java | 5 ++ .../domain/dto/ModificationResponse.java | 5 ++ .../domain/dto/PageResponse.java | 5 ++ .../domain/dto/TuningRequest.java | 5 ++ .../domain/dto/TuningResponse.java | 5 ++ .../domain/dto/UserResponse.java | 5 ++ .../domain/entity/Accessory.java | 5 ++ .../domain/entity/Firearm.java | 5 ++ .../domain/entity/Modification.java | 5 ++ .../deltaforceguide/domain/entity/Tuning.java | 5 ++ .../deltaforceguide/domain/entity/User.java | 5 ++ .../domain/entity/UserCredential.java | 7 +- .../domain/entity/UserCredentialId.java | 5 ++ .../enumeration/FirearmType.java | 13 ++++ .../exeption/BizException.java | 5 ++ .../filter/TokenAuthenticationFilter.java | 13 +++- .../manager/CookieManager.java | 5 ++ .../manager/DailyPasswordManager.java | 9 +++ .../manager/UserCredentialManager.java | 41 ++++++++-- .../deltaforceguide/manager/UserManager.java | 43 +++++++++-- .../properties/CookieProperties.java | 10 +++ .../properties/CorsProperties.java | 12 +++ .../properties/TokenProperties.java | 8 ++ .../repository/FirearmRepository.java | 5 ++ .../repository/ModificationRepository.java | 20 +++++ .../repository/UserCredentialRepository.java | 29 +++++++ .../repository/UserRepository.java | 11 +++ .../security/annotation/RequiresAuth.java | 5 ++ .../UsernamePasswordAuthentication.java | 5 ++ ...sernamePasswordAuthenticationProvider.java | 17 ++++ .../deltaforceguide/service/AuthService.java | 18 ++++- .../service/CookieService.java | 20 +++++ .../service/DailyPasswordService.java | 9 +++ .../service/FirearmService.java | 41 ++++++++-- .../service/ModificationService.java | 63 ++++++++++++--- .../deltaforceguide/service/UserService.java | 77 ++++++++++++++++--- .../deltaforceguide/shared/CookieName.java | 5 ++ .../shared/CredentialProvider.java | 5 ++ .../shared/JacksonModules.java | 6 ++ .../shared/JacksonRedisSerialiser.java | 6 ++ .../deltaforceguide/utils/DateTimeUtil.java | 11 +++ 67 files changed, 689 insertions(+), 41 deletions(-) diff --git a/src/main/java/com/onixbyte/deltaforceguide/DeltaForceGuideApplication.java b/src/main/java/com/onixbyte/deltaforceguide/DeltaForceGuideApplication.java index 4d69512..a23fb88 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/DeltaForceGuideApplication.java +++ b/src/main/java/com/onixbyte/deltaforceguide/DeltaForceGuideApplication.java @@ -3,6 +3,11 @@ package com.onixbyte.deltaforceguide; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +/** + * Entry point for the Delta Force Guide Server application. + * + * @author zihluwang + */ @SpringBootApplication public class DeltaForceGuideApplication { diff --git a/src/main/java/com/onixbyte/deltaforceguide/client/TokenClient.java b/src/main/java/com/onixbyte/deltaforceguide/client/TokenClient.java index d4b745c..a7ad3f1 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/client/TokenClient.java +++ b/src/main/java/com/onixbyte/deltaforceguide/client/TokenClient.java @@ -12,6 +12,11 @@ import org.springframework.stereotype.Component; import java.time.LocalDateTime; +/** + * Client for generating and verifying JSON Web Tokens using the Auth0 java-jwt library. + * + * @author zihluwang + */ @Component public class TokenClient { diff --git a/src/main/java/com/onixbyte/deltaforceguide/config/CorsConfig.java b/src/main/java/com/onixbyte/deltaforceguide/config/CorsConfig.java index 291695b..1e13243 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/config/CorsConfig.java +++ b/src/main/java/com/onixbyte/deltaforceguide/config/CorsConfig.java @@ -12,10 +12,21 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.List; import java.util.stream.Stream; +/** + * Configuration for CORS (Cross-Origin Resource Sharing) policies. + * + * @author zihluwang + */ @Configuration @EnableConfigurationProperties({CorsProperties.class}) public class CorsConfig { + /** + * Creates the CORS configuration source with properties from configuration. + * + * @param properties the CORS configuration properties + * @return the CORS configuration source + */ @Bean public CorsConfigurationSource corsConfigurationSource( CorsProperties properties diff --git a/src/main/java/com/onixbyte/deltaforceguide/config/JacksonConfig.java b/src/main/java/com/onixbyte/deltaforceguide/config/JacksonConfig.java index a64a1ea..895bcd1 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/config/JacksonConfig.java +++ b/src/main/java/com/onixbyte/deltaforceguide/config/JacksonConfig.java @@ -6,6 +6,11 @@ import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilde import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +/** + * Configuration for Jackson JSON serialisation and deserialisation settings. + * + * @author zihluwang + */ @Configuration public class JacksonConfig { diff --git a/src/main/java/com/onixbyte/deltaforceguide/config/MyBatisConfig.java b/src/main/java/com/onixbyte/deltaforceguide/config/MyBatisConfig.java index 61acbfc..0e2ae74 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/config/MyBatisConfig.java +++ b/src/main/java/com/onixbyte/deltaforceguide/config/MyBatisConfig.java @@ -3,6 +3,11 @@ package com.onixbyte.deltaforceguide.config; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Configuration; +/** + * Configuration for MyBatis SQL mapping framework integration. + * + * @author zihluwang + */ @Configuration @MapperScan(basePackages = {"com.onixbyte.deltaforceguide.mapper"}) public class MyBatisConfig { diff --git a/src/main/java/com/onixbyte/deltaforceguide/config/SecurityConfig.java b/src/main/java/com/onixbyte/deltaforceguide/config/SecurityConfig.java index 8e8cc50..b32a691 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/config/SecurityConfig.java +++ b/src/main/java/com/onixbyte/deltaforceguide/config/SecurityConfig.java @@ -23,12 +23,23 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.ExceptionTranslationFilter; import org.springframework.web.cors.CorsConfigurationSource; +/** + * Spring Security configuration defining authentication, authorisation, and filter chains. + * + * @author zihluwang + */ @Configuration @EnableWebSecurity @EnableMethodSecurity @EnableConfigurationProperties({TokenProperties.class, CookieProperties.class}) public class SecurityConfig { + /** + * Configures the HTTP security filter chain including endpoint authorisation and JWT filter. + * + * @param http the HTTP security builder + * @return the configured security filter chain + */ @Bean public SecurityFilterChain securityFilterChain( HttpSecurity httpSecurity, @@ -48,11 +59,20 @@ public class SecurityConfig { .build(); } + /** + * Provides the BCrypt password encoder for credential hashing. + * @return the BCrypt password encoder + */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + /** + * Provides the authentication manager for the security configuration. + * + * @return the authentication manager + */ @Bean public AuthenticationManager authenticationManager( UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider diff --git a/src/main/java/com/onixbyte/deltaforceguide/config/SpringDataConfig.java b/src/main/java/com/onixbyte/deltaforceguide/config/SpringDataConfig.java index 44e3a4c..1dfe3ff 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/config/SpringDataConfig.java +++ b/src/main/java/com/onixbyte/deltaforceguide/config/SpringDataConfig.java @@ -3,6 +3,11 @@ package com.onixbyte.deltaforceguide.config; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +/** + * Configuration for Spring Data JPA auditing and repository settings. + * + * @author zihluwang + */ @Configuration @EnableJpaRepositories(basePackages = {"com.onixbyte.deltaforceguide.repository"}) public class SpringDataConfig { diff --git a/src/main/java/com/onixbyte/deltaforceguide/controller/AuthController.java b/src/main/java/com/onixbyte/deltaforceguide/controller/AuthController.java index 86229aa..0a42067 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/controller/AuthController.java +++ b/src/main/java/com/onixbyte/deltaforceguide/controller/AuthController.java @@ -19,6 +19,11 @@ import org.springframework.web.bind.annotation.RestController; import java.time.Duration; +/** + * REST controller for user authentication endpoints (login, logout). + * + * @author zihluwang + */ @Tag(name = "用户鉴权", description = "处理用户登录与退出功能") @RestController @RequestMapping("/auth") diff --git a/src/main/java/com/onixbyte/deltaforceguide/controller/DailyPasswordController.java b/src/main/java/com/onixbyte/deltaforceguide/controller/DailyPasswordController.java index f817f66..527e730 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/controller/DailyPasswordController.java +++ b/src/main/java/com/onixbyte/deltaforceguide/controller/DailyPasswordController.java @@ -8,6 +8,11 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +/** + * REST controller for retrieving daily-generated passwords. + * + * @author zihluwang + */ @Tag(name = "每日密码", description = "获取每日密码信息") @RestController @RequestMapping("/daily-passwords") diff --git a/src/main/java/com/onixbyte/deltaforceguide/controller/FirearmController.java b/src/main/java/com/onixbyte/deltaforceguide/controller/FirearmController.java index 7d95b69..4843c2d 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/controller/FirearmController.java +++ b/src/main/java/com/onixbyte/deltaforceguide/controller/FirearmController.java @@ -15,6 +15,11 @@ import org.springframework.data.domain.Sort; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +/** + * REST controller for firearm CRUD operations. + * + * @author zihluwang + */ @Tag(name = "武器管理", description = "与武器有关的操作") @RestController @RequestMapping("/firearms") diff --git a/src/main/java/com/onixbyte/deltaforceguide/controller/GlobalExceptionHandler.java b/src/main/java/com/onixbyte/deltaforceguide/controller/GlobalExceptionHandler.java index c0b19f1..4edd154 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/controller/GlobalExceptionHandler.java +++ b/src/main/java/com/onixbyte/deltaforceguide/controller/GlobalExceptionHandler.java @@ -6,6 +6,11 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +/** + * Global exception handler that translates exceptions into standard error responses. + * + * @author zihluwang + */ @RestControllerAdvice public class GlobalExceptionHandler { diff --git a/src/main/java/com/onixbyte/deltaforceguide/controller/ModificationController.java b/src/main/java/com/onixbyte/deltaforceguide/controller/ModificationController.java index b7c7a8d..7eaf94c 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/controller/ModificationController.java +++ b/src/main/java/com/onixbyte/deltaforceguide/controller/ModificationController.java @@ -27,6 +27,11 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; +/** + * REST controller for modification CRUD operations, including batch creation and deletion. + * + * @author zihluwang + */ @Tag(name = "改装管理", description = "对枪械改装的管理") @RestController @RequestMapping("/modifications") diff --git a/src/main/java/com/onixbyte/deltaforceguide/controller/TagController.java b/src/main/java/com/onixbyte/deltaforceguide/controller/TagController.java index e22097d..b9f4e59 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/controller/TagController.java +++ b/src/main/java/com/onixbyte/deltaforceguide/controller/TagController.java @@ -10,6 +10,11 @@ import com.onixbyte.deltaforceguide.service.ModificationService; import java.util.List; +/** + * REST controller for retrieving available modification tags. + * + * @author zihluwang + */ @Tag(name = "标签管理", description = "管理标签信息") @RestController @RequestMapping("/tags") diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/converter/FirearmTypeConverter.java b/src/main/java/com/onixbyte/deltaforceguide/domain/converter/FirearmTypeConverter.java index f6f46f8..8108db3 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/converter/FirearmTypeConverter.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/converter/FirearmTypeConverter.java @@ -4,6 +4,11 @@ import com.onixbyte.deltaforceguide.enumeration.FirearmType; import jakarta.persistence.AttributeConverter; import jakarta.persistence.Converter; +/** + * JPA attribute converter that maps {@link FirearmType} enum to/from its integer database representation. + * + * @author zihluwang + */ @Converter public class FirearmTypeConverter implements AttributeConverter { diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/AccessoryRequest.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/AccessoryRequest.java index 29fa638..b94b61c 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/AccessoryRequest.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/AccessoryRequest.java @@ -6,6 +6,11 @@ import jakarta.validation.constraints.NotBlank; import java.util.ArrayList; import java.util.List; +/** + * Request DTO for creating or updating an accessory attached to a modification. + * + * @author zihluwang + */ public record AccessoryRequest( @NotBlank(message = "插槽名称不能为空") String slotName, diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/AccessoryResponse.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/AccessoryResponse.java index 421410d..ade3eac 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/AccessoryResponse.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/AccessoryResponse.java @@ -4,6 +4,11 @@ import com.onixbyte.deltaforceguide.domain.entity.Accessory; import java.util.List; +/** + * Response DTO for an accessory attached to a modification. + * + * @author zihluwang + */ public record AccessoryResponse( String slotName, String accessoryName, diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPassword.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPassword.java index 2a381ad..edfa9c0 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPassword.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPassword.java @@ -1,5 +1,10 @@ package com.onixbyte.deltaforceguide.domain.dto; +/** + * DTO representing a single daily-generated password for a map. + * + * @author zihluwang + */ public record DailyPassword( String mapName, String password diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordData.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordData.java index c685542..5e3c12f 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordData.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordData.java @@ -3,6 +3,11 @@ package com.onixbyte.deltaforceguide.domain.dto; import java.time.LocalDateTime; import java.util.List; +/** + * DTO containing daily password data including update information and password list. + * + * @author zihluwang + */ public record DailyPasswordData( String updateDate, Integer totalCount, diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordMetadata.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordMetadata.java index a2fa36b..9af994f 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordMetadata.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordMetadata.java @@ -1,5 +1,10 @@ package com.onixbyte.deltaforceguide.domain.dto; +/** + * DTO holding metadata about the daily password source and update tracking. + * + * @author zihluwang + */ public record DailyPasswordMetadata( String version, String author diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordResponse.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordResponse.java index 7d29a27..d3ce808 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordResponse.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordResponse.java @@ -1,5 +1,10 @@ package com.onixbyte.deltaforceguide.domain.dto; +/** + * Response DTO wrapping daily password data with metadata. + * + * @author zihluwang + */ public record DailyPasswordResponse( String status, String message, diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/ErrorResponse.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/ErrorResponse.java index f1efbfa..11879fc 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/ErrorResponse.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/ErrorResponse.java @@ -1,5 +1,10 @@ package com.onixbyte.deltaforceguide.domain.dto; +/** + * Standard error response body returned on API failures. + * + * @author zihluwang + */ public record ErrorResponse( String message ) { diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/FirearmRequest.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/FirearmRequest.java index 74e1993..02fb855 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/FirearmRequest.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/FirearmRequest.java @@ -2,6 +2,11 @@ package com.onixbyte.deltaforceguide.domain.dto; import com.onixbyte.deltaforceguide.enumeration.FirearmType; +/** + * Request DTO for creating or updating a firearm. + * + * @author zihluwang + */ public record FirearmRequest( String name, FirearmType type, diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/FirearmResponse.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/FirearmResponse.java index eb5b1ac..0e9eaba 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/FirearmResponse.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/FirearmResponse.java @@ -3,6 +3,11 @@ package com.onixbyte.deltaforceguide.domain.dto; import com.onixbyte.deltaforceguide.domain.entity.Firearm; import com.onixbyte.deltaforceguide.enumeration.FirearmType; +/** + * Response DTO for a firearm record, including associated modifications. + * + * @author zihluwang + */ public record FirearmResponse( Long id, String name, diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/LoginRequest.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/LoginRequest.java index ab820ec..106d533 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/LoginRequest.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/LoginRequest.java @@ -3,6 +3,11 @@ package com.onixbyte.deltaforceguide.domain.dto; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +/** + * Login request containing principle (username/email) and credential (password). + * + * @author zihluwang + */ @Schema(description = "登录请求") public record LoginRequest( @NotBlank(message = "登录名称不能为空") diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/ModificationBatchCreateRequest.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/ModificationBatchCreateRequest.java index cdb3d93..c0aa59f 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/ModificationBatchCreateRequest.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/ModificationBatchCreateRequest.java @@ -5,6 +5,11 @@ import jakarta.validation.constraints.NotEmpty; import java.util.List; +/** + * Request DTO for batch creation of modifications. + * + * @author zihluwang + */ public record ModificationBatchCreateRequest( @NotEmpty(message = "批量创建列表不能为空") List<@Valid ModificationRequest> modifications diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/ModificationBatchDeleteRequest.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/ModificationBatchDeleteRequest.java index 7e46400..4243895 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/ModificationBatchDeleteRequest.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/ModificationBatchDeleteRequest.java @@ -5,6 +5,11 @@ import jakarta.validation.constraints.Positive; import java.util.List; +/** + * Request DTO for batch deletion of modifications by ID. + * + * @author zihluwang + */ public record ModificationBatchDeleteRequest( @NotEmpty(message = "批量删除ID列表不能为空") List<@Positive(message = "ID必须为正数") Long> ids diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/ModificationRequest.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/ModificationRequest.java index c602dbd..e4f6708 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/ModificationRequest.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/ModificationRequest.java @@ -8,6 +8,11 @@ import jakarta.validation.constraints.Positive; import java.util.ArrayList; import java.util.List; +/** + * Request DTO for creating or updating a modification. + * + * @author zihluwang + */ public record ModificationRequest( @NotNull(message = "武器ID不能为空") @Positive(message = "武器ID必须为正数") diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/ModificationResponse.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/ModificationResponse.java index c4d4492..aca6ef5 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/ModificationResponse.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/ModificationResponse.java @@ -4,6 +4,11 @@ import com.onixbyte.deltaforceguide.domain.entity.Modification; import java.util.List; +/** + * Response DTO for a modification record including accessories and tags. + * + * @author zihluwang + */ public record ModificationResponse( Long id, Long firearmId, diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/PageResponse.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/PageResponse.java index 0eefb62..20738bd 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/PageResponse.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/PageResponse.java @@ -4,6 +4,11 @@ import org.springframework.data.domain.Page; import java.util.List; +/** + * Generic paginated response wrapper for list endpoints. + * + * @author zihluwang + */ public record PageResponse( List items, int page, diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/TuningRequest.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/TuningRequest.java index 414becd..a180eb3 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/TuningRequest.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/TuningRequest.java @@ -3,6 +3,11 @@ package com.onixbyte.deltaforceguide.domain.dto; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +/** + * Request DTO for a tuning adjustment on an accessory. + * + * @author zihluwang + */ public record TuningRequest( @NotBlank(message = "调校项名称不能为空") String tuningName, diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/TuningResponse.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/TuningResponse.java index 3f6febf..405ecde 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/TuningResponse.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/TuningResponse.java @@ -2,6 +2,11 @@ package com.onixbyte.deltaforceguide.domain.dto; import com.onixbyte.deltaforceguide.domain.entity.Tuning; +/** + * Response DTO for a tuning adjustment on an accessory. + * + * @author zihluwang + */ public record TuningResponse( String tuningName, Double tuningValue diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/UserResponse.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/UserResponse.java index 75d9188..b20231b 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/UserResponse.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/UserResponse.java @@ -2,6 +2,11 @@ package com.onixbyte.deltaforceguide.domain.dto; import com.onixbyte.deltaforceguide.domain.entity.User; +/** + * Response DTO for a user account, including associated credentials. + * + * @author zihluwang + */ public record UserResponse( Long id, String username, 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..fc9802d 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/Accessory.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/Accessory.java @@ -3,6 +3,11 @@ package com.onixbyte.deltaforceguide.domain.entity; import java.util.ArrayList; import java.util.List; +/** + * Entity representing an accessory attached to a modification, stored as JSONB. + * + * @author zihluwang + */ public class Accessory { private String slotName; diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/Firearm.java b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/Firearm.java index 65942d3..c71cee7 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/Firearm.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/Firearm.java @@ -15,6 +15,11 @@ import jakarta.persistence.Table; import java.util.ArrayList; import java.util.List; +/** + * Entity representing a firearm in the Delta Force game. + * + * @author zihluwang + */ @Entity @Table(name = "firearm") public class Firearm { diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/Modification.java b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/Modification.java index ed145b5..a1942f7 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/Modification.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/Modification.java @@ -7,6 +7,11 @@ import org.hibernate.annotations.Type; import java.util.ArrayList; import java.util.List; +/** + * Entity representing a firearm modification or build configuration. + * + * @author zihluwang + */ @Entity @Table( name = "modification", 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..102ea63 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,10 @@ package com.onixbyte.deltaforceguide.domain.entity; +/** + * Entity representing a tuning adjustment for an accessory, stored as JSONB within Accessory. + * + * @author zihluwang + */ public class Tuning { private String tuningName; diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/User.java b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/User.java index 67142df..d696b2d 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/User.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/User.java @@ -12,6 +12,11 @@ import jakarta.persistence.Table; import java.util.ArrayList; import java.util.List; +/** + * Entity representing an application user with authentication credentials. + * + * @author zihluwang + */ @Entity @Table(name = "app_user") public class User { diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserCredential.java b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserCredential.java index b8a09a6..2ecdd37 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserCredential.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserCredential.java @@ -12,6 +12,11 @@ import jakarta.persistence.MapsId; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +/** + * Entity representing a user credential linked to an authentication provider. + * + * @author zihluwang + */ @Entity @Table(name = "app_user_credential") public class UserCredential { @@ -28,7 +33,7 @@ public class UserCredential { @JoinColumn(name = "user_id", nullable = false, foreignKey = @ForeignKey(name = "fk_user_credential_user")) private User user; - @Column(name = "credential", nullable = false, length = 255) + @Column(name = "credential", nullable = false) private String credential; public UserCredentialId getId() { diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserCredentialId.java b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserCredentialId.java index 57a819e..1e25f45 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserCredentialId.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserCredentialId.java @@ -5,6 +5,11 @@ import jakarta.persistence.Embeddable; import java.io.Serializable; import java.util.Objects; +/** + * Composite key for the UserCredential entity, combining user ID and provider. + * + * @author zihluwang + */ @Embeddable public class UserCredentialId implements Serializable { diff --git a/src/main/java/com/onixbyte/deltaforceguide/enumeration/FirearmType.java b/src/main/java/com/onixbyte/deltaforceguide/enumeration/FirearmType.java index d4965fa..77bdbdb 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/enumeration/FirearmType.java +++ b/src/main/java/com/onixbyte/deltaforceguide/enumeration/FirearmType.java @@ -1,5 +1,11 @@ package com.onixbyte.deltaforceguide.enumeration; +/** + * Enumeration of firearm types in the Delta Force game. + * Each type is associated with an integer code used for database persistence. + * + * @author zihluwang + */ public enum FirearmType { RIFLE(0), @@ -21,6 +27,13 @@ public enum FirearmType { return code; } + /** + * Resolve a FirearmType from its integer code. + * + * @param code the integer code, may be null + * @return the corresponding FirearmType, or null if the code is null + * @throws IllegalArgumentException if the code does not match any known type + */ public static FirearmType fromCode(Integer code) { if (code == null) { return null; diff --git a/src/main/java/com/onixbyte/deltaforceguide/exeption/BizException.java b/src/main/java/com/onixbyte/deltaforceguide/exeption/BizException.java index aad96d2..d1b7ff2 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/exeption/BizException.java +++ b/src/main/java/com/onixbyte/deltaforceguide/exeption/BizException.java @@ -2,6 +2,11 @@ package com.onixbyte.deltaforceguide.exeption; import org.springframework.http.HttpStatus; +/** + * Custom runtime exception that carries an HTTP status code for API error responses. + * + * @author zihluwang + */ public class BizException extends RuntimeException { /** diff --git a/src/main/java/com/onixbyte/deltaforceguide/filter/TokenAuthenticationFilter.java b/src/main/java/com/onixbyte/deltaforceguide/filter/TokenAuthenticationFilter.java index f4b9220..e2068a6 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/filter/TokenAuthenticationFilter.java +++ b/src/main/java/com/onixbyte/deltaforceguide/filter/TokenAuthenticationFilter.java @@ -16,7 +16,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; @@ -29,6 +28,11 @@ import java.time.Instant; import java.util.Objects; import java.util.Optional; +/** + * Servlet filter that extracts and validates JWT tokens from httpOnly cookies for each request. + * + * @author zihluwang + */ @Component public class TokenAuthenticationFilter extends OncePerRequestFilter { @@ -52,6 +56,13 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { this.handlerExceptionResolver = handlerExceptionResolver; } + /** + * Extracts JWT from httpOnly cookie, validates it, and sets the security context. + * + * @param request the HTTP request + * @param response the HTTP response + * @param filterChain the filter chain + */ @Override protected void doFilterInternal( @NonNull HttpServletRequest request, diff --git a/src/main/java/com/onixbyte/deltaforceguide/manager/CookieManager.java b/src/main/java/com/onixbyte/deltaforceguide/manager/CookieManager.java index 7bec528..f6ef16f 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/manager/CookieManager.java +++ b/src/main/java/com/onixbyte/deltaforceguide/manager/CookieManager.java @@ -6,6 +6,11 @@ import org.springframework.stereotype.Component; import java.time.Duration; +/** + * Manager providing cookie construction operations with configurable properties. + * + * @author zihluwang + */ @Component public class CookieManager { diff --git a/src/main/java/com/onixbyte/deltaforceguide/manager/DailyPasswordManager.java b/src/main/java/com/onixbyte/deltaforceguide/manager/DailyPasswordManager.java index 3ec28f9..6b6ac59 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/manager/DailyPasswordManager.java +++ b/src/main/java/com/onixbyte/deltaforceguide/manager/DailyPasswordManager.java @@ -17,6 +17,11 @@ import java.time.Duration; import java.time.LocalDate; import java.util.Objects; +/** + * Manager for daily password data access and caching coordination. + * + * @author zihluwang + */ @Component public class DailyPasswordManager { @@ -49,6 +54,10 @@ public class DailyPasswordManager { this.redisTemplate = redisTemplate; } + /** + * Retrieves the daily password from cache or generates a new one. + * @return the daily password response + */ public DailyPasswordResponse getDailyPassword() { var key = CACHE_KEY_PREFIX + LocalDate.now(); diff --git a/src/main/java/com/onixbyte/deltaforceguide/manager/UserCredentialManager.java b/src/main/java/com/onixbyte/deltaforceguide/manager/UserCredentialManager.java index f4aa494..d77c687 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/manager/UserCredentialManager.java +++ b/src/main/java/com/onixbyte/deltaforceguide/manager/UserCredentialManager.java @@ -3,11 +3,15 @@ package com.onixbyte.deltaforceguide.manager; import com.onixbyte.deltaforceguide.domain.entity.UserCredential; import com.onixbyte.deltaforceguide.repository.UserCredentialRepository; import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; +/** + * Manager for user credential persistence and authentication data access. + * + * @author zihluwang + */ @Component public class UserCredentialManager { @@ -17,27 +21,52 @@ public class UserCredentialManager { this.userCredentialRepository = userCredentialRepository; } - @Transactional(readOnly = true) + /** + * Finds all credentials belonging to a specific user. + * + * @param userId the user ID + * @return list of matching credentials + */ public List findAllByUserId(Long userId) { return userCredentialRepository.findAllByUserId(userId); } - @Transactional(readOnly = true) + /** + * Finds a credential for a specific user and provider combination. + * + * @param userId the user ID + * @param provider the authentication provider + * @return the matching credential, if found + */ public Optional findByUserIdAndProvider(Long userId, String provider) { return userCredentialRepository.findByUserIdAndProvider(userId, provider); } - @Transactional + /** + * Persists a new or updated credential. + * + * @param userCredential the credential to save + * @return the saved credential + */ public UserCredential save(UserCredential userCredential) { return userCredentialRepository.save(userCredential); } - @Transactional + /** + * Deletes a credential for a specific user and provider. + * + * @param userId the user ID + * @param provider the authentication provider + */ public void deleteByUserIdAndProvider(Long userId, String provider) { userCredentialRepository.deleteByUserIdAndProvider(userId, provider); } - @Transactional + /** + * Deletes all credentials belonging to a user. + * + * @param userId the user ID + */ public void deleteAllByUserId(Long userId) { userCredentialRepository.deleteAllByUserId(userId); } diff --git a/src/main/java/com/onixbyte/deltaforceguide/manager/UserManager.java b/src/main/java/com/onixbyte/deltaforceguide/manager/UserManager.java index fb8ac56..d63c0f7 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/manager/UserManager.java +++ b/src/main/java/com/onixbyte/deltaforceguide/manager/UserManager.java @@ -8,6 +8,11 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; +/** + * Manager for user entity persistence and query operations. + * + * @author zihluwang + */ @Component public class UserManager { @@ -17,17 +22,30 @@ public class UserManager { this.userRepository = userRepository; } - @Transactional(readOnly = true) + /** + * Finds a user by their ID. + * + * @param id the user ID + * @return the matching user, if found + */ public Optional findById(Long id) { return userRepository.findById(id); } - @Transactional(readOnly = true) + /** + * Retrieves all registered users. + * @return list of all users + */ public List findAll() { return userRepository.findAll(); } - @Transactional(readOnly = true) + /** + * Finds a user by their username. + * + * @param username the username to search for + * @return the matching user, if found + */ public Optional findByUsername(String username) { return userRepository.findByUsername(username); } @@ -37,16 +55,31 @@ public class UserManager { return userRepository.findByEmail(email); } - @Transactional + /** + * Persists a new or updated user. + * + * @param user the user to save + * @return the saved user + */ public User save(User user) { return userRepository.save(user); } - @Transactional + /** + * Deletes a user by their ID. + * + * @param id the user ID to delete + */ public void deleteById(Long id) { userRepository.deleteById(id); } + /** + * Finds a user by their username or email address. + * + * @param principal the username or email to search for + * @return the matching user, if found + */ public Optional findByUsernameOrEmail(String principal) { return userRepository.findByUsernameOrEmail(principal); } diff --git a/src/main/java/com/onixbyte/deltaforceguide/properties/CookieProperties.java b/src/main/java/com/onixbyte/deltaforceguide/properties/CookieProperties.java index 86dab77..0cfde08 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/properties/CookieProperties.java +++ b/src/main/java/com/onixbyte/deltaforceguide/properties/CookieProperties.java @@ -6,6 +6,16 @@ import org.springframework.boot.web.server.Cookie; import java.time.Duration; +/** + * Configuration properties for HTTP cookies used in authentication, prefixed with "app.cookie". + * + * @param httpOnly whether the cookie is httpOnly + * @param secure whether the cookie is secure + * @param path the cookie path + * @param maxAge the maximum age of the cookie + * @param sameSite the SameSite policy for the cookie + * @author zihluwang + */ @ConfigurationProperties(prefix = "app.cookie") public record CookieProperties( @DefaultValue("true") Boolean httpOnly, diff --git a/src/main/java/com/onixbyte/deltaforceguide/properties/CorsProperties.java b/src/main/java/com/onixbyte/deltaforceguide/properties/CorsProperties.java index ff064d5..193640f 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/properties/CorsProperties.java +++ b/src/main/java/com/onixbyte/deltaforceguide/properties/CorsProperties.java @@ -6,6 +6,18 @@ import org.springframework.http.HttpMethod; import java.time.Duration; +/** + * Configuration properties for CORS settings, prefixed with "app.cors". + * + * @param allowedHeaders headers allowed in CORS requests + * @param allowedMethods HTTP methods allowed in CORS requests + * @param allowedOrigins origins permitted to make cross-origin requests + * @param allowCredentials whether credentials are allowed in CORS requests + * @param allowPrivateNetwork whether private network access is permitted + * @param maxAge how long the CORS preflight response may be cached + * @param exposedHeaders headers exposed to the client in CORS responses + * @author zihluwang + */ @ConfigurationProperties(prefix = "app.cors") public record CorsProperties( @DefaultValue({"Content-Type", "Authorization"}) diff --git a/src/main/java/com/onixbyte/deltaforceguide/properties/TokenProperties.java b/src/main/java/com/onixbyte/deltaforceguide/properties/TokenProperties.java index 0d7a618..74c693c 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/properties/TokenProperties.java +++ b/src/main/java/com/onixbyte/deltaforceguide/properties/TokenProperties.java @@ -4,6 +4,14 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import java.time.Duration; +/** + * Configuration properties for JWT token generation and validation, prefixed with "app.jwt". + * + * @param issuer the JWT issuer claim + * @param secret the signing secret for JWT tokens + * @param validTime the duration for which a token remains valid + * @author zihluwang + */ @ConfigurationProperties(prefix = "app.jwt") public record TokenProperties( String issuer, diff --git a/src/main/java/com/onixbyte/deltaforceguide/repository/FirearmRepository.java b/src/main/java/com/onixbyte/deltaforceguide/repository/FirearmRepository.java index dcf85f8..e9ad930 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/repository/FirearmRepository.java +++ b/src/main/java/com/onixbyte/deltaforceguide/repository/FirearmRepository.java @@ -7,6 +7,11 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +/** + * Spring Data JPA repository for {@link Firearm} entity operations. + * + * @author zihluwang + */ @Repository public interface FirearmRepository extends JpaRepository { diff --git a/src/main/java/com/onixbyte/deltaforceguide/repository/ModificationRepository.java b/src/main/java/com/onixbyte/deltaforceguide/repository/ModificationRepository.java index d4f891a..3d9fcc0 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/repository/ModificationRepository.java +++ b/src/main/java/com/onixbyte/deltaforceguide/repository/ModificationRepository.java @@ -13,6 +13,12 @@ import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; +/** + * Spring Data JPA repository for {@link Modification} entity operations, + * including native JSONB tag filtering for Postgres. + * + * @author zihluwang + */ @Repository public interface ModificationRepository extends JpaRepository { @@ -27,6 +33,14 @@ public interface ModificationRepository extends JpaRepository findById(@NonNull Long id); + /** + * Page query modifications with optional firearm and JSONB tag filtering. + * + * @param firearmId optional firearm ID filter (nullable) + * @param tagsJson optional JSON array of tags to match via Postgres {@code @>} operator (nullable) + * @param pageable pagination information + * @return a page of matching modifications + */ @Query(value = """ SELECT * FROM modification m WHERE (:firearmId IS NULL OR m.firearm_id = :firearmId) @@ -40,6 +54,12 @@ public interface ModificationRepository extends JpaRepository pageQueryByFirearmAndTags(@Param("firearmId") Long firearmId, @Param("tagsJson") String tagsJson, Pageable pageable); + /** + * Retrieve all distinct tag values from modifications, optionally filtered by firearm. + * + * @param firearmId optional firearm ID filter (nullable) + * @return list of distinct tag strings + */ @Query(value = "SELECT DISTINCT jsonb_array_elements_text(cast(tags as jsonb)) FROM modification WHERE (:firearmId IS NULL OR firearm_id = :firearmId)", nativeQuery = true) List findAllTags(@Param("firearmId") Long firearmId); } diff --git a/src/main/java/com/onixbyte/deltaforceguide/repository/UserCredentialRepository.java b/src/main/java/com/onixbyte/deltaforceguide/repository/UserCredentialRepository.java index 94fb8ec..53776e3 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/repository/UserCredentialRepository.java +++ b/src/main/java/com/onixbyte/deltaforceguide/repository/UserCredentialRepository.java @@ -12,9 +12,20 @@ import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; +/** + * Spring Data JPA repository for {@link UserCredential} entity operations. + * + * @author zihluwang + */ @Repository public interface UserCredentialRepository extends JpaRepository { + /** + * Find all credentials belonging to a given user. + * + * @param userId the user ID + * @return list of matching credentials + */ @EntityGraph(attributePaths = {"user"}) @Query(""" select uc @@ -23,6 +34,13 @@ public interface UserCredentialRepository extends JpaRepository findAllByUserId(@Param("userId") Long userId); + /** + * Find a specific credential for a user by provider. + * + * @param userId the user ID + * @param provider the authentication provider identifier + * @return an optional containing the matching credential, or empty if not found + */ @EntityGraph(attributePaths = {"user"}) @Query(""" select uc @@ -32,6 +50,12 @@ public interface UserCredentialRepository extends JpaRepository findByUserIdAndProvider(@Param("userId") Long userId, @Param("provider") String provider); + /** + * Delete a specific credential for a user by provider. + * + * @param userId the user ID + * @param provider the authentication provider identifier + */ @Modifying @Query(""" delete from UserCredential uc @@ -40,6 +64,11 @@ public interface UserCredentialRepository extends JpaRepository { @@ -28,6 +33,12 @@ public interface UserRepository extends JpaRepository { boolean existsByEmail(String email); + /** + * Find a user by either username or email. + * + * @param principal the username or email to search for + * @return an optional containing the matching user, or empty if not found + */ @EntityGraph(attributePaths = {"credentials"}) @Query(""" select u diff --git a/src/main/java/com/onixbyte/deltaforceguide/security/annotation/RequiresAuth.java b/src/main/java/com/onixbyte/deltaforceguide/security/annotation/RequiresAuth.java index f022be3..9b6b014 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/security/annotation/RequiresAuth.java +++ b/src/main/java/com/onixbyte/deltaforceguide/security/annotation/RequiresAuth.java @@ -7,6 +7,11 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Annotation to mark controller endpoints that require authentication. + * + * @author zihluwang + */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @PreAuthorize("isAuthenticated()") diff --git a/src/main/java/com/onixbyte/deltaforceguide/security/authentication/UsernamePasswordAuthentication.java b/src/main/java/com/onixbyte/deltaforceguide/security/authentication/UsernamePasswordAuthentication.java index 692efaf..9bd2ae3 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/security/authentication/UsernamePasswordAuthentication.java +++ b/src/main/java/com/onixbyte/deltaforceguide/security/authentication/UsernamePasswordAuthentication.java @@ -8,6 +8,11 @@ import org.springframework.security.core.GrantedAuthority; import java.util.Collection; import java.util.List; +/** + * Custom authentication token for username/password-based login flows. + * + * @author zihluwang + */ public class UsernamePasswordAuthentication implements Authentication, CredentialsContainer { private final String username; private String password; diff --git a/src/main/java/com/onixbyte/deltaforceguide/security/provider/UsernamePasswordAuthenticationProvider.java b/src/main/java/com/onixbyte/deltaforceguide/security/provider/UsernamePasswordAuthenticationProvider.java index d5e9a65..01e6baa 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/security/provider/UsernamePasswordAuthenticationProvider.java +++ b/src/main/java/com/onixbyte/deltaforceguide/security/provider/UsernamePasswordAuthenticationProvider.java @@ -17,6 +17,11 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; +/** + * Authentication provider that validates username/password credentials against stored BCrypt hashes. + * + * @author zihluwang + */ @Component public class UsernamePasswordAuthenticationProvider implements AuthenticationProvider { @@ -36,6 +41,12 @@ public class UsernamePasswordAuthenticationProvider implements AuthenticationPro this.userCredentialRepository = userCredentialRepository; } + /** + * Validates the username/password credentials against stored BCrypt hashes. + * + * @param authentication the authentication request object + * @return a fully authenticated object including user details + */ @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { if (!(authentication instanceof UsernamePasswordAuthentication usernamePasswordAuthentication)) { @@ -75,6 +86,12 @@ public class UsernamePasswordAuthenticationProvider implements AuthenticationPro return usernamePasswordAuthentication; } + /** + * Checks if this provider supports the given authentication type. + * + * @param authentication the authentication class to check + * @return true if this provider supports the given type + */ @Override public boolean supports(Class authentication) { return UsernamePasswordAuthentication.class.isAssignableFrom(authentication); diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/AuthService.java b/src/main/java/com/onixbyte/deltaforceguide/service/AuthService.java index b1bbfc7..da41902 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/service/AuthService.java +++ b/src/main/java/com/onixbyte/deltaforceguide/service/AuthService.java @@ -10,9 +10,11 @@ import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.stereotype.Service; -import java.util.Objects; -import java.util.Optional; - +/** + * Service handling user authentication, login, and session management. + * + * @author zihluwang + */ @Service public class AuthService { @@ -23,6 +25,16 @@ public class AuthService { this.authenticationManager = authenticationManager; } + /** + * Authenticates a user with the given login credentials. + *

+ * Delegates authentication to Spring Security's {@link AuthenticationManager} and verifies + * that the result is of the expected {@link UsernamePasswordAuthentication} type. + * + * @param request the login credentials containing principle and password + * @return the authenticated {@link User} + * @throws BizException if authentication fails or the result type is unexpected + */ public User login(LoginRequest request) { var _authentication = authenticationManager.authenticate(UsernamePasswordAuthentication .unauthenticated(request.principle(), request.credential())); diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/CookieService.java b/src/main/java/com/onixbyte/deltaforceguide/service/CookieService.java index 764b47b..eb93086 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/service/CookieService.java +++ b/src/main/java/com/onixbyte/deltaforceguide/service/CookieService.java @@ -6,6 +6,11 @@ import org.springframework.stereotype.Service; import java.time.Duration; +/** + * Service for building HTTP cookies with configurable properties. + * + * @author zihluwang + */ @Service public class CookieService { @@ -15,10 +20,25 @@ public class CookieService { this.cookieManager = cookieManager; } + /** + * Builds a response cookie with the default max age from configuration. + * + * @param cookieName the cookie name + * @param value the cookie value + * @return a configured ResponseCookie + */ public ResponseCookie buildCookie(String cookieName, String value) { return buildCookieInternal(cookieName, value, cookieManager.getMaxAge()); } + /** + * Builds a response cookie with a custom valid duration. + * + * @param cookieName the cookie name + * @param value the cookie value + * @param validDuration the cookie's max age + * @return a configured ResponseCookie + */ public ResponseCookie buildCookie(String cookieName, String value, Duration validDuration) { return buildCookieInternal(cookieName, value, validDuration); } diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/DailyPasswordService.java b/src/main/java/com/onixbyte/deltaforceguide/service/DailyPasswordService.java index 3ad3d4e..fa2118d 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/service/DailyPasswordService.java +++ b/src/main/java/com/onixbyte/deltaforceguide/service/DailyPasswordService.java @@ -4,6 +4,11 @@ import com.onixbyte.deltaforceguide.domain.dto.DailyPasswordResponse; import com.onixbyte.deltaforceguide.manager.DailyPasswordManager; import org.springframework.stereotype.Service; +/** + * Service for generating and caching daily rotation passwords. + * + * @author zihluwang + */ @Service public class DailyPasswordService { @@ -13,6 +18,10 @@ public class DailyPasswordService { this.dailyPasswordManager = dailyPasswordManager; } + /** + * Retrieves the daily password for the current day. + * @return the daily password response + */ public DailyPasswordResponse getDailyPassword() { return dailyPasswordManager.getDailyPassword(); } diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/FirearmService.java b/src/main/java/com/onixbyte/deltaforceguide/service/FirearmService.java index 3b69c15..5f5098b 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/service/FirearmService.java +++ b/src/main/java/com/onixbyte/deltaforceguide/service/FirearmService.java @@ -11,9 +11,13 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import org.springframework.web.server.ResponseStatusException; +/** + * Service handling firearm business logic including CRUD operations and queries. + * + * @author zihluwang + */ @Service public class FirearmService { @@ -23,7 +27,13 @@ public class FirearmService { this.firearmRepository = firearmRepository; } - @Transactional(readOnly = true) + /** + * Queries firearms with optional type filter and pagination. + * + * @param type optional firearm type filter + * @param pageable pagination parameters + * @return a paginated response of firearm records + */ public PageResponse pageQuery(FirearmType type, Pageable pageable) { Page page = type == null ? firearmRepository.findAll(pageable) @@ -32,13 +42,24 @@ public class FirearmService { return PageResponse.from(page.map(FirearmResponse::from)); } - @Transactional(readOnly = true) + /** + * Finds a firearm by its ID. + * + * @param id the firearm ID + * @return the firearm response + */ public FirearmResponse queryById(Long id) { return firearmRepository.findById(id) .map(FirearmResponse::from) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Firearm not found: " + id)); } + /** + * Creates a new firearm from the provided request data. + * + * @param request the firearm creation request + * @return the created firearm response + */ public FirearmResponse addFirearm(FirearmRequest request) { var firearm = firearmRepository.save(Firearm.builder() .name(request.name()) @@ -54,7 +75,13 @@ public class FirearmService { return FirearmResponse.from(firearm); } - @Transactional + /** + * Updates an existing firearm identified by ID. + * + * @param id the firearm ID + * @param request the updated firearm data + * @return the updated firearm response + */ public FirearmResponse updateFirearm(Long id, FirearmRequest request) { var firearm = firearmRepository.findById(id) .orElseThrow(() -> new BizException(HttpStatus.NOT_FOUND, "Firearm not found: " + id)); @@ -71,7 +98,11 @@ public class FirearmService { return FirearmResponse.from(firearmRepository.save(firearm)); } - @Transactional + /** + * Deletes a firearm by its ID. + * + * @param id the firearm ID to delete + */ public void deleteFirearm(Long id) { Firearm firearm = firearmRepository.findById(id) .orElseThrow(() -> new BizException(HttpStatus.NOT_FOUND, "Firearm not found: " + id)); diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/ModificationService.java b/src/main/java/com/onixbyte/deltaforceguide/service/ModificationService.java index 39e5ec0..7a9657e 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/service/ModificationService.java +++ b/src/main/java/com/onixbyte/deltaforceguide/service/ModificationService.java @@ -17,7 +17,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import org.springframework.web.server.ResponseStatusException; import java.util.ArrayList; @@ -27,6 +26,11 @@ import java.util.List; import java.util.Map; import java.util.Set; +/** + * Service handling modification business logic including CRUD, batch operations, and tag filtering. + * + * @author zihluwang + */ @Service public class ModificationService { @@ -44,7 +48,14 @@ public class ModificationService { this.objectMapper = objectMapper; } - @Transactional(readOnly = true) + /** + * Queries modifications with optional firearm and tag filters. + * + * @param firearmId optional firearm ID filter + * @param tags optional tag list filter + * @param pageable pagination parameters + * @return a paginated response of modification records + */ public PageResponse pageQuery(Long firearmId, List tags, Pageable pageable) { String tagsJson = null; if (tags != null && !tags.isEmpty()) { @@ -65,19 +76,34 @@ public class ModificationService { return PageResponse.from(page.map(ModificationResponse::from)); } - @Transactional(readOnly = true) + /** + * Finds a modification by its ID. + * + * @param id the modification ID + * @return the modification response + */ public ModificationResponse queryById(Long id) { return modificationRepository.findById(id) .map(ModificationResponse::from) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Modification not found: " + id)); } - @Transactional(readOnly = true) + /** + * Finds all unique tags across modifications, optionally scoped to a firearm. + * + * @param firearmId optional firearm ID to scope the tag search + * @return list of unique tag strings + */ public List findAllTags(Long firearmId) { return modificationRepository.findAllTags(firearmId); } - @Transactional + /** + * Creates a new modification for a given firearm. + * + * @param request the modification creation request + * @return the created modification response + */ public ModificationResponse create(ModificationRequest request) { Firearm firearm = firearmRepository.findById(request.firearmId()) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Firearm not found: " + request.firearmId())); @@ -86,7 +112,12 @@ public class ModificationService { return ModificationResponse.from(modificationRepository.save(modification)); } - @Transactional + /** + * Creates multiple modifications in a single batch operation. + * + * @param requests list of modification creation requests + * @return list of created modification responses + */ public List batchCreate(List requests) { Set firearmIds = requests.stream() .map(ModificationRequest::firearmId) @@ -111,7 +142,13 @@ public class ModificationService { .toList(); } - @Transactional + /** + * Updates an existing modification identified by ID. + * + * @param id the modification ID + * @param request the updated modification data + * @return the updated modification response + */ public ModificationResponse update(Long id, ModificationRequest request) { Modification modification = modificationRepository.findById(id) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Modification not found: " + id)); @@ -130,14 +167,22 @@ public class ModificationService { return ModificationResponse.from(modificationRepository.save(modification)); } - @Transactional + /** + * Deletes a modification by its ID. + * + * @param id the modification ID to delete + */ public void delete(Long id) { Modification modification = modificationRepository.findById(id) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Modification not found: " + id)); modificationRepository.delete(modification); } - @Transactional + /** + * Deletes multiple modifications in a single batch operation. + * + * @param ids list of modification IDs to delete + */ public void batchDelete(List ids) { Set uniqueIds = new LinkedHashSet<>(ids); List modifications = modificationRepository.findAllById(uniqueIds); diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/UserService.java b/src/main/java/com/onixbyte/deltaforceguide/service/UserService.java index c57ce4f..700e26d 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/service/UserService.java +++ b/src/main/java/com/onixbyte/deltaforceguide/service/UserService.java @@ -6,11 +6,15 @@ import com.onixbyte.deltaforceguide.manager.UserCredentialManager; import com.onixbyte.deltaforceguide.manager.UserManager; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import org.springframework.web.server.ResponseStatusException; import java.util.List; +/** + * Service for user account management and profile operations. + * + * @author zihluwang + */ @Service public class UserService { @@ -22,29 +26,53 @@ public class UserService { this.userCredentialManager = userCredentialManager; } - @Transactional(readOnly = true) + /** + * Retrieves all registered users. + * + * @return list of all users + */ public List findAll() { return userManager.findAll(); } - @Transactional(readOnly = true) + /** + * Finds a user by their ID. + * + * @param id the user ID + * @return the user + */ public User queryById(Long id) { return userManager.findById(id) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found: " + id)); } - @Transactional(readOnly = true) + /** + * Finds a user by their username. + * + * @param username the username to search for + * @return the user + */ public User queryByUsername(String username) { return userManager.findByUsername(username) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found: " + username)); } - @Transactional + /** + * Creates a new user account. + * + * @param user the user entity to persist + * @return the saved user entity + */ public User create(User user) { return userManager.save(user); } - @Transactional + /** + * Updates an existing user account. + * + * @param user the user entity with updated fields + * @return the saved user entity + */ public User update(User user) { if (user.getId() == null || userManager.findById(user.getId()).isEmpty()) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found: " + user.getId()); @@ -52,13 +80,24 @@ public class UserService { return userManager.save(user); } - @Transactional(readOnly = true) + /** + * Finds all credentials associated with a user. + * + * @param userId the user ID + * @return list of user credentials + */ public List findCredentials(Long userId) { ensureUserExists(userId); return userCredentialManager.findAllByUserId(userId); } - @Transactional(readOnly = true) + /** + * Queries a specific credential for a user by provider. + * + * @param userId the user ID + * @param provider the authentication provider + * @return the matching credential + */ public UserCredential queryCredential(Long userId, String provider) { ensureUserExists(userId); return userCredentialManager.findByUserIdAndProvider(userId, provider) @@ -67,7 +106,14 @@ public class UserService { "User credential not found: userId=" + userId + ", provider=" + provider)); } - @Transactional + /** + * Creates or updates a credential for a user and provider. + * + * @param userId the user ID + * @param provider the authentication provider + * @param credential the credential value + * @return the saved credential + */ public UserCredential upsertCredential(Long userId, String provider, String credential) { User user = ensureUserExists(userId); UserCredential userCredential = userCredentialManager.findByUserIdAndProvider(userId, provider) @@ -78,13 +124,22 @@ public class UserService { return userCredentialManager.save(userCredential); } - @Transactional + /** + * Deletes a specific credential for a user by provider. + * + * @param userId the user ID + * @param provider the authentication provider + */ public void deleteCredential(Long userId, String provider) { ensureUserExists(userId); userCredentialManager.deleteByUserIdAndProvider(userId, provider); } - @Transactional + /** + * Deletes a user and all associated credentials. + * + * @param id the user ID to delete + */ public void deleteById(Long id) { ensureUserExists(id); userCredentialManager.deleteAllByUserId(id); diff --git a/src/main/java/com/onixbyte/deltaforceguide/shared/CookieName.java b/src/main/java/com/onixbyte/deltaforceguide/shared/CookieName.java index 329852b..26f0112 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/shared/CookieName.java +++ b/src/main/java/com/onixbyte/deltaforceguide/shared/CookieName.java @@ -1,5 +1,10 @@ package com.onixbyte.deltaforceguide.shared; +/** + * Constants for HTTP cookie names used for authentication tokens. + * + * @author zihluwang + */ public class CookieName { public static final String ACCESS_TOKEN = "AccessToken"; diff --git a/src/main/java/com/onixbyte/deltaforceguide/shared/CredentialProvider.java b/src/main/java/com/onixbyte/deltaforceguide/shared/CredentialProvider.java index 115f935..9488536 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/shared/CredentialProvider.java +++ b/src/main/java/com/onixbyte/deltaforceguide/shared/CredentialProvider.java @@ -1,5 +1,10 @@ package com.onixbyte.deltaforceguide.shared; +/** + * Constants for supported authentication provider identifiers. + * + * @author zihluwang + */ public class CredentialProvider { public static final String LOCAL = "LOCAL"; diff --git a/src/main/java/com/onixbyte/deltaforceguide/shared/JacksonModules.java b/src/main/java/com/onixbyte/deltaforceguide/shared/JacksonModules.java index 36332aa..ae47823 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/shared/JacksonModules.java +++ b/src/main/java/com/onixbyte/deltaforceguide/shared/JacksonModules.java @@ -13,6 +13,12 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +/** + * Shared Jackson {@link com.fasterxml.jackson.databind.Module} instances for custom date/time + * serialisation and deserialisation across the application. + * + * @author zihluwang + */ public class JacksonModules { public static final SimpleModule DATE_TIME_MODULE = initialiseDateTimeModule(); diff --git a/src/main/java/com/onixbyte/deltaforceguide/shared/JacksonRedisSerialiser.java b/src/main/java/com/onixbyte/deltaforceguide/shared/JacksonRedisSerialiser.java index 5a300ba..4e50c38 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/shared/JacksonRedisSerialiser.java +++ b/src/main/java/com/onixbyte/deltaforceguide/shared/JacksonRedisSerialiser.java @@ -4,6 +4,12 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +/** + * Singleton serialiser for Redis cache operations using + * {@link GenericJackson2JsonRedisSerializer} with JavaTime module support. + * + * @author zihluwang + */ public class JacksonRedisSerialiser { public static final GenericJackson2JsonRedisSerializer INSTANCE = initialiseSerializer(); diff --git a/src/main/java/com/onixbyte/deltaforceguide/utils/DateTimeUtil.java b/src/main/java/com/onixbyte/deltaforceguide/utils/DateTimeUtil.java index f1360c8..1d576a5 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/utils/DateTimeUtil.java +++ b/src/main/java/com/onixbyte/deltaforceguide/utils/DateTimeUtil.java @@ -4,8 +4,19 @@ import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; +/** + * Utility class for date and time operations using system-default time zone. + * + * @author zihluwang + */ public class DateTimeUtil { + /** + * Convert a {@link LocalDateTime} to an {@link Instant} using the system-default time zone. + * + * @param ldt the local date-time to convert + * @return the corresponding instant + */ public static Instant asInstant(LocalDateTime ldt) { return ldt.atZone(ZoneId.systemDefault()) .toInstant(); From ce330bca871011d1d22307a9130cbdcd7926b465 Mon Sep 17 00:00:00 2001 From: siujamo Date: Fri, 29 May 2026 15:10:48 +0800 Subject: [PATCH 13/35] 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 14/35] 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 15/35] 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 16/35] 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 17/35] 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 18/35] 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 19/35] 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 20/35] 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 21/35] 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 22/35] 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 23/35] 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 24/35] 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 25/35] 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 26/35] 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 27/35] 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 28/35] 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 29/35] 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"); From eb2d9b33695adc9e7e39d6c21fea8f4c376701c5 Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 1 Jun 2026 17:18:42 +0800 Subject: [PATCH 30/35] fix: rename webhook filter bean to avoid name collision with WebhookFilter class --- .../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 b0c3e0f..893852a 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 webhookFilter(WebhookFilter webhookFilter) { + public FilterRegistrationBean webhookFilterBean(WebhookFilter webhookFilter) { var registrationBean = new FilterRegistrationBean(); registrationBean.setFilter(webhookFilter); From d323e4f8f7bd683e19e211c93eeb873262ae76e4 Mon Sep 17 00:00:00 2001 From: zihluwang Date: Tue, 2 Jun 2026 01:46:49 +0800 Subject: [PATCH 31/35] chore: change traffic interceptor log level from info to debug --- .../deltaforceguide/interceptor/TrafficInterceptor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/onixbyte/deltaforceguide/interceptor/TrafficInterceptor.java b/src/main/java/com/onixbyte/deltaforceguide/interceptor/TrafficInterceptor.java index ab3b657..c7aac50 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/interceptor/TrafficInterceptor.java +++ b/src/main/java/com/onixbyte/deltaforceguide/interceptor/TrafficInterceptor.java @@ -27,7 +27,7 @@ public class TrafficInterceptor implements HandlerInterceptor { var contentLength = request.getContentLength(); var userAgent = request.getHeader("User-Agent"); - log.info("Request method={}, uri={}, query={}, ip={}, content-type={}, content-length={}, user-agent={}", + log.debug("Request method={}, uri={}, query={}, ip={}, content-type={}, content-length={}, user-agent={}", method, uri, query, ip, contentType, contentLength, userAgent); return true; } From 0815d1d6186bfcde741b230acd1ad7870589d596 Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 4 Jun 2026 14:42:14 +0800 Subject: [PATCH 32/35] chore: optimise code style --- .../deltaforceguide/interceptor/GitHubWebhookInterceptor.java | 2 +- .../com/onixbyte/deltaforceguide/service/WebhookService.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitHubWebhookInterceptor.java b/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitHubWebhookInterceptor.java index 9aacbe7..40df977 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitHubWebhookInterceptor.java +++ b/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitHubWebhookInterceptor.java @@ -64,7 +64,7 @@ public class GitHubWebhookInterceptor implements HandlerInterceptor { var body = req.getBodyString(); try { - var computed = "sha256=" + CryptoUtil.hmacSha256(secret, body); + var computed = "sha256=" + CryptoUtil.hmacSha256(body, secret); if (!MessageDigest.isEqual( computed.getBytes(StandardCharsets.UTF_8), diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java b/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java index 3476958..c5c2b33 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java +++ b/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java @@ -73,8 +73,7 @@ public class WebhookService { return; } - @SuppressWarnings("unchecked") - var data = (Map) yaml.load(parsedYaml); + var data = yaml.>load(parsedYaml); if (data == null) { log.warn("Empty YAML block in issue #{}", issue.number()); return; From 4e2da0debce14b94aad76dedce4c02a7a36bc7e1 Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 4 Jun 2026 14:47:45 +0800 Subject: [PATCH 33/35] feat: add expire time into login response --- .../deltaforceguide/controller/AuthController.java | 5 ++++- .../deltaforceguide/domain/dto/UserResponse.java | 10 +++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/onixbyte/deltaforceguide/controller/AuthController.java b/src/main/java/com/onixbyte/deltaforceguide/controller/AuthController.java index 0a42067..db0f42e 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/controller/AuthController.java +++ b/src/main/java/com/onixbyte/deltaforceguide/controller/AuthController.java @@ -18,6 +18,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.time.Duration; +import java.time.LocalDateTime; /** * REST controller for user authentication endpoints (login, logout). @@ -43,12 +44,14 @@ public class AuthController { @PostMapping("/login") public ResponseEntity login(@Validated @RequestBody LoginRequest request) { var user = authService.login(request); + var currentTime = LocalDateTime.now(); var accessToken = tokenClient.generateToken(user); var accessTokenCookie = cookieService.buildCookie(CookieName.ACCESS_TOKEN, accessToken); + var cookieMaxAge = accessTokenCookie.getMaxAge(); return ResponseEntity.ok() .header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()) - .body(UserResponse.from(user)); + .body(UserResponse.from(user, currentTime.plus(cookieMaxAge))); } @RequiresAuth diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/UserResponse.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/UserResponse.java index b20231b..4a2d82d 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/UserResponse.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/UserResponse.java @@ -2,6 +2,8 @@ package com.onixbyte.deltaforceguide.domain.dto; import com.onixbyte.deltaforceguide.domain.entity.User; +import java.time.LocalDateTime; + /** * Response DTO for a user account, including associated credentials. * @@ -10,13 +12,15 @@ import com.onixbyte.deltaforceguide.domain.entity.User; public record UserResponse( Long id, String username, - String email + String email, + LocalDateTime expiration ) { - public static UserResponse from(User user) { + public static UserResponse from(User user, LocalDateTime expiration) { return new UserResponse( user.getId(), user.getUsername(), - user.getEmail() + user.getEmail(), + expiration ); } } From 17cd87c7025d8317aa14b3af6a9492c479c38684 Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 4 Jun 2026 17:12:48 +0800 Subject: [PATCH 34/35] feat: inject build-time variables via Gradle processResources Replace hardcoded AppProperties values with Gradle ${} placeholders, allowing version/channel/vendor to be configured via gradle.properties or -P flags at build time. Also refactor webhook configuration to flatten the properties hierarchy by removing the intermediate WebhookProperties wrapper. --- build.gradle.kts | 12 ++++++++++++ .../deltaforceguide/config/WebhookConfig.java | 4 ++-- .../interceptor/GitHubWebhookInterceptor.java | 2 +- .../deltaforceguide/manager/AppManager.java | 6 +++++- .../manager/WebhookManager.java | 19 +++++++++++-------- .../properties/AppProperties.java | 4 +++- .../properties/GitHubWebhookProperties.java | 3 +++ .../properties/WebhookProperties.java | 9 --------- .../service/WebhookService.java | 2 +- src/main/resources/application.yaml | 12 ++++++------ 10 files changed, 44 insertions(+), 29 deletions(-) delete mode 100644 src/main/java/com/onixbyte/deltaforceguide/properties/WebhookProperties.java diff --git a/build.gradle.kts b/build.gradle.kts index 878912f..a98947a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,6 +5,8 @@ plugins { } val artefactVersion: String by project +val buildChannel: String by project +val vendor: String by project group = "com.onixbyte.helix" version = artefactVersion @@ -61,6 +63,16 @@ dependencies { testRuntimeOnly(libs.junit.launcher) } +tasks.processResources { + filesMatching("application.yaml") { + expand( + "appVersion" to artefactVersion, + "channel" to buildChannel, + "vendor" to vendor + ) + } +} + tasks.test { useJUnitPlatform() } diff --git a/src/main/java/com/onixbyte/deltaforceguide/config/WebhookConfig.java b/src/main/java/com/onixbyte/deltaforceguide/config/WebhookConfig.java index 1cb1a6c..d9c5db7 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/config/WebhookConfig.java +++ b/src/main/java/com/onixbyte/deltaforceguide/config/WebhookConfig.java @@ -1,10 +1,10 @@ package com.onixbyte.deltaforceguide.config; -import com.onixbyte.deltaforceguide.properties.WebhookProperties; +import com.onixbyte.deltaforceguide.properties.GitHubWebhookProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; @Configuration -@EnableConfigurationProperties(WebhookProperties.class) +@EnableConfigurationProperties({GitHubWebhookProperties.class}) public class WebhookConfig { } diff --git a/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitHubWebhookInterceptor.java b/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitHubWebhookInterceptor.java index 40df977..c1b552a 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitHubWebhookInterceptor.java +++ b/src/main/java/com/onixbyte/deltaforceguide/interceptor/GitHubWebhookInterceptor.java @@ -48,7 +48,7 @@ public class GitHubWebhookInterceptor implements HandlerInterceptor { "Request body is not readable"); } - var secret = webhookManager.github().secret(); + var secret = webhookManager.secret(); if (secret == null || secret.isBlank()) { log.debug("No GitHub webhook secret configured, skipping signature verification"); return true; diff --git a/src/main/java/com/onixbyte/deltaforceguide/manager/AppManager.java b/src/main/java/com/onixbyte/deltaforceguide/manager/AppManager.java index 64f8ddd..378fe9b 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/manager/AppManager.java +++ b/src/main/java/com/onixbyte/deltaforceguide/manager/AppManager.java @@ -18,6 +18,10 @@ public class AppManager { * @return the version string of this application */ public String getVersion() { - return appProperties.version(); + return "v%s-%s by @%s".formatted( + appProperties.version(), + appProperties.channel(), + appProperties.vendor() + ); } } diff --git a/src/main/java/com/onixbyte/deltaforceguide/manager/WebhookManager.java b/src/main/java/com/onixbyte/deltaforceguide/manager/WebhookManager.java index 1e63f36..21c5b52 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/manager/WebhookManager.java +++ b/src/main/java/com/onixbyte/deltaforceguide/manager/WebhookManager.java @@ -1,21 +1,24 @@ package com.onixbyte.deltaforceguide.manager; import com.onixbyte.deltaforceguide.properties.GitHubWebhookProperties; -import com.onixbyte.deltaforceguide.properties.WebhookProperties; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import java.util.List; + @Component public class WebhookManager { - private final WebhookProperties webhookProperties; + private final GitHubWebhookProperties gitHubWebhookProperties; - @Autowired - public WebhookManager(WebhookProperties webhookProperties) { - this.webhookProperties = webhookProperties; + public WebhookManager(GitHubWebhookProperties gitHubWebhookProperties) { + this.gitHubWebhookProperties = gitHubWebhookProperties; } - public GitHubWebhookProperties github() { - return webhookProperties.github(); + public String secret() { + return gitHubWebhookProperties.secret(); + } + + public List allowedUsers() { + return gitHubWebhookProperties.allowedUsers(); } } diff --git a/src/main/java/com/onixbyte/deltaforceguide/properties/AppProperties.java b/src/main/java/com/onixbyte/deltaforceguide/properties/AppProperties.java index 79b056c..b13bca2 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/properties/AppProperties.java +++ b/src/main/java/com/onixbyte/deltaforceguide/properties/AppProperties.java @@ -4,6 +4,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "app.common") public record AppProperties( - String version + String version, + String channel, + String vendor ) { } diff --git a/src/main/java/com/onixbyte/deltaforceguide/properties/GitHubWebhookProperties.java b/src/main/java/com/onixbyte/deltaforceguide/properties/GitHubWebhookProperties.java index bf91c6a..673051a 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/properties/GitHubWebhookProperties.java +++ b/src/main/java/com/onixbyte/deltaforceguide/properties/GitHubWebhookProperties.java @@ -1,7 +1,10 @@ package com.onixbyte.deltaforceguide.properties; +import org.springframework.boot.context.properties.ConfigurationProperties; + import java.util.List; +@ConfigurationProperties(prefix = "app.webhook.github") public record GitHubWebhookProperties( String secret, List allowedUsers diff --git a/src/main/java/com/onixbyte/deltaforceguide/properties/WebhookProperties.java b/src/main/java/com/onixbyte/deltaforceguide/properties/WebhookProperties.java deleted file mode 100644 index d420105..0000000 --- a/src/main/java/com/onixbyte/deltaforceguide/properties/WebhookProperties.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.onixbyte.deltaforceguide.properties; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(prefix = "app.webhook") -public record WebhookProperties( - GitHubWebhookProperties github -) { -} diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java b/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java index c5c2b33..e864a77 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java +++ b/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java @@ -172,7 +172,7 @@ public class WebhookService { private boolean isAllowedSender( GitHubWebhookSender sender ) { - var allowedUsers = webhookManager.github().allowedUsers(); + var allowedUsers = webhookManager.allowedUsers(); if (allowedUsers == null || allowedUsers.isEmpty()) { return true; } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 74de069..8fcdf2f 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -39,13 +39,13 @@ 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: orm.connections.pooling: off + +app: + common: + version: ${appVersion} + channel: ${channel} + vendor: ${vendor} From a065b60cae0de0bf7c7c75668fe8327ab5ad9b8a Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 4 Jun 2026 17:36:43 +0800 Subject: [PATCH 35/35] chore: migrate CI from GitHub Actions to Gitea Actions Replace GitHub Actions workflow with Gitea Actions, and switch container registry from GHCR to Docker Hub. --- .gitea/workflows/build-and-deploy.yml | 72 +++++++++++++++++++++ .github/workflows/build-and-deploy.yml | 86 -------------------------- 2 files changed, 72 insertions(+), 86 deletions(-) create mode 100644 .gitea/workflows/build-and-deploy.yml delete mode 100644 .github/workflows/build-and-deploy.yml diff --git a/.gitea/workflows/build-and-deploy.yml b/.gitea/workflows/build-and-deploy.yml new file mode 100644 index 0000000..1d83a90 --- /dev/null +++ b/.gitea/workflows/build-and-deploy.yml @@ -0,0 +1,72 @@ +name: Build and Deploy + +on: + release: + types: [published] + +env: + APP_NAME: delta-force-guide-server + +jobs: + build-and-release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 21 (Corretto) + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: corretto + cache: gradle + + - name: Build with Gradle + run: > + ./gradlew bootJar -x test + -PartefactVersion="${{ gitea.event.release.tag_name }}" + -PbuildChannel=stable + + - name: Resolve JAR file path + id: jar + run: | + JAR_PATH=$(find build/libs -name '*.jar' | head -1) + echo "file=$JAR_PATH" >> "$GITHUB_OUTPUT" + + - name: Upload JAR to Gitea Release + run: | + TAG="${{ gitea.event.release.tag_name }}" + FILE="${{ steps.jar.outputs.file }}" + ASSET_NAME="${APP_NAME}-${TAG}.jar" + curl -X POST \ + -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \ + -H "Content-Type: multipart/form-data" \ + -F "attachment=@${FILE};filename=${ASSET_NAME}" \ + "${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/${{ gitea.event.release.id }}/assets?name=${ASSET_NAME}" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Generate image tags + id: meta + run: | + DOCKERHUB_USER="${{ secrets.DOCKER_HUB_USERNAME }}" + REPO_NAME=$(echo "${{ gitea.repository.name }}" | tr '[:upper:]' '[:lower:]') + echo "tag_version=${DOCKERHUB_USER}/${REPO_NAME}:${{ gitea.event.release.tag_name }}" >> "$GITHUB_OUTPUT" + echo "tag_latest=${DOCKERHUB_USER}/${REPO_NAME}:latest" >> "$GITHUB_OUTPUT" + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile.ci + build-args: JAR_FILE=${{ steps.jar.outputs.file }} + push: true + tags: | + ${{ steps.meta.outputs.tag_version }} + ${{ steps.meta.outputs.tag_latest }} diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml deleted file mode 100644 index 116f12f..0000000 --- a/.github/workflows/build-and-deploy.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Build and Deploy - -on: - release: - types: [published] - -env: - APP_NAME: delta-force-guide-server - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - # ================================================================ - # Single Job: Build, Upload JAR to Release, and Push to GHCR - # ================================================================ - build-and-release: - runs-on: ubuntu-latest - permissions: - contents: write - packages: write - steps: - - uses: actions/checkout@v4 - - - name: Set up JDK 21 (Corretto) - uses: actions/setup-java@v4 - with: - java-version: 21 - distribution: corretto - cache: gradle - - - name: Set up Gradle - uses: gradle/actions/setup-gradle@v4 - - # 使用 Release Tag 做为 Gradle 属性传入 - - name: Build with Gradle - run: ./gradlew bootJar -x test -PartefactVersion="${{ github.event.release.tag_name }}" - - - name: Resolve JAR file path - id: jar - run: | - JAR_PATH=$(find build/libs -name '*.jar' | head -1) - echo "file=$JAR_PATH" >> "$GITHUB_OUTPUT" - - # 上传 JAR 包到 GitHub Release 中 - - name: Upload JAR to GitHub Release - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ${{ steps.jar.outputs.file }} - asset_name: ${{ github.event.repository.name }}-${{ github.event.release.tag_name }}.jar - tag: ${{ github.event.release.tag_name }} - overwrite: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - # 登录到 GitHub Container Registry (GHCR) - - name: Log in to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - # 镜像打标签准备 - - name: Generate image tags - id: meta - run: | - OWNER_LC=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') - REPO_LC=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]') - echo "tag_version=ghcr.io/$OWNER_LC/$REPO_LC:${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT" - echo "tag_latest=ghcr.io/$OWNER_LC/$REPO_LC:latest" >> "$GITHUB_OUTPUT" - - # 构建并上传镜像到 GHCR - - name: Build and push Docker image - uses: docker/build-push-action@v6 - with: - context: . - file: Dockerfile.ci - build-args: JAR_FILE=${{ steps.jar.outputs.file }} - push: true - tags: | - ${{ steps.meta.outputs.tag_version }} - ${{ steps.meta.outputs.tag_latest }} - cache-from: type=gha - cache-to: type=gha,mode=max -