Compare commits

...

6 Commits

Author SHA1 Message Date
siujamo 0aeaac17a4 feat: custom 401/403 error handling for unauthenticated requests
Replace default Spring Security 403 responses with custom 401 (not logged in)
and 403 (insufficient permissions) JSON error responses via exception handling
and global exception handler. Also bump API version to 1.4.0 and add Swagger
annotations for the GitHub webhook controller.
2026-06-29 09:38:12 +08:00
siujamo a2469c7573 docs: update application configuration example
Build and Deploy / build-and-release (release) Failing after 5m14s
2026-06-08 13:32:16 +08:00
siujamo 62fcce3005 feat: add OCI labels and README push for Docker Hub
Add OCI standard labels to Dockerfile.ci and push README.md to
Docker Hub image page via docker-pushrm in CI workflow.
2026-06-05 10:20:15 +08:00
siujamo c18a108a2b ci: fix build error
Build and Deploy / build-and-release (release) Successful in 14m20s
2026-06-05 09:40:12 +08:00
siujamo fb2732508a ci: fix build error
Build and Deploy / build-and-release (release) Failing after 11m24s
2026-06-05 09:30:28 +08:00
siujamo ceac8a3f8c ci: fix build error
Build and Deploy / build-and-release (release) Failing after 11m22s
2026-06-05 09:14:37 +08:00
9 changed files with 85 additions and 51 deletions
+18 -6
View File
@@ -50,24 +50,36 @@ jobs:
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ vars.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }} password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Generate image tags - name: Generate image tags
id: meta id: meta
run: | run: |
DOCKERHUB_USER="${{ secrets.DOCKER_HUB_USERNAME }}" DOCKERHUB_USER="${{ vars.DOCKER_HUB_USERNAME }}"
REPO_NAME=$(echo "${{ gitea.repository.name }}" | tr '[:upper:]' '[:lower:]') echo "tag_version=${DOCKERHUB_USER}/${APP_NAME}:${{ gitea.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
echo "tag_version=${DOCKERHUB_USER}/${REPO_NAME}:${{ gitea.event.release.tag_name }}" >> "$GITHUB_OUTPUT" echo "tag_latest=${DOCKERHUB_USER}/${APP_NAME}:latest" >> "$GITHUB_OUTPUT"
echo "tag_latest=${DOCKERHUB_USER}/${REPO_NAME}:latest" >> "$GITHUB_OUTPUT"
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: Dockerfile.ci file: Dockerfile.ci
build-args: JAR_FILE=${{ steps.jar.outputs.file }} build-args: |
JAR_FILE=${{ steps.jar.outputs.file }}
IMAGE_VERSION=${{ gitea.event.release.tag_name }}
IMAGE_VENDOR=${{ vars.VENDOR }}
BUILD_DATE=${{ gitea.event.release.published_at }}
push: true push: true
tags: | tags: |
${{ steps.meta.outputs.tag_version }} ${{ steps.meta.outputs.tag_version }}
${{ steps.meta.outputs.tag_latest }} ${{ steps.meta.outputs.tag_latest }}
- name: Push README to Docker Hub
run: >
docker run --rm
-v "$(pwd)/README.md:/workspace/README.md:ro"
-e DOCKER_USER="${{ vars.DOCKER_HUB_USERNAME }}"
-e DOCKER_PASS="${{ secrets.DOCKER_HUB_TOKEN }}"
chko/docker-pushrm:latest
"${{ vars.DOCKER_HUB_USERNAME }}/${{ env.APP_NAME }}"
-34
View File
@@ -1,34 +0,0 @@
variables:
GRADLE_OPTS: -Dorg.gradle.daemon=false
DOCKER_HOST: unix:///var/run/docker.sock
stages:
- release
release:
stage: release
image: amazoncorretto:21-alpine
cache:
key: gradle
paths:
- .gradle/wrapper
- .gradle/caches
before_script:
- chmod +x gradlew
- apk add --no-cache docker-cli
script:
- ./gradlew bootJar -x test -PartefactVersion="$CI_COMMIT_TAG"
- JAR_FILE=$(find build/libs -name '*.jar' | head -1)
- echo "Building Docker image for tag $CI_COMMIT_TAG with JAR $JAR_FILE"
- docker build
-f Dockerfile.ci
--build-arg JAR_FILE="$JAR_FILE"
-t "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG"
.
- docker tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG" "$CI_REGISTRY_IMAGE:latest"
- echo "Pushing image $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG"
- docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD"
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG"
- docker push "$CI_REGISTRY_IMAGE:latest"
rules:
- if: $CI_COMMIT_TAG
+12
View File
@@ -2,6 +2,18 @@ FROM amazoncorretto:21-alpine
WORKDIR /app WORKDIR /app
ARG JAR_FILE ARG JAR_FILE
ARG IMAGE_VERSION=dev
ARG IMAGE_VENDOR=OnixByte
ARG BUILD_DATE
COPY ${JAR_FILE} app.jar COPY ${JAR_FILE} app.jar
LABEL org.opencontainers.image.title="delta-force-guide-server" \
org.opencontainers.image.description="REST API backend for managing Delta Force game firearm builds and modifications" \
org.opencontainers.image.version="${IMAGE_VERSION}" \
org.opencontainers.image.vendor="${IMAGE_VENDOR}" \
org.opencontainers.image.source="https://git.onixbyte.com/onixbyte/delta-force-guide-server" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.created="${BUILD_DATE}"
ENTRYPOINT ["java", "-jar", "app.jar"] ENTRYPOINT ["java", "-jar", "app.jar"]
+1
View File
@@ -65,6 +65,7 @@ dependencies {
tasks.processResources { tasks.processResources {
filesMatching("application.yaml") { filesMatching("application.yaml") {
println("appVersion = ${artefactVersion}, channel = ${buildChannel}, vendor = ${vendor}")
expand( expand(
"appVersion" to artefactVersion, "appVersion" to artefactVersion,
"channel" to buildChannel, "channel" to buildChannel,
+11 -9
View File
@@ -9,15 +9,19 @@ spring:
host: localhost host: localhost
port: 6379 port: 6379
database: 0 database: 0
# password: 6hLFVqfGPviTYukn # Uncomment if password is necessary # password: qwerty # Uncomment if password is necessary
logging: logging:
pattern: pattern:
# dateformat: dd MMM yyyy HH:mm:ss.SSS # Modify this for custom date format. # dateformat: dd MMM yyyy HH:mm:ss.SSS # Modify this for custom date format.
app: app:
common: cookie:
version: 1.3.0.8-dev # Application version, you can change to any version you like, used for communication with frontend. http-only: true
secure: false
same-site: none
path: '/'
max-age: P1D
cors: cors:
allowed-origins: # Cross-origin allowed origins allowed-origins: # Cross-origin allowed origins
- "http://localhost:5173" # Dev server for vite. - "http://localhost:5173" # Dev server for vite.
@@ -41,12 +45,10 @@ app:
issuer: dfguide.local # Issuer host issuer: dfguide.local # Issuer host
secret: qwertyuiopasdfghjklzxcvbnm123456 # JWT singing secret, a 32-byte long or longer string is recommended secret: qwertyuiopasdfghjklzxcvbnm123456 # JWT singing secret, a 32-byte long or longer string is recommended
valid-time: PT2H # JWT valid duration valid-time: PT2H # JWT valid duration
cookie: # Cookie settings. webhook:
http-only: true github:
secure: false secret: 123456
same-site: lax allowed-users: [ octotcat, onixbyte ]
path: '/'
max-age: PT2H
springdoc: springdoc:
api-docs: api-docs:
@@ -10,14 +10,14 @@ import org.springframework.context.annotation.Configuration;
info = @Info( info = @Info(
title = "Delta Force Guide Server", title = "Delta Force Guide Server",
description = "API for managing Delta Force game firearm builds", description = "API for managing Delta Force game firearm builds",
version = "1.3.4", version = "1.4.0",
contact = @Contact( contact = @Contact(
name = "Zihlu Wang", name = "Zihlu Wang",
email = "zihlu.wang@onixbyte.com" email = "zihlu.wang@onixbyte.com"
), ),
license = @License( license = @License(
name = "MIT", name = "MIT",
url = "https://git.onixbyte.cn/onixbyte/delta-force-guide-server/-/raw/main/LICENCE" url = "https://onixbyte.dev/onixbyte/delta-force-guide-server/raw/branch/main/LICENCE"
) )
) )
) )
@@ -7,6 +7,8 @@ import com.onixbyte.deltaforceguide.filter.TokenAuthenticationFilter;
import com.onixbyte.deltaforceguide.properties.CookieProperties; import com.onixbyte.deltaforceguide.properties.CookieProperties;
import com.onixbyte.deltaforceguide.properties.TokenProperties; import com.onixbyte.deltaforceguide.properties.TokenProperties;
import com.onixbyte.deltaforceguide.security.provider.UsernamePasswordAuthenticationProvider; import com.onixbyte.deltaforceguide.security.provider.UsernamePasswordAuthenticationProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -19,6 +21,8 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import com.onixbyte.deltaforceguide.exeption.BizException;
import org.springframework.http.HttpStatus;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.ExceptionTranslationFilter; import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.CorsConfigurationSource;
@@ -34,6 +38,8 @@ import org.springframework.web.cors.CorsConfigurationSource;
@EnableConfigurationProperties({TokenProperties.class, CookieProperties.class}) @EnableConfigurationProperties({TokenProperties.class, CookieProperties.class})
public class SecurityConfig { public class SecurityConfig {
private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class);
/** /**
* Configures the HTTP security filter chain including endpoint authorisation and JWT filter. * Configures the HTTP security filter chain including endpoint authorisation and JWT filter.
* *
@@ -55,6 +61,16 @@ public class SecurityConfig {
.authorizeHttpRequests((customiser) -> customiser .authorizeHttpRequests((customiser) -> customiser
.anyRequest().permitAll() .anyRequest().permitAll()
) )
.exceptionHandling(customiser -> customiser
.authenticationEntryPoint((request, response, authException) -> {
log.error("Unauthenticated request: {}", request, authException);
throw new BizException(HttpStatus.UNAUTHORIZED, "请先登录");
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
log.error("Denied request: {}", request, accessDeniedException);
throw new BizException(HttpStatus.FORBIDDEN, "权限不足");
})
)
.addFilterAfter(tokenAuthenticationFilter, ExceptionTranslationFilter.class) .addFilterAfter(tokenAuthenticationFilter, ExceptionTranslationFilter.class)
.build(); .build();
} }
@@ -3,6 +3,8 @@ package com.onixbyte.deltaforceguide.controller;
import com.onixbyte.deltaforceguide.domain.dto.GitHubIssueRequest; import com.onixbyte.deltaforceguide.domain.dto.GitHubIssueRequest;
import com.onixbyte.deltaforceguide.service.WebhookService; import com.onixbyte.deltaforceguide.service.WebhookService;
import com.onixbyte.deltaforceguide.shared.GitHubWebhookHeader; import com.onixbyte.deltaforceguide.shared.GitHubWebhookHeader;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -12,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@Tag(name = "GitHub WebHook")
@RestController @RestController
@RequestMapping("/webhooks/github") @RequestMapping("/webhooks/github")
public class GitHubWebhookController { public class GitHubWebhookController {
@@ -24,6 +27,7 @@ public class GitHubWebhookController {
this.webhookService = webhookService; this.webhookService = webhookService;
} }
@Operation(description = "GitHub WebHook 接口")
@PostMapping @PostMapping
public ResponseEntity<Void> handleWebhook( public ResponseEntity<Void> handleWebhook(
@RequestHeader(GitHubWebhookHeader.EVENT) String event, @RequestHeader(GitHubWebhookHeader.EVENT) String event,
@@ -2,7 +2,11 @@ package com.onixbyte.deltaforceguide.controller;
import com.onixbyte.deltaforceguide.domain.dto.ErrorResponse; import com.onixbyte.deltaforceguide.domain.dto.ErrorResponse;
import com.onixbyte.deltaforceguide.exeption.BizException; import com.onixbyte.deltaforceguide.exeption.BizException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
@@ -20,5 +24,22 @@ public class GlobalExceptionHandler {
return ResponseEntity.status(status) return ResponseEntity.status(status)
.body(new ErrorResponse(exception.getMessage())); .body(new ErrorResponse(exception.getMessage()));
} }
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<ErrorResponse> handleAuthenticationException(AuthenticationException exception) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse("请先登录"));
}
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ErrorResponse> handleAccessDeniedException(AccessDeniedException exception) {
var authentication = SecurityContextHolder.getContext().getAuthentication();
var httpStatus = authentication == null || !authentication.isAuthenticated()
? HttpStatus.UNAUTHORIZED
: HttpStatus.FORBIDDEN;
var message = httpStatus == HttpStatus.UNAUTHORIZED ? "请先登录" : "权限不足";
return ResponseEntity.status(httpStatus)
.body(new ErrorResponse(message));
}
} }