Compare commits
110
Commits
27bba95250
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a17131b62e | ||
|
|
8700555ae6 | ||
|
|
ee7b96532d | ||
|
|
d8b3ad2dee | ||
|
|
a8aabaa5a1 | ||
|
|
aca203020e | ||
|
|
685646f88e | ||
|
|
704abc7389 | ||
|
|
33a7c32b1e | ||
|
|
a2b94dfda5 | ||
|
|
25da2564a4 | ||
|
|
2a3d7fdb25 | ||
|
|
099f138206 | ||
|
|
ffcd234221 | ||
|
|
812ab0ba91 | ||
|
|
3aaf7feb4f | ||
|
|
e4df329f0f | ||
|
|
00a62de5ee
|
||
|
|
2936b6aece
|
||
|
|
4d9bf64ef4
|
||
|
|
d9b74ae2e3
|
||
|
|
8f98d02044
|
||
|
|
1aeefb6733
|
||
|
|
dadd9a7221
|
||
|
|
17b919705e
|
||
|
|
0a7882c5ce
|
||
|
|
a765c705e6
|
||
|
|
3f60df9de2
|
||
|
|
471eda5353
|
||
|
|
ac877ab8f0 | ||
|
|
b131738dda | ||
|
|
4aafbaeadc
|
||
|
|
581634f33a | ||
|
|
c1d893db5f
|
||
|
|
db5056e8d5
|
||
|
|
0aeaac17a4 | ||
|
|
e4aa5b988b | ||
|
|
7343f68a4e
|
||
|
|
782974f96d | ||
|
|
3d692ece7a | ||
|
|
4dad97fcbf | ||
|
|
dde712476d | ||
|
|
b5afa8e9f3 | ||
|
|
6c7f12ddb6 | ||
|
|
05d61ee62c | ||
|
|
0681ca8b34 | ||
|
|
5b0f037567
|
||
|
|
4b9c7d3e0d | ||
|
|
b2fea5df8e | ||
|
|
a447bffa77 | ||
|
|
4d009a0195 | ||
|
|
7a0a74bfea | ||
|
|
fd537af916 | ||
|
|
e7f4fc3374 | ||
|
|
92703d4985
|
||
|
|
1dfe3f7221
|
||
|
|
86e259a500
|
||
|
|
b91855095b
|
||
|
|
1f30f70f21
|
||
|
|
13242deb6c
|
||
|
|
3b5153f386
|
||
|
|
5502643466
|
||
|
|
5790750124
|
||
|
|
26bca96575
|
||
|
|
381ccae5fd | ||
|
|
0113ece426 | ||
|
|
37adfddf3f | ||
|
|
752e64f259
|
||
|
|
84a2a2ffea
|
||
|
|
fd2352b6ad | ||
|
|
0efcf75221 | ||
|
|
0479744ce5 | ||
|
|
af58edbafd | ||
|
|
1a16199f2f | ||
|
|
9b2d527576
|
||
|
|
d663cc5d20
|
||
|
|
5d84cf0589
|
||
|
|
84fa103555
|
||
|
|
ff967da485
|
||
|
|
6271b22708 | ||
|
|
6e18d2efa9
|
||
|
|
5803d057fd
|
||
|
|
22da81a102 | ||
|
|
a8959c28ec | ||
|
|
fbbef5c28b
|
||
|
|
6d98ecef30
|
||
|
|
0e695e4266
|
||
|
|
d524b3814c
|
||
|
|
8e98f5b9da
|
||
|
|
9a65fd04c3
|
||
|
|
49fbcb221c
|
||
|
|
e4acb7fd6f | ||
|
|
a66ed2e216
|
||
|
|
0a2f58a91b
|
||
|
|
3f3ff08d25
|
||
|
|
05b355e709
|
||
|
|
a9ddf3e3f8 | ||
|
|
e76e684b4d
|
||
|
|
2fc865ea57
|
||
|
|
ab9be06d5e | ||
|
|
abc4c68a0f
|
||
|
|
ff487064a2
|
||
|
|
e7373d6e98 | ||
|
|
d52ce8828d | ||
|
|
745c98bc20
|
||
|
|
16db0eb0ee
|
||
|
|
a2e3676d05
|
||
|
|
088b0e87ce | ||
|
|
d6b8d12b2e
|
||
|
|
ac76150915
|
No files matched your search
@@ -1,6 +0,0 @@
|
|||||||
delta-force-guide-server.iml
|
|
||||||
build/
|
|
||||||
!.gitlab-ci.yml
|
|
||||||
!build/libs/*.jar
|
|
||||||
.idea/
|
|
||||||
.gradle
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
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
|
|
||||||
-Pvendor=${{ vars.VENDOR }}
|
|
||||||
|
|
||||||
- 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: ${{ vars.DOCKER_HUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Generate image tags
|
|
||||||
id: meta
|
|
||||||
run: |
|
|
||||||
DOCKERHUB_USER="${{ vars.DOCKER_HUB_USERNAME }}"
|
|
||||||
echo "tag_version=${DOCKERHUB_USER}/${APP_NAME}:${{ gitea.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "tag_latest=${DOCKERHUB_USER}/${APP_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 }}
|
|
||||||
IMAGE_VERSION=${{ gitea.event.release.tag_name }}
|
|
||||||
IMAGE_VENDOR=${{ vars.VENDOR }}
|
|
||||||
BUILD_DATE=${{ gitea.event.release.published_at }}
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
${{ steps.meta.outputs.tag_version }}
|
|
||||||
${{ steps.meta.outputs.tag_latest }}
|
|
||||||
|
|
||||||
- name: Push README to Docker Hub
|
|
||||||
uses: christian-korneck/update-container-description-action@v1
|
|
||||||
env:
|
|
||||||
DOCKER_USER: ${{ vars.DOCKER_HUB_USERNAME }}
|
|
||||||
DOCKER_PASS: ${{ secrets.DOCKER_HUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
destination_container_repo: ${{ vars.DOCKER_HUB_USERNAME }}/${{ env.APP_NAME }}
|
|
||||||
provider: dockerhub
|
|
||||||
readme_file: README.md
|
|
||||||
+18
-12
@@ -1,9 +1,12 @@
|
|||||||
name: Build and Deploy
|
name: Server Build and Deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
APP_NAME: ahsarah-guide-server
|
APP_NAME: ahsarah-guide-server
|
||||||
|
|
||||||
@@ -26,7 +29,7 @@ jobs:
|
|||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: >
|
run: >
|
||||||
./gradlew bootJar -x test
|
./gradlew bootJar -x test
|
||||||
-PartefactVersion="${{ gitea.event.release.tag_name }}"
|
-PartefactVersion="${{ github.event.release.tag_name }}"
|
||||||
-PbuildChannel=stable
|
-PbuildChannel=stable
|
||||||
-Pvendor=${{ vars.VENDOR }}
|
-Pvendor=${{ vars.VENDOR }}
|
||||||
|
|
||||||
@@ -36,16 +39,19 @@ jobs:
|
|||||||
JAR_PATH=$(find build/libs -name '*.jar' | head -1)
|
JAR_PATH=$(find build/libs -name '*.jar' | head -1)
|
||||||
echo "file=$JAR_PATH" >> "$GITHUB_OUTPUT"
|
echo "file=$JAR_PATH" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Upload JAR to Gitea Release
|
- name: Prepare release asset
|
||||||
|
id: asset
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ gitea.event.release.tag_name }}"
|
TAG="${{ github.event.release.tag_name }}"
|
||||||
FILE="${{ steps.jar.outputs.file }}"
|
FILE="${{ steps.jar.outputs.file }}"
|
||||||
ASSET_NAME="${APP_NAME}-${TAG}.jar"
|
ASSET_NAME="${APP_NAME}-${TAG}.jar"
|
||||||
curl -X POST \
|
cp "${FILE}" "../${ASSET_NAME}"
|
||||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
echo "path=${ASSET_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
-H "Content-Type: multipart/form-data" \
|
|
||||||
-F "attachment=@${FILE};filename=${ASSET_NAME}" \
|
- name: Upload JAR to GitHub Release
|
||||||
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/${{ gitea.event.release.id }}/assets?name=${ASSET_NAME}"
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: ${{ steps.asset.outputs.path }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
@@ -60,7 +66,7 @@ jobs:
|
|||||||
id: meta
|
id: meta
|
||||||
run: |
|
run: |
|
||||||
DOCKERHUB_USER="${{ vars.DOCKER_HUB_USERNAME }}"
|
DOCKERHUB_USER="${{ vars.DOCKER_HUB_USERNAME }}"
|
||||||
echo "tag_version=${DOCKERHUB_USER}/${APP_NAME}:${{ gitea.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
|
echo "tag_version=${DOCKERHUB_USER}/${APP_NAME}:${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
|
||||||
echo "tag_latest=${DOCKERHUB_USER}/${APP_NAME}:latest" >> "$GITHUB_OUTPUT"
|
echo "tag_latest=${DOCKERHUB_USER}/${APP_NAME}:latest" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
@@ -70,9 +76,9 @@ jobs:
|
|||||||
file: server/Dockerfile.ci
|
file: server/Dockerfile.ci
|
||||||
build-args: |
|
build-args: |
|
||||||
JAR_FILE=${{ steps.jar.outputs.file }}
|
JAR_FILE=${{ steps.jar.outputs.file }}
|
||||||
IMAGE_VERSION=${{ gitea.event.release.tag_name }}
|
IMAGE_VERSION=${{ github.event.release.tag_name }}
|
||||||
IMAGE_VENDOR=${{ vars.VENDOR }}
|
IMAGE_VENDOR=${{ vars.VENDOR }}
|
||||||
BUILD_DATE=${{ gitea.event.release.published_at }}
|
BUILD_DATE=${{ github.event.release.published_at }}
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
${{ steps.meta.outputs.tag_version }}
|
${{ steps.meta.outputs.tag_version }}
|
||||||
+10
-16
@@ -1,4 +1,4 @@
|
|||||||
name: Build and Deploy
|
name: Web Build and Deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
@@ -21,6 +21,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
|
cache-dependency-path: web/pnpm-lock.yaml
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
@@ -33,7 +34,7 @@ jobs:
|
|||||||
- name: Build release archive
|
- name: Build release archive
|
||||||
run: pnpm build:tar
|
run: pnpm build:tar
|
||||||
|
|
||||||
- name: Clean up previous build artifacts on server
|
- name: Clean up previous build artefacts on server
|
||||||
uses: appleboy/ssh-action@v1.0.3
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
with:
|
with:
|
||||||
host: ${{ vars.DEPLOY_HOST }}
|
host: ${{ vars.DEPLOY_HOST }}
|
||||||
@@ -42,7 +43,7 @@ jobs:
|
|||||||
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
command: rm -rf /tmp/dist.tar.gz /tmp/dist.tar.gz/
|
command: rm -rf /tmp/dist.tar.gz /tmp/dist.tar.gz/
|
||||||
|
|
||||||
- name: Upload artifact to server
|
- name: Upload artefact to server
|
||||||
uses: appleboy/scp-action@v0.1.7
|
uses: appleboy/scp-action@v0.1.7
|
||||||
with:
|
with:
|
||||||
host: ${{ vars.DEPLOY_HOST }}
|
host: ${{ vars.DEPLOY_HOST }}
|
||||||
@@ -53,12 +54,12 @@ jobs:
|
|||||||
target: "/tmp/"
|
target: "/tmp/"
|
||||||
|
|
||||||
upload-release-asset:
|
upload-release-asset:
|
||||||
name: Upload to Gitea Release
|
name: Upload to GitHub Release
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download artifact from server
|
- name: Download artefact from server
|
||||||
env:
|
env:
|
||||||
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
|
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
|
||||||
DEPLOY_USER: ${{ vars.DEPLOY_USER }}
|
DEPLOY_USER: ${{ vars.DEPLOY_USER }}
|
||||||
@@ -79,17 +80,10 @@ jobs:
|
|||||||
./dist.tar.gz
|
./dist.tar.gz
|
||||||
rm -rf "$SSH_DIR"
|
rm -rf "$SSH_DIR"
|
||||||
|
|
||||||
- name: Upload release asset via Gitea API
|
- name: Upload release asset to GitHub
|
||||||
run: |
|
uses: softprops/action-gh-release@v2
|
||||||
set -e
|
with:
|
||||||
RELEASE_ID=$(jq -r '.release.id' "$GITEA_EVENT_PATH")
|
files: dist.tar.gz
|
||||||
URL="${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/${RELEASE_ID}/assets?name=dist.tar.gz"
|
|
||||||
curl -fsSL \
|
|
||||||
-X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/gzip" \
|
|
||||||
--data-binary "@dist.tar.gz" \
|
|
||||||
"${URL}"
|
|
||||||
|
|
||||||
deploy-to-server:
|
deploy-to-server:
|
||||||
name: Deploy to onixbyte.cn
|
name: Deploy to onixbyte.cn
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
FROM amazoncorretto:21 AS builder
|
|
||||||
WORKDIR /app
|
|
||||||
ADD . .
|
|
||||||
RUN chmod +x ./gradlew && \
|
|
||||||
./gradlew build -x test
|
|
||||||
|
|
||||||
FROM amazoncorretto:21-alpine AS runner
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=builder /app/build/libs/delta-force-guide-server-1.0.0.jar app.jar
|
|
||||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
FROM amazoncorretto:21-alpine
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ARG JAR_FILE
|
|
||||||
ARG IMAGE_VERSION=dev
|
|
||||||
ARG IMAGE_VENDOR=OnixByte
|
|
||||||
ARG BUILD_DATE
|
|
||||||
|
|
||||||
COPY ${JAR_FILE} app.jar
|
|
||||||
|
|
||||||
LABEL org.opencontainers.image.title="delta-force-guide-server" \
|
|
||||||
org.opencontainers.image.description="REST API backend for managing Delta Force game firearm builds and modifications" \
|
|
||||||
org.opencontainers.image.version="${IMAGE_VERSION}" \
|
|
||||||
org.opencontainers.image.vendor="${IMAGE_VENDOR}" \
|
|
||||||
org.opencontainers.image.source="https://git.onixbyte.com/onixbyte/delta-force-guide-server" \
|
|
||||||
org.opencontainers.image.licenses="MIT" \
|
|
||||||
org.opencontainers.image.created="${BUILD_DATE}"
|
|
||||||
|
|
||||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
plugins {
|
|
||||||
java
|
|
||||||
alias(libs.plugins.spring.boot)
|
|
||||||
alias(libs.plugins.spring.dependency.management)
|
|
||||||
}
|
|
||||||
|
|
||||||
val artefactVersion: String by project
|
|
||||||
val buildChannel: String by project
|
|
||||||
val vendor: String by project
|
|
||||||
|
|
||||||
group = "com.onixbyte.helix"
|
|
||||||
version = artefactVersion
|
|
||||||
|
|
||||||
tasks.withType<JavaCompile> {
|
|
||||||
options.encoding = "UTF-8"
|
|
||||||
}
|
|
||||||
|
|
||||||
java {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_21
|
|
||||||
targetCompatibility = JavaVersion.VERSION_21
|
|
||||||
toolchain {
|
|
||||||
languageVersion = JavaLanguageVersion.of(21)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(libs.jspecify.core)
|
|
||||||
implementation(platform(libs.aws.sdk.bom))
|
|
||||||
implementation(libs.aws.sdk.s3)
|
|
||||||
implementation(libs.commons.io)
|
|
||||||
implementation(libs.commons.lang)
|
|
||||||
implementation(libs.commons.collections)
|
|
||||||
implementation(libs.hypersistence.core)
|
|
||||||
implementation(platform(libs.onixbyte.versionCatalogue))
|
|
||||||
implementation(libs.onixbyte.tuple)
|
|
||||||
implementation(libs.onixbyte.commonToolbox)
|
|
||||||
implementation(libs.onixbyte.mathToolbox)
|
|
||||||
implementation(libs.onixbyte.identityGenerator)
|
|
||||||
implementation(libs.onixbyte.captcha)
|
|
||||||
implementation(libs.onixbyte.regions)
|
|
||||||
implementation(libs.onixbyte.cryptoToolbox)
|
|
||||||
implementation(libs.jwt.core)
|
|
||||||
implementation(libs.spring.boot.configurationProcessor)
|
|
||||||
implementation(libs.spring.boot.actuator)
|
|
||||||
implementation(libs.spring.boot.starter.web)
|
|
||||||
implementation(libs.spring.boot.starter.webFlux)
|
|
||||||
implementation(libs.spring.boot.starter.validation)
|
|
||||||
implementation(libs.spring.boot.starter.redis)
|
|
||||||
implementation(libs.spring.boot.starter.cache)
|
|
||||||
implementation(libs.spring.boot.starter.jpa)
|
|
||||||
implementation(libs.mybatis.starter.core)
|
|
||||||
implementation(libs.flyway.core)
|
|
||||||
implementation(libs.flyway.postgresql)
|
|
||||||
implementation(libs.jackson.jsr310)
|
|
||||||
implementation(libs.spring.boot.starter.doc)
|
|
||||||
implementation(libs.spring.boot.starter.security)
|
|
||||||
testImplementation(libs.spring.boot.starter.test)
|
|
||||||
testImplementation(libs.reactor.test)
|
|
||||||
testImplementation(libs.mybatis.starter.test)
|
|
||||||
testImplementation(libs.spring.security.test)
|
|
||||||
runtimeOnly(libs.postgres.driver)
|
|
||||||
testRuntimeOnly(libs.h2.database)
|
|
||||||
testRuntimeOnly(libs.junit.launcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.processResources {
|
|
||||||
filesMatching("application.yaml") {
|
|
||||||
expand(
|
|
||||||
"appVersion" to artefactVersion,
|
|
||||||
"channel" to buildChannel,
|
|
||||||
"vendor" to vendor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.test {
|
|
||||||
useJUnitPlatform()
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
spring:
|
|
||||||
datasource:
|
|
||||||
url: jdbc:postgresql://localhost:5432/dfguide_dev
|
|
||||||
username: postgres
|
|
||||||
password: 123456
|
|
||||||
driver-class-name: org.postgresql.Driver
|
|
||||||
data:
|
|
||||||
redis:
|
|
||||||
host: localhost
|
|
||||||
port: 6379
|
|
||||||
database: 0
|
|
||||||
# password: qwerty # Uncomment if password is necessary
|
|
||||||
|
|
||||||
logging:
|
|
||||||
pattern:
|
|
||||||
# dateformat: dd MMM yyyy HH:mm:ss.SSS # Modify this for custom date format.
|
|
||||||
|
|
||||||
app:
|
|
||||||
cookie:
|
|
||||||
http-only: true
|
|
||||||
secure: false
|
|
||||||
same-site: none
|
|
||||||
path: '/'
|
|
||||||
max-age: P1D
|
|
||||||
cors:
|
|
||||||
allowed-origins: # Cross-origin allowed origins
|
|
||||||
- "http://localhost:5173" # Dev server for vite.
|
|
||||||
- "http://localhost:4173" # Preview server for vite.
|
|
||||||
allow-credentials: true # Must be set to `true` since we are using cookie. You can change it only when you have modified how this application executing authentication.
|
|
||||||
allow-private-network: false
|
|
||||||
allowed-headers:
|
|
||||||
- "Content-Type"
|
|
||||||
- "Authorization"
|
|
||||||
allowed-methods:
|
|
||||||
- GET
|
|
||||||
- POST
|
|
||||||
- PUT
|
|
||||||
- PATCH
|
|
||||||
- DELETE
|
|
||||||
exposed-headers:
|
|
||||||
- "Content-Type"
|
|
||||||
- "Authorization"
|
|
||||||
max-age: PT2H
|
|
||||||
jwt:
|
|
||||||
issuer: dfguide.local # Issuer host
|
|
||||||
secret: qwertyuiopasdfghjklzxcvbnm123456 # JWT singing secret, a 32-byte long or longer string is recommended
|
|
||||||
valid-time: PT2H # JWT valid duration
|
|
||||||
webhook:
|
|
||||||
github:
|
|
||||||
secret: 123456
|
|
||||||
allowed-users: [ octotcat, onixbyte ]
|
|
||||||
|
|
||||||
springdoc:
|
|
||||||
api-docs:
|
|
||||||
enabled: true # Set to `false` if you do not need api docs (recommended in production mode).
|
|
||||||
swagger-ui:
|
|
||||||
enabled: true # Set to `false` if you do not need swagger ui (recommended in production mode).
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
[versions]
|
|
||||||
jspecifyVersion = "1.0.0"
|
|
||||||
javaJwtVersion = "4.5.1"
|
|
||||||
postgresDriverVersion = "42.7.9"
|
|
||||||
h2Version = "2.2.224"
|
|
||||||
springBootVersion = "3.5.13"
|
|
||||||
reactorVersion = "3.7.8"
|
|
||||||
junitPlatformVersion = "1.12.2"
|
|
||||||
onixbyteVersion = "3.4.0"
|
|
||||||
onixbyteCaptcha = "1.1.0"
|
|
||||||
onixbyteRegions = "2025.12.0"
|
|
||||||
awsSdkVersion = "2.25.48"
|
|
||||||
commonsIoVersion = "2.16.1"
|
|
||||||
commonsCollections = "4.5.0"
|
|
||||||
commonsLangVersion = "3.20.0"
|
|
||||||
mybatisVersion = "3.0.5"
|
|
||||||
jacksonVersion = "2.19.2"
|
|
||||||
hypersistenceVersion = "3.14.0"
|
|
||||||
springDependencyManagementVersion = "1.1.7"
|
|
||||||
springDocVersion = "2.8.16"
|
|
||||||
|
|
||||||
[libraries]
|
|
||||||
# General Utilities
|
|
||||||
jspecify-core = { group = "org.jspecify", name = "jspecify", version.ref = "jspecifyVersion" }
|
|
||||||
commons-io = { group = "commons-io", name = "commons-io", version.ref = "commonsIoVersion" }
|
|
||||||
commons-collections = { group = "org.apache.commons", name = "commons-collections4", version.ref = "commonsCollections" }
|
|
||||||
commons-lang = { group = "org.apache.commons", name = "commons-lang3", version.ref = "commonsLangVersion" }
|
|
||||||
jackson-jsr310 = { group = "com.fasterxml.jackson.datatype", name = "jackson-datatype-jsr310", version.ref = "jacksonVersion" }
|
|
||||||
|
|
||||||
# Onixbyte Ecosystem
|
|
||||||
onixbyte-versionCatalogue = { group = "com.onixbyte", name = "version-catalogue", version.ref = "onixbyteVersion" }
|
|
||||||
onixbyte-tuple = { group = "com.onixbyte", name = "tuple" }
|
|
||||||
onixbyte-commonToolbox = { group = "com.onixbyte", name = "common-toolbox" }
|
|
||||||
onixbyte-cryptoToolbox = { group = "com.onixbyte", name = "crypto-toolbox", version.ref = "onixbyteVersion" }
|
|
||||||
onixbyte-mathToolbox = { group = "com.onixbyte", name = "math-toolbox" }
|
|
||||||
onixbyte-identityGenerator = { group = "com.onixbyte", name = "identity-generator" }
|
|
||||||
onixbyte-captcha = { group = "com.onixbyte", name = "captcha", version.ref = "onixbyteCaptcha" }
|
|
||||||
onixbyte-regions = { group = "com.onixbyte", name = "regions4j", version.ref = "onixbyteRegions" }
|
|
||||||
|
|
||||||
# Persistence & Database
|
|
||||||
mybatis-starter-core = { group = "org.mybatis.spring.boot", name = "mybatis-spring-boot-starter", version.ref = "mybatisVersion" }
|
|
||||||
spring-boot-starter-jpa = { group = "org.springframework.boot", name = "spring-boot-starter-data-jpa" }
|
|
||||||
hypersistence-core = { group = "io.hypersistence", name = "hypersistence-utils-hibernate-63", version.ref = "hypersistenceVersion" }
|
|
||||||
postgres-driver = { group = "org.postgresql", name = "postgresql", version.ref = "postgresDriverVersion" }
|
|
||||||
h2-database = { group = "com.h2database", name = "h2", version.ref = "h2Version" }
|
|
||||||
spring-boot-starter-redis = { group = "org.springframework.boot", name = "spring-boot-starter-data-redis", version.ref = "springBootVersion" }
|
|
||||||
flyway-core = { group = "org.flywaydb", name = "flyway-core" }
|
|
||||||
flyway-postgresql = { group = "org.flywaydb", name = "flyway-database-postgresql" }
|
|
||||||
|
|
||||||
# Spring Boot Core & Web
|
|
||||||
spring-boot-starter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" }
|
|
||||||
spring-boot-starter-webFlux = { group = "org.springframework.boot", name = "spring-boot-starter-webflux" }
|
|
||||||
spring-boot-starter-validation = { group = "org.springframework.boot", name = "spring-boot-starter-validation" }
|
|
||||||
spring-boot-starter-cache = { group = "org.springframework.boot", name = "spring-boot-starter-cache" }
|
|
||||||
spring-boot-configurationProcessor = { group = "org.springframework.boot", name = "spring-boot-configuration-processor" }
|
|
||||||
spring-boot-actuator = { group = "org.springframework.boot", name = "spring-boot-starter-actuator" }
|
|
||||||
|
|
||||||
# Security & Auth
|
|
||||||
spring-boot-starter-security = { group = "org.springframework.boot", name = "spring-boot-starter-security" }
|
|
||||||
jwt-core = { group = "com.auth0", name = "java-jwt", version.ref = "javaJwtVersion" }
|
|
||||||
|
|
||||||
# Spring Doc
|
|
||||||
spring-boot-starter-doc = { group = "org.springdoc", name = "springdoc-openapi-starter-webmvc-ui", version.ref = "springDocVersion" }
|
|
||||||
|
|
||||||
# Cloud Services
|
|
||||||
aws-sdk-bom = { group = "software.amazon.awssdk", name = "bom", version.ref = "awsSdkVersion" }
|
|
||||||
aws-sdk-s3 = { group = "software.amazon.awssdk", name = "s3" }
|
|
||||||
|
|
||||||
# Testing
|
|
||||||
spring-boot-starter-test = { group = "org.springframework.boot", name = "spring-boot-starter-test", version.ref = "springBootVersion" }
|
|
||||||
spring-security-test = { group = "org.springframework.security", name = "spring-security-test" }
|
|
||||||
reactor-test = { group = "io.projectreactor", name = "reactor-test", version.ref = "reactorVersion" }
|
|
||||||
junit-launcher = { group = "org.junit.platform", name = "junit-platform-launcher", version.ref = "junitPlatformVersion" }
|
|
||||||
mybatis-starter-test = { group = "org.mybatis.spring.boot", name = "mybatis-spring-boot-starter-test", version.ref = "mybatisVersion" }
|
|
||||||
|
|
||||||
[plugins]
|
|
||||||
spring-boot = { id = "org.springframework.boot", version.ref = "springBootVersion" }
|
|
||||||
spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "springDependencyManagementVersion" }
|
|
||||||
Vendored
BIN
Binary file not shown.
-7
@@ -1,7 +0,0 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
|
||||||
distributionPath=wrapper/dists
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip
|
|
||||||
networkTimeout=10000
|
|
||||||
validateDistributionUrl=true
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
#
|
|
||||||
# Copyright © 2015-2021 the original authors.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Gradle start up script for POSIX generated by Gradle.
|
|
||||||
#
|
|
||||||
# Important for running:
|
|
||||||
#
|
|
||||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
|
||||||
# noncompliant, but you have some other compliant shell such as ksh or
|
|
||||||
# bash, then to run this script, type that shell name before the whole
|
|
||||||
# command line, like:
|
|
||||||
#
|
|
||||||
# ksh Gradle
|
|
||||||
#
|
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
|
||||||
# requires all of these POSIX shell features:
|
|
||||||
# * functions;
|
|
||||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
|
||||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
|
||||||
# * compound commands having a testable exit status, especially «case»;
|
|
||||||
# * various built-in commands including «command», «set», and «ulimit».
|
|
||||||
#
|
|
||||||
# Important for patching:
|
|
||||||
#
|
|
||||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
|
||||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
|
||||||
#
|
|
||||||
# The "traditional" practice of packing multiple parameters into a
|
|
||||||
# space-separated string is a well documented source of bugs and security
|
|
||||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
|
||||||
# options in "$@", and eventually passing that to Java.
|
|
||||||
#
|
|
||||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
|
||||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
|
||||||
# see the in-line comments for details.
|
|
||||||
#
|
|
||||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
|
||||||
# Darwin, MinGW, and NonStop.
|
|
||||||
#
|
|
||||||
# (3) This script is generated from the Groovy template
|
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
|
||||||
# within the Gradle project.
|
|
||||||
#
|
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
|
||||||
app_path=$0
|
|
||||||
|
|
||||||
# Need this for daisy-chained symlinks.
|
|
||||||
while
|
|
||||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
|
||||||
[ -h "$app_path" ]
|
|
||||||
do
|
|
||||||
ls=$( ls -ld "$app_path" )
|
|
||||||
link=${ls#*' -> '}
|
|
||||||
case $link in #(
|
|
||||||
/*) app_path=$link ;; #(
|
|
||||||
*) app_path=$APP_HOME$link ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# This is normally unused
|
|
||||||
# shellcheck disable=SC2034
|
|
||||||
APP_BASE_NAME=${0##*/}
|
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
|
||||||
MAX_FD=maximum
|
|
||||||
|
|
||||||
warn () {
|
|
||||||
echo "$*"
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
die () {
|
|
||||||
echo
|
|
||||||
echo "$*"
|
|
||||||
echo
|
|
||||||
exit 1
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
|
||||||
cygwin=false
|
|
||||||
msys=false
|
|
||||||
darwin=false
|
|
||||||
nonstop=false
|
|
||||||
case "$( uname )" in #(
|
|
||||||
CYGWIN* ) cygwin=true ;; #(
|
|
||||||
Darwin* ) darwin=true ;; #(
|
|
||||||
MSYS* | MINGW* ) msys=true ;; #(
|
|
||||||
NONSTOP* ) nonstop=true ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
CLASSPATH="\\\"\\\""
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
|
||||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
|
||||||
else
|
|
||||||
JAVACMD=$JAVA_HOME/bin/java
|
|
||||||
fi
|
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
JAVACMD=java
|
|
||||||
if ! command -v java >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|
||||||
case $MAX_FD in #(
|
|
||||||
max*)
|
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC2039,SC3045
|
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
|
||||||
warn "Could not query maximum file descriptor limit"
|
|
||||||
esac
|
|
||||||
case $MAX_FD in #(
|
|
||||||
'' | soft) :;; #(
|
|
||||||
*)
|
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC2039,SC3045
|
|
||||||
ulimit -n "$MAX_FD" ||
|
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Collect all arguments for the java command, stacking in reverse order:
|
|
||||||
# * args from the command line
|
|
||||||
# * the main class name
|
|
||||||
# * -classpath
|
|
||||||
# * -D...appname settings
|
|
||||||
# * --module-path (only if needed)
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
|
||||||
if "$cygwin" || "$msys" ; then
|
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
|
||||||
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
for arg do
|
|
||||||
if
|
|
||||||
case $arg in #(
|
|
||||||
-*) false ;; # don't mess with options #(
|
|
||||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
|
||||||
[ -e "$t" ] ;; #(
|
|
||||||
*) false ;;
|
|
||||||
esac
|
|
||||||
then
|
|
||||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
|
||||||
fi
|
|
||||||
# Roll the args list around exactly as many times as the number of
|
|
||||||
# args, so each arg winds up back in the position where it started, but
|
|
||||||
# possibly modified.
|
|
||||||
#
|
|
||||||
# NB: a `for` loop captures its iteration list before it begins, so
|
|
||||||
# changing the positional parameters here affects neither the number of
|
|
||||||
# iterations, nor the values presented in `arg`.
|
|
||||||
shift # remove old arg
|
|
||||||
set -- "$@" "$arg" # push replacement arg
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
|
||||||
# and any embedded shellness will be escaped.
|
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
|
||||||
# treated as '${Hostname}' itself on the command line.
|
|
||||||
|
|
||||||
set -- \
|
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
|
||||||
-classpath "$CLASSPATH" \
|
|
||||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
|
||||||
"$@"
|
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
|
||||||
if ! command -v xargs >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
die "xargs is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use "xargs" to parse quoted args.
|
|
||||||
#
|
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
|
||||||
#
|
|
||||||
# In Bash we could simply go:
|
|
||||||
#
|
|
||||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
|
||||||
# set -- "${ARGS[@]}" "$@"
|
|
||||||
#
|
|
||||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
|
||||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
|
||||||
# character that might be a shell metacharacter, then use eval to reverse
|
|
||||||
# that process (while maintaining the separation between arguments), and wrap
|
|
||||||
# the whole thing up as a single "set" statement.
|
|
||||||
#
|
|
||||||
# This will of course break if any of these variables contains a newline or
|
|
||||||
# an unmatched quote.
|
|
||||||
#
|
|
||||||
|
|
||||||
eval "set -- $(
|
|
||||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
|
||||||
xargs -n1 |
|
|
||||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
|
||||||
tr '\n' ' '
|
|
||||||
)" '"$@"'
|
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
|
||||||
Vendored
-94
@@ -1,94 +0,0 @@
|
|||||||
@rem
|
|
||||||
@rem Copyright 2015 the original author or authors.
|
|
||||||
@rem
|
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
@rem you may not use this file except in compliance with the License.
|
|
||||||
@rem You may obtain a copy of the License at
|
|
||||||
@rem
|
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
@rem
|
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
@rem See the License for the specific language governing permissions and
|
|
||||||
@rem limitations under the License.
|
|
||||||
@rem
|
|
||||||
@rem SPDX-License-Identifier: Apache-2.0
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
|
||||||
@rem ##########################################################################
|
|
||||||
@rem
|
|
||||||
@rem Gradle startup script for Windows
|
|
||||||
@rem
|
|
||||||
@rem ##########################################################################
|
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
|
||||||
@rem This is normally unused
|
|
||||||
set APP_BASE_NAME=%~n0
|
|
||||||
set APP_HOME=%DIRNAME%
|
|
||||||
|
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
|
||||||
|
|
||||||
@rem Find java.exe
|
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
|
||||||
|
|
||||||
echo. 1>&2
|
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
|
||||||
echo. 1>&2
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
|
||||||
echo location of your Java installation. 1>&2
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
|
||||||
|
|
||||||
echo. 1>&2
|
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
|
||||||
echo. 1>&2
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
|
||||||
echo location of your Java installation. 1>&2
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:execute
|
|
||||||
@rem Setup the command line
|
|
||||||
|
|
||||||
set CLASSPATH=
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
|
||||||
|
|
||||||
:end
|
|
||||||
@rem End local scope for the variables with windows NT shell
|
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
|
||||||
|
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
|
||||||
exit /b %EXIT_CODE%
|
|
||||||
|
|
||||||
:mainEnd
|
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
|
||||||
|
|
||||||
:omega
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
/gradlew text eol=lf
|
|
||||||
*.bat text eol=crlf
|
|
||||||
*.jar binary
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
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
|
|
||||||
-Pvendor=${{ vars.VENDOR }}
|
|
||||||
|
|
||||||
- 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: ${{ vars.DOCKER_HUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Generate image tags
|
|
||||||
id: meta
|
|
||||||
run: |
|
|
||||||
DOCKERHUB_USER="${{ vars.DOCKER_HUB_USERNAME }}"
|
|
||||||
echo "tag_version=${DOCKERHUB_USER}/${APP_NAME}:${{ gitea.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "tag_latest=${DOCKERHUB_USER}/${APP_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 }}
|
|
||||||
IMAGE_VERSION=${{ gitea.event.release.tag_name }}
|
|
||||||
IMAGE_VENDOR=${{ vars.VENDOR }}
|
|
||||||
BUILD_DATE=${{ gitea.event.release.published_at }}
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
${{ steps.meta.outputs.tag_version }}
|
|
||||||
${{ steps.meta.outputs.tag_latest }}
|
|
||||||
|
|
||||||
- name: Push README to Docker Hub
|
|
||||||
uses: christian-korneck/update-container-description-action@v1
|
|
||||||
env:
|
|
||||||
DOCKER_USER: ${{ vars.DOCKER_HUB_USERNAME }}
|
|
||||||
DOCKER_PASS: ${{ secrets.DOCKER_HUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
destination_container_repo: ${{ vars.DOCKER_HUB_USERNAME }}/${{ env.APP_NAME }}
|
|
||||||
provider: dockerhub
|
|
||||||
readme_file: README.md
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
### macOS
|
|
||||||
# General
|
|
||||||
.DS_Store
|
|
||||||
.AppleDouble
|
|
||||||
.LSOverride
|
|
||||||
|
|
||||||
# Thumbnails
|
|
||||||
._*
|
|
||||||
|
|
||||||
# Files that might appear in the root of a volume
|
|
||||||
.DocumentRevisions-V100
|
|
||||||
.fseventsd
|
|
||||||
.Spotlight-V100
|
|
||||||
.TemporaryItems
|
|
||||||
.Trashes
|
|
||||||
.VolumeIcon.icns
|
|
||||||
.com.apple.timemachine.donotpresent
|
|
||||||
|
|
||||||
# Directories potentially created on remote AFP share
|
|
||||||
.AppleDB
|
|
||||||
.AppleDesktop
|
|
||||||
Network Trash Folder
|
|
||||||
Temporary Items
|
|
||||||
.apdisk
|
|
||||||
|
|
||||||
### Linux
|
|
||||||
*~
|
|
||||||
|
|
||||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
|
||||||
.fuse_hidden*
|
|
||||||
|
|
||||||
# Metadata left by Dolphin file manager, which comes with KDE Plasma
|
|
||||||
.directory
|
|
||||||
|
|
||||||
# Linux trash folder which might appear on any partition or disk
|
|
||||||
.Trash-*
|
|
||||||
|
|
||||||
# .nfs files are created when an open file is removed but is still being accessed
|
|
||||||
.nfs*
|
|
||||||
|
|
||||||
# Log files created by default by the nohup command
|
|
||||||
nohup.out
|
|
||||||
|
|
||||||
### Windows
|
|
||||||
|
|
||||||
# Windows thumbnail cache files
|
|
||||||
Thumbs.db
|
|
||||||
Thumbs.db:encryptable
|
|
||||||
ehthumbs.db
|
|
||||||
ehthumbs_vista.db
|
|
||||||
|
|
||||||
# Dump file
|
|
||||||
*.stackdump
|
|
||||||
|
|
||||||
# Folder config file
|
|
||||||
[Dd]esktop.ini
|
|
||||||
|
|
||||||
# Recycle Bin used on file shares
|
|
||||||
$RECYCLE.BIN/
|
|
||||||
|
|
||||||
# Windows Installer files
|
|
||||||
*.cab
|
|
||||||
*.msi
|
|
||||||
*.msix
|
|
||||||
*.msm
|
|
||||||
*.msp
|
|
||||||
|
|
||||||
# Windows shortcuts
|
|
||||||
*.lnk
|
|
||||||
|
|
||||||
### JetBrains IDE
|
|
||||||
# Covers JetBrains IDEs: IntelliJ, GoLand, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
|
||||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
|
||||||
|
|
||||||
# User-specific stuff
|
|
||||||
.idea
|
|
||||||
|
|
||||||
# Gradle and Maven with auto-import
|
|
||||||
*.iml
|
|
||||||
*.ipr
|
|
||||||
|
|
||||||
# File-based project format
|
|
||||||
*.iws
|
|
||||||
|
|
||||||
# IntelliJ
|
|
||||||
out/
|
|
||||||
|
|
||||||
# JIRA plugin
|
|
||||||
atlassian-ide-plugin.xml
|
|
||||||
|
|
||||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
|
||||||
com_crashlytics_export_strings.xml
|
|
||||||
crashlytics.properties
|
|
||||||
crashlytics-build.properties
|
|
||||||
fabric.properties
|
|
||||||
|
|
||||||
# Editor-based HTTP Client
|
|
||||||
http-client.private.env.json
|
|
||||||
|
|
||||||
### Server
|
|
||||||
# Compiled class file
|
|
||||||
*.class
|
|
||||||
|
|
||||||
# Log file
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# BlueJ files
|
|
||||||
*.ctxt
|
|
||||||
|
|
||||||
# Mobile Tools for Java (J2ME)
|
|
||||||
.mtj.tmp/
|
|
||||||
|
|
||||||
# Package Files #
|
|
||||||
*.war
|
|
||||||
*.nar
|
|
||||||
*.ear
|
|
||||||
*.zip
|
|
||||||
*.tar.gz
|
|
||||||
*.rar
|
|
||||||
*.jar
|
|
||||||
|
|
||||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
|
||||||
hs_err_pid*
|
|
||||||
replay_pid*
|
|
||||||
|
|
||||||
# Config
|
|
||||||
config/*.yml
|
|
||||||
config/*.yaml
|
|
||||||
config/*.properties
|
|
||||||
|
|
||||||
# Tests
|
|
||||||
test/
|
|
||||||
|
|
||||||
### Gradle
|
|
||||||
.gradle
|
|
||||||
**/build/
|
|
||||||
!**/src/**/build/
|
|
||||||
|
|
||||||
!gradle/
|
|
||||||
!gradle/wrapper
|
|
||||||
!gradle/wrapper/gradle-wrapper.jar
|
|
||||||
!gradle/wrapper/gradle-wrapper.properties
|
|
||||||
gradle-app.setting
|
|
||||||
.gradletasknamecache
|
|
||||||
|
|
||||||
### Claude Code
|
|
||||||
.claude/settings.local.json
|
|
||||||
.claude/memory/
|
|
||||||
.claude/plans/
|
|
||||||
.claude/worktrees/
|
|
||||||
.claude/scheduled_tasks.json
|
|
||||||
|
|
||||||
# Eclipse Gradle plugin generated files
|
|
||||||
# Eclipse Core
|
|
||||||
.project
|
|
||||||
# JDT-specific (Eclipse Java Development Tools)
|
|
||||||
.classpath
|
|
||||||
|
|
||||||
gradle.properties
|
|
||||||
|
|
||||||
# frp config
|
|
||||||
frpc.toml
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Coding Standards
|
|
||||||
|
|
||||||
- **Style**: Follow the Google Java Coding Style as the foundation.
|
|
||||||
- **Indentation**: Use 4 spaces — no tabs.
|
|
||||||
- **Line length**: Maximum 100 characters per line.
|
|
||||||
- **Comments**: All code comments must use British English spelling (e.g. "colour" not "color", "behaviour" not "behavior", "serialise" not "serialize", "analyse" not "analyze", "traveller" not "traveler").
|
|
||||||
|
|
||||||
## Build & Test Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build the project (skip tests)
|
|
||||||
./gradlew build -x test
|
|
||||||
|
|
||||||
# Run all tests
|
|
||||||
./gradlew test
|
|
||||||
|
|
||||||
# Run a single test class
|
|
||||||
./gradlew test --tests "com.onixbyte.deltaforceguide.service.PasswordEncoderTest"
|
|
||||||
|
|
||||||
# Run a specific test method
|
|
||||||
./gradlew test --tests "com.onixbyte.deltaforceguide.service.PasswordEncoderTest.generatePassword"
|
|
||||||
|
|
||||||
# Build the full JAR
|
|
||||||
./gradlew bootJar
|
|
||||||
```
|
|
||||||
|
|
||||||
The project uses Gradle with Java 21 (Amazon Corretto). Tests use JUnit 5 with the Spring Boot test framework, H2 in-memory database for test runtime, and Spring Security test support. Tests require an active `dev` profile.
|
|
||||||
|
|
||||||
## Code Architecture
|
|
||||||
|
|
||||||
**Delta Force Guide Server** — A REST API backend for managing Delta Force game firearm builds/modifications.
|
|
||||||
|
|
||||||
### Package structure
|
|
||||||
|
|
||||||
```
|
|
||||||
com.onixbyte.deltaforceguide
|
|
||||||
├── client/ # External service clients (TokenClient for JWT)
|
|
||||||
├── config/ # Spring beans: Security, CORS, Cache/Redis, Jackson, MyBatis, Spring Data
|
|
||||||
├── controller/ # REST controllers (Firearm, Modification, Tag, Auth)
|
|
||||||
├── domain/
|
|
||||||
│ ├── converter/ # JPA attribute converters (FirearmTypeConverter)
|
|
||||||
│ ├── dto/ # Request/response records (FirearmRequest, ModificationResponse, etc.)
|
|
||||||
│ └── entity/ # JPA entities (Firearm, Modification, User, Accessory, Tuning)
|
|
||||||
├── enumeration/ # Enums (FirearmType)
|
|
||||||
├── exeption/ # BizException (custom runtime exception with HTTP status)
|
|
||||||
├── filter/ # TokenAuthenticationFilter (JWT auth via OncePerRequestFilter)
|
|
||||||
├── manager/ # Thin @Transactional wrappers around repositories
|
|
||||||
├── mapper/ # MyBatis mappers (configured but currently unused)
|
|
||||||
├── properties/ # @ConfigurationProperties records (Cors, Token, Cookie)
|
|
||||||
├── repository/ # Spring Data JPA repositories
|
|
||||||
├── security/
|
|
||||||
│ ├── authentication/ # Custom UsernamePasswordAuthentication impl
|
|
||||||
│ └── provider/ # UsernamePasswordAuthenticationProvider
|
|
||||||
├── service/ # Business logic layer (FirearmService, ModificationService, AuthService, etc.)
|
|
||||||
├── shared/ # Constants and utility classes (CookieName, CredentialProvider, JacksonModules)
|
|
||||||
└── utils/ # Helpers (DateTimeUtil)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key design decisions
|
|
||||||
|
|
||||||
- **JPA + native queries**: Most CRUD uses Spring Data JPA. Native queries (in `ModificationRepository`) handle JSONB tag filtering with Postgres `@>` operator.
|
|
||||||
- **Custom auth flow**: JWT tokens in httpOnly cookies (`AccessToken`). Spring Security with a custom `UsernamePasswordAuthenticationProvider` and `TokenAuthenticationFilter`. Tokens are auto-renewed within 5 min of expiry.
|
|
||||||
- **JSONB storage**: `Modification.tags` and `Modification.accessories` (including nested `Tuning` objects) are stored as JSONB columns using Hypersistence Utils `JsonType`.
|
|
||||||
- **Strict layering**: The call chain must follow `Controller → Service → Manager → Repository/Mapper`. Skipping layers (e.g. Controller calling Manager directly, Service calling Repository directly) is not permitted. Each layer has a distinct responsibility: Controller handles HTTP concerns, Service contains business logic, Manager manages `@Transactional` boundaries and data access coordination, Repository/Mapper handles raw data access.
|
|
||||||
- **DTOs as Java records**: All request/response objects are immutable records with static `from()` factory methods for entity→DTO conversion.
|
|
||||||
- **Flyway migrations**: SQL migrations in `src/main/resources/db/migration/` — V2 (init), V3 (bullet/damage fields), V4 (user), V5 (accessories JSONB column).
|
|
||||||
|
|
||||||
### Data model
|
|
||||||
|
|
||||||
- `firearm` table: id, name, type (int→FirearmType enum), level, calibre, fire_rate, armour_damage, body_damage, review
|
|
||||||
- `modification` table: id, firearm_id (FK→firearm), name, code, tags (jsonb), accessories (jsonb), note, author, video_url
|
|
||||||
- `app_user` table: id, username, email
|
|
||||||
- `user_credential` table: user_id, provider, credential (hashed)
|
|
||||||
|
|
||||||
### API endpoints
|
|
||||||
|
|
||||||
| Path | Methods | Auth |
|
|
||||||
|-------------------------------|------------------|--------------------------------------|
|
|
||||||
| `/firearms` | GET, POST | GET public, POST requires auth |
|
|
||||||
| `/firearms/{id}` | GET, PUT, DELETE | GET public, PUT/DELETE requires auth |
|
|
||||||
| `/modifications` | GET, POST | GET public, POST requires auth |
|
|
||||||
| `/modifications/{id}` | GET, PUT, DELETE | GET public, PUT/DELETE requires auth |
|
|
||||||
| `/modifications/batch` | POST | Requires auth |
|
|
||||||
| `/modifications/batch-delete` | DELETE | Requires auth |
|
|
||||||
| `/tags` | GET | Public |
|
|
||||||
| `/auth/login` | POST | Public |
|
|
||||||
| `/auth/logout` | POST | Authenticated |
|
|
||||||
|
|
||||||
### Commit convention
|
|
||||||
|
|
||||||
Conventional commits: `feat:`, `chore:`, `fix:`. Messages are in English, present tense imperative style.
|
|
||||||
|
|
||||||
### External dependencies
|
|
||||||
|
|
||||||
- **DB**: PostgreSQL (via Flyway migrations), H2 in test
|
|
||||||
- **Cache**: Redis (via Spring Cache + RedisTemplate with GenericJackson2JsonRedisSerializer)
|
|
||||||
- **Auth**: java-jwt (auth0), BCrypt
|
|
||||||
- **Docs**: springdoc-openapi (Swagger UI) on dev profile
|
|
||||||
- **Onixbyte internal libs**: version-catalogue, tuple, common-toolbox, math-toolbox, identity-generator, captcha, regions
|
|
||||||
- **AWS**: S3 SDK
|
|
||||||
|
|
||||||
### Profiles
|
|
||||||
|
|
||||||
- `dev`: Enables Swagger UI, connects to dev DB/Redis at `dfguide.onixbyte.cn`. Config in `config/application-dev.yaml`.
|
|
||||||
- Default profile: Used for production, no Swagger, connects to production datasource.
|
|
||||||
@@ -12,7 +12,7 @@ LABEL org.opencontainers.image.title="ahsarah-guide-server" \
|
|||||||
org.opencontainers.image.description="REST API backend for Ahsarah Guide" \
|
org.opencontainers.image.description="REST API backend for Ahsarah Guide" \
|
||||||
org.opencontainers.image.version="${IMAGE_VERSION}" \
|
org.opencontainers.image.version="${IMAGE_VERSION}" \
|
||||||
org.opencontainers.image.vendor="${IMAGE_VENDOR}" \
|
org.opencontainers.image.vendor="${IMAGE_VENDOR}" \
|
||||||
org.opencontainers.image.source="https://onixbyte.dev/onixbyte/ahsarah-guide" \
|
org.opencontainers.image.source="https://github.com/onixbyte/ahsarah-guide" \
|
||||||
org.opencontainers.image.licenses="MIT" \
|
org.opencontainers.image.licenses="MIT" \
|
||||||
org.opencontainers.image.created="${BUILD_DATE}"
|
org.opencontainers.image.created="${BUILD_DATE}"
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT Licence
|
|
||||||
|
|
||||||
Copyright (c) 2026 OnixByte
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
# Delta Force Guide Server
|
|
||||||
|
|
||||||
REST API backend for managing **Delta Force** game firearm builds and modifications. Provides endpoints to browse, create, and share weapon configurations including attachments, tuning setups, tags, and build reviews.
|
|
||||||
|
|
||||||
|
|
||||||
## Tech Stack
|
|
||||||
|
|
||||||
| Layer | Technology |
|
|
||||||
|-----------|------------------------------------------------------------|
|
|
||||||
| Language | Java 21 (Amazon Corretto) |
|
|
||||||
| Framework | Spring Boot 3.x (Web, Security, Data JPA, Cache, Actuator) |
|
|
||||||
| Build | Gradle (Kotlin DSL) |
|
|
||||||
| Database | PostgreSQL (Flyway migrations, JSONB columns) |
|
|
||||||
| Cache | Redis (2-hour TTL) |
|
|
||||||
| Auth | Custom JWT via httpOnly cookies + BCrypt |
|
|
||||||
| API Docs | springdoc-openapi (Swagger UI, dev profile only) |
|
|
||||||
| Container | Multi-stage Docker image (Amazon Corretto 21 → Alpine) |
|
|
||||||
| CI/CD | GitHub Actions — builds on release publish |
|
|
||||||
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- JDK 21 (Amazon Corretto recommended)
|
|
||||||
- PostgreSQL
|
|
||||||
- Redis
|
|
||||||
|
|
||||||
### Configure
|
|
||||||
|
|
||||||
Copy and customise the development config:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp config/application-prod.yaml.example \
|
|
||||||
config/application-dev.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
Fill in your datasource and Redis connection details, then place the production config at `config/`, then enable it by environment variable `SPRING_PROFILES_ACTIVE`.
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Compile (skip tests)
|
|
||||||
./gradlew build -x test
|
|
||||||
|
|
||||||
# Run all tests
|
|
||||||
./gradlew test
|
|
||||||
|
|
||||||
# Build executable JAR
|
|
||||||
./gradlew bootJar
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run
|
|
||||||
|
|
||||||
```bash
|
|
||||||
SPRING_PROFILES_ACTIVE=prod java -jar build/libs/delta-force-guide-server-$version.jar
|
|
||||||
```
|
|
||||||
|
|
||||||
Swagger UI is available at `http://localhost:8080/swagger-ui.html` when the `dev` profile is active.
|
|
||||||
|
|
||||||
## Data Model
|
|
||||||
|
|
||||||
- **Firearm** — weapon base stats (name, type, level, calibre, fire rate, armour/body damage, review)
|
|
||||||
- **Modification** — a build attached to a firearm, with name, code, tags (JSONB), accessories including nested tuning objects (JSONB), author notes, and video links
|
|
||||||
- **App User** — registered user (username, email) with hashed credentials via BCrypt
|
|
||||||
|
|
||||||
Tags and accessories are stored as PostgreSQL JSONB columns using [Hypersistence Utils](https://github.com/vladmihalcea/hypersistence-utils), enabling flexible per-build metadata and filtering with the `@>` operator.
|
|
||||||
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
Controller → Service → Manager → Repository / Mapper
|
|
||||||
(HTTP) (logic) (@Transactional) (data access)
|
|
||||||
```
|
|
||||||
|
|
||||||
The call chain is strictly enforced — skipping layers is not permitted. All request/response objects are Java records with static `from()` factory methods for entity-to-DTO conversion.
|
|
||||||
|
|
||||||
|
|
||||||
## Docker
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build image
|
|
||||||
docker pull registry.onixbyte.cn/onixbyte/delta-force-guide-server:latest
|
|
||||||
|
|
||||||
# Run container
|
|
||||||
docker run -p 8080:8080 \
|
|
||||||
-v /path/to/config:/app/config \
|
|
||||||
-e SPRING_PROFILES_ACTIVE=$your_active_profiles
|
|
||||||
delta-force-guide-server
|
|
||||||
```
|
|
||||||
|
|
||||||
Pre-built images are published to **Self-hosted GitLab Container Registry** (`registry.onixbyte.cn/onixbyte/delta-force-guide-server`) on every release.
|
|
||||||
|
|
||||||
|
|
||||||
## CI/CD
|
|
||||||
|
|
||||||
GitLab CI triggers on **tags**. The pipeline:
|
|
||||||
|
|
||||||
1. Builds the boot JAR with the release tag as the version
|
|
||||||
2. Uploads the JAR as a release asset
|
|
||||||
3. Builds and pushes a multi-arch Docker image to GHCR tagged with both `:latest` and `:<version>`
|
|
||||||
|
|
||||||
No tests are run in CI by design — tests are expected to pass locally before a release is cut.
|
|
||||||
|
|
||||||
|
|
||||||
## Profiles
|
|
||||||
|
|
||||||
| Profile | Purpose |
|
|
||||||
|---------|-----------------------------------------------------------------------|
|
|
||||||
| `dev` | Enables Swagger UI, connects to dev DB/Redis at `dfguide.onixbyte.cn` |
|
|
||||||
| default | Production mode, no Swagger, uses production datasource |
|
|
||||||
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
src/main/java/com/onixbyte/deltaforceguide/
|
|
||||||
├── client/ External service HTTP clients
|
|
||||||
├── config/ Spring bean definitions (Security, CORS, Cache, Jackson, MyBatis)
|
|
||||||
├── controller/ REST controllers
|
|
||||||
├── domain/
|
|
||||||
│ ├── converter/ JPA attribute converters
|
|
||||||
│ ├── dto/ Request/response records
|
|
||||||
│ └── entity/ JPA entities
|
|
||||||
├── enumeration/ Enums (FirearmType)
|
|
||||||
├── exception/ Custom BizException with HTTP status mapping
|
|
||||||
├── filter/ TokenAuthenticationFilter (JWT through OncePerRequestFilter)
|
|
||||||
├── manager/ @Transactional wrappers around repositories
|
|
||||||
├── mapper/ MyBatis mapper interfaces (reserved for future use)
|
|
||||||
├── properties/ @ConfigurationProperties records
|
|
||||||
├── repository/ Spring Data JPA repositories
|
|
||||||
├── security/
|
|
||||||
│ ├── authentication/ Custom UsernamePasswordAuthentication
|
|
||||||
│ └── provider/ UsernamePasswordAuthenticationProvider
|
|
||||||
├── service/ Business logic
|
|
||||||
├── shared/ Constants and utility classes
|
|
||||||
└── utils/ General-purpose helpers
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Versioning
|
|
||||||
|
|
||||||
The application exposes its version at `/versions` (GET).
|
|
||||||
|
|
||||||
|
|
||||||
## Licence
|
|
||||||
|
|
||||||
This project is licensed under the [MIT Licence](LICENCE). Copyright © 2026 OnixByte.
|
|
||||||
|
|
||||||
+16
-11
@@ -23,6 +23,22 @@ java {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate META-INF/build-info.properties at build time. The build version comes
|
||||||
|
// from the project version (driven by the git tag in CI); channel and vendor are
|
||||||
|
// stamped alongside it so the runtime no longer needs template placeholders.
|
||||||
|
springBoot {
|
||||||
|
buildInfo {
|
||||||
|
properties {
|
||||||
|
additional.set(
|
||||||
|
mapOf(
|
||||||
|
"channel" to buildChannel,
|
||||||
|
"vendor" to vendor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// BOMs are imported as platforms, so they cannot be bundled.
|
// BOMs are imported as platforms, so they cannot be bundled.
|
||||||
implementation(platform(libs.aws.sdk.bom))
|
implementation(platform(libs.aws.sdk.bom))
|
||||||
@@ -48,17 +64,6 @@ dependencies {
|
|||||||
testRuntimeOnly(libs.bundles.testRuntime)
|
testRuntimeOnly(libs.bundles.testRuntime)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.processResources {
|
|
||||||
filesMatching("application.yaml") {
|
|
||||||
println("appVersion = ${artefactVersion}, channel = ${buildChannel}, vendor = ${vendor}")
|
|
||||||
expand(
|
|
||||||
"appVersion" to artefactVersion,
|
|
||||||
"channel" to buildChannel,
|
|
||||||
"vendor" to vendor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
@@ -29,6 +29,47 @@ spring:
|
|||||||
# Uncomment if the server requires a password.
|
# Uncomment if the server requires a password.
|
||||||
#
|
#
|
||||||
# password: qwerty
|
# password: qwerty
|
||||||
|
mail:
|
||||||
|
# SMTP endpoint.
|
||||||
|
host: smtp.example.com
|
||||||
|
|
||||||
|
# SMTP port.
|
||||||
|
#
|
||||||
|
# 465 uses implicit TLS (SMTPS). If outbound 465 is blocked on the network, switch to 587 and
|
||||||
|
# replace the `ssl` property below with `starttls.enable` instead.
|
||||||
|
port: 465
|
||||||
|
|
||||||
|
# Sending address, which is also used as the `From` address of every dispatched message.
|
||||||
|
#
|
||||||
|
# It MUST belong to a verified sending domain in the Tencent Cloud Email console.
|
||||||
|
username: app@example.com
|
||||||
|
|
||||||
|
# SMTP password of the sending address above, as configured in the Tencent Cloud Email console.
|
||||||
|
password: 123456
|
||||||
|
|
||||||
|
# Encoding applied to the message headers and body.
|
||||||
|
default-encoding: UTF-8
|
||||||
|
|
||||||
|
properties:
|
||||||
|
mail:
|
||||||
|
smtp:
|
||||||
|
# Whether the SMTP server requires authentication.
|
||||||
|
auth: true
|
||||||
|
|
||||||
|
# Implicit TLS, required by port 465.
|
||||||
|
#
|
||||||
|
# When using port 587 + STARTTLS instead, drop this property, then enable STARTTLS in
|
||||||
|
# its place:
|
||||||
|
#
|
||||||
|
# starttls:
|
||||||
|
# enable: true
|
||||||
|
ssl:
|
||||||
|
enable: true
|
||||||
|
|
||||||
|
# Fail fast instead of hanging when the SMTP server is unreachable.
|
||||||
|
connectiontimeout: 10000
|
||||||
|
timeout: 10000
|
||||||
|
writetimeout: 10000
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
pattern:
|
pattern:
|
||||||
|
|||||||
@@ -1,14 +1 @@
|
|||||||
pluginManagement {
|
|
||||||
repositories {
|
|
||||||
gradlePluginPortal()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencyResolutionManagement {
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rootProject.name = "ahsarah-guide-server"
|
rootProject.name = "ahsarah-guide-server"
|
||||||
@@ -2,10 +2,8 @@ package com.onixbyte.ahsarahguide.config;
|
|||||||
|
|
||||||
import com.onixbyte.ahsarahguide.interceptor.GitHubWebhookInterceptor;
|
import com.onixbyte.ahsarahguide.interceptor.GitHubWebhookInterceptor;
|
||||||
import com.onixbyte.ahsarahguide.interceptor.TrafficInterceptor;
|
import com.onixbyte.ahsarahguide.interceptor.TrafficInterceptor;
|
||||||
import com.onixbyte.ahsarahguide.properties.AppProperties;
|
|
||||||
import com.onixbyte.ahsarahguide.security.resolver.CurrentUserArgumentResolver;
|
import com.onixbyte.ahsarahguide.security.resolver.CurrentUserArgumentResolver;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
@@ -14,7 +12,6 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableConfigurationProperties(AppProperties.class)
|
|
||||||
public class AppConfig implements WebMvcConfigurer {
|
public class AppConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
private final TrafficInterceptor trafficInterceptor;
|
private final TrafficInterceptor trafficInterceptor;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package com.onixbyte.ahsarahguide.config;
|
package com.onixbyte.ahsarahguide.config;
|
||||||
|
|
||||||
import com.onixbyte.ahsarahguide.shared.JacksonRedisSerialiser;
|
import com.onixbyte.ahsarahguide.shared.JacksonRedisSerialiser;
|
||||||
|
import org.springframework.cache.CacheManager;
|
||||||
import org.springframework.cache.annotation.EnableCaching;
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
import org.springframework.data.redis.cache.RedisCacheConfiguration;
|
import org.springframework.data.redis.cache.RedisCacheConfiguration;
|
||||||
import org.springframework.data.redis.cache.RedisCacheManager;
|
import org.springframework.data.redis.cache.RedisCacheManager;
|
||||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
@@ -39,21 +41,24 @@ import java.time.Duration;
|
|||||||
public class CacheConfig {
|
public class CacheConfig {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a custom Redis cache manager with JSON serialisation support.
|
* Creates the application's primary cache manager, backed by Redis.
|
||||||
* <p>
|
* <p>
|
||||||
* This method configures a {@link RedisCacheManager} that uses string serialisation for cache
|
* This manager is marked {@link Primary}, so it is the one selected for any
|
||||||
* keys and {@link GenericJackson2JsonRedisSerializer} for cache values. This setup ensures that
|
* {@code @Cacheable} or {@code @CacheEvict} annotation that does not name a manager
|
||||||
* complex objects can be stored and retrieved from Redis cache whilst maintaining readability
|
* explicitly, as well as for any {@link CacheManager} that is injected by type.
|
||||||
* and compatibility with JSON-based systems.
|
* <p>
|
||||||
|
* Cache entries are keyed using string serialisation and values are stored as JSON via
|
||||||
|
* {@link JacksonRedisSerialiser}. Every entry expires after two hours, making this manager
|
||||||
|
* suitable for data that is expensive to compute but not required to survive for long.
|
||||||
*
|
*
|
||||||
* @param connectionFactory the Redis connection factory used to establish connections
|
* @param connectionFactory the Redis connection factory used to establish connections
|
||||||
* @return a configured {@link RedisCacheManager} with custom serialisation settings
|
* @return a fully configured {@link CacheManager} with a two-hour default entry TTL
|
||||||
|
* @see #longTermCacheManager(RedisConnectionFactory)
|
||||||
* @see RedisCacheManager
|
* @see RedisCacheManager
|
||||||
* @see GenericJackson2JsonRedisSerializer
|
|
||||||
* @see RedisSerializationContext
|
|
||||||
*/
|
*/
|
||||||
|
@Primary
|
||||||
@Bean
|
@Bean
|
||||||
public RedisCacheManager cacheManager(
|
public CacheManager defaultCacheManager(
|
||||||
RedisConnectionFactory connectionFactory
|
RedisConnectionFactory connectionFactory
|
||||||
) {
|
) {
|
||||||
var _keySerializer = RedisSerializer.string();
|
var _keySerializer = RedisSerializer.string();
|
||||||
@@ -63,7 +68,40 @@ public class CacheConfig {
|
|||||||
.fromSerializer(_keySerializer))
|
.fromSerializer(_keySerializer))
|
||||||
.serializeValuesWith(RedisSerializationContext.SerializationPair
|
.serializeValuesWith(RedisSerializationContext.SerializationPair
|
||||||
.fromSerializer(JacksonRedisSerialiser.INSTANCE))
|
.fromSerializer(JacksonRedisSerialiser.INSTANCE))
|
||||||
.entryTtl(Duration.ofMinutes(90L));
|
.entryTtl(Duration.ofHours(2L));
|
||||||
|
|
||||||
|
return RedisCacheManager.RedisCacheManagerBuilder
|
||||||
|
.fromConnectionFactory(connectionFactory)
|
||||||
|
.cacheDefaults(cacheConfiguration)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a secondary cache manager for data that must be retained for longer.
|
||||||
|
* <p>
|
||||||
|
* This manager is not {@link Primary}; it must be requested by name wherever it is needed,
|
||||||
|
* as in {@code @Cacheable(cacheManager = "longTermCacheManager")}.
|
||||||
|
* <p>
|
||||||
|
* Serialisation is configured identically to {@link #defaultCacheManager}, but entries
|
||||||
|
* expire after one day rather than two hours.
|
||||||
|
*
|
||||||
|
* @param connectionFactory the Redis connection factory used to establish connections
|
||||||
|
* @return a fully configured {@link CacheManager} with a one-day default entry TTL
|
||||||
|
* @see #defaultCacheManager(RedisConnectionFactory)
|
||||||
|
* @see RedisCacheManager
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public CacheManager longTermCacheManager(
|
||||||
|
RedisConnectionFactory connectionFactory
|
||||||
|
) {
|
||||||
|
var _keySerializer = RedisSerializer.string();
|
||||||
|
|
||||||
|
var cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
|
||||||
|
.serializeKeysWith(RedisSerializationContext.SerializationPair
|
||||||
|
.fromSerializer(_keySerializer))
|
||||||
|
.serializeValuesWith(RedisSerializationContext.SerializationPair
|
||||||
|
.fromSerializer(JacksonRedisSerialiser.INSTANCE))
|
||||||
|
.entryTtl(Duration.ofDays(1L));
|
||||||
|
|
||||||
return RedisCacheManager.RedisCacheManagerBuilder
|
return RedisCacheManager.RedisCacheManagerBuilder
|
||||||
.fromConnectionFactory(connectionFactory)
|
.fromConnectionFactory(connectionFactory)
|
||||||
|
|||||||
@@ -4,13 +4,16 @@ import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
|||||||
import io.swagger.v3.oas.annotations.info.Contact;
|
import io.swagger.v3.oas.annotations.info.Contact;
|
||||||
import io.swagger.v3.oas.annotations.info.Info;
|
import io.swagger.v3.oas.annotations.info.Info;
|
||||||
import io.swagger.v3.oas.annotations.info.License;
|
import io.swagger.v3.oas.annotations.info.License;
|
||||||
|
import io.swagger.v3.oas.models.OpenAPI;
|
||||||
|
import org.springdoc.core.customizers.OpenApiCustomizer;
|
||||||
|
import org.springframework.boot.info.BuildProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
@OpenAPIDefinition(
|
@OpenAPIDefinition(
|
||||||
info = @Info(
|
info = @Info(
|
||||||
title = "Ahsarah Guide Server",
|
title = "Ahsarah Guide Server",
|
||||||
description = "API for Ahsarah Guide",
|
description = "API for Ahsarah Guide",
|
||||||
version = "1.4.0",
|
|
||||||
contact = @Contact(
|
contact = @Contact(
|
||||||
name = "Zihlu Wang",
|
name = "Zihlu Wang",
|
||||||
email = "zihlu.wang@onixbyte.com"
|
email = "zihlu.wang@onixbyte.com"
|
||||||
@@ -23,4 +26,20 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
)
|
)
|
||||||
@Configuration
|
@Configuration
|
||||||
public class OpenApiConfiguration {
|
public class OpenApiConfiguration {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides the OpenAPI info version with the packaged build version so the
|
||||||
|
* docs always match the released artefact instead of a hard-coded value.
|
||||||
|
*
|
||||||
|
* @param buildProperties the build-info properties generated at build time
|
||||||
|
* @return customiser that stamps the build version onto the API info
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public OpenApiCustomizer versionCustomizer(BuildProperties buildProperties) {
|
||||||
|
return openApi -> {
|
||||||
|
if (openApi.getInfo() != null) {
|
||||||
|
openApi.getInfo().setVersion(buildProperties.getVersion());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
package com.onixbyte.ahsarahguide.manager;
|
package com.onixbyte.ahsarahguide.manager;
|
||||||
|
|
||||||
import com.onixbyte.ahsarahguide.properties.AppProperties;
|
import org.springframework.boot.info.BuildProperties;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class AppManager {
|
public class AppManager {
|
||||||
|
|
||||||
private final AppProperties appProperties;
|
private final BuildProperties buildProperties;
|
||||||
|
|
||||||
public AppManager(AppProperties appProperties) {
|
public AppManager(BuildProperties buildProperties) {
|
||||||
this.appProperties = appProperties;
|
this.buildProperties = buildProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,9 +19,9 @@ public class AppManager {
|
|||||||
*/
|
*/
|
||||||
public String getVersion() {
|
public String getVersion() {
|
||||||
return "v%s-%s by @%s".formatted(
|
return "v%s-%s by @%s".formatted(
|
||||||
appProperties.version(),
|
buildProperties.getVersion(),
|
||||||
appProperties.channel(),
|
buildProperties.get("channel"),
|
||||||
appProperties.vendor()
|
buildProperties.get("vendor")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,16 +4,15 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||||
import com.onixbyte.ahsarahguide.domain.dto.DailyPasswordResponse;
|
import com.onixbyte.ahsarahguide.domain.dto.DailyPasswordResponse;
|
||||||
import com.onixbyte.ahsarahguide.exeption.InternalServerErrorException;
|
|
||||||
import com.onixbyte.ahsarahguide.shared.JacksonModules;
|
import com.onixbyte.ahsarahguide.shared.JacksonModules;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.client.RestClient;
|
import org.springframework.web.client.RestClient;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,15 +23,13 @@ import java.util.Objects;
|
|||||||
@Component
|
@Component
|
||||||
public class DailyPasswordManager {
|
public class DailyPasswordManager {
|
||||||
|
|
||||||
private static final String CACHE_KEY_PREFIX = "daily-password:";
|
private static final String CACHE_NAME = "daily-password";
|
||||||
|
|
||||||
private final RestClient restClient;
|
private final RestClient restClient;
|
||||||
private final RedisTemplate<String, Object> redisTemplate;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public DailyPasswordManager(
|
public DailyPasswordManager(
|
||||||
RestClient.Builder restClientBuilder,
|
RestClient.Builder restClientBuilder
|
||||||
RedisTemplate<String, Object> redisTemplate
|
|
||||||
) {
|
) {
|
||||||
var snakeCaseMapper = new ObjectMapper();
|
var snakeCaseMapper = new ObjectMapper();
|
||||||
snakeCaseMapper.setPropertyNamingStrategy(
|
snakeCaseMapper.setPropertyNamingStrategy(
|
||||||
@@ -50,22 +47,23 @@ public class DailyPasswordManager {
|
|||||||
new MappingJackson2HttpMessageConverter(snakeCaseMapper));
|
new MappingJackson2HttpMessageConverter(snakeCaseMapper));
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
this.redisTemplate = redisTemplate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the daily password from cache or generates a new one.
|
* Retrieves the daily password from cache or generates a new one.
|
||||||
* @return the daily password response
|
* @return the daily password response
|
||||||
*/
|
*/
|
||||||
|
@Cacheable(
|
||||||
|
cacheNames = CACHE_NAME,
|
||||||
|
key = "T(java.time.LocalDate).now().toString()",
|
||||||
|
sync = true,
|
||||||
|
unless = "#result == null",
|
||||||
|
cacheManager = "longTermCacheManager"
|
||||||
|
)
|
||||||
public DailyPasswordResponse getDailyPassword() {
|
public DailyPasswordResponse getDailyPassword() {
|
||||||
var key = CACHE_KEY_PREFIX + LocalDate.now();
|
DailyPasswordResponse response;
|
||||||
|
try {
|
||||||
var cached = redisTemplate.opsForValue().get(key);
|
response = restClient.get()
|
||||||
if (cached != null) {
|
|
||||||
return (DailyPasswordResponse) cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
var response = restClient.get()
|
|
||||||
.uri((uriBuilder) -> uriBuilder
|
.uri((uriBuilder) -> uriBuilder
|
||||||
.path("/sjzmm")
|
.path("/sjzmm")
|
||||||
.queryParam("ckey", "")
|
.queryParam("ckey", "")
|
||||||
@@ -73,12 +71,15 @@ public class DailyPasswordManager {
|
|||||||
.build())
|
.build())
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.body(DailyPasswordResponse.class);
|
.body(DailyPasswordResponse.class);
|
||||||
|
} catch (Exception e) {
|
||||||
if (Objects.isNull(response)) {
|
// Catch network errors, timeouts, or 4xx/5xx responses from the upstream service
|
||||||
throw new InternalServerErrorException("暂无每日密码数据。");
|
throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "无法获取每日密码数据。", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Objects.isNull(response)) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "无法获取每日密码数据。");
|
||||||
}
|
}
|
||||||
|
|
||||||
redisTemplate.opsForValue().set(key, response, Duration.ofDays(1L));
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.onixbyte.ahsarahguide.properties;
|
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
|
|
||||||
@ConfigurationProperties(prefix = "app.common")
|
|
||||||
public record AppProperties(
|
|
||||||
String version,
|
|
||||||
String channel,
|
|
||||||
String vendor
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide;
|
|
||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entry point for the Delta Force Guide Server application.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@SpringBootApplication
|
|
||||||
public class DeltaForceGuideApplication {
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
SpringApplication.run(DeltaForceGuideApplication.class, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.client;
|
|
||||||
|
|
||||||
import com.auth0.jwt.JWT;
|
|
||||||
import com.auth0.jwt.JWTVerifier;
|
|
||||||
import com.auth0.jwt.algorithms.Algorithm;
|
|
||||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
|
||||||
import com.onixbyte.deltaforceguide.domain.entity.User;
|
|
||||||
import com.onixbyte.deltaforceguide.properties.TokenProperties;
|
|
||||||
import com.onixbyte.deltaforceguide.utils.DateTimeUtil;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Client for generating and verifying JSON Web Tokens using the Auth0 java-jwt library.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
public class TokenClient {
|
|
||||||
|
|
||||||
private final Algorithm algorithm;
|
|
||||||
private final TokenProperties tokenProperties;
|
|
||||||
private final JWTVerifier verifier;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new TokenClient with the necessary algorithm and token properties.
|
|
||||||
*
|
|
||||||
* @param algorithm the signing algorithm used to secure the JWT
|
|
||||||
* @param tokenProperties the configuration properties for the token, such as issuer and
|
|
||||||
* validity period
|
|
||||||
*/
|
|
||||||
@Autowired
|
|
||||||
public TokenClient(
|
|
||||||
Algorithm algorithm,
|
|
||||||
TokenProperties tokenProperties,
|
|
||||||
JWTVerifier verifier
|
|
||||||
) {
|
|
||||||
this.algorithm = algorithm;
|
|
||||||
this.tokenProperties = tokenProperties;
|
|
||||||
this.verifier = verifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a JSON Web Token to the current user.
|
|
||||||
*
|
|
||||||
* @param user the current user for whom the token is being generated
|
|
||||||
* @return a JWT string
|
|
||||||
*/
|
|
||||||
public String generateToken(User user) {
|
|
||||||
var issuedAt = LocalDateTime.now();
|
|
||||||
var expiresAt = issuedAt.plus(tokenProperties.validTime());
|
|
||||||
|
|
||||||
return JWT.create()
|
|
||||||
.withSubject(user.getUsername())
|
|
||||||
.withIssuer(tokenProperties.issuer())
|
|
||||||
.withIssuedAt(DateTimeUtil.asInstant(issuedAt))
|
|
||||||
.withExpiresAt(DateTimeUtil.asInstant(expiresAt))
|
|
||||||
.sign(algorithm);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify and decode token.
|
|
||||||
*
|
|
||||||
* @param token a JWT token
|
|
||||||
* @return information included in the given token
|
|
||||||
* @throws com.auth0.jwt.exceptions.JWTVerificationException if the token is invalid, such as
|
|
||||||
* expired, or not signed by
|
|
||||||
* specific server
|
|
||||||
*/
|
|
||||||
public DecodedJWT verifyToken(String token) {
|
|
||||||
return verifier.verify(token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.config;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.interceptor.GitHubWebhookInterceptor;
|
|
||||||
import com.onixbyte.deltaforceguide.interceptor.TrafficInterceptor;
|
|
||||||
import com.onixbyte.deltaforceguide.properties.AppProperties;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@EnableConfigurationProperties(AppProperties.class)
|
|
||||||
public class AppConfig implements WebMvcConfigurer {
|
|
||||||
|
|
||||||
private final TrafficInterceptor trafficInterceptor;
|
|
||||||
|
|
||||||
private final GitHubWebhookInterceptor gitHubWebhookInterceptor;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public AppConfig(
|
|
||||||
TrafficInterceptor trafficInterceptor,
|
|
||||||
GitHubWebhookInterceptor gitHubWebhookInterceptor
|
|
||||||
) {
|
|
||||||
this.trafficInterceptor = trafficInterceptor;
|
|
||||||
this.gitHubWebhookInterceptor = gitHubWebhookInterceptor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addInterceptors(InterceptorRegistry registry) {
|
|
||||||
registry.addInterceptor(trafficInterceptor);
|
|
||||||
registry.addInterceptor(gitHubWebhookInterceptor)
|
|
||||||
.addPathPatterns("/webhooks/github");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.config;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.shared.JacksonRedisSerialiser;
|
|
||||||
import org.springframework.cache.annotation.EnableCaching;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.data.redis.cache.RedisCacheConfiguration;
|
|
||||||
import org.springframework.data.redis.cache.RedisCacheManager;
|
|
||||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
|
||||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
|
||||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
|
||||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration class for Redis-based caching components.
|
|
||||||
* <p>
|
|
||||||
* This configuration class provides beans for Redis cache management and template operations
|
|
||||||
* within the Helix application. It configures custom serialisation strategies using
|
|
||||||
* {@link GenericJackson2JsonRedisSerializer} for values and string serialisation for keys,
|
|
||||||
* ensuring optimal performance and compatibility with JSON-based data structures.
|
|
||||||
* <p>
|
|
||||||
* The configuration includes:
|
|
||||||
* <ul>
|
|
||||||
* <li>Custom {@link RedisCacheManager} with JSON serialisation support</li>
|
|
||||||
* <li>Configured {@link RedisTemplate} for direct Redis operations</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
* @see RedisCacheManager
|
|
||||||
* @see RedisTemplate
|
|
||||||
* @see GenericJackson2JsonRedisSerializer
|
|
||||||
* @since 1.0.0
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
@EnableCaching
|
|
||||||
public class CacheConfig {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a custom Redis cache manager with JSON serialisation support.
|
|
||||||
* <p>
|
|
||||||
* This method configures a {@link RedisCacheManager} that uses string serialisation for cache
|
|
||||||
* keys and {@link GenericJackson2JsonRedisSerializer} for cache values. This setup ensures that
|
|
||||||
* complex objects can be stored and retrieved from Redis cache whilst maintaining readability
|
|
||||||
* and compatibility with JSON-based systems.
|
|
||||||
*
|
|
||||||
* @param connectionFactory the Redis connection factory used to establish connections
|
|
||||||
* @return a configured {@link RedisCacheManager} with custom serialisation settings
|
|
||||||
* @see RedisCacheManager
|
|
||||||
* @see GenericJackson2JsonRedisSerializer
|
|
||||||
* @see RedisSerializationContext
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public RedisCacheManager cacheManager(
|
|
||||||
RedisConnectionFactory connectionFactory
|
|
||||||
) {
|
|
||||||
var _keySerializer = RedisSerializer.string();
|
|
||||||
|
|
||||||
var cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
|
|
||||||
.serializeKeysWith(RedisSerializationContext.SerializationPair
|
|
||||||
.fromSerializer(_keySerializer))
|
|
||||||
.serializeValuesWith(RedisSerializationContext.SerializationPair
|
|
||||||
.fromSerializer(JacksonRedisSerialiser.INSTANCE))
|
|
||||||
.entryTtl(Duration.ofMinutes(90L));
|
|
||||||
|
|
||||||
return RedisCacheManager.RedisCacheManagerBuilder
|
|
||||||
.fromConnectionFactory(connectionFactory)
|
|
||||||
.cacheDefaults(cacheConfiguration)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a Redis template for direct Redis operations with custom serialisation.
|
|
||||||
* <p>
|
|
||||||
* This method configures a {@link RedisTemplate} that uses string serialisation for keys
|
|
||||||
* and {@link GenericJackson2JsonRedisSerializer} for values. This template provides low-level
|
|
||||||
* access to Redis operations whilst ensuring consistent serialisation strategies across
|
|
||||||
* the application.
|
|
||||||
* <p>
|
|
||||||
* The template is fully configured and ready for use after bean creation.
|
|
||||||
*
|
|
||||||
* @param connectionFactory the Redis connection factory used to establish connections
|
|
||||||
* @return a fully configured {@link RedisTemplate} for Redis operations
|
|
||||||
* @see RedisTemplate
|
|
||||||
* @see GenericJackson2JsonRedisSerializer
|
|
||||||
* @see RedisSerializer
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public RedisTemplate<String, Object> redisTemplate(
|
|
||||||
RedisConnectionFactory connectionFactory
|
|
||||||
) {
|
|
||||||
var redisTemplate = new RedisTemplate<String, Object>();
|
|
||||||
redisTemplate.setConnectionFactory(connectionFactory);
|
|
||||||
redisTemplate.setKeySerializer(RedisSerializer.string());
|
|
||||||
redisTemplate.setValueSerializer(JacksonRedisSerialiser.INSTANCE);
|
|
||||||
redisTemplate.afterPropertiesSet();
|
|
||||||
return redisTemplate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.config;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.properties.CorsProperties;
|
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.http.HttpMethod;
|
|
||||||
import org.springframework.web.cors.CorsConfiguration;
|
|
||||||
import org.springframework.web.cors.CorsConfigurationSource;
|
|
||||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration for CORS (Cross-Origin Resource Sharing) policies.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
@EnableConfigurationProperties({CorsProperties.class})
|
|
||||||
public class CorsConfig {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the CORS configuration source with properties from configuration.
|
|
||||||
*
|
|
||||||
* @param properties the CORS configuration properties
|
|
||||||
* @return the CORS configuration source
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public CorsConfigurationSource corsConfigurationSource(
|
|
||||||
CorsProperties properties
|
|
||||||
) {
|
|
||||||
var corsConfiguration = new CorsConfiguration();
|
|
||||||
corsConfiguration.setAllowCredentials(properties.allowCredentials());
|
|
||||||
corsConfiguration.setAllowedOrigins(List.of(properties.allowedOrigins()));
|
|
||||||
corsConfiguration.setAllowedHeaders(List.of(properties.allowedHeaders()));
|
|
||||||
corsConfiguration.setAllowedMethods(Stream.of(properties.allowedMethods())
|
|
||||||
.map(HttpMethod::name)
|
|
||||||
.toList());
|
|
||||||
corsConfiguration.setMaxAge(properties.maxAge());
|
|
||||||
corsConfiguration.setAllowPrivateNetwork(properties.allowPrivateNetwork());
|
|
||||||
corsConfiguration.setExposedHeaders(List.of(properties.exposedHeaders()));
|
|
||||||
|
|
||||||
var corsConfigurationSource = new UrlBasedCorsConfigurationSource();
|
|
||||||
corsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
|
|
||||||
return corsConfigurationSource;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.config;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.filter.WebhookFilter;
|
|
||||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class FilterConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public FilterRegistrationBean<WebhookFilter> webhookFilterBean(WebhookFilter webhookFilter) {
|
|
||||||
var registrationBean = new FilterRegistrationBean<WebhookFilter>();
|
|
||||||
|
|
||||||
registrationBean.setFilter(webhookFilter);
|
|
||||||
registrationBean.addUrlPatterns("/webhooks/*");
|
|
||||||
registrationBean.setOrder(1);
|
|
||||||
|
|
||||||
return registrationBean;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.config;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
|
||||||
import com.onixbyte.deltaforceguide.shared.JacksonModules;
|
|
||||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration for Jackson JSON serialisation and deserialisation settings.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
public class JacksonConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public Jackson2ObjectMapperBuilderCustomizer jacksonCustomiser() {
|
|
||||||
return (builder) -> {
|
|
||||||
builder.modules(JacksonModules.DATE_TIME_MODULE);
|
|
||||||
builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.config;
|
|
||||||
|
|
||||||
import org.mybatis.spring.annotation.MapperScan;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration for MyBatis SQL mapping framework integration.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
@MapperScan(basePackages = {"com.onixbyte.deltaforceguide.mapper"})
|
|
||||||
public class MyBatisConfig {
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.config;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
|
||||||
import io.swagger.v3.oas.annotations.info.Contact;
|
|
||||||
import io.swagger.v3.oas.annotations.info.Info;
|
|
||||||
import io.swagger.v3.oas.annotations.info.License;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
@OpenAPIDefinition(
|
|
||||||
info = @Info(
|
|
||||||
title = "Delta Force Guide Server",
|
|
||||||
description = "API for managing Delta Force game firearm builds",
|
|
||||||
version = "1.3.4",
|
|
||||||
contact = @Contact(
|
|
||||||
name = "Zihlu Wang",
|
|
||||||
email = "zihlu.wang@onixbyte.com"
|
|
||||||
),
|
|
||||||
license = @License(
|
|
||||||
name = "MIT",
|
|
||||||
url = "https://git.onixbyte.cn/onixbyte/delta-force-guide-server/-/raw/main/LICENCE"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@Configuration
|
|
||||||
public class OpenApiConfiguration {
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.config;
|
|
||||||
|
|
||||||
import com.auth0.jwt.JWT;
|
|
||||||
import com.auth0.jwt.JWTVerifier;
|
|
||||||
import com.auth0.jwt.algorithms.Algorithm;
|
|
||||||
import com.onixbyte.deltaforceguide.filter.TokenAuthenticationFilter;
|
|
||||||
import com.onixbyte.deltaforceguide.properties.CookieProperties;
|
|
||||||
import com.onixbyte.deltaforceguide.properties.TokenProperties;
|
|
||||||
import com.onixbyte.deltaforceguide.security.provider.UsernamePasswordAuthenticationProvider;
|
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
|
||||||
import org.springframework.security.authentication.ProviderManager;
|
|
||||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
|
||||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
|
||||||
import org.springframework.security.web.access.ExceptionTranslationFilter;
|
|
||||||
import org.springframework.web.cors.CorsConfigurationSource;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Spring Security configuration defining authentication, authorisation, and filter chains.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
@EnableWebSecurity
|
|
||||||
@EnableMethodSecurity
|
|
||||||
@EnableConfigurationProperties({TokenProperties.class, CookieProperties.class})
|
|
||||||
public class SecurityConfig {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures the HTTP security filter chain including endpoint authorisation and JWT filter.
|
|
||||||
*
|
|
||||||
* @param http the HTTP security builder
|
|
||||||
* @return the configured security filter chain
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public SecurityFilterChain securityFilterChain(
|
|
||||||
HttpSecurity httpSecurity,
|
|
||||||
CorsConfigurationSource corsConfigurationSource,
|
|
||||||
TokenAuthenticationFilter tokenAuthenticationFilter
|
|
||||||
) throws Exception {
|
|
||||||
return httpSecurity
|
|
||||||
.cors((cors) -> cors
|
|
||||||
.configurationSource(corsConfigurationSource))
|
|
||||||
.csrf(AbstractHttpConfigurer::disable)
|
|
||||||
.sessionManagement((customiser) -> customiser
|
|
||||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
|
||||||
.authorizeHttpRequests((customiser) -> customiser
|
|
||||||
.anyRequest().permitAll()
|
|
||||||
)
|
|
||||||
.addFilterAfter(tokenAuthenticationFilter, ExceptionTranslationFilter.class)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the BCrypt password encoder for credential hashing.
|
|
||||||
* @return the BCrypt password encoder
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public PasswordEncoder passwordEncoder() {
|
|
||||||
return new BCryptPasswordEncoder();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the authentication manager for the security configuration.
|
|
||||||
*
|
|
||||||
* @return the authentication manager
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public AuthenticationManager authenticationManager(
|
|
||||||
UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider
|
|
||||||
) {
|
|
||||||
return new ProviderManager(
|
|
||||||
usernamePasswordAuthenticationProvider
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public Algorithm algorithm(TokenProperties properties) {
|
|
||||||
return Algorithm.HMAC256(properties.secret());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public JWTVerifier verifier(Algorithm algorithm, TokenProperties tokenProperties) {
|
|
||||||
return JWT.require(algorithm)
|
|
||||||
.withIssuer(tokenProperties.issuer())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.config;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration for Spring Data JPA auditing and repository settings.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
@EnableJpaRepositories(basePackages = {"com.onixbyte.deltaforceguide.repository"})
|
|
||||||
public class SpringDataConfig {
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.config;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.properties.GitHubWebhookProperties;
|
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@EnableConfigurationProperties({GitHubWebhookProperties.class})
|
|
||||||
public class WebhookConfig {
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.controller;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.domain.dto.LoginRequest;
|
|
||||||
import com.onixbyte.deltaforceguide.domain.dto.UserResponse;
|
|
||||||
import com.onixbyte.deltaforceguide.client.TokenClient;
|
|
||||||
import com.onixbyte.deltaforceguide.security.annotation.RequiresAuth;
|
|
||||||
import com.onixbyte.deltaforceguide.service.AuthService;
|
|
||||||
import com.onixbyte.deltaforceguide.service.CookieService;
|
|
||||||
import com.onixbyte.deltaforceguide.shared.CookieName;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.validation.annotation.Validated;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* REST controller for user authentication endpoints (login, logout).
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Tag(name = "用户鉴权", description = "处理用户登录与退出功能")
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/auth")
|
|
||||||
public class AuthController {
|
|
||||||
|
|
||||||
private final AuthService authService;
|
|
||||||
private final TokenClient tokenClient;
|
|
||||||
private final CookieService cookieService;
|
|
||||||
|
|
||||||
public AuthController(AuthService authService, TokenClient tokenClient, CookieService cookieService) {
|
|
||||||
this.authService = authService;
|
|
||||||
this.tokenClient = tokenClient;
|
|
||||||
this.cookieService = cookieService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(description = "用户登录")
|
|
||||||
@PostMapping("/login")
|
|
||||||
public ResponseEntity<UserResponse> login(@Validated @RequestBody LoginRequest request) {
|
|
||||||
var user = authService.login(request);
|
|
||||||
var currentTime = LocalDateTime.now();
|
|
||||||
var accessToken = tokenClient.generateToken(user);
|
|
||||||
var accessTokenCookie = cookieService.buildCookie(CookieName.ACCESS_TOKEN, accessToken);
|
|
||||||
var cookieMaxAge = accessTokenCookie.getMaxAge();
|
|
||||||
|
|
||||||
return ResponseEntity.ok()
|
|
||||||
.header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString())
|
|
||||||
.body(UserResponse.from(user, currentTime.plus(cookieMaxAge)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresAuth
|
|
||||||
@Operation(description = "退出登录")
|
|
||||||
@PostMapping("/logout")
|
|
||||||
public ResponseEntity<Void> logout() {
|
|
||||||
var expiredCookie = cookieService.buildCookie(CookieName.ACCESS_TOKEN, "", Duration.ZERO);
|
|
||||||
return ResponseEntity.noContent()
|
|
||||||
.header(HttpHeaders.SET_COOKIE, expiredCookie.toString())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-32
@@ -1,32 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.controller;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.domain.dto.DailyPasswordResponse;
|
|
||||||
import com.onixbyte.deltaforceguide.service.DailyPasswordService;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* REST controller for retrieving daily-generated passwords.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Tag(name = "每日密码", description = "获取每日密码信息")
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/daily-passwords")
|
|
||||||
public class DailyPasswordController {
|
|
||||||
|
|
||||||
private final DailyPasswordService dailyPasswordService;
|
|
||||||
|
|
||||||
public DailyPasswordController(DailyPasswordService dailyPasswordService) {
|
|
||||||
this.dailyPasswordService = dailyPasswordService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(description = "获取当日的每日密码数据,该数据将被缓存一天")
|
|
||||||
@GetMapping
|
|
||||||
public DailyPasswordResponse getDailyPassword() {
|
|
||||||
return dailyPasswordService.getDailyPassword();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.controller;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.domain.dto.FirearmRequest;
|
|
||||||
import com.onixbyte.deltaforceguide.domain.dto.FirearmResponse;
|
|
||||||
import com.onixbyte.deltaforceguide.domain.dto.PageResponse;
|
|
||||||
import com.onixbyte.deltaforceguide.enumeration.FirearmType;
|
|
||||||
import com.onixbyte.deltaforceguide.security.annotation.RequiresAuth;
|
|
||||||
import com.onixbyte.deltaforceguide.service.FirearmService;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
import jakarta.validation.constraints.Max;
|
|
||||||
import jakarta.validation.constraints.Min;
|
|
||||||
import org.springframework.data.domain.PageRequest;
|
|
||||||
import org.springframework.data.domain.Sort;
|
|
||||||
import org.springframework.validation.annotation.Validated;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* REST controller for firearm CRUD operations.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Tag(name = "武器管理", description = "与武器有关的操作")
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/firearms")
|
|
||||||
public class FirearmController {
|
|
||||||
|
|
||||||
private final FirearmService firearmService;
|
|
||||||
|
|
||||||
public FirearmController(FirearmService firearmService) {
|
|
||||||
this.firearmService = firearmService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(description = "获取分页武器数据")
|
|
||||||
@Validated
|
|
||||||
@GetMapping
|
|
||||||
public PageResponse<FirearmResponse> pageQuery(
|
|
||||||
@RequestParam(defaultValue = "0") @Min(0) int page,
|
|
||||||
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int size,
|
|
||||||
@RequestParam(defaultValue = "id") String sortBy,
|
|
||||||
@RequestParam(defaultValue = "DESC") Sort.Direction direction,
|
|
||||||
@RequestParam(required = false) FirearmType type
|
|
||||||
) {
|
|
||||||
return firearmService.pageQuery(type, PageRequest.of(page, size, Sort.by(direction, sortBy)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(description = "获取指定武器的数据")
|
|
||||||
@GetMapping("/{id}")
|
|
||||||
public FirearmResponse queryById(@PathVariable Long id) {
|
|
||||||
return firearmService.queryById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresAuth
|
|
||||||
@PostMapping
|
|
||||||
public FirearmResponse addFirearm(@Validated @RequestBody FirearmRequest request) {
|
|
||||||
return firearmService.addFirearm(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresAuth
|
|
||||||
@Operation(description = "更新指定武器的数据")
|
|
||||||
@PutMapping("/{id}")
|
|
||||||
public FirearmResponse updateFirearm(@PathVariable Long id, @Validated @RequestBody FirearmRequest request) {
|
|
||||||
return firearmService.updateFirearm(id, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresAuth
|
|
||||||
@Operation(description = "删除指定武器的数据")
|
|
||||||
@DeleteMapping("/{id}")
|
|
||||||
public void deleteFirearm(@PathVariable Long id) {
|
|
||||||
firearmService.deleteFirearm(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-43
@@ -1,43 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.controller;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.domain.dto.GitHubIssueRequest;
|
|
||||||
import com.onixbyte.deltaforceguide.service.WebhookService;
|
|
||||||
import com.onixbyte.deltaforceguide.shared.GitHubWebhookHeader;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
import org.springframework.web.bind.annotation.RequestHeader;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/webhooks/github")
|
|
||||||
public class GitHubWebhookController {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(GitHubWebhookController.class);
|
|
||||||
|
|
||||||
private final WebhookService webhookService;
|
|
||||||
|
|
||||||
public GitHubWebhookController(WebhookService webhookService) {
|
|
||||||
this.webhookService = webhookService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping
|
|
||||||
public ResponseEntity<Void> handleWebhook(
|
|
||||||
@RequestHeader(GitHubWebhookHeader.EVENT) String event,
|
|
||||||
@RequestBody GitHubIssueRequest request
|
|
||||||
) {
|
|
||||||
if (!"issues".equals(event)) {
|
|
||||||
log.debug("Ignoring non-issue event: {}", event);
|
|
||||||
return ResponseEntity.ok().build();
|
|
||||||
}
|
|
||||||
if (!"opened".equals(request.action())) {
|
|
||||||
log.debug("Ignoring issue action: {}", request.action());
|
|
||||||
return ResponseEntity.ok().build();
|
|
||||||
}
|
|
||||||
webhookService.processIssueEvent(request);
|
|
||||||
return ResponseEntity.ok().build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.controller;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.domain.dto.ErrorResponse;
|
|
||||||
import com.onixbyte.deltaforceguide.exeption.BizException;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Global exception handler that translates exceptions into standard error responses.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@RestControllerAdvice
|
|
||||||
public class GlobalExceptionHandler {
|
|
||||||
|
|
||||||
@ExceptionHandler(BizException.class)
|
|
||||||
public ResponseEntity<ErrorResponse> handleBizException(BizException exception) {
|
|
||||||
var status = exception.getStatus();
|
|
||||||
return ResponseEntity.status(status)
|
|
||||||
.body(new ErrorResponse(exception.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
-101
@@ -1,101 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.controller;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.domain.dto.ModificationBatchCreateRequest;
|
|
||||||
import com.onixbyte.deltaforceguide.domain.dto.ModificationRequest;
|
|
||||||
import com.onixbyte.deltaforceguide.domain.dto.ModificationResponse;
|
|
||||||
import com.onixbyte.deltaforceguide.domain.dto.PageResponse;
|
|
||||||
import com.onixbyte.deltaforceguide.security.annotation.RequiresAuth;
|
|
||||||
import com.onixbyte.deltaforceguide.service.ModificationService;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import jakarta.validation.constraints.Max;
|
|
||||||
import jakarta.validation.constraints.Min;
|
|
||||||
import jakarta.validation.constraints.Positive;
|
|
||||||
import org.springframework.data.domain.PageRequest;
|
|
||||||
import org.springframework.data.domain.Sort;
|
|
||||||
import org.springframework.validation.annotation.Validated;
|
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PutMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* REST controller for modification CRUD operations, including batch creation and deletion.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Tag(name = "改装管理", description = "对枪械改装的管理")
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/modifications")
|
|
||||||
public class ModificationController {
|
|
||||||
|
|
||||||
private final ModificationService modificationService;
|
|
||||||
|
|
||||||
public ModificationController(ModificationService modificationService) {
|
|
||||||
this.modificationService = modificationService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(description = "分页查询改装信息")
|
|
||||||
@Validated
|
|
||||||
@GetMapping
|
|
||||||
public PageResponse<ModificationResponse> pageQuery(
|
|
||||||
@RequestParam(defaultValue = "0") @Min(0) int page,
|
|
||||||
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int size,
|
|
||||||
@RequestParam(required = false) @Positive Long firearmId,
|
|
||||||
@RequestParam(defaultValue = "id") String sortBy,
|
|
||||||
@RequestParam(defaultValue = "DESC") Sort.Direction direction,
|
|
||||||
@RequestParam(required = false) List<String> tags
|
|
||||||
) {
|
|
||||||
return modificationService.pageQuery(firearmId, tags, PageRequest.of(page, size, Sort.by(direction, sortBy)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(description = "查询指定改装的信息")
|
|
||||||
@GetMapping("/{id}")
|
|
||||||
public ModificationResponse queryById(@PathVariable Long id) {
|
|
||||||
return modificationService.queryById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresAuth
|
|
||||||
@Operation(description = "创建改装")
|
|
||||||
@PostMapping
|
|
||||||
public ModificationResponse create(@Valid @RequestBody ModificationRequest request) {
|
|
||||||
return modificationService.create(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresAuth
|
|
||||||
@Operation(description = "批量创建改装")
|
|
||||||
@PostMapping("/batch")
|
|
||||||
public List<ModificationResponse> batchCreate(@Valid @RequestBody ModificationBatchCreateRequest request) {
|
|
||||||
return modificationService.batchCreate(request.modifications());
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresAuth
|
|
||||||
@Operation(description = "修改指定改装")
|
|
||||||
@PutMapping("/{id}")
|
|
||||||
public ModificationResponse update(@PathVariable Long id, @Valid @RequestBody ModificationRequest request) {
|
|
||||||
return modificationService.update(id, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresAuth
|
|
||||||
@Operation(description = "删除指定改装")
|
|
||||||
@DeleteMapping("/{id}")
|
|
||||||
public void delete(@PathVariable Long id) {
|
|
||||||
modificationService.delete(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresAuth
|
|
||||||
@Operation(description = "批量删除改装")
|
|
||||||
@DeleteMapping("/batch-delete")
|
|
||||||
@Validated
|
|
||||||
public void batchDelete(@RequestParam List<@Positive Long> ids) {
|
|
||||||
modificationService.batchDelete(ids);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.controller;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import com.onixbyte.deltaforceguide.service.ModificationService;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* REST controller for retrieving available modification tags.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Tag(name = "标签管理", description = "管理标签信息")
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/tags")
|
|
||||||
public class TagController {
|
|
||||||
|
|
||||||
private final ModificationService modificationService;
|
|
||||||
|
|
||||||
public TagController(ModificationService modificationService) {
|
|
||||||
this.modificationService = modificationService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(description = "查询指定武器或所有武器的标签")
|
|
||||||
@GetMapping
|
|
||||||
public List<String> getTags(@RequestParam(required = false) Long firearmId) {
|
|
||||||
return modificationService.findAllTags(firearmId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.controller;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.service.AppService;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
@Tag(name = "版本信息")
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/versions")
|
|
||||||
public class VersionController {
|
|
||||||
|
|
||||||
private final AppService appService;
|
|
||||||
|
|
||||||
public VersionController(AppService appService) {
|
|
||||||
this.appService = appService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(description = "获取当前应用版本号")
|
|
||||||
@GetMapping
|
|
||||||
public String getVersion() {
|
|
||||||
return appService.getVersion();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-26
@@ -1,26 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.converter;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.enumeration.FirearmType;
|
|
||||||
import jakarta.persistence.AttributeConverter;
|
|
||||||
import jakarta.persistence.Converter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JPA attribute converter that maps {@link FirearmType} enum to/from its integer database representation.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Converter
|
|
||||||
public class FirearmTypeConverter implements AttributeConverter<FirearmType, Integer> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Integer convertToDatabaseColumn(FirearmType attribute) {
|
|
||||||
return attribute == null ? null : attribute.getCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FirearmType convertToEntityAttribute(Integer dbData) {
|
|
||||||
return FirearmType.fromCode(dbData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request DTO for creating or updating an accessory attached to a modification.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
public record AccessoryRequest(
|
|
||||||
@NotBlank(message = "插槽名称不能为空")
|
|
||||||
String slotName,
|
|
||||||
@NotBlank(message = "配件名称不能为空")
|
|
||||||
String accessoryName,
|
|
||||||
List<@Valid TuningRequest> tunings
|
|
||||||
) {
|
|
||||||
public List<TuningRequest> tunings() {
|
|
||||||
return tunings == null ? new ArrayList<>() : tunings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.domain.entity.Accessory;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Response DTO for an accessory attached to a modification.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
public record AccessoryResponse(
|
|
||||||
String slotName,
|
|
||||||
String accessoryName,
|
|
||||||
List<TuningResponse> tunings
|
|
||||||
) {
|
|
||||||
public static AccessoryResponse from(Accessory accessory) {
|
|
||||||
return new AccessoryResponse(
|
|
||||||
accessory.getSlotName(),
|
|
||||||
accessory.getAccessoryName(),
|
|
||||||
accessory.getTunings() == null
|
|
||||||
? List.of()
|
|
||||||
: accessory.getTunings().stream().map(TuningResponse::from).toList()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DTO representing a single daily-generated password for a map.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
public record DailyPassword(
|
|
||||||
String mapName,
|
|
||||||
String password
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DTO containing daily password data including update information and password list.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
public record DailyPasswordData(
|
|
||||||
String updateDate,
|
|
||||||
Integer totalCount,
|
|
||||||
List<DailyPassword> passwords,
|
|
||||||
String source,
|
|
||||||
LocalDateTime lastUpdated,
|
|
||||||
Long timestamp
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DTO holding metadata about the daily password source and update tracking.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
public record DailyPasswordMetadata(
|
|
||||||
String version,
|
|
||||||
String author
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Response DTO wrapping daily password data with metadata.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
public record DailyPasswordResponse(
|
|
||||||
String status,
|
|
||||||
String message,
|
|
||||||
DailyPasswordData data,
|
|
||||||
DailyPasswordMetadata metadata
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Standard error response body returned on API failures.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
public record ErrorResponse(
|
|
||||||
String message
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.enumeration.FirearmType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request DTO for creating or updating a firearm.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
public record FirearmRequest(
|
|
||||||
String name,
|
|
||||||
FirearmType type,
|
|
||||||
String level,
|
|
||||||
String calibre,
|
|
||||||
Integer fireRate,
|
|
||||||
Integer armourDamage,
|
|
||||||
Integer bodyDamage,
|
|
||||||
String review
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.domain.entity.Firearm;
|
|
||||||
import com.onixbyte.deltaforceguide.enumeration.FirearmType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Response DTO for a firearm record, including associated modifications.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
public record FirearmResponse(
|
|
||||||
Long id,
|
|
||||||
String name,
|
|
||||||
FirearmType type,
|
|
||||||
String level,
|
|
||||||
String calibre,
|
|
||||||
Integer fireRate,
|
|
||||||
Integer armourDamage,
|
|
||||||
Integer bodyDamage,
|
|
||||||
String review
|
|
||||||
) {
|
|
||||||
public static FirearmResponse from(Firearm firearm) {
|
|
||||||
return new FirearmResponse(
|
|
||||||
firearm.getId(),
|
|
||||||
firearm.getName(),
|
|
||||||
firearm.getType(),
|
|
||||||
firearm.getLevel(),
|
|
||||||
firearm.getCalibre(),
|
|
||||||
firearm.getFireRate(),
|
|
||||||
firearm.getArmourDamage(),
|
|
||||||
firearm.getBodyDamage(),
|
|
||||||
firearm.getReview()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
|
||||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
|
||||||
|
|
||||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
public record GitHubIssueRequest(
|
|
||||||
String action,
|
|
||||||
GitHubWebhookIssue issue,
|
|
||||||
GitHubWebhookRepository repository,
|
|
||||||
GitHubWebhookSender sender
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
|
||||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
public record GitHubWebhookIssue(
|
|
||||||
String url,
|
|
||||||
Long id,
|
|
||||||
Long number,
|
|
||||||
String title,
|
|
||||||
String body,
|
|
||||||
List<GitHubWebhookLabel> labels
|
|
||||||
) {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
|
||||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
|
||||||
|
|
||||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
public record GitHubWebhookLabel(
|
|
||||||
String name
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
|
||||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
|
||||||
|
|
||||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
public record GitHubWebhookRepository(
|
|
||||||
String fullName
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
|
||||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
|
||||||
|
|
||||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
public record GitHubWebhookSender(
|
|
||||||
String login
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Login request containing principle (username/email) and credential (password).
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Schema(description = "登录请求")
|
|
||||||
public record LoginRequest(
|
|
||||||
@NotBlank(message = "登录名称不能为空")
|
|
||||||
@Schema(description = "用户名或电子邮箱", requiredMode = Schema.RequiredMode.REQUIRED)
|
|
||||||
String principle,
|
|
||||||
@NotBlank(message = "登录口令不能为空")
|
|
||||||
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED)
|
|
||||||
String credential
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
-18
@@ -1,18 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request DTO for batch creation of modifications.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
public record ModificationBatchCreateRequest(
|
|
||||||
@NotEmpty(message = "批量创建列表不能为空")
|
|
||||||
List<@Valid ModificationRequest> modifications
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
-18
@@ -1,18 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
|
||||||
import jakarta.validation.constraints.Positive;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request DTO for batch deletion of modifications by ID.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
public record ModificationBatchDeleteRequest(
|
|
||||||
@NotEmpty(message = "批量删除ID列表不能为空")
|
|
||||||
List<@Positive(message = "ID必须为正数") Long> ids
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import jakarta.validation.constraints.Positive;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request DTO for creating or updating a modification.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
public record ModificationRequest(
|
|
||||||
@NotNull(message = "武器ID不能为空")
|
|
||||||
@Positive(message = "武器ID必须为正数")
|
|
||||||
Long firearmId,
|
|
||||||
@NotBlank(message = "改装名称不能为空")
|
|
||||||
String name,
|
|
||||||
@NotBlank(message = "改装代码不能为空")
|
|
||||||
String code,
|
|
||||||
List<@NotBlank(message = "标签不能为空") String> tags,
|
|
||||||
List<@Valid AccessoryRequest> accessories,
|
|
||||||
String note,
|
|
||||||
String author,
|
|
||||||
String videoUrl
|
|
||||||
) {
|
|
||||||
public List<String> tags() {
|
|
||||||
return tags == null ? new ArrayList<>() : tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<AccessoryRequest> accessories() {
|
|
||||||
return accessories == null ? new ArrayList<>() : accessories;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
-39
@@ -1,39 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.domain.entity.Modification;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Response DTO for a modification record including accessories and tags.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
public record ModificationResponse(
|
|
||||||
Long id,
|
|
||||||
Long firearmId,
|
|
||||||
String name,
|
|
||||||
String code,
|
|
||||||
List<String> tags,
|
|
||||||
List<AccessoryResponse> accessories,
|
|
||||||
String note,
|
|
||||||
String author,
|
|
||||||
String videoUrl
|
|
||||||
) {
|
|
||||||
public static ModificationResponse from(Modification modification) {
|
|
||||||
return new ModificationResponse(
|
|
||||||
modification.getId(),
|
|
||||||
modification.getFirearm().getId(),
|
|
||||||
modification.getName(),
|
|
||||||
modification.getCode(),
|
|
||||||
modification.getTags(),
|
|
||||||
modification.getAccessories() == null
|
|
||||||
? List.of()
|
|
||||||
: modification.getAccessories().stream().map(AccessoryResponse::from).toList(),
|
|
||||||
modification.getNote(),
|
|
||||||
modification.getAuthor(),
|
|
||||||
modification.getVideoUrl()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
import org.springframework.data.domain.Page;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic paginated response wrapper for list endpoints.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
public record PageResponse<T>(
|
|
||||||
List<T> items,
|
|
||||||
int page,
|
|
||||||
int size,
|
|
||||||
long totalElements,
|
|
||||||
int totalPages
|
|
||||||
) {
|
|
||||||
public static <T> PageResponse<T> from(Page<T> source) {
|
|
||||||
return new PageResponse<>(
|
|
||||||
source.getContent(),
|
|
||||||
source.getNumber(),
|
|
||||||
source.getSize(),
|
|
||||||
source.getTotalElements(),
|
|
||||||
source.getTotalPages()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request DTO for a tuning adjustment on an accessory.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
public record TuningRequest(
|
|
||||||
@NotBlank(message = "调校项名称不能为空")
|
|
||||||
String tuningName,
|
|
||||||
@NotNull(message = "调校值不能为空")
|
|
||||||
Double tuningValue
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.domain.entity.Tuning;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Response DTO for a tuning adjustment on an accessory.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
public record TuningResponse(
|
|
||||||
String tuningName,
|
|
||||||
Double tuningValue
|
|
||||||
) {
|
|
||||||
public static TuningResponse from(Tuning tuning) {
|
|
||||||
return new TuningResponse(
|
|
||||||
tuning.getTuningName(),
|
|
||||||
tuning.getTuningValue()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.domain.entity.User;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Response DTO for a user account, including associated credentials.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
public record UserResponse(
|
|
||||||
Long id,
|
|
||||||
String username,
|
|
||||||
String email,
|
|
||||||
LocalDateTime expiration
|
|
||||||
) {
|
|
||||||
public static UserResponse from(User user, LocalDateTime expiration) {
|
|
||||||
return new UserResponse(
|
|
||||||
user.getId(),
|
|
||||||
user.getUsername(),
|
|
||||||
user.getEmail(),
|
|
||||||
expiration
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.entity;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entity representing an accessory attached to a modification, stored as JSONB.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
public class Accessory {
|
|
||||||
|
|
||||||
private String slotName;
|
|
||||||
|
|
||||||
private String accessoryName;
|
|
||||||
|
|
||||||
private List<Tuning> tunings = new ArrayList<>();
|
|
||||||
|
|
||||||
public Accessory() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSlotName() {
|
|
||||||
return slotName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSlotName(String slotName) {
|
|
||||||
this.slotName = slotName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAccessoryName() {
|
|
||||||
return accessoryName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAccessoryName(String accessoryName) {
|
|
||||||
this.accessoryName = accessoryName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Tuning> getTunings() {
|
|
||||||
return tunings;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTunings(List<Tuning> tunings) {
|
|
||||||
this.tunings = tunings;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addTuning(Tuning tuning) {
|
|
||||||
this.tunings.add(tuning);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeTuning(Tuning tuning) {
|
|
||||||
this.tunings.remove(tuning);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!(o instanceof Accessory accessory)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return Objects.equals(slotName, accessory.slotName)
|
|
||||||
&& Objects.equals(accessoryName, accessory.accessoryName)
|
|
||||||
&& Objects.equals(tunings, accessory.tunings);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return Objects.hash(slotName, accessoryName, tunings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.entity;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.domain.converter.FirearmTypeConverter;
|
|
||||||
import com.onixbyte.deltaforceguide.enumeration.FirearmType;
|
|
||||||
import jakarta.persistence.CascadeType;
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Convert;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.GeneratedValue;
|
|
||||||
import jakarta.persistence.GenerationType;
|
|
||||||
import jakarta.persistence.Id;
|
|
||||||
import jakarta.persistence.OneToMany;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entity representing a firearm in the Delta Force game.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Entity
|
|
||||||
@Table(name = "firearm")
|
|
||||||
public class Firearm {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(name = "name", nullable = false, length = 64)
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
@Column(name = "type", nullable = false)
|
|
||||||
@Convert(converter = FirearmTypeConverter.class)
|
|
||||||
private FirearmType type;
|
|
||||||
|
|
||||||
@Column(name = "level", nullable = false)
|
|
||||||
private String level;
|
|
||||||
|
|
||||||
@Column(name = "review", columnDefinition = "TEXT")
|
|
||||||
private String review;
|
|
||||||
|
|
||||||
@Column(name = "calibre")
|
|
||||||
private String calibre;
|
|
||||||
|
|
||||||
@Column(name = "fire_rate")
|
|
||||||
private Integer fireRate;
|
|
||||||
|
|
||||||
@Column(name = "armour_damage")
|
|
||||||
private Integer armourDamage;
|
|
||||||
|
|
||||||
@Column(name = "body_damage")
|
|
||||||
private Integer bodyDamage;
|
|
||||||
|
|
||||||
@OneToMany(mappedBy = "firearm", cascade = CascadeType.ALL, orphanRemoval = true)
|
|
||||||
private List<Modification> modifications = new ArrayList<>();
|
|
||||||
|
|
||||||
public Long getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setId(Long id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setName(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public FirearmType getType() {
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setType(FirearmType type) {
|
|
||||||
this.type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLevel() {
|
|
||||||
return level;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLevel(String level) {
|
|
||||||
this.level = level;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getReview() {
|
|
||||||
return review;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setReview(String review) {
|
|
||||||
this.review = review;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCalibre() {
|
|
||||||
return calibre;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCalibre(String calibre) {
|
|
||||||
this.calibre = calibre;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getFireRate() {
|
|
||||||
return fireRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFireRate(Integer fireRate) {
|
|
||||||
this.fireRate = fireRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getArmourDamage() {
|
|
||||||
return armourDamage;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setArmourDamage(Integer armourDamage) {
|
|
||||||
this.armourDamage = armourDamage;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getBodyDamage() {
|
|
||||||
return bodyDamage;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setBodyDamage(Integer bodyDamage) {
|
|
||||||
this.bodyDamage = bodyDamage;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Modification> getModifications() {
|
|
||||||
return modifications;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setModifications(List<Modification> modifications) {
|
|
||||||
this.modifications = modifications;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addModification(Modification modification) {
|
|
||||||
this.modifications.add(modification);
|
|
||||||
modification.setFirearm(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeModification(Modification modification) {
|
|
||||||
this.modifications.remove(modification);
|
|
||||||
modification.setFirearm(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Builder builder() {
|
|
||||||
return new Builder();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Builder {
|
|
||||||
|
|
||||||
private Long id;
|
|
||||||
private String name;
|
|
||||||
private FirearmType type;
|
|
||||||
private String level;
|
|
||||||
private String review;
|
|
||||||
private String calibre;
|
|
||||||
private Integer fireRate;
|
|
||||||
private Integer armourDamage;
|
|
||||||
private Integer bodyDamage;
|
|
||||||
private List<Modification> modifications;
|
|
||||||
|
|
||||||
public Builder id(Long id) {
|
|
||||||
this.id = id;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder name(String name) {
|
|
||||||
this.name = name;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder type(FirearmType type) {
|
|
||||||
this.type = type;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder level(String level) {
|
|
||||||
this.level = level;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder review(String review) {
|
|
||||||
this.review = review;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder calibre(String calibre) {
|
|
||||||
this.calibre = calibre;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder fireRate(Integer fireRate) {
|
|
||||||
this.fireRate = fireRate;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder armourDamage(Integer armourDamage) {
|
|
||||||
this.armourDamage = armourDamage;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder bodyDamage(Integer bodyDamage) {
|
|
||||||
this.bodyDamage = bodyDamage;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder modifications(List<Modification> modifications) {
|
|
||||||
this.modifications = modifications;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Firearm build() {
|
|
||||||
Firearm firearm = new Firearm();
|
|
||||||
firearm.id = this.id;
|
|
||||||
firearm.name = this.name;
|
|
||||||
firearm.type = this.type;
|
|
||||||
firearm.level = this.level;
|
|
||||||
firearm.review = this.review;
|
|
||||||
firearm.calibre = this.calibre;
|
|
||||||
firearm.fireRate = this.fireRate;
|
|
||||||
firearm.armourDamage = this.armourDamage;
|
|
||||||
firearm.bodyDamage = this.bodyDamage;
|
|
||||||
firearm.modifications = this.modifications == null ? new ArrayList<>() : this.modifications;
|
|
||||||
return firearm;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.entity;
|
|
||||||
|
|
||||||
import io.hypersistence.utils.hibernate.type.json.JsonType;
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import org.hibernate.annotations.Type;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entity representing a firearm modification or build configuration.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Entity
|
|
||||||
@Table(
|
|
||||||
name = "modification",
|
|
||||||
indexes = {
|
|
||||||
@Index(name = "idx_modification_firearm_id", columnList = "firearm_id")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
public class Modification {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
|
||||||
@JoinColumn(name = "firearm_id", nullable = false, foreignKey = @ForeignKey(name = "fk_modification_firearm"))
|
|
||||||
private Firearm firearm;
|
|
||||||
|
|
||||||
@Column(name = "name", nullable = false, length = 64)
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
@Column(name = "code", nullable = false, length = 64)
|
|
||||||
private String code;
|
|
||||||
|
|
||||||
@Type(JsonType.class)
|
|
||||||
@Column(name = "tags", columnDefinition = "jsonb")
|
|
||||||
private List<String> tags = new ArrayList<>();
|
|
||||||
|
|
||||||
@Type(JsonType.class)
|
|
||||||
@Column(name = "accessories", columnDefinition = "jsonb")
|
|
||||||
private List<Accessory> accessories = new ArrayList<>();
|
|
||||||
|
|
||||||
@Column(name = "note", columnDefinition = "TEXT")
|
|
||||||
private String note;
|
|
||||||
|
|
||||||
@Column(name = "author", length = 64)
|
|
||||||
private String author;
|
|
||||||
|
|
||||||
@Column(name = "video_url", length = 512)
|
|
||||||
private String videoUrl;
|
|
||||||
|
|
||||||
public Long getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setId(Long id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Firearm getFirearm() {
|
|
||||||
return firearm;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFirearm(Firearm firearm) {
|
|
||||||
this.firearm = firearm;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setName(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCode() {
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCode(String code) {
|
|
||||||
this.code = code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getTags() {
|
|
||||||
return tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTags(List<String> tags) {
|
|
||||||
this.tags = tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Accessory> getAccessories() {
|
|
||||||
return accessories;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAccessories(List<Accessory> accessories) {
|
|
||||||
this.accessories = accessories;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addAccessory(Accessory modificationAccessory) {
|
|
||||||
this.accessories.add(modificationAccessory);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeAccessory(Accessory modificationAccessory) {
|
|
||||||
this.accessories.remove(modificationAccessory);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getNote() {
|
|
||||||
return note;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setNote(String note) {
|
|
||||||
this.note = note;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAuthor() {
|
|
||||||
return author;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAuthor(String author) {
|
|
||||||
this.author = author;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getVideoUrl() {
|
|
||||||
return videoUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setVideoUrl(String videoUrl) {
|
|
||||||
this.videoUrl = videoUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Builder builder() {
|
|
||||||
return new Builder();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Builder {
|
|
||||||
|
|
||||||
private Long id;
|
|
||||||
private Firearm firearm;
|
|
||||||
private String name;
|
|
||||||
private String code;
|
|
||||||
private List<String> tags;
|
|
||||||
private List<Accessory> accessories;
|
|
||||||
private String note;
|
|
||||||
private String author;
|
|
||||||
private String videoUrl;
|
|
||||||
|
|
||||||
public Builder id(Long id) {
|
|
||||||
this.id = id;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder firearm(Firearm firearm) {
|
|
||||||
this.firearm = firearm;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder name(String name) {
|
|
||||||
this.name = name;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder code(String code) {
|
|
||||||
this.code = code;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder tags(List<String> tags) {
|
|
||||||
this.tags = tags;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder accessories(List<Accessory> accessories) {
|
|
||||||
this.accessories = accessories;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder note(String note) {
|
|
||||||
this.note = note;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder author(String author) {
|
|
||||||
this.author = author;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder videoUrl(String videoUrl) {
|
|
||||||
this.videoUrl = videoUrl;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Modification build() {
|
|
||||||
Modification modification = new Modification();
|
|
||||||
modification.id = this.id;
|
|
||||||
modification.firearm = this.firearm;
|
|
||||||
modification.name = this.name;
|
|
||||||
modification.code = this.code;
|
|
||||||
modification.tags = this.tags == null ? new ArrayList<>() : this.tags;
|
|
||||||
modification.accessories = this.accessories == null ? new ArrayList<>() : this.accessories;
|
|
||||||
modification.note = this.note;
|
|
||||||
modification.author = this.author;
|
|
||||||
modification.videoUrl = this.videoUrl;
|
|
||||||
return modification;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.entity;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entity representing a tuning adjustment for an accessory, stored as JSONB within Accessory.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
public class Tuning {
|
|
||||||
|
|
||||||
private String tuningName;
|
|
||||||
private Double tuningValue;
|
|
||||||
|
|
||||||
public Tuning() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTuningName() {
|
|
||||||
return tuningName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTuningName(String tuningName) {
|
|
||||||
this.tuningName = tuningName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Double getTuningValue() {
|
|
||||||
return tuningValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTuningValue(Double tuningValue) {
|
|
||||||
this.tuningValue = tuningValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!(o instanceof Tuning tuning)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return Objects.equals(tuningName, tuning.tuningName)
|
|
||||||
&& Objects.equals(tuningValue, tuning.tuningValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return Objects.hash(tuningName, tuningValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.entity;
|
|
||||||
|
|
||||||
import jakarta.persistence.CascadeType;
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.GeneratedValue;
|
|
||||||
import jakarta.persistence.GenerationType;
|
|
||||||
import jakarta.persistence.Id;
|
|
||||||
import jakarta.persistence.OneToMany;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entity representing an application user with authentication credentials.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Entity
|
|
||||||
@Table(name = "app_user")
|
|
||||||
public class User {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(name = "username", nullable = false)
|
|
||||||
private String username;
|
|
||||||
|
|
||||||
@Column(name = "email", nullable = false)
|
|
||||||
private String email;
|
|
||||||
|
|
||||||
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
|
|
||||||
private List<UserCredential> credentials = new ArrayList<>();
|
|
||||||
|
|
||||||
public Long getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setId(Long id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUsername() {
|
|
||||||
return username;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUsername(String username) {
|
|
||||||
this.username = username;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getEmail() {
|
|
||||||
return email;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEmail(String email) {
|
|
||||||
this.email = email;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<UserCredential> getCredentials() {
|
|
||||||
return credentials;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCredentials(List<UserCredential> credentials) {
|
|
||||||
this.credentials = credentials;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addCredential(UserCredential credential) {
|
|
||||||
this.credentials.add(credential);
|
|
||||||
credential.setUser(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeCredential(UserCredential credential) {
|
|
||||||
this.credentials.remove(credential);
|
|
||||||
credential.setUser(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Builder builder() {
|
|
||||||
return new Builder();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Builder {
|
|
||||||
|
|
||||||
private Long id;
|
|
||||||
private String username;
|
|
||||||
private String email;
|
|
||||||
private List<UserCredential> credentials;
|
|
||||||
|
|
||||||
public Builder id(Long id) {
|
|
||||||
this.id = id;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder username(String username) {
|
|
||||||
this.username = username;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder email(String email) {
|
|
||||||
this.email = email;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder credentials(List<UserCredential> credentials) {
|
|
||||||
this.credentials = credentials;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public User build() {
|
|
||||||
User user = new User();
|
|
||||||
user.id = this.id;
|
|
||||||
user.username = this.username;
|
|
||||||
user.email = this.email;
|
|
||||||
user.credentials = this.credentials == null ? new ArrayList<>() : this.credentials;
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.entity;
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.AttributeOverride;
|
|
||||||
import jakarta.persistence.AttributeOverrides;
|
|
||||||
import jakarta.persistence.EmbeddedId;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.FetchType;
|
|
||||||
import jakarta.persistence.ForeignKey;
|
|
||||||
import jakarta.persistence.JoinColumn;
|
|
||||||
import jakarta.persistence.MapsId;
|
|
||||||
import jakarta.persistence.ManyToOne;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entity representing a user credential linked to an authentication provider.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Entity
|
|
||||||
@Table(name = "app_user_credential")
|
|
||||||
public class UserCredential {
|
|
||||||
|
|
||||||
@EmbeddedId
|
|
||||||
@AttributeOverrides({
|
|
||||||
@AttributeOverride(name = "userId", column = @Column(name = "user_id")),
|
|
||||||
@AttributeOverride(name = "provider", column = @Column(name = "provider"))
|
|
||||||
})
|
|
||||||
private UserCredentialId id = new UserCredentialId();
|
|
||||||
|
|
||||||
@MapsId("userId")
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
|
||||||
@JoinColumn(name = "user_id", nullable = false, foreignKey = @ForeignKey(name = "fk_user_credential_user"))
|
|
||||||
private User user;
|
|
||||||
|
|
||||||
@Column(name = "credential", nullable = false)
|
|
||||||
private String credential;
|
|
||||||
|
|
||||||
public UserCredentialId getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setId(UserCredentialId id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public User getUser() {
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUser(User user) {
|
|
||||||
this.user = user;
|
|
||||||
if (this.id == null) {
|
|
||||||
this.id = new UserCredentialId();
|
|
||||||
}
|
|
||||||
this.id.setUserId(user == null ? null : user.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getUserId() {
|
|
||||||
return id == null ? null : id.getUserId();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUserId(Long userId) {
|
|
||||||
if (this.id == null) {
|
|
||||||
this.id = new UserCredentialId();
|
|
||||||
}
|
|
||||||
this.id.setUserId(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getProvider() {
|
|
||||||
return id == null ? null : id.getProvider();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProvider(String provider) {
|
|
||||||
if (this.id == null) {
|
|
||||||
this.id = new UserCredentialId();
|
|
||||||
}
|
|
||||||
this.id.setProvider(provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCredential() {
|
|
||||||
return credential;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCredential(String credential) {
|
|
||||||
this.credential = credential;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Builder builder() {
|
|
||||||
return new Builder();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Builder {
|
|
||||||
|
|
||||||
private UserCredentialId id;
|
|
||||||
private User user;
|
|
||||||
private Long userId;
|
|
||||||
private String provider;
|
|
||||||
private String credential;
|
|
||||||
|
|
||||||
public Builder id(UserCredentialId id) {
|
|
||||||
this.id = id;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder user(User user) {
|
|
||||||
this.user = user;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder userId(Long userId) {
|
|
||||||
this.userId = userId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder provider(String provider) {
|
|
||||||
this.provider = provider;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder credential(String credential) {
|
|
||||||
this.credential = credential;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public UserCredential build() {
|
|
||||||
UserCredential userCredential = new UserCredential();
|
|
||||||
userCredential.id = this.id == null ? new UserCredentialId() : this.id;
|
|
||||||
userCredential.user = this.user;
|
|
||||||
if (this.user != null) {
|
|
||||||
userCredential.id.setUserId(this.user.getId());
|
|
||||||
}
|
|
||||||
if (this.userId != null) {
|
|
||||||
userCredential.id.setUserId(this.userId);
|
|
||||||
}
|
|
||||||
if (this.provider != null) {
|
|
||||||
userCredential.id.setProvider(this.provider);
|
|
||||||
}
|
|
||||||
userCredential.credential = this.credential;
|
|
||||||
return userCredential;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.entity;
|
|
||||||
|
|
||||||
import jakarta.persistence.Embeddable;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Composite key for the UserCredential entity, combining user ID and provider.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Embeddable
|
|
||||||
public class UserCredentialId implements Serializable {
|
|
||||||
|
|
||||||
private Long userId;
|
|
||||||
|
|
||||||
private String provider;
|
|
||||||
|
|
||||||
public Long getUserId() {
|
|
||||||
return userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUserId(Long userId) {
|
|
||||||
this.userId = userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getProvider() {
|
|
||||||
return provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProvider(String provider) {
|
|
||||||
this.provider = provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (o == null || getClass() != o.getClass()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
UserCredentialId that = (UserCredentialId) o;
|
|
||||||
return Objects.equals(userId, that.userId) && Objects.equals(provider, that.provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return Objects.hash(userId, provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Builder builder() {
|
|
||||||
return new Builder();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Builder {
|
|
||||||
|
|
||||||
private Long userId;
|
|
||||||
private String provider;
|
|
||||||
|
|
||||||
public Builder userId(Long userId) {
|
|
||||||
this.userId = userId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder provider(String provider) {
|
|
||||||
this.provider = provider;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public UserCredentialId build() {
|
|
||||||
UserCredentialId id = new UserCredentialId();
|
|
||||||
id.userId = this.userId;
|
|
||||||
id.provider = this.provider;
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.enumeration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enumeration of firearm types in the Delta Force game.
|
|
||||||
* Each type is associated with an integer code used for database persistence.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
public enum FirearmType {
|
|
||||||
|
|
||||||
RIFLE(0),
|
|
||||||
SUB_MACHINE_GUN(1),
|
|
||||||
SHOTGUN(2),
|
|
||||||
LIGHT_MACHINE_GUN(3),
|
|
||||||
DESIGNATED_MARKSMAN_RIFLE(4),
|
|
||||||
SNIPER_RIFLE(5),
|
|
||||||
PISTOL(6),
|
|
||||||
SPECIAL(7);
|
|
||||||
|
|
||||||
private final int code;
|
|
||||||
|
|
||||||
FirearmType(int code) {
|
|
||||||
this.code = code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCode() {
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a FirearmType from its integer code.
|
|
||||||
*
|
|
||||||
* @param code the integer code, may be null
|
|
||||||
* @return the corresponding FirearmType, or null if the code is null
|
|
||||||
* @throws IllegalArgumentException if the code does not match any known type
|
|
||||||
*/
|
|
||||||
public static FirearmType fromCode(Integer code) {
|
|
||||||
if (code == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (FirearmType type : values()) {
|
|
||||||
if (type.code == code) {
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new IllegalArgumentException("Unknown FirearmType code: " + code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.exeption;
|
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom runtime exception that carries an HTTP status code for API error responses.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
public class BizException extends RuntimeException {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The HTTP status code associated with this business exception.
|
|
||||||
* <p>
|
|
||||||
* This status code indicates the appropriate HTTP response status that should be returned to
|
|
||||||
* clients when this exception occurs. It enables consistent error handling across
|
|
||||||
* REST API endpoints.
|
|
||||||
*/
|
|
||||||
private final HttpStatus status;
|
|
||||||
|
|
||||||
public BizException(String message) {
|
|
||||||
super(message);
|
|
||||||
this.status = HttpStatus.INTERNAL_SERVER_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BizException(HttpStatus status, String message) {
|
|
||||||
super(message);
|
|
||||||
this.status = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the HTTP status code associated with this business exception.
|
|
||||||
*
|
|
||||||
* @return the HTTP status code that should be used in the error response
|
|
||||||
*/
|
|
||||||
public HttpStatus getStatus() {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
-113
@@ -1,113 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.filter;
|
|
||||||
|
|
||||||
import com.auth0.jwt.exceptions.JWTVerificationException;
|
|
||||||
import com.onixbyte.deltaforceguide.client.TokenClient;
|
|
||||||
import com.onixbyte.deltaforceguide.exeption.BizException;
|
|
||||||
import com.onixbyte.deltaforceguide.manager.UserManager;
|
|
||||||
import com.onixbyte.deltaforceguide.security.authentication.UsernamePasswordAuthentication;
|
|
||||||
import com.onixbyte.deltaforceguide.service.CookieService;
|
|
||||||
import jakarta.servlet.FilterChain;
|
|
||||||
import jakarta.servlet.ServletException;
|
|
||||||
import jakarta.servlet.http.Cookie;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import org.jspecify.annotations.NonNull;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
|
||||||
import org.springframework.web.servlet.HandlerExceptionResolver;
|
|
||||||
import org.springframework.web.util.WebUtils;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Servlet filter that extracts and validates JWT tokens from httpOnly cookies for each request.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
public class TokenAuthenticationFilter extends OncePerRequestFilter {
|
|
||||||
|
|
||||||
private final static Logger log = LoggerFactory.getLogger(TokenAuthenticationFilter.class);
|
|
||||||
private static final Duration ACCESS_TOKEN_RENEW_THRESHOLD = Duration.ofMinutes(5);
|
|
||||||
|
|
||||||
private final UserManager userManager;
|
|
||||||
private final TokenClient tokenClient;
|
|
||||||
private final CookieService cookieService;
|
|
||||||
private final HandlerExceptionResolver handlerExceptionResolver;
|
|
||||||
|
|
||||||
public TokenAuthenticationFilter(
|
|
||||||
UserManager userManager,
|
|
||||||
TokenClient tokenClient,
|
|
||||||
CookieService cookieService,
|
|
||||||
HandlerExceptionResolver handlerExceptionResolver
|
|
||||||
) {
|
|
||||||
this.userManager = userManager;
|
|
||||||
this.tokenClient = tokenClient;
|
|
||||||
this.cookieService = cookieService;
|
|
||||||
this.handlerExceptionResolver = handlerExceptionResolver;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts JWT from httpOnly cookie, validates it, and sets the security context.
|
|
||||||
*
|
|
||||||
* @param request the HTTP request
|
|
||||||
* @param response the HTTP response
|
|
||||||
* @param filterChain the filter chain
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void doFilterInternal(
|
|
||||||
@NonNull HttpServletRequest request,
|
|
||||||
@NonNull HttpServletResponse response,
|
|
||||||
@NonNull FilterChain filterChain
|
|
||||||
) throws ServletException, IOException {
|
|
||||||
var token = Optional.ofNullable(WebUtils.getCookie(request, "AccessToken"))
|
|
||||||
.map(Cookie::getValue)
|
|
||||||
.orElse(null);
|
|
||||||
if (Objects.isNull(token) || token.isBlank()) {
|
|
||||||
filterChain.doFilter(request, response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
var decodedToken = tokenClient.verifyToken(token);
|
|
||||||
var username = decodedToken.getSubject();
|
|
||||||
|
|
||||||
var userWrapper = userManager.findByUsername(username);
|
|
||||||
if (userWrapper.isEmpty()) {
|
|
||||||
throw new BizException(HttpStatus.UNAUTHORIZED, "登录已过期,请重新登录");
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = userWrapper.get();
|
|
||||||
var authentication = UsernamePasswordAuthentication.authenticated(user);
|
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
|
||||||
|
|
||||||
if (shouldRenew(decodedToken.getExpiresAt().toInstant())) {
|
|
||||||
var renewedToken = tokenClient.generateToken(user);
|
|
||||||
var renewedTokenCookie = cookieService.buildCookie("AccessToken", renewedToken);
|
|
||||||
response.addHeader(HttpHeaders.SET_COOKIE, renewedTokenCookie.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
filterChain.doFilter(request, response);
|
|
||||||
} catch (JWTVerificationException e) {
|
|
||||||
log.error("JWT verification failed.", e);
|
|
||||||
handlerExceptionResolver.resolveException(request, response, null,
|
|
||||||
new BizException(HttpStatus.UNAUTHORIZED, "登录已过期,请重新登录"));
|
|
||||||
} catch (BizException e) {
|
|
||||||
handlerExceptionResolver.resolveException(request, response, null, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean shouldRenew(Instant expiresAt) {
|
|
||||||
return Duration.between(Instant.now(), expiresAt).compareTo(ACCESS_TOKEN_RENEW_THRESHOLD) < 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.filter;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.wrapper.RepeatedlyReadRequestWrapper;
|
|
||||||
import jakarta.servlet.*;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class WebhookFilter implements Filter {
|
|
||||||
@Override
|
|
||||||
public void doFilter(
|
|
||||||
ServletRequest request,
|
|
||||||
ServletResponse response,
|
|
||||||
FilterChain chain
|
|
||||||
) throws IOException, ServletException {
|
|
||||||
var httpRequest = (HttpServletRequest) request;
|
|
||||||
var wrappedRequest = new RepeatedlyReadRequestWrapper(httpRequest);
|
|
||||||
chain.doFilter(wrappedRequest, response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-83
@@ -1,83 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.interceptor;
|
|
||||||
|
|
||||||
import com.onixbyte.crypto.util.CryptoUtil;
|
|
||||||
import com.onixbyte.deltaforceguide.exeption.BizException;
|
|
||||||
import com.onixbyte.deltaforceguide.manager.WebhookManager;
|
|
||||||
import com.onixbyte.deltaforceguide.shared.GitHubWebhookHeader;
|
|
||||||
import com.onixbyte.deltaforceguide.wrapper.RepeatedlyReadRequestWrapper;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import org.jspecify.annotations.NonNull;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies GitHub webhook requests by validating the {@code X-Hub-Signature-256}
|
|
||||||
* header against the configured secret using HMAC-SHA256.
|
|
||||||
*
|
|
||||||
* <p>Verification is skipped when no secret is configured. The signature format is
|
|
||||||
* {@code sha256=<hex-digest>} as documented by GitHub.
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
public class GitHubWebhookInterceptor implements HandlerInterceptor {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(GitHubWebhookInterceptor.class);
|
|
||||||
|
|
||||||
private final WebhookManager webhookManager;
|
|
||||||
|
|
||||||
public GitHubWebhookInterceptor(WebhookManager webhookManager) {
|
|
||||||
this.webhookManager = webhookManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean preHandle(
|
|
||||||
@NonNull HttpServletRequest request,
|
|
||||||
@NonNull HttpServletResponse response,
|
|
||||||
@NonNull Object handler
|
|
||||||
) {
|
|
||||||
if (!(request instanceof RepeatedlyReadRequestWrapper req)) {
|
|
||||||
throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
"Request body is not readable");
|
|
||||||
}
|
|
||||||
|
|
||||||
var secret = webhookManager.secret();
|
|
||||||
if (secret == null || secret.isBlank()) {
|
|
||||||
log.debug("No GitHub webhook secret configured, skipping signature verification");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var signatureHeader = req.getHeader(GitHubWebhookHeader.SIGNATURE_256);
|
|
||||||
if (signatureHeader == null || signatureHeader.isBlank()) {
|
|
||||||
log.warn("Missing {} header from ip={}",
|
|
||||||
GitHubWebhookHeader.SIGNATURE_256, request.getRemoteAddr());
|
|
||||||
throw new BizException(HttpStatus.UNAUTHORIZED,
|
|
||||||
"Missing webhook signature header");
|
|
||||||
}
|
|
||||||
|
|
||||||
var body = req.getBodyString();
|
|
||||||
try {
|
|
||||||
var computed = "sha256=" + CryptoUtil.hmacSha256(body, secret);
|
|
||||||
|
|
||||||
if (!MessageDigest.isEqual(
|
|
||||||
computed.getBytes(StandardCharsets.UTF_8),
|
|
||||||
signatureHeader.getBytes(StandardCharsets.UTF_8))) {
|
|
||||||
log.warn("Invalid webhook signature from ip={}", request.getRemoteAddr());
|
|
||||||
throw new BizException(HttpStatus.UNAUTHORIZED, "Invalid webhook signature");
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
|
||||||
log.error("Failed to compute HMAC-SHA256", e);
|
|
||||||
throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
"Failed to verify webhook signature");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.interceptor;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import org.jspecify.annotations.NonNull;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class TrafficInterceptor implements HandlerInterceptor {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(TrafficInterceptor.class);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean preHandle(
|
|
||||||
@NonNull HttpServletRequest request,
|
|
||||||
@NonNull HttpServletResponse response,
|
|
||||||
@NonNull Object handler
|
|
||||||
) {
|
|
||||||
var ip = resolveClientIp(request);
|
|
||||||
var method = request.getMethod();
|
|
||||||
var uri = request.getRequestURI();
|
|
||||||
var query = request.getQueryString();
|
|
||||||
var contentType = request.getContentType();
|
|
||||||
var contentLength = request.getContentLength();
|
|
||||||
var userAgent = request.getHeader("User-Agent");
|
|
||||||
|
|
||||||
log.debug("Request method={}, uri={}, query={}, ip={}, content-type={}, content-length={}, user-agent={}",
|
|
||||||
method, uri, query, ip, contentType, contentLength, userAgent);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveClientIp(HttpServletRequest request) {
|
|
||||||
var xForwardedFor = request.getHeader("X-Forwarded-For");
|
|
||||||
if (xForwardedFor != null && !xForwardedFor.isBlank()) {
|
|
||||||
return xForwardedFor.split(",")[0].trim();
|
|
||||||
}
|
|
||||||
var xRealIp = request.getHeader("X-Real-IP");
|
|
||||||
if (xRealIp != null && !xRealIp.isBlank()) {
|
|
||||||
return xRealIp.trim();
|
|
||||||
}
|
|
||||||
return request.getRemoteAddr();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.manager;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.properties.AppProperties;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class AppManager {
|
|
||||||
|
|
||||||
private final AppProperties appProperties;
|
|
||||||
|
|
||||||
public AppManager(AppProperties appProperties) {
|
|
||||||
this.appProperties = appProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the application version.
|
|
||||||
*
|
|
||||||
* @return the version string of this application
|
|
||||||
*/
|
|
||||||
public String getVersion() {
|
|
||||||
return "v%s-%s by @%s".formatted(
|
|
||||||
appProperties.version(),
|
|
||||||
appProperties.channel(),
|
|
||||||
appProperties.vendor()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.manager;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.properties.CookieProperties;
|
|
||||||
import org.springframework.boot.web.server.Cookie;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manager providing cookie construction operations with configurable properties.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
public class CookieManager {
|
|
||||||
|
|
||||||
private final CookieProperties cookieProperties;
|
|
||||||
|
|
||||||
public CookieManager(CookieProperties cookieProperties) {
|
|
||||||
this.cookieProperties = cookieProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean getHttpOnly() {
|
|
||||||
return cookieProperties.httpOnly();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean getSecure() {
|
|
||||||
return cookieProperties.secure();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Cookie.SameSite getSameSite() {
|
|
||||||
return cookieProperties.sameSite();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPath() {
|
|
||||||
return cookieProperties.path();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Duration getMaxAge() {
|
|
||||||
return cookieProperties.maxAge();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.manager;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
|
||||||
import com.onixbyte.deltaforceguide.domain.dto.DailyPasswordResponse;
|
|
||||||
import com.onixbyte.deltaforceguide.exeption.BizException;
|
|
||||||
import com.onixbyte.deltaforceguide.shared.JacksonModules;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.client.RestClient;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manager for daily password data access and caching coordination.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
public class DailyPasswordManager {
|
|
||||||
|
|
||||||
private static final String CACHE_KEY_PREFIX = "daily-password:";
|
|
||||||
|
|
||||||
private final RestClient restClient;
|
|
||||||
private final RedisTemplate<String, Object> redisTemplate;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public DailyPasswordManager(
|
|
||||||
RestClient.Builder restClientBuilder,
|
|
||||||
RedisTemplate<String, Object> redisTemplate
|
|
||||||
) {
|
|
||||||
var snakeCaseMapper = new ObjectMapper();
|
|
||||||
snakeCaseMapper.setPropertyNamingStrategy(
|
|
||||||
PropertyNamingStrategies.SnakeCaseStrategy.INSTANCE);
|
|
||||||
snakeCaseMapper.configure(
|
|
||||||
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
|
||||||
snakeCaseMapper.registerModule(JacksonModules.DATE_TIME_MODULE);
|
|
||||||
|
|
||||||
this.restClient = restClientBuilder
|
|
||||||
.baseUrl("https://tmini.net/api")
|
|
||||||
.messageConverters(converters -> {
|
|
||||||
converters.removeIf(
|
|
||||||
MappingJackson2HttpMessageConverter.class::isInstance);
|
|
||||||
converters.add(
|
|
||||||
new MappingJackson2HttpMessageConverter(snakeCaseMapper));
|
|
||||||
})
|
|
||||||
.build();
|
|
||||||
this.redisTemplate = redisTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the daily password from cache or generates a new one.
|
|
||||||
* @return the daily password response
|
|
||||||
*/
|
|
||||||
public DailyPasswordResponse getDailyPassword() {
|
|
||||||
var key = CACHE_KEY_PREFIX + LocalDate.now();
|
|
||||||
|
|
||||||
var cached = redisTemplate.opsForValue().get(key);
|
|
||||||
if (cached != null) {
|
|
||||||
return (DailyPasswordResponse) cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
var response = restClient.get()
|
|
||||||
.uri((uriBuilder) -> uriBuilder
|
|
||||||
.path("/sjzmm")
|
|
||||||
.queryParam("ckey", "")
|
|
||||||
.queryParam("type", "json")
|
|
||||||
.build())
|
|
||||||
.retrieve()
|
|
||||||
.body(DailyPasswordResponse.class);
|
|
||||||
|
|
||||||
if (Objects.isNull(response)) {
|
|
||||||
throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR, "暂无每日密码数据。");
|
|
||||||
}
|
|
||||||
|
|
||||||
redisTemplate.opsForValue().set(key, response, Duration.ofDays(1L));
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.manager;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.domain.entity.Accessory;
|
|
||||||
import com.onixbyte.deltaforceguide.domain.entity.Firearm;
|
|
||||||
import com.onixbyte.deltaforceguide.domain.entity.Modification;
|
|
||||||
import com.onixbyte.deltaforceguide.domain.entity.Tuning;
|
|
||||||
import com.onixbyte.deltaforceguide.domain.dto.AccessoryRequest;
|
|
||||||
import com.onixbyte.deltaforceguide.domain.dto.ModificationRequest;
|
|
||||||
import com.onixbyte.deltaforceguide.domain.dto.ModificationResponse;
|
|
||||||
import com.onixbyte.deltaforceguide.domain.dto.TuningRequest;
|
|
||||||
import com.onixbyte.deltaforceguide.repository.FirearmRepository;
|
|
||||||
import com.onixbyte.deltaforceguide.repository.ModificationRepository;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class ModificationManager {
|
|
||||||
|
|
||||||
private final ModificationRepository modificationRepository;
|
|
||||||
private final FirearmRepository firearmRepository;
|
|
||||||
|
|
||||||
public ModificationManager(
|
|
||||||
ModificationRepository modificationRepository,
|
|
||||||
FirearmRepository firearmRepository
|
|
||||||
) {
|
|
||||||
this.modificationRepository = modificationRepository;
|
|
||||||
this.firearmRepository = firearmRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public ModificationResponse create(ModificationRequest request) {
|
|
||||||
var firearm = firearmRepository.findById(request.firearmId())
|
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
|
|
||||||
"Firearm not found: " + request.firearmId()));
|
|
||||||
var modification = toEntity(request, firearm);
|
|
||||||
return ModificationResponse.from(modificationRepository.save(modification));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public List<ModificationResponse> batchCreate(List<ModificationRequest> requests) {
|
|
||||||
var firearmIds = requests.stream()
|
|
||||||
.map(ModificationRequest::firearmId)
|
|
||||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
|
||||||
|
|
||||||
Map<Long, Firearm> firearmMap = new HashMap<>();
|
|
||||||
firearmRepository.findAllById(firearmIds)
|
|
||||||
.forEach(firearm -> firearmMap.put(firearm.getId(), firearm));
|
|
||||||
|
|
||||||
if (firearmMap.size() != firearmIds.size()) {
|
|
||||||
var missing = firearmIds.stream()
|
|
||||||
.filter((id) -> !firearmMap.containsKey(id))
|
|
||||||
.toList();
|
|
||||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
|
|
||||||
"Firearm not found: " + missing);
|
|
||||||
}
|
|
||||||
|
|
||||||
var modifications = requests.stream()
|
|
||||||
.map(req -> toEntity(req, firearmMap.get(req.firearmId())))
|
|
||||||
.toList();
|
|
||||||
return modificationRepository.saveAll(modifications)
|
|
||||||
.stream()
|
|
||||||
.map(ModificationResponse::from)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long resolveFirearmId(Long firearmId, String firearmName) {
|
|
||||||
if (firearmId != null) {
|
|
||||||
return firearmId;
|
|
||||||
}
|
|
||||||
if (firearmName == null || firearmName.isBlank()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var matches = firearmRepository.findByName(firearmName);
|
|
||||||
if (matches.isEmpty()) {
|
|
||||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
|
|
||||||
"Firearm not found by name: " + firearmName);
|
|
||||||
}
|
|
||||||
return matches.getFirst().getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Modification toEntity(ModificationRequest request, Firearm firearm) {
|
|
||||||
return Modification.builder()
|
|
||||||
.firearm(firearm)
|
|
||||||
.name(request.name())
|
|
||||||
.code(request.code())
|
|
||||||
.tags(request.tags())
|
|
||||||
.accessories(toAccessories(request.accessories()))
|
|
||||||
.note(request.note())
|
|
||||||
.author(request.author())
|
|
||||||
.videoUrl(request.videoUrl())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Accessory> toAccessories(List<AccessoryRequest> requests) {
|
|
||||||
if (requests == null) {
|
|
||||||
return new ArrayList<>();
|
|
||||||
}
|
|
||||||
return requests.stream().map(this::toAccessory).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Accessory toAccessory(AccessoryRequest request) {
|
|
||||||
var accessory = new Accessory();
|
|
||||||
accessory.setSlotName(request.slotName());
|
|
||||||
accessory.setAccessoryName(request.accessoryName());
|
|
||||||
accessory.setTunings(toTunings(request.tunings()));
|
|
||||||
return accessory;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Tuning> toTunings(List<TuningRequest> requests) {
|
|
||||||
if (requests == null) {
|
|
||||||
return new ArrayList<>();
|
|
||||||
}
|
|
||||||
return requests.stream().map(this::toTuning).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Tuning toTuning(TuningRequest request) {
|
|
||||||
var tuning = new Tuning();
|
|
||||||
tuning.setTuningName(request.tuningName());
|
|
||||||
tuning.setTuningValue(request.tuningValue());
|
|
||||||
return tuning;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.manager;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.domain.entity.UserCredential;
|
|
||||||
import com.onixbyte.deltaforceguide.repository.UserCredentialRepository;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manager for user credential persistence and authentication data access.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
public class UserCredentialManager {
|
|
||||||
|
|
||||||
private final UserCredentialRepository userCredentialRepository;
|
|
||||||
|
|
||||||
public UserCredentialManager(UserCredentialRepository userCredentialRepository) {
|
|
||||||
this.userCredentialRepository = userCredentialRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds all credentials belonging to a specific user.
|
|
||||||
*
|
|
||||||
* @param userId the user ID
|
|
||||||
* @return list of matching credentials
|
|
||||||
*/
|
|
||||||
public List<UserCredential> findAllByUserId(Long userId) {
|
|
||||||
return userCredentialRepository.findAllByUserId(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a credential for a specific user and provider combination.
|
|
||||||
*
|
|
||||||
* @param userId the user ID
|
|
||||||
* @param provider the authentication provider
|
|
||||||
* @return the matching credential, if found
|
|
||||||
*/
|
|
||||||
public Optional<UserCredential> findByUserIdAndProvider(Long userId, String provider) {
|
|
||||||
return userCredentialRepository.findByUserIdAndProvider(userId, provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Persists a new or updated credential.
|
|
||||||
*
|
|
||||||
* @param userCredential the credential to save
|
|
||||||
* @return the saved credential
|
|
||||||
*/
|
|
||||||
public UserCredential save(UserCredential userCredential) {
|
|
||||||
return userCredentialRepository.save(userCredential);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a credential for a specific user and provider.
|
|
||||||
*
|
|
||||||
* @param userId the user ID
|
|
||||||
* @param provider the authentication provider
|
|
||||||
*/
|
|
||||||
public void deleteByUserIdAndProvider(Long userId, String provider) {
|
|
||||||
userCredentialRepository.deleteByUserIdAndProvider(userId, provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes all credentials belonging to a user.
|
|
||||||
*
|
|
||||||
* @param userId the user ID
|
|
||||||
*/
|
|
||||||
public void deleteAllByUserId(Long userId) {
|
|
||||||
userCredentialRepository.deleteAllByUserId(userId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.manager;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.domain.entity.User;
|
|
||||||
import com.onixbyte.deltaforceguide.repository.UserRepository;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manager for user entity persistence and query operations.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
public class UserManager {
|
|
||||||
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
|
|
||||||
public UserManager(UserRepository userRepository) {
|
|
||||||
this.userRepository = userRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a user by their ID.
|
|
||||||
*
|
|
||||||
* @param id the user ID
|
|
||||||
* @return the matching user, if found
|
|
||||||
*/
|
|
||||||
public Optional<User> findById(Long id) {
|
|
||||||
return userRepository.findById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves all registered users.
|
|
||||||
* @return list of all users
|
|
||||||
*/
|
|
||||||
public List<User> findAll() {
|
|
||||||
return userRepository.findAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a user by their username.
|
|
||||||
*
|
|
||||||
* @param username the username to search for
|
|
||||||
* @return the matching user, if found
|
|
||||||
*/
|
|
||||||
public Optional<User> findByUsername(String username) {
|
|
||||||
return userRepository.findByUsername(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public Optional<User> findByEmail(String email) {
|
|
||||||
return userRepository.findByEmail(email);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Persists a new or updated user.
|
|
||||||
*
|
|
||||||
* @param user the user to save
|
|
||||||
* @return the saved user
|
|
||||||
*/
|
|
||||||
public User save(User user) {
|
|
||||||
return userRepository.save(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a user by their ID.
|
|
||||||
*
|
|
||||||
* @param id the user ID to delete
|
|
||||||
*/
|
|
||||||
public void deleteById(Long id) {
|
|
||||||
userRepository.deleteById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a user by their username or email address.
|
|
||||||
*
|
|
||||||
* @param principal the username or email to search for
|
|
||||||
* @return the matching user, if found
|
|
||||||
*/
|
|
||||||
public Optional<User> findByUsernameOrEmail(String principal) {
|
|
||||||
return userRepository.findByUsernameOrEmail(principal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.manager;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.properties.GitHubWebhookProperties;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class WebhookManager {
|
|
||||||
|
|
||||||
private final GitHubWebhookProperties gitHubWebhookProperties;
|
|
||||||
|
|
||||||
public WebhookManager(GitHubWebhookProperties gitHubWebhookProperties) {
|
|
||||||
this.gitHubWebhookProperties = gitHubWebhookProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String secret() {
|
|
||||||
return gitHubWebhookProperties.secret();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> allowedUsers() {
|
|
||||||
return gitHubWebhookProperties.allowedUsers();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.properties;
|
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
|
|
||||||
@ConfigurationProperties(prefix = "app.common")
|
|
||||||
public record AppProperties(
|
|
||||||
String version,
|
|
||||||
String channel,
|
|
||||||
String vendor
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.properties;
|
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
import org.springframework.boot.context.properties.bind.DefaultValue;
|
|
||||||
import org.springframework.boot.web.server.Cookie;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration properties for HTTP cookies used in authentication, prefixed with "app.cookie".
|
|
||||||
*
|
|
||||||
* @param httpOnly whether the cookie is httpOnly
|
|
||||||
* @param secure whether the cookie is secure
|
|
||||||
* @param path the cookie path
|
|
||||||
* @param maxAge the maximum age of the cookie
|
|
||||||
* @param sameSite the SameSite policy for the cookie
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@ConfigurationProperties(prefix = "app.cookie")
|
|
||||||
public record CookieProperties(
|
|
||||||
@DefaultValue("true") Boolean httpOnly,
|
|
||||||
@DefaultValue("true") Boolean secure,
|
|
||||||
@DefaultValue("/") String path,
|
|
||||||
@DefaultValue("PT2H") Duration maxAge,
|
|
||||||
@DefaultValue("LAX") Cookie.SameSite sameSite
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.properties;
|
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
import org.springframework.boot.context.properties.bind.DefaultValue;
|
|
||||||
import org.springframework.http.HttpMethod;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration properties for CORS settings, prefixed with "app.cors".
|
|
||||||
*
|
|
||||||
* @param allowedHeaders headers allowed in CORS requests
|
|
||||||
* @param allowedMethods HTTP methods allowed in CORS requests
|
|
||||||
* @param allowedOrigins origins permitted to make cross-origin requests
|
|
||||||
* @param allowCredentials whether credentials are allowed in CORS requests
|
|
||||||
* @param allowPrivateNetwork whether private network access is permitted
|
|
||||||
* @param maxAge how long the CORS preflight response may be cached
|
|
||||||
* @param exposedHeaders headers exposed to the client in CORS responses
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@ConfigurationProperties(prefix = "app.cors")
|
|
||||||
public record CorsProperties(
|
|
||||||
@DefaultValue({"Content-Type", "Authorization"})
|
|
||||||
String[] allowedHeaders,
|
|
||||||
@DefaultValue({"GET", "POST", "PUT", "PATCH", "DELETE"})
|
|
||||||
HttpMethod[] allowedMethods,
|
|
||||||
String[] allowedOrigins,
|
|
||||||
boolean allowCredentials,
|
|
||||||
boolean allowPrivateNetwork,
|
|
||||||
@DefaultValue("PT2H")
|
|
||||||
Duration maxAge,
|
|
||||||
@DefaultValue({"Content-Type", "Authorization"})
|
|
||||||
String[] exposedHeaders
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.properties;
|
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@ConfigurationProperties(prefix = "app.webhook.github")
|
|
||||||
public record GitHubWebhookProperties(
|
|
||||||
String secret,
|
|
||||||
List<String> allowedUsers
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.properties;
|
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration properties for JWT token generation and validation, prefixed with "app.jwt".
|
|
||||||
*
|
|
||||||
* @param issuer the JWT issuer claim
|
|
||||||
* @param secret the signing secret for JWT tokens
|
|
||||||
* @param validTime the duration for which a token remains valid
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@ConfigurationProperties(prefix = "app.jwt")
|
|
||||||
public record TokenProperties(
|
|
||||||
String issuer,
|
|
||||||
String secret,
|
|
||||||
Duration validTime
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.repository;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.domain.entity.Firearm;
|
|
||||||
import com.onixbyte.deltaforceguide.enumeration.FirearmType;
|
|
||||||
import org.springframework.data.domain.Page;
|
|
||||||
import org.springframework.data.domain.Pageable;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Spring Data JPA repository for {@link Firearm} entity operations.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Repository
|
|
||||||
public interface FirearmRepository extends JpaRepository<Firearm, Long> {
|
|
||||||
|
|
||||||
Page<Firearm> findAllByType(FirearmType type, Pageable pageable);
|
|
||||||
|
|
||||||
List<Firearm> findByName(String name);
|
|
||||||
}
|
|
||||||
|
|
||||||
-65
@@ -1,65 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.repository;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.domain.entity.Modification;
|
|
||||||
import org.jspecify.annotations.NonNull;
|
|
||||||
import org.springframework.data.domain.Page;
|
|
||||||
import org.springframework.data.domain.Pageable;
|
|
||||||
import org.springframework.data.jpa.repository.EntityGraph;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.data.jpa.repository.Query;
|
|
||||||
import org.springframework.data.repository.query.Param;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Spring Data JPA repository for {@link Modification} entity operations,
|
|
||||||
* including native JSONB tag filtering for Postgres.
|
|
||||||
*
|
|
||||||
* @author zihluwang
|
|
||||||
*/
|
|
||||||
@Repository
|
|
||||||
public interface ModificationRepository extends JpaRepository<Modification, Long> {
|
|
||||||
|
|
||||||
@EntityGraph(attributePaths = {"firearm"})
|
|
||||||
Page<Modification> findAllBy(Pageable pageable);
|
|
||||||
|
|
||||||
@EntityGraph(attributePaths = {"firearm"})
|
|
||||||
Page<Modification> findAllByFirearm_Id(Long firearmId, Pageable pageable);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@EntityGraph(attributePaths = {"firearm"})
|
|
||||||
@NonNull
|
|
||||||
Optional<Modification> findById(@NonNull Long id);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page query modifications with optional firearm and JSONB tag filtering.
|
|
||||||
*
|
|
||||||
* @param firearmId optional firearm ID filter (nullable)
|
|
||||||
* @param tagsJson optional JSON array of tags to match via Postgres {@code @>} operator (nullable)
|
|
||||||
* @param pageable pagination information
|
|
||||||
* @return a page of matching modifications
|
|
||||||
*/
|
|
||||||
@Query(value = """
|
|
||||||
SELECT * FROM modification m
|
|
||||||
WHERE (:firearmId IS NULL OR m.firearm_id = :firearmId)
|
|
||||||
AND (CAST(:tagsJson AS text) IS NULL OR cast(m.tags as jsonb) @> cast(CAST(:tagsJson AS text) as jsonb))
|
|
||||||
""",
|
|
||||||
countQuery = """
|
|
||||||
SELECT count(*) FROM modification m
|
|
||||||
WHERE (:firearmId IS NULL OR m.firearm_id = :firearmId)
|
|
||||||
AND (CAST(:tagsJson AS text) IS NULL OR cast(m.tags as jsonb) @> cast(CAST(:tagsJson AS text) as jsonb))
|
|
||||||
""",
|
|
||||||
nativeQuery = true)
|
|
||||||
Page<Modification> pageQueryByFirearmAndTags(@Param("firearmId") Long firearmId, @Param("tagsJson") String tagsJson, Pageable pageable);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve all distinct tag values from modifications, optionally filtered by firearm.
|
|
||||||
*
|
|
||||||
* @param firearmId optional firearm ID filter (nullable)
|
|
||||||
* @return list of distinct tag strings
|
|
||||||
*/
|
|
||||||
@Query(value = "SELECT DISTINCT jsonb_array_elements_text(cast(tags as jsonb)) FROM modification WHERE (:firearmId IS NULL OR firearm_id = :firearmId)", nativeQuery = true)
|
|
||||||
List<String> findAllTags(@Param("firearmId") Long firearmId);
|
|
||||||
}
|
|
||||||
Loaded 100 of 226 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user