Compare commits
17 Commits
20bc18d416
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4803ae78c9 | |||
|
a065b60cae
|
|||
|
17cd87c702
|
|||
|
4e2da0debc
|
|||
|
0815d1d618
|
|||
|
d323e4f8f7
|
|||
|
eb2d9b3369
|
|||
| a0d54cc12d | |||
| 4eafb3ade7 | |||
|
9594efe716
|
|||
|
5b5062aae9
|
|||
|
b0c41e08ea
|
|||
|
ed2a0f4ae0
|
|||
|
de61e1feb7
|
|||
|
3616ad9eab
|
|||
|
bd4fe65b03
|
|||
|
eb22b3c4bb
|
@@ -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 }}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
public class FilterConfig {
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean<WebhookFilter> webhookFilter(WebhookFilter webhookFilter) {
|
||||
public FilterRegistrationBean<WebhookFilter> webhookFilterBean(WebhookFilter webhookFilter) {
|
||||
var registrationBean = new FilterRegistrationBean<WebhookFilter>();
|
||||
|
||||
registrationBean.setFilter(webhookFilter);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
@@ -18,7 +18,13 @@ 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).
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
@Tag(name = "用户鉴权", description = "处理用户登录与退出功能")
|
||||
@RestController
|
||||
@RequestMapping("/auth")
|
||||
@@ -38,12 +44,14 @@ public class AuthController {
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<UserResponse> 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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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<FirearmType, Integer> {
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = "登录名称不能为空")
|
||||
|
||||
+5
@@ -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
|
||||
|
||||
+5
@@ -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
|
||||
|
||||
@@ -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必须为正数")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<T>(
|
||||
List<T> items,
|
||||
int page,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,16 +2,25 @@ 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.
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,11 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Entity representing an accessory attached to a modification, stored as JSONB.
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
public class Accessory {
|
||||
|
||||
private String slotName;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -2,6 +2,11 @@ package com.onixbyte.deltaforceguide.domain.entity;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Entity representing a tuning adjustment for an accessory, stored as JSONB within Accessory.
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
public class Tuning {
|
||||
|
||||
private String tuningName;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
+2
-4
@@ -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;
|
||||
@@ -50,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;
|
||||
@@ -66,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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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<UserCredential> 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<UserCredential> 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);
|
||||
}
|
||||
|
||||
@@ -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<User> findById(Long id) {
|
||||
return userRepository.findById(id);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
/**
|
||||
* Retrieves all registered users.
|
||||
* @return list of all users
|
||||
*/
|
||||
public List<User> 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<User> 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<User> findByUsernameOrEmail(String principal) {
|
||||
return userRepository.findByUsernameOrEmail(principal);
|
||||
}
|
||||
|
||||
@@ -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<String> allowedUsers() {
|
||||
return gitHubWebhookProperties.allowedUsers();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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<String> allowedUsers
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
@@ -9,6 +9,11 @@ import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Spring Data JPA repository for {@link Firearm} entity operations.
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
@Repository
|
||||
public interface FirearmRepository extends JpaRepository<Firearm, Long> {
|
||||
|
||||
|
||||
@@ -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<Modification, Long> {
|
||||
|
||||
@@ -27,6 +33,14 @@ public interface ModificationRepository extends JpaRepository<Modification, Long
|
||||
@NonNull
|
||||
Optional<Modification> 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<Modification, Long
|
||||
nativeQuery = true)
|
||||
Page<Modification> 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<String> findAllTags(@Param("firearmId") Long firearmId);
|
||||
}
|
||||
|
||||
@@ -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<UserCredential, UserCredentialId> {
|
||||
|
||||
/**
|
||||
* 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<UserCredential,
|
||||
""")
|
||||
List<UserCredential> 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<UserCredential,
|
||||
""")
|
||||
Optional<UserCredential> 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<UserCredential,
|
||||
""")
|
||||
void deleteByUserIdAndProvider(@Param("userId") Long userId, @Param("provider") String provider);
|
||||
|
||||
/**
|
||||
* Delete all credentials for a given user.
|
||||
*
|
||||
* @param userId the user ID
|
||||
*/
|
||||
@Modifying
|
||||
@Query("""
|
||||
delete from UserCredential uc
|
||||
|
||||
@@ -10,6 +10,11 @@ import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Spring Data JPA repository for {@link User} entity operations.
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
|
||||
@@ -28,6 +33,12 @@ public interface UserRepository extends JpaRepository<User, Long> {
|
||||
|
||||
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
|
||||
|
||||
@@ -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()")
|
||||
|
||||
+5
@@ -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;
|
||||
|
||||
+17
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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()));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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<FirearmResponse> pageQuery(FirearmType type, Pageable pageable) {
|
||||
Page<Firearm> 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));
|
||||
|
||||
@@ -18,7 +18,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;
|
||||
@@ -26,6 +25,11 @@ import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Service handling modification business logic including CRUD, batch operations, and tag filtering.
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
@Service
|
||||
public class ModificationService {
|
||||
|
||||
@@ -46,7 +50,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<ModificationResponse> pageQuery(Long firearmId, List<String> tags, Pageable pageable) {
|
||||
String tagsJson = null;
|
||||
if (tags != null && !tags.isEmpty()) {
|
||||
@@ -67,29 +78,55 @@ 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<String> 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) {
|
||||
return modificationManager.create(request);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
/**
|
||||
* Creates multiple modifications in a single batch operation.
|
||||
*
|
||||
* @param requests list of modification creation requests
|
||||
* @return list of created modification responses
|
||||
*/
|
||||
public List<ModificationResponse> batchCreate(List<ModificationRequest> requests) {
|
||||
return modificationManager.batchCreate(requests);
|
||||
}
|
||||
|
||||
@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));
|
||||
@@ -108,14 +145,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<Long> ids) {
|
||||
Set<Long> uniqueIds = new LinkedHashSet<>(ids);
|
||||
List<Modification> modifications = modificationRepository.findAllById(uniqueIds);
|
||||
|
||||
@@ -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<User> 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<UserCredential> 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);
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
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;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.onixbyte.deltaforceguide.exeption.BizException;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import java.time.Duration;
|
||||
@@ -25,7 +24,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<String, Object> redisTemplate;
|
||||
@@ -74,8 +73,7 @@ public class WebhookService {
|
||||
return;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
var data = (Map<String, Object>) yaml.load(parsedYaml);
|
||||
var data = yaml.<Map<String, Object>>load(parsedYaml);
|
||||
if (data == null) {
|
||||
log.warn("Empty YAML block in issue #{}", issue.number());
|
||||
return;
|
||||
@@ -112,6 +110,10 @@ public class WebhookService {
|
||||
Long firearmId = modificationManager.resolveFirearmId(
|
||||
toLong(data.get("firearmId")),
|
||||
(String) data.get("firearmName"));
|
||||
if (firearmId == null) {
|
||||
throw new BizException(HttpStatus.BAD_REQUEST,
|
||||
"YAML must contain firearmId or firearmName");
|
||||
}
|
||||
String name = (String) data.get("name");
|
||||
String code = (String) data.get("code");
|
||||
List<String> tags = toStringList(data.get("tags"));
|
||||
@@ -168,9 +170,9 @@ public class WebhookService {
|
||||
}
|
||||
|
||||
private boolean isAllowedSender(
|
||||
com.onixbyte.deltaforceguide.domain.dto.GitHubWebhookSender sender
|
||||
GitHubWebhookSender sender
|
||||
) {
|
||||
var allowedUsers = webhookManager.github().allowedUsers();
|
||||
var allowedUsers = webhookManager.allowedUsers();
|
||||
if (allowedUsers == null || allowedUsers.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
@@ -180,7 +182,7 @@ public class WebhookService {
|
||||
return allowedUsers.contains(sender.login());
|
||||
}
|
||||
|
||||
private boolean hasTriggerLabel(List<com.onixbyte.deltaforceguide.domain.dto.GitHubWebhookLabel> labels) {
|
||||
private boolean hasTriggerLabel(List<GitHubWebhookLabel> labels) {
|
||||
if (labels == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user