Compare commits
17 Commits
| 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 artefactVersion: String by project
|
||||||
|
val buildChannel: String by project
|
||||||
|
val vendor: String by project
|
||||||
|
|
||||||
group = "com.onixbyte.helix"
|
group = "com.onixbyte.helix"
|
||||||
version = artefactVersion
|
version = artefactVersion
|
||||||
@@ -61,6 +63,16 @@ dependencies {
|
|||||||
testRuntimeOnly(libs.junit.launcher)
|
testRuntimeOnly(libs.junit.launcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.processResources {
|
||||||
|
filesMatching("application.yaml") {
|
||||||
|
expand(
|
||||||
|
"appVersion" to artefactVersion,
|
||||||
|
"channel" to buildChannel,
|
||||||
|
"vendor" to vendor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ package com.onixbyte.deltaforceguide;
|
|||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for the Delta Force Guide Server application.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class DeltaForceGuideApplication {
|
public class DeltaForceGuideApplication {
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ import org.springframework.stereotype.Component;
|
|||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client for generating and verifying JSON Web Tokens using the Auth0 java-jwt library.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class TokenClient {
|
public class TokenClient {
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,21 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for CORS (Cross-Origin Resource Sharing) policies.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableConfigurationProperties({CorsProperties.class})
|
@EnableConfigurationProperties({CorsProperties.class})
|
||||||
public class CorsConfig {
|
public class CorsConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the CORS configuration source with properties from configuration.
|
||||||
|
*
|
||||||
|
* @param properties the CORS configuration properties
|
||||||
|
* @return the CORS configuration source
|
||||||
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public CorsConfigurationSource corsConfigurationSource(
|
public CorsConfigurationSource corsConfigurationSource(
|
||||||
CorsProperties properties
|
CorsProperties properties
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
public class FilterConfig {
|
public class FilterConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public FilterRegistrationBean<WebhookFilter> webhookFilter(WebhookFilter webhookFilter) {
|
public FilterRegistrationBean<WebhookFilter> webhookFilterBean(WebhookFilter webhookFilter) {
|
||||||
var registrationBean = new FilterRegistrationBean<WebhookFilter>();
|
var registrationBean = new FilterRegistrationBean<WebhookFilter>();
|
||||||
|
|
||||||
registrationBean.setFilter(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.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for Jackson JSON serialisation and deserialisation settings.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
public class JacksonConfig {
|
public class JacksonConfig {
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ package com.onixbyte.deltaforceguide.config;
|
|||||||
import org.mybatis.spring.annotation.MapperScan;
|
import org.mybatis.spring.annotation.MapperScan;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for MyBatis SQL mapping framework integration.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
@MapperScan(basePackages = {"com.onixbyte.deltaforceguide.mapper"})
|
@MapperScan(basePackages = {"com.onixbyte.deltaforceguide.mapper"})
|
||||||
public class MyBatisConfig {
|
public class MyBatisConfig {
|
||||||
|
|||||||
@@ -23,12 +23,23 @@ 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Security configuration defining authentication, authorisation, and filter chains.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@EnableMethodSecurity
|
@EnableMethodSecurity
|
||||||
@EnableConfigurationProperties({TokenProperties.class, CookieProperties.class})
|
@EnableConfigurationProperties({TokenProperties.class, CookieProperties.class})
|
||||||
public class SecurityConfig {
|
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
|
@Bean
|
||||||
public SecurityFilterChain securityFilterChain(
|
public SecurityFilterChain securityFilterChain(
|
||||||
HttpSecurity httpSecurity,
|
HttpSecurity httpSecurity,
|
||||||
@@ -48,11 +59,20 @@ public class SecurityConfig {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the BCrypt password encoder for credential hashing.
|
||||||
|
* @return the BCrypt password encoder
|
||||||
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
return new BCryptPasswordEncoder();
|
return new BCryptPasswordEncoder();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the authentication manager for the security configuration.
|
||||||
|
*
|
||||||
|
* @return the authentication manager
|
||||||
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public AuthenticationManager authenticationManager(
|
public AuthenticationManager authenticationManager(
|
||||||
UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider
|
UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ package com.onixbyte.deltaforceguide.config;
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for Spring Data JPA auditing and repository settings.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableJpaRepositories(basePackages = {"com.onixbyte.deltaforceguide.repository"})
|
@EnableJpaRepositories(basePackages = {"com.onixbyte.deltaforceguide.repository"})
|
||||||
public class SpringDataConfig {
|
public class SpringDataConfig {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.onixbyte.deltaforceguide.config;
|
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.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableConfigurationProperties(WebhookProperties.class)
|
@EnableConfigurationProperties({GitHubWebhookProperties.class})
|
||||||
public class WebhookConfig {
|
public class WebhookConfig {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,13 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST controller for user authentication endpoints (login, logout).
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Tag(name = "用户鉴权", description = "处理用户登录与退出功能")
|
@Tag(name = "用户鉴权", description = "处理用户登录与退出功能")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/auth")
|
@RequestMapping("/auth")
|
||||||
@@ -38,12 +44,14 @@ public class AuthController {
|
|||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public ResponseEntity<UserResponse> login(@Validated @RequestBody LoginRequest request) {
|
public ResponseEntity<UserResponse> login(@Validated @RequestBody LoginRequest request) {
|
||||||
var user = authService.login(request);
|
var user = authService.login(request);
|
||||||
|
var currentTime = LocalDateTime.now();
|
||||||
var accessToken = tokenClient.generateToken(user);
|
var accessToken = tokenClient.generateToken(user);
|
||||||
var accessTokenCookie = cookieService.buildCookie(CookieName.ACCESS_TOKEN, accessToken);
|
var accessTokenCookie = cookieService.buildCookie(CookieName.ACCESS_TOKEN, accessToken);
|
||||||
|
var cookieMaxAge = accessTokenCookie.getMaxAge();
|
||||||
|
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString())
|
.header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString())
|
||||||
.body(UserResponse.from(user));
|
.body(UserResponse.from(user, currentTime.plus(cookieMaxAge)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresAuth
|
@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.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST controller for retrieving daily-generated passwords.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Tag(name = "每日密码", description = "获取每日密码信息")
|
@Tag(name = "每日密码", description = "获取每日密码信息")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/daily-passwords")
|
@RequestMapping("/daily-passwords")
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ import org.springframework.data.domain.Sort;
|
|||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST controller for firearm CRUD operations.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Tag(name = "武器管理", description = "与武器有关的操作")
|
@Tag(name = "武器管理", description = "与武器有关的操作")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/firearms")
|
@RequestMapping("/firearms")
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import org.springframework.http.ResponseEntity;
|
|||||||
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global exception handler that translates exceptions into standard error responses.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
public class GlobalExceptionHandler {
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST controller for modification CRUD operations, including batch creation and deletion.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Tag(name = "改装管理", description = "对枪械改装的管理")
|
@Tag(name = "改装管理", description = "对枪械改装的管理")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/modifications")
|
@RequestMapping("/modifications")
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ import com.onixbyte.deltaforceguide.service.ModificationService;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST controller for retrieving available modification tags.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Tag(name = "标签管理", description = "管理标签信息")
|
@Tag(name = "标签管理", description = "管理标签信息")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/tags")
|
@RequestMapping("/tags")
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import com.onixbyte.deltaforceguide.enumeration.FirearmType;
|
|||||||
import jakarta.persistence.AttributeConverter;
|
import jakarta.persistence.AttributeConverter;
|
||||||
import jakarta.persistence.Converter;
|
import jakarta.persistence.Converter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JPA attribute converter that maps {@link FirearmType} enum to/from its integer database representation.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Converter
|
@Converter
|
||||||
public class FirearmTypeConverter implements AttributeConverter<FirearmType, Integer> {
|
public class FirearmTypeConverter implements AttributeConverter<FirearmType, Integer> {
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import jakarta.validation.constraints.NotBlank;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request DTO for creating or updating an accessory attached to a modification.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
public record AccessoryRequest(
|
public record AccessoryRequest(
|
||||||
@NotBlank(message = "插槽名称不能为空")
|
@NotBlank(message = "插槽名称不能为空")
|
||||||
String slotName,
|
String slotName,
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import com.onixbyte.deltaforceguide.domain.entity.Accessory;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response DTO for an accessory attached to a modification.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
public record AccessoryResponse(
|
public record AccessoryResponse(
|
||||||
String slotName,
|
String slotName,
|
||||||
String accessoryName,
|
String accessoryName,
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
package com.onixbyte.deltaforceguide.domain.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO representing a single daily-generated password for a map.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
public record DailyPassword(
|
public record DailyPassword(
|
||||||
String mapName,
|
String mapName,
|
||||||
String password
|
String password
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ package com.onixbyte.deltaforceguide.domain.dto;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO containing daily password data including update information and password list.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
public record DailyPasswordData(
|
public record DailyPasswordData(
|
||||||
String updateDate,
|
String updateDate,
|
||||||
Integer totalCount,
|
Integer totalCount,
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
package com.onixbyte.deltaforceguide.domain.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO holding metadata about the daily password source and update tracking.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
public record DailyPasswordMetadata(
|
public record DailyPasswordMetadata(
|
||||||
String version,
|
String version,
|
||||||
String author
|
String author
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
package com.onixbyte.deltaforceguide.domain.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response DTO wrapping daily password data with metadata.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
public record DailyPasswordResponse(
|
public record DailyPasswordResponse(
|
||||||
String status,
|
String status,
|
||||||
String message,
|
String message,
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
package com.onixbyte.deltaforceguide.domain.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard error response body returned on API failures.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
public record ErrorResponse(
|
public record ErrorResponse(
|
||||||
String message
|
String message
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ package com.onixbyte.deltaforceguide.domain.dto;
|
|||||||
|
|
||||||
import com.onixbyte.deltaforceguide.enumeration.FirearmType;
|
import com.onixbyte.deltaforceguide.enumeration.FirearmType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request DTO for creating or updating a firearm.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
public record FirearmRequest(
|
public record FirearmRequest(
|
||||||
String name,
|
String name,
|
||||||
FirearmType type,
|
FirearmType type,
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ package com.onixbyte.deltaforceguide.domain.dto;
|
|||||||
import com.onixbyte.deltaforceguide.domain.entity.Firearm;
|
import com.onixbyte.deltaforceguide.domain.entity.Firearm;
|
||||||
import com.onixbyte.deltaforceguide.enumeration.FirearmType;
|
import com.onixbyte.deltaforceguide.enumeration.FirearmType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response DTO for a firearm record, including associated modifications.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
public record FirearmResponse(
|
public record FirearmResponse(
|
||||||
Long id,
|
Long id,
|
||||||
String name,
|
String name,
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ package com.onixbyte.deltaforceguide.domain.dto;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login request containing principle (username/email) and credential (password).
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Schema(description = "登录请求")
|
@Schema(description = "登录请求")
|
||||||
public record LoginRequest(
|
public record LoginRequest(
|
||||||
@NotBlank(message = "登录名称不能为空")
|
@NotBlank(message = "登录名称不能为空")
|
||||||
|
|||||||
+5
@@ -5,6 +5,11 @@ import jakarta.validation.constraints.NotEmpty;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request DTO for batch creation of modifications.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
public record ModificationBatchCreateRequest(
|
public record ModificationBatchCreateRequest(
|
||||||
@NotEmpty(message = "批量创建列表不能为空")
|
@NotEmpty(message = "批量创建列表不能为空")
|
||||||
List<@Valid ModificationRequest> modifications
|
List<@Valid ModificationRequest> modifications
|
||||||
|
|||||||
+5
@@ -5,6 +5,11 @@ import jakarta.validation.constraints.Positive;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request DTO for batch deletion of modifications by ID.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
public record ModificationBatchDeleteRequest(
|
public record ModificationBatchDeleteRequest(
|
||||||
@NotEmpty(message = "批量删除ID列表不能为空")
|
@NotEmpty(message = "批量删除ID列表不能为空")
|
||||||
List<@Positive(message = "ID必须为正数") Long> ids
|
List<@Positive(message = "ID必须为正数") Long> ids
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ import jakarta.validation.constraints.Positive;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request DTO for creating or updating a modification.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
public record ModificationRequest(
|
public record ModificationRequest(
|
||||||
@NotNull(message = "武器ID不能为空")
|
@NotNull(message = "武器ID不能为空")
|
||||||
@Positive(message = "武器ID必须为正数")
|
@Positive(message = "武器ID必须为正数")
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import com.onixbyte.deltaforceguide.domain.entity.Modification;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response DTO for a modification record including accessories and tags.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
public record ModificationResponse(
|
public record ModificationResponse(
|
||||||
Long id,
|
Long id,
|
||||||
Long firearmId,
|
Long firearmId,
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import org.springframework.data.domain.Page;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic paginated response wrapper for list endpoints.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
public record PageResponse<T>(
|
public record PageResponse<T>(
|
||||||
List<T> items,
|
List<T> items,
|
||||||
int page,
|
int page,
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ package com.onixbyte.deltaforceguide.domain.dto;
|
|||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request DTO for a tuning adjustment on an accessory.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
public record TuningRequest(
|
public record TuningRequest(
|
||||||
@NotBlank(message = "调校项名称不能为空")
|
@NotBlank(message = "调校项名称不能为空")
|
||||||
String tuningName,
|
String tuningName,
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ package com.onixbyte.deltaforceguide.domain.dto;
|
|||||||
|
|
||||||
import com.onixbyte.deltaforceguide.domain.entity.Tuning;
|
import com.onixbyte.deltaforceguide.domain.entity.Tuning;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response DTO for a tuning adjustment on an accessory.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
public record TuningResponse(
|
public record TuningResponse(
|
||||||
String tuningName,
|
String tuningName,
|
||||||
Double tuningValue
|
Double tuningValue
|
||||||
|
|||||||
@@ -2,16 +2,25 @@ package com.onixbyte.deltaforceguide.domain.dto;
|
|||||||
|
|
||||||
import com.onixbyte.deltaforceguide.domain.entity.User;
|
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(
|
public record UserResponse(
|
||||||
Long id,
|
Long id,
|
||||||
String username,
|
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(
|
return new UserResponse(
|
||||||
user.getId(),
|
user.getId(),
|
||||||
user.getUsername(),
|
user.getUsername(),
|
||||||
user.getEmail()
|
user.getEmail(),
|
||||||
|
expiration
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity representing an accessory attached to a modification, stored as JSONB.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
public class Accessory {
|
public class Accessory {
|
||||||
|
|
||||||
private String slotName;
|
private String slotName;
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ import jakarta.persistence.Table;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity representing a firearm in the Delta Force game.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "firearm")
|
@Table(name = "firearm")
|
||||||
public class Firearm {
|
public class Firearm {
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import org.hibernate.annotations.Type;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity representing a firearm modification or build configuration.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(
|
@Table(
|
||||||
name = "modification",
|
name = "modification",
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ package com.onixbyte.deltaforceguide.domain.entity;
|
|||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity representing a tuning adjustment for an accessory, stored as JSONB within Accessory.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
public class Tuning {
|
public class Tuning {
|
||||||
|
|
||||||
private String tuningName;
|
private String tuningName;
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ import jakarta.persistence.Table;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity representing an application user with authentication credentials.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "app_user")
|
@Table(name = "app_user")
|
||||||
public class User {
|
public class User {
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ import jakarta.persistence.MapsId;
|
|||||||
import jakarta.persistence.ManyToOne;
|
import jakarta.persistence.ManyToOne;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity representing a user credential linked to an authentication provider.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "app_user_credential")
|
@Table(name = "app_user_credential")
|
||||||
public class UserCredential {
|
public class UserCredential {
|
||||||
@@ -28,7 +33,7 @@ public class UserCredential {
|
|||||||
@JoinColumn(name = "user_id", nullable = false, foreignKey = @ForeignKey(name = "fk_user_credential_user"))
|
@JoinColumn(name = "user_id", nullable = false, foreignKey = @ForeignKey(name = "fk_user_credential_user"))
|
||||||
private User user;
|
private User user;
|
||||||
|
|
||||||
@Column(name = "credential", nullable = false, length = 255)
|
@Column(name = "credential", nullable = false)
|
||||||
private String credential;
|
private String credential;
|
||||||
|
|
||||||
public UserCredentialId getId() {
|
public UserCredentialId getId() {
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ import jakarta.persistence.Embeddable;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composite key for the UserCredential entity, combining user ID and provider.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Embeddable
|
@Embeddable
|
||||||
public class UserCredentialId implements Serializable {
|
public class UserCredentialId implements Serializable {
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
package com.onixbyte.deltaforceguide.enumeration;
|
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 {
|
public enum FirearmType {
|
||||||
|
|
||||||
RIFLE(0),
|
RIFLE(0),
|
||||||
@@ -21,6 +27,13 @@ public enum FirearmType {
|
|||||||
return code;
|
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) {
|
public static FirearmType fromCode(Integer code) {
|
||||||
if (code == null) {
|
if (code == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ package com.onixbyte.deltaforceguide.exeption;
|
|||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
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 {
|
public class BizException extends RuntimeException {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
@@ -29,6 +28,11 @@ import java.time.Instant;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Servlet filter that extracts and validates JWT tokens from httpOnly cookies for each request.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class TokenAuthenticationFilter extends OncePerRequestFilter {
|
public class TokenAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
@@ -52,6 +56,13 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
this.handlerExceptionResolver = handlerExceptionResolver;
|
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
|
@Override
|
||||||
protected void doFilterInternal(
|
protected void doFilterInternal(
|
||||||
@NonNull HttpServletRequest request,
|
@NonNull HttpServletRequest request,
|
||||||
|
|||||||
+2
-4
@@ -14,8 +14,6 @@ import org.springframework.http.HttpStatus;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
import javax.crypto.Mac;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
@@ -50,7 +48,7 @@ public class GitHubWebhookInterceptor implements HandlerInterceptor {
|
|||||||
"Request body is not readable");
|
"Request body is not readable");
|
||||||
}
|
}
|
||||||
|
|
||||||
var secret = webhookManager.github().secret();
|
var secret = webhookManager.secret();
|
||||||
if (secret == null || secret.isBlank()) {
|
if (secret == null || secret.isBlank()) {
|
||||||
log.debug("No GitHub webhook secret configured, skipping signature verification");
|
log.debug("No GitHub webhook secret configured, skipping signature verification");
|
||||||
return true;
|
return true;
|
||||||
@@ -66,7 +64,7 @@ public class GitHubWebhookInterceptor implements HandlerInterceptor {
|
|||||||
|
|
||||||
var body = req.getBodyString();
|
var body = req.getBodyString();
|
||||||
try {
|
try {
|
||||||
var computed = "sha256=" + CryptoUtil.hmacSha256(secret, body);
|
var computed = "sha256=" + CryptoUtil.hmacSha256(body, secret);
|
||||||
|
|
||||||
if (!MessageDigest.isEqual(
|
if (!MessageDigest.isEqual(
|
||||||
computed.getBytes(StandardCharsets.UTF_8),
|
computed.getBytes(StandardCharsets.UTF_8),
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public class TrafficInterceptor implements HandlerInterceptor {
|
|||||||
var contentLength = request.getContentLength();
|
var contentLength = request.getContentLength();
|
||||||
var userAgent = request.getHeader("User-Agent");
|
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);
|
method, uri, query, ip, contentType, contentLength, userAgent);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ public class AppManager {
|
|||||||
* @return the version string of this application
|
* @return the version string of this application
|
||||||
*/
|
*/
|
||||||
public String getVersion() {
|
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;
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manager providing cookie construction operations with configurable properties.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class CookieManager {
|
public class CookieManager {
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ import java.time.Duration;
|
|||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manager for daily password data access and caching coordination.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class DailyPasswordManager {
|
public class DailyPasswordManager {
|
||||||
|
|
||||||
@@ -49,6 +54,10 @@ public class DailyPasswordManager {
|
|||||||
this.redisTemplate = redisTemplate;
|
this.redisTemplate = redisTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the daily password from cache or generates a new one.
|
||||||
|
* @return the daily password response
|
||||||
|
*/
|
||||||
public DailyPasswordResponse getDailyPassword() {
|
public DailyPasswordResponse getDailyPassword() {
|
||||||
var key = CACHE_KEY_PREFIX + LocalDate.now();
|
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.domain.entity.UserCredential;
|
||||||
import com.onixbyte.deltaforceguide.repository.UserCredentialRepository;
|
import com.onixbyte.deltaforceguide.repository.UserCredentialRepository;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manager for user credential persistence and authentication data access.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class UserCredentialManager {
|
public class UserCredentialManager {
|
||||||
|
|
||||||
@@ -17,27 +21,52 @@ public class UserCredentialManager {
|
|||||||
this.userCredentialRepository = userCredentialRepository;
|
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) {
|
public List<UserCredential> findAllByUserId(Long userId) {
|
||||||
return userCredentialRepository.findAllByUserId(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) {
|
public Optional<UserCredential> findByUserIdAndProvider(Long userId, String provider) {
|
||||||
return userCredentialRepository.findByUserIdAndProvider(userId, 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) {
|
public UserCredential save(UserCredential userCredential) {
|
||||||
return userCredentialRepository.save(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) {
|
public void deleteByUserIdAndProvider(Long userId, String provider) {
|
||||||
userCredentialRepository.deleteByUserIdAndProvider(userId, provider);
|
userCredentialRepository.deleteByUserIdAndProvider(userId, provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
/**
|
||||||
|
* Deletes all credentials belonging to a user.
|
||||||
|
*
|
||||||
|
* @param userId the user ID
|
||||||
|
*/
|
||||||
public void deleteAllByUserId(Long userId) {
|
public void deleteAllByUserId(Long userId) {
|
||||||
userCredentialRepository.deleteAllByUserId(userId);
|
userCredentialRepository.deleteAllByUserId(userId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manager for user entity persistence and query operations.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class UserManager {
|
public class UserManager {
|
||||||
|
|
||||||
@@ -17,17 +22,30 @@ public class UserManager {
|
|||||||
this.userRepository = userRepository;
|
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) {
|
public Optional<User> findById(Long id) {
|
||||||
return userRepository.findById(id);
|
return userRepository.findById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
/**
|
||||||
|
* Retrieves all registered users.
|
||||||
|
* @return list of all users
|
||||||
|
*/
|
||||||
public List<User> findAll() {
|
public List<User> findAll() {
|
||||||
return userRepository.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) {
|
public Optional<User> findByUsername(String username) {
|
||||||
return userRepository.findByUsername(username);
|
return userRepository.findByUsername(username);
|
||||||
}
|
}
|
||||||
@@ -37,16 +55,31 @@ public class UserManager {
|
|||||||
return userRepository.findByEmail(email);
|
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) {
|
public User save(User user) {
|
||||||
return userRepository.save(user);
|
return userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
/**
|
||||||
|
* Deletes a user by their ID.
|
||||||
|
*
|
||||||
|
* @param id the user ID to delete
|
||||||
|
*/
|
||||||
public void deleteById(Long id) {
|
public void deleteById(Long id) {
|
||||||
userRepository.deleteById(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) {
|
public Optional<User> findByUsernameOrEmail(String principal) {
|
||||||
return userRepository.findByUsernameOrEmail(principal);
|
return userRepository.findByUsernameOrEmail(principal);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
package com.onixbyte.deltaforceguide.manager;
|
package com.onixbyte.deltaforceguide.manager;
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.properties.GitHubWebhookProperties;
|
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 org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class WebhookManager {
|
public class WebhookManager {
|
||||||
|
|
||||||
private final WebhookProperties webhookProperties;
|
private final GitHubWebhookProperties gitHubWebhookProperties;
|
||||||
|
|
||||||
@Autowired
|
public WebhookManager(GitHubWebhookProperties gitHubWebhookProperties) {
|
||||||
public WebhookManager(WebhookProperties webhookProperties) {
|
this.gitHubWebhookProperties = gitHubWebhookProperties;
|
||||||
this.webhookProperties = webhookProperties;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public GitHubWebhookProperties github() {
|
public String secret() {
|
||||||
return webhookProperties.github();
|
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")
|
@ConfigurationProperties(prefix = "app.common")
|
||||||
public record AppProperties(
|
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;
|
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")
|
@ConfigurationProperties(prefix = "app.cookie")
|
||||||
public record CookieProperties(
|
public record CookieProperties(
|
||||||
@DefaultValue("true") Boolean httpOnly,
|
@DefaultValue("true") Boolean httpOnly,
|
||||||
|
|||||||
@@ -6,6 +6,18 @@ import org.springframework.http.HttpMethod;
|
|||||||
|
|
||||||
import java.time.Duration;
|
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")
|
@ConfigurationProperties(prefix = "app.cors")
|
||||||
public record CorsProperties(
|
public record CorsProperties(
|
||||||
@DefaultValue({"Content-Type", "Authorization"})
|
@DefaultValue({"Content-Type", "Authorization"})
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package com.onixbyte.deltaforceguide.properties;
|
package com.onixbyte.deltaforceguide.properties;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ConfigurationProperties(prefix = "app.webhook.github")
|
||||||
public record GitHubWebhookProperties(
|
public record GitHubWebhookProperties(
|
||||||
String secret,
|
String secret,
|
||||||
List<String> allowedUsers
|
List<String> allowedUsers
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
|||||||
|
|
||||||
import java.time.Duration;
|
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")
|
@ConfigurationProperties(prefix = "app.jwt")
|
||||||
public record TokenProperties(
|
public record TokenProperties(
|
||||||
String issuer,
|
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;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Data JPA repository for {@link Firearm} entity operations.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Repository
|
@Repository
|
||||||
public interface FirearmRepository extends JpaRepository<Firearm, Long> {
|
public interface FirearmRepository extends JpaRepository<Firearm, Long> {
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ import org.springframework.stereotype.Repository;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Data JPA repository for {@link Modification} entity operations,
|
||||||
|
* including native JSONB tag filtering for Postgres.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Repository
|
@Repository
|
||||||
public interface ModificationRepository extends JpaRepository<Modification, Long> {
|
public interface ModificationRepository extends JpaRepository<Modification, Long> {
|
||||||
|
|
||||||
@@ -27,6 +33,14 @@ public interface ModificationRepository extends JpaRepository<Modification, Long
|
|||||||
@NonNull
|
@NonNull
|
||||||
Optional<Modification> findById(@NonNull Long id);
|
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 = """
|
@Query(value = """
|
||||||
SELECT * FROM modification m
|
SELECT * FROM modification m
|
||||||
WHERE (:firearmId IS NULL OR m.firearm_id = :firearmId)
|
WHERE (:firearmId IS NULL OR m.firearm_id = :firearmId)
|
||||||
@@ -40,6 +54,12 @@ public interface ModificationRepository extends JpaRepository<Modification, Long
|
|||||||
nativeQuery = true)
|
nativeQuery = true)
|
||||||
Page<Modification> pageQueryByFirearmAndTags(@Param("firearmId") Long firearmId, @Param("tagsJson") String tagsJson, Pageable pageable);
|
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)
|
@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);
|
List<String> findAllTags(@Param("firearmId") Long firearmId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,20 @@ import org.springframework.stereotype.Repository;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Data JPA repository for {@link UserCredential} entity operations.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Repository
|
@Repository
|
||||||
public interface UserCredentialRepository extends JpaRepository<UserCredential, UserCredentialId> {
|
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"})
|
@EntityGraph(attributePaths = {"user"})
|
||||||
@Query("""
|
@Query("""
|
||||||
select uc
|
select uc
|
||||||
@@ -23,6 +34,13 @@ public interface UserCredentialRepository extends JpaRepository<UserCredential,
|
|||||||
""")
|
""")
|
||||||
List<UserCredential> findAllByUserId(@Param("userId") Long userId);
|
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"})
|
@EntityGraph(attributePaths = {"user"})
|
||||||
@Query("""
|
@Query("""
|
||||||
select uc
|
select uc
|
||||||
@@ -32,6 +50,12 @@ public interface UserCredentialRepository extends JpaRepository<UserCredential,
|
|||||||
""")
|
""")
|
||||||
Optional<UserCredential> findByUserIdAndProvider(@Param("userId") Long userId, @Param("provider") String provider);
|
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
|
@Modifying
|
||||||
@Query("""
|
@Query("""
|
||||||
delete from UserCredential uc
|
delete from UserCredential uc
|
||||||
@@ -40,6 +64,11 @@ public interface UserCredentialRepository extends JpaRepository<UserCredential,
|
|||||||
""")
|
""")
|
||||||
void deleteByUserIdAndProvider(@Param("userId") Long userId, @Param("provider") String provider);
|
void deleteByUserIdAndProvider(@Param("userId") Long userId, @Param("provider") String provider);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all credentials for a given user.
|
||||||
|
*
|
||||||
|
* @param userId the user ID
|
||||||
|
*/
|
||||||
@Modifying
|
@Modifying
|
||||||
@Query("""
|
@Query("""
|
||||||
delete from UserCredential uc
|
delete from UserCredential uc
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ import org.springframework.stereotype.Repository;
|
|||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Data JPA repository for {@link User} entity operations.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Repository
|
@Repository
|
||||||
public interface UserRepository extends JpaRepository<User, Long> {
|
public interface UserRepository extends JpaRepository<User, Long> {
|
||||||
|
|
||||||
@@ -28,6 +33,12 @@ public interface UserRepository extends JpaRepository<User, Long> {
|
|||||||
|
|
||||||
boolean existsByEmail(String email);
|
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"})
|
@EntityGraph(attributePaths = {"credentials"})
|
||||||
@Query("""
|
@Query("""
|
||||||
select u
|
select u
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import java.lang.annotation.Retention;
|
|||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation to mark controller endpoints that require authentication.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@PreAuthorize("isAuthenticated()")
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
|||||||
+5
@@ -8,6 +8,11 @@ import org.springframework.security.core.GrantedAuthority;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom authentication token for username/password-based login flows.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
public class UsernamePasswordAuthentication implements Authentication, CredentialsContainer {
|
public class UsernamePasswordAuthentication implements Authentication, CredentialsContainer {
|
||||||
private final String username;
|
private final String username;
|
||||||
private String password;
|
private String password;
|
||||||
|
|||||||
+17
@@ -17,6 +17,11 @@ import org.springframework.security.core.AuthenticationException;
|
|||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication provider that validates username/password credentials against stored BCrypt hashes.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class UsernamePasswordAuthenticationProvider implements AuthenticationProvider {
|
public class UsernamePasswordAuthenticationProvider implements AuthenticationProvider {
|
||||||
|
|
||||||
@@ -36,6 +41,12 @@ public class UsernamePasswordAuthenticationProvider implements AuthenticationPro
|
|||||||
this.userCredentialRepository = userCredentialRepository;
|
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
|
@Override
|
||||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||||
if (!(authentication instanceof UsernamePasswordAuthentication usernamePasswordAuthentication)) {
|
if (!(authentication instanceof UsernamePasswordAuthentication usernamePasswordAuthentication)) {
|
||||||
@@ -75,6 +86,12 @@ public class UsernamePasswordAuthenticationProvider implements AuthenticationPro
|
|||||||
return usernamePasswordAuthentication;
|
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
|
@Override
|
||||||
public boolean supports(Class<?> authentication) {
|
public boolean supports(Class<?> authentication) {
|
||||||
return UsernamePasswordAuthentication.class.isAssignableFrom(authentication);
|
return UsernamePasswordAuthentication.class.isAssignableFrom(authentication);
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ import org.springframework.http.HttpStatus;
|
|||||||
import org.springframework.security.authentication.AuthenticationManager;
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.Objects;
|
/**
|
||||||
import java.util.Optional;
|
* Service handling user authentication, login, and session management.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class AuthService {
|
public class AuthService {
|
||||||
|
|
||||||
@@ -23,6 +25,16 @@ public class AuthService {
|
|||||||
this.authenticationManager = authenticationManager;
|
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) {
|
public User login(LoginRequest request) {
|
||||||
var _authentication = authenticationManager.authenticate(UsernamePasswordAuthentication
|
var _authentication = authenticationManager.authenticate(UsernamePasswordAuthentication
|
||||||
.unauthenticated(request.principle(), request.credential()));
|
.unauthenticated(request.principle(), request.credential()));
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import org.springframework.stereotype.Service;
|
|||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for building HTTP cookies with configurable properties.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class CookieService {
|
public class CookieService {
|
||||||
|
|
||||||
@@ -15,10 +20,25 @@ public class CookieService {
|
|||||||
this.cookieManager = cookieManager;
|
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) {
|
public ResponseCookie buildCookie(String cookieName, String value) {
|
||||||
return buildCookieInternal(cookieName, value, cookieManager.getMaxAge());
|
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) {
|
public ResponseCookie buildCookie(String cookieName, String value, Duration validDuration) {
|
||||||
return buildCookieInternal(cookieName, value, validDuration);
|
return buildCookieInternal(cookieName, value, validDuration);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import com.onixbyte.deltaforceguide.domain.dto.DailyPasswordResponse;
|
|||||||
import com.onixbyte.deltaforceguide.manager.DailyPasswordManager;
|
import com.onixbyte.deltaforceguide.manager.DailyPasswordManager;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for generating and caching daily rotation passwords.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class DailyPasswordService {
|
public class DailyPasswordService {
|
||||||
|
|
||||||
@@ -13,6 +18,10 @@ public class DailyPasswordService {
|
|||||||
this.dailyPasswordManager = dailyPasswordManager;
|
this.dailyPasswordManager = dailyPasswordManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the daily password for the current day.
|
||||||
|
* @return the daily password response
|
||||||
|
*/
|
||||||
public DailyPasswordResponse getDailyPassword() {
|
public DailyPasswordResponse getDailyPassword() {
|
||||||
return dailyPasswordManager.getDailyPassword();
|
return dailyPasswordManager.getDailyPassword();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,13 @@ import org.springframework.data.domain.Page;
|
|||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service handling firearm business logic including CRUD operations and queries.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class FirearmService {
|
public class FirearmService {
|
||||||
|
|
||||||
@@ -23,7 +27,13 @@ public class FirearmService {
|
|||||||
this.firearmRepository = firearmRepository;
|
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) {
|
public PageResponse<FirearmResponse> pageQuery(FirearmType type, Pageable pageable) {
|
||||||
Page<Firearm> page = type == null
|
Page<Firearm> page = type == null
|
||||||
? firearmRepository.findAll(pageable)
|
? firearmRepository.findAll(pageable)
|
||||||
@@ -32,13 +42,24 @@ public class FirearmService {
|
|||||||
return PageResponse.from(page.map(FirearmResponse::from));
|
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) {
|
public FirearmResponse queryById(Long id) {
|
||||||
return firearmRepository.findById(id)
|
return firearmRepository.findById(id)
|
||||||
.map(FirearmResponse::from)
|
.map(FirearmResponse::from)
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Firearm not found: " + id));
|
.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) {
|
public FirearmResponse addFirearm(FirearmRequest request) {
|
||||||
var firearm = firearmRepository.save(Firearm.builder()
|
var firearm = firearmRepository.save(Firearm.builder()
|
||||||
.name(request.name())
|
.name(request.name())
|
||||||
@@ -54,7 +75,13 @@ public class FirearmService {
|
|||||||
return FirearmResponse.from(firearm);
|
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) {
|
public FirearmResponse updateFirearm(Long id, FirearmRequest request) {
|
||||||
var firearm = firearmRepository.findById(id)
|
var firearm = firearmRepository.findById(id)
|
||||||
.orElseThrow(() -> new BizException(HttpStatus.NOT_FOUND, "Firearm not found: " + id));
|
.orElseThrow(() -> new BizException(HttpStatus.NOT_FOUND, "Firearm not found: " + id));
|
||||||
@@ -71,7 +98,11 @@ public class FirearmService {
|
|||||||
return FirearmResponse.from(firearmRepository.save(firearm));
|
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) {
|
public void deleteFirearm(Long id) {
|
||||||
Firearm firearm = firearmRepository.findById(id)
|
Firearm firearm = firearmRepository.findById(id)
|
||||||
.orElseThrow(() -> new BizException(HttpStatus.NOT_FOUND, "Firearm not found: " + 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.data.domain.Pageable;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -26,6 +25,11 @@ import java.util.LinkedHashSet;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service handling modification business logic including CRUD, batch operations, and tag filtering.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class ModificationService {
|
public class ModificationService {
|
||||||
|
|
||||||
@@ -46,7 +50,14 @@ public class ModificationService {
|
|||||||
this.objectMapper = objectMapper;
|
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) {
|
public PageResponse<ModificationResponse> pageQuery(Long firearmId, List<String> tags, Pageable pageable) {
|
||||||
String tagsJson = null;
|
String tagsJson = null;
|
||||||
if (tags != null && !tags.isEmpty()) {
|
if (tags != null && !tags.isEmpty()) {
|
||||||
@@ -67,29 +78,55 @@ public class ModificationService {
|
|||||||
return PageResponse.from(page.map(ModificationResponse::from));
|
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) {
|
public ModificationResponse queryById(Long id) {
|
||||||
return modificationRepository.findById(id)
|
return modificationRepository.findById(id)
|
||||||
.map(ModificationResponse::from)
|
.map(ModificationResponse::from)
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Modification not found: " + id));
|
.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) {
|
public List<String> findAllTags(Long firearmId) {
|
||||||
return modificationRepository.findAllTags(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) {
|
public ModificationResponse create(ModificationRequest request) {
|
||||||
return modificationManager.create(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) {
|
public List<ModificationResponse> batchCreate(List<ModificationRequest> requests) {
|
||||||
return modificationManager.batchCreate(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) {
|
public ModificationResponse update(Long id, ModificationRequest request) {
|
||||||
Modification modification = modificationRepository.findById(id)
|
Modification modification = modificationRepository.findById(id)
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Modification not found: " + id));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Modification not found: " + id));
|
||||||
@@ -108,14 +145,22 @@ public class ModificationService {
|
|||||||
return ModificationResponse.from(modificationRepository.save(modification));
|
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) {
|
public void delete(Long id) {
|
||||||
Modification modification = modificationRepository.findById(id)
|
Modification modification = modificationRepository.findById(id)
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Modification not found: " + id));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Modification not found: " + id));
|
||||||
modificationRepository.delete(modification);
|
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) {
|
public void batchDelete(List<Long> ids) {
|
||||||
Set<Long> uniqueIds = new LinkedHashSet<>(ids);
|
Set<Long> uniqueIds = new LinkedHashSet<>(ids);
|
||||||
List<Modification> modifications = modificationRepository.findAllById(uniqueIds);
|
List<Modification> modifications = modificationRepository.findAllById(uniqueIds);
|
||||||
|
|||||||
@@ -6,11 +6,15 @@ import com.onixbyte.deltaforceguide.manager.UserCredentialManager;
|
|||||||
import com.onixbyte.deltaforceguide.manager.UserManager;
|
import com.onixbyte.deltaforceguide.manager.UserManager;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for user account management and profile operations.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class UserService {
|
public class UserService {
|
||||||
|
|
||||||
@@ -22,29 +26,53 @@ public class UserService {
|
|||||||
this.userCredentialManager = userCredentialManager;
|
this.userCredentialManager = userCredentialManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
/**
|
||||||
|
* Retrieves all registered users.
|
||||||
|
*
|
||||||
|
* @return list of all users
|
||||||
|
*/
|
||||||
public List<User> findAll() {
|
public List<User> findAll() {
|
||||||
return userManager.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) {
|
public User queryById(Long id) {
|
||||||
return userManager.findById(id)
|
return userManager.findById(id)
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found: " + 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) {
|
public User queryByUsername(String username) {
|
||||||
return userManager.findByUsername(username)
|
return userManager.findByUsername(username)
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found: " + 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) {
|
public User create(User user) {
|
||||||
return userManager.save(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) {
|
public User update(User user) {
|
||||||
if (user.getId() == null || userManager.findById(user.getId()).isEmpty()) {
|
if (user.getId() == null || userManager.findById(user.getId()).isEmpty()) {
|
||||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found: " + user.getId());
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found: " + user.getId());
|
||||||
@@ -52,13 +80,24 @@ public class UserService {
|
|||||||
return userManager.save(user);
|
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) {
|
public List<UserCredential> findCredentials(Long userId) {
|
||||||
ensureUserExists(userId);
|
ensureUserExists(userId);
|
||||||
return userCredentialManager.findAllByUserId(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) {
|
public UserCredential queryCredential(Long userId, String provider) {
|
||||||
ensureUserExists(userId);
|
ensureUserExists(userId);
|
||||||
return userCredentialManager.findByUserIdAndProvider(userId, provider)
|
return userCredentialManager.findByUserIdAndProvider(userId, provider)
|
||||||
@@ -67,7 +106,14 @@ public class UserService {
|
|||||||
"User credential not found: userId=" + userId + ", provider=" + provider));
|
"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) {
|
public UserCredential upsertCredential(Long userId, String provider, String credential) {
|
||||||
User user = ensureUserExists(userId);
|
User user = ensureUserExists(userId);
|
||||||
UserCredential userCredential = userCredentialManager.findByUserIdAndProvider(userId, provider)
|
UserCredential userCredential = userCredentialManager.findByUserIdAndProvider(userId, provider)
|
||||||
@@ -78,13 +124,22 @@ public class UserService {
|
|||||||
return userCredentialManager.save(userCredential);
|
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) {
|
public void deleteCredential(Long userId, String provider) {
|
||||||
ensureUserExists(userId);
|
ensureUserExists(userId);
|
||||||
userCredentialManager.deleteByUserIdAndProvider(userId, provider);
|
userCredentialManager.deleteByUserIdAndProvider(userId, provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
/**
|
||||||
|
* Deletes a user and all associated credentials.
|
||||||
|
*
|
||||||
|
* @param id the user ID to delete
|
||||||
|
*/
|
||||||
public void deleteById(Long id) {
|
public void deleteById(Long id) {
|
||||||
ensureUserExists(id);
|
ensureUserExists(id);
|
||||||
userCredentialManager.deleteAllByUserId(id);
|
userCredentialManager.deleteAllByUserId(id);
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
package com.onixbyte.deltaforceguide.service;
|
package com.onixbyte.deltaforceguide.service;
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.domain.dto.AccessoryRequest;
|
import com.onixbyte.deltaforceguide.domain.dto.*;
|
||||||
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.ModificationManager;
|
||||||
import com.onixbyte.deltaforceguide.manager.WebhookManager;
|
import com.onixbyte.deltaforceguide.manager.WebhookManager;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import com.onixbyte.deltaforceguide.exeption.BizException;
|
||||||
import org.yaml.snakeyaml.Yaml;
|
import org.yaml.snakeyaml.Yaml;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
@@ -25,7 +24,7 @@ public class WebhookService {
|
|||||||
private static final String TRIGGER_LABEL = "weapon-mod";
|
private static final String TRIGGER_LABEL = "weapon-mod";
|
||||||
private static final Duration DEDUP_TTL = Duration.ofHours(12);
|
private static final Duration DEDUP_TTL = Duration.ofHours(12);
|
||||||
private static final Pattern YAML_FENCE =
|
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 ModificationManager modificationManager;
|
||||||
private final RedisTemplate<String, Object> redisTemplate;
|
private final RedisTemplate<String, Object> redisTemplate;
|
||||||
@@ -74,8 +73,7 @@ public class WebhookService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
var data = yaml.<Map<String, Object>>load(parsedYaml);
|
||||||
var data = (Map<String, Object>) yaml.load(parsedYaml);
|
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
log.warn("Empty YAML block in issue #{}", issue.number());
|
log.warn("Empty YAML block in issue #{}", issue.number());
|
||||||
return;
|
return;
|
||||||
@@ -112,6 +110,10 @@ public class WebhookService {
|
|||||||
Long firearmId = modificationManager.resolveFirearmId(
|
Long firearmId = modificationManager.resolveFirearmId(
|
||||||
toLong(data.get("firearmId")),
|
toLong(data.get("firearmId")),
|
||||||
(String) data.get("firearmName"));
|
(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 name = (String) data.get("name");
|
||||||
String code = (String) data.get("code");
|
String code = (String) data.get("code");
|
||||||
List<String> tags = toStringList(data.get("tags"));
|
List<String> tags = toStringList(data.get("tags"));
|
||||||
@@ -168,9 +170,9 @@ public class WebhookService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isAllowedSender(
|
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()) {
|
if (allowedUsers == null || allowedUsers.isEmpty()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -180,7 +182,7 @@ public class WebhookService {
|
|||||||
return allowedUsers.contains(sender.login());
|
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) {
|
if (labels == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
package com.onixbyte.deltaforceguide.shared;
|
package com.onixbyte.deltaforceguide.shared;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants for HTTP cookie names used for authentication tokens.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
public class CookieName {
|
public class CookieName {
|
||||||
|
|
||||||
public static final String ACCESS_TOKEN = "AccessToken";
|
public static final String ACCESS_TOKEN = "AccessToken";
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
package com.onixbyte.deltaforceguide.shared;
|
package com.onixbyte.deltaforceguide.shared;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants for supported authentication provider identifiers.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
public class CredentialProvider {
|
public class CredentialProvider {
|
||||||
|
|
||||||
public static final String LOCAL = "LOCAL";
|
public static final String LOCAL = "LOCAL";
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ import java.time.LocalDate;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.LocalTime;
|
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 class JacksonModules {
|
||||||
|
|
||||||
public static final SimpleModule DATE_TIME_MODULE = initialiseDateTimeModule();
|
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 com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
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 class JacksonRedisSerialiser {
|
||||||
|
|
||||||
public static final GenericJackson2JsonRedisSerializer INSTANCE = initialiseSerializer();
|
public static final GenericJackson2JsonRedisSerializer INSTANCE = initialiseSerializer();
|
||||||
|
|||||||
@@ -4,8 +4,19 @@ import java.time.Instant;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for date and time operations using system-default time zone.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
public class DateTimeUtil {
|
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) {
|
public static Instant asInstant(LocalDateTime ldt) {
|
||||||
return ldt.atZone(ZoneId.systemDefault())
|
return ldt.atZone(ZoneId.systemDefault())
|
||||||
.toInstant();
|
.toInstant();
|
||||||
|
|||||||
@@ -39,13 +39,13 @@ mybatis:
|
|||||||
type-handlers-package: com.onixbyte.deltaforceguide.mapper.handler
|
type-handlers-package: com.onixbyte.deltaforceguide.mapper.handler
|
||||||
mapper-locations: classpath:/mapper/*.xml
|
mapper-locations: classpath:/mapper/*.xml
|
||||||
|
|
||||||
app:
|
|
||||||
webhook:
|
|
||||||
github:
|
|
||||||
secret: ${GITHUB_WEBHOOK_SECRET:}
|
|
||||||
allowed-users: []
|
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
org.hibernate:
|
org.hibernate:
|
||||||
orm.connections.pooling: off
|
orm.connections.pooling: off
|
||||||
|
|
||||||
|
app:
|
||||||
|
common:
|
||||||
|
version: ${appVersion}
|
||||||
|
channel: ${channel}
|
||||||
|
vendor: ${vendor}
|
||||||
|
|||||||
Reference in New Issue
Block a user