Compare commits
93
Commits
1.3.3
...
27bba95250
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27bba95250 | ||
|
|
9e86447c45 | ||
|
|
8776f45684 | ||
|
|
694bebe2c8 | ||
|
|
bb08f1e4b6 | ||
|
|
2b099a6b2c | ||
|
|
0269683b68 | ||
|
|
3f2066f8b2 | ||
|
|
a2469c7573 | ||
|
|
62fcce3005 | ||
|
|
c18a108a2b | ||
|
|
fb2732508a | ||
|
|
ceac8a3f8c | ||
|
|
f8c0e3e8b6 | ||
|
|
4803ae78c9 | ||
|
|
a065b60cae | ||
|
|
17cd87c702 | ||
|
|
4e2da0debc | ||
|
|
0815d1d618 | ||
|
|
d323e4f8f7
|
||
|
|
eb2d9b3369 | ||
|
|
a0d54cc12d | ||
|
|
4eafb3ade7 | ||
|
|
9594efe716 | ||
|
|
5b5062aae9 | ||
|
|
b0c41e08ea | ||
|
|
ed2a0f4ae0 | ||
|
|
de61e1feb7 | ||
|
|
3616ad9eab | ||
|
|
20bc18d416 | ||
|
|
4ee741d307 | ||
|
|
0530c1f633 | ||
|
|
8a9cf110af | ||
|
|
c30b5701e4 | ||
|
|
7fafa0d903 | ||
|
|
8c8ca58b74 | ||
|
|
12469f1b27 | ||
|
|
44271eeec4 | ||
|
|
f9c210c8b3 | ||
|
|
ce330bca87 | ||
|
|
bd4fe65b03
|
||
|
|
eb22b3c4bb
|
||
|
|
7032343487 | ||
|
|
243283b788 | ||
|
|
4810ef2b1f | ||
|
|
72ec875802 | ||
|
|
6240ec1016 | ||
|
|
8d24b6082d | ||
|
|
9bc70d5370 | ||
|
|
d44f5f74fe | ||
|
|
f866d93fb4 | ||
|
|
66b37ec20d | ||
|
|
0d70b27653 | ||
|
|
673ba03f2b | ||
|
|
f6255d396c | ||
|
|
26cea1db82 | ||
|
|
8f102f54c7 | ||
|
|
d3681916b2 | ||
|
|
b000336d22 | ||
|
|
cf2b57f745
|
||
|
|
a1984c3ecb
|
||
|
|
3af4650d32 | ||
|
|
a36c539ef1 | ||
|
|
dad5c191fa
|
||
|
|
78fa02e618
|
||
|
|
63f9885aa2
|
||
|
|
7f50fafee8
|
||
|
|
3da706402d
|
||
|
|
9dfd52eb2d
|
||
|
|
4294495ffa
|
||
|
|
a98902b08b
|
||
|
|
bad31c6653
|
||
|
|
ae912050b6
|
||
|
|
feeb44bf6a
|
||
|
|
452b807b62
|
||
|
|
38b25099de
|
||
|
|
a0a5c835aa
|
||
|
|
864895d932
|
||
|
|
a4e5891189
|
||
|
|
335de44487
|
||
|
|
3573a23acf | ||
|
|
6858e07603 | ||
|
|
aa2415a0cb | ||
|
|
f9ff917360 | ||
|
|
405c520cab | ||
|
|
026843c6f0 | ||
|
|
353e8a597b
|
||
|
|
86b743dcc0
|
||
|
|
00215decc5
|
||
|
|
c22bc08282 | ||
|
|
b3caeb8ce5 | ||
|
|
5d44e24c95 | ||
|
|
8b3ccc6a51 |
No files matched your search
@@ -0,0 +1,86 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
name: Build and Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
env:
|
||||||
|
APP_NAME: ahsarah-guide-server
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: server
|
||||||
|
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: server
|
||||||
|
file: server/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: server/README.md
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
name: Build and Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build release archive
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: web
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 11
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build release archive
|
||||||
|
run: pnpm build:tar
|
||||||
|
|
||||||
|
- name: Clean up previous build artifacts on server
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ vars.DEPLOY_HOST }}
|
||||||
|
username: ${{ vars.DEPLOY_USER }}
|
||||||
|
port: ${{ vars.DEPLOY_PORT }}
|
||||||
|
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
command: rm -rf /tmp/dist.tar.gz /tmp/dist.tar.gz/
|
||||||
|
|
||||||
|
- name: Upload artifact to server
|
||||||
|
uses: appleboy/scp-action@v0.1.7
|
||||||
|
with:
|
||||||
|
host: ${{ vars.DEPLOY_HOST }}
|
||||||
|
username: ${{ vars.DEPLOY_USER }}
|
||||||
|
port: ${{ vars.DEPLOY_PORT }}
|
||||||
|
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
source: "dist.tar.gz"
|
||||||
|
target: "/tmp/"
|
||||||
|
|
||||||
|
upload-release-asset:
|
||||||
|
name: Upload to Gitea Release
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download artifact from server
|
||||||
|
env:
|
||||||
|
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
|
||||||
|
DEPLOY_USER: ${{ vars.DEPLOY_USER }}
|
||||||
|
DEPLOY_PORT: ${{ vars.DEPLOY_PORT }}
|
||||||
|
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
SSH_DIR="${RUNNER_TEMP}/ssh"
|
||||||
|
mkdir -p "$SSH_DIR"
|
||||||
|
chmod 700 "$SSH_DIR"
|
||||||
|
printf '%s\n' "$DEPLOY_SSH_KEY" > "$SSH_DIR/key"
|
||||||
|
chmod 600 "$SSH_DIR/key"
|
||||||
|
scp -i "$SSH_DIR/key" \
|
||||||
|
-P "$DEPLOY_PORT" \
|
||||||
|
-o StrictHostKeyChecking=accept-new \
|
||||||
|
-o UserKnownHostsFile="$SSH_DIR/known_hosts" \
|
||||||
|
"${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/dist.tar.gz" \
|
||||||
|
./dist.tar.gz
|
||||||
|
rm -rf "$SSH_DIR"
|
||||||
|
|
||||||
|
- name: Upload release asset via Gitea API
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
RELEASE_ID=$(jq -r '.release.id' "$GITEA_EVENT_PATH")
|
||||||
|
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:
|
||||||
|
name: Deploy to onixbyte.cn
|
||||||
|
needs: [build, upload-release-asset]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Extract archive and deploy
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ vars.DEPLOY_HOST }}
|
||||||
|
username: ${{ vars.DEPLOY_USER }}
|
||||||
|
port: ${{ vars.DEPLOY_PORT }}
|
||||||
|
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
script_stop: true
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
DEPLOY_PATH="${{ vars.DEPLOY_PATH }}"
|
||||||
|
mkdir -p "$DEPLOY_PATH"
|
||||||
|
rm -rf "$DEPLOY_PATH"/*
|
||||||
|
tar -xzf /tmp/dist.tar.gz -C "$DEPLOY_PATH" --strip-components=1
|
||||||
|
chown -R caddy:caddy "$DEPLOY_PATH"
|
||||||
|
rm -f /tmp/dist.tar.gz
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
name: Build and Deploy
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
env:
|
|
||||||
APP_NAME: delta-force-guide-server
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# ================================================================
|
|
||||||
# Single Job: Build, Upload JAR to Release, and Push to GHCR
|
|
||||||
# ================================================================
|
|
||||||
build-and-release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
packages: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up JDK 21 (Corretto)
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
java-version: 21
|
|
||||||
distribution: corretto
|
|
||||||
cache: gradle
|
|
||||||
|
|
||||||
- name: Set up Gradle
|
|
||||||
uses: gradle/actions/setup-gradle@v4
|
|
||||||
|
|
||||||
# 使用 Release Tag 做为 Gradle 属性传入
|
|
||||||
- name: Build with Gradle
|
|
||||||
run: ./gradlew bootJar -x test -PartefactVersion="${{ github.event.release.tag_name }}"
|
|
||||||
|
|
||||||
- name: Resolve JAR file path
|
|
||||||
id: jar
|
|
||||||
run: |
|
|
||||||
JAR_PATH=$(find build/libs -name '*.jar' | head -1)
|
|
||||||
echo "file=$JAR_PATH" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# 上传 JAR 包到 GitHub Release 中
|
|
||||||
- name: Upload JAR to GitHub Release
|
|
||||||
uses: svenstaro/upload-release-action@v2
|
|
||||||
with:
|
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
file: ${{ steps.jar.outputs.file }}
|
|
||||||
asset_name: ${{ github.event.repository.name }}-${{ github.event.release.tag_name }}.jar
|
|
||||||
tag: ${{ github.event.release.tag_name }}
|
|
||||||
overwrite: true
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
# 登录到 GitHub Container Registry (GHCR)
|
|
||||||
- name: Log in to GHCR
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
# 镜像打标签准备
|
|
||||||
- name: Generate image tags
|
|
||||||
id: meta
|
|
||||||
run: |
|
|
||||||
OWNER_LC=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
|
|
||||||
REPO_LC=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]')
|
|
||||||
echo "tag_version=ghcr.io/$OWNER_LC/$REPO_LC:${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "tag_latest=ghcr.io/$OWNER_LC/$REPO_LC:latest" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# 构建并上传镜像到 GHCR
|
|
||||||
- name: Build and push Docker image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: Dockerfile.ci
|
|
||||||
build-args: JAR_FILE=${{ steps.jar.outputs.file }}
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
${{ steps.meta.outputs.tag_version }}
|
|
||||||
${{ steps.meta.outputs.tag_latest }}
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
|
|
||||||
+100
-17
@@ -97,7 +97,7 @@ fabric.properties
|
|||||||
# Editor-based HTTP Client
|
# Editor-based HTTP Client
|
||||||
http-client.private.env.json
|
http-client.private.env.json
|
||||||
|
|
||||||
### Server
|
### Java
|
||||||
# Compiled class file
|
# Compiled class file
|
||||||
*.class
|
*.class
|
||||||
|
|
||||||
@@ -111,26 +111,18 @@ http-client.private.env.json
|
|||||||
.mtj.tmp/
|
.mtj.tmp/
|
||||||
|
|
||||||
# Package Files #
|
# Package Files #
|
||||||
|
*.jar
|
||||||
*.war
|
*.war
|
||||||
*.nar
|
*.nar
|
||||||
*.ear
|
*.ear
|
||||||
*.zip
|
*.zip
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.rar
|
*.rar
|
||||||
*.jar
|
|
||||||
|
|
||||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||||
hs_err_pid*
|
hs_err_pid*
|
||||||
replay_pid*
|
replay_pid*
|
||||||
|
|
||||||
# Config
|
|
||||||
config/*.yml
|
|
||||||
config/*.yaml
|
|
||||||
config/*.properties
|
|
||||||
|
|
||||||
# Tests
|
|
||||||
test/
|
|
||||||
|
|
||||||
### Gradle
|
### Gradle
|
||||||
.gradle
|
.gradle
|
||||||
**/build/
|
**/build/
|
||||||
@@ -143,13 +135,6 @@ test/
|
|||||||
gradle-app.setting
|
gradle-app.setting
|
||||||
.gradletasknamecache
|
.gradletasknamecache
|
||||||
|
|
||||||
### Claude Code
|
|
||||||
.claude/settings.local.json
|
|
||||||
.claude/memory/
|
|
||||||
.claude/plans/
|
|
||||||
.claude/worktrees/
|
|
||||||
.claude/scheduled_tasks.json
|
|
||||||
|
|
||||||
# Eclipse Gradle plugin generated files
|
# Eclipse Gradle plugin generated files
|
||||||
# Eclipse Core
|
# Eclipse Core
|
||||||
.project
|
.project
|
||||||
@@ -157,3 +142,101 @@ gradle-app.setting
|
|||||||
.classpath
|
.classpath
|
||||||
|
|
||||||
gradle.properties
|
gradle.properties
|
||||||
|
|
||||||
|
### Node.js
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Vite logs files
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v3
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
### Server-specific
|
||||||
|
# Config
|
||||||
|
server/config/*.yml
|
||||||
|
server/config/*.yaml
|
||||||
|
server/config/*.properties
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
server/test/
|
||||||
|
|
||||||
|
# frp config
|
||||||
|
frpc.toml
|
||||||
|
|
||||||
|
### Claude Code
|
||||||
|
.claude/settings.local.json
|
||||||
|
.claude/memory/
|
||||||
|
.claude/plans/
|
||||||
|
.claude/worktrees/
|
||||||
|
.claude/scheduled_tasks.json
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
variables:
|
|
||||||
GRADLE_OPTS: -Dorg.gradle.daemon=false
|
|
||||||
DOCKER_HOST: unix:///var/run/docker.sock
|
|
||||||
|
|
||||||
stages:
|
|
||||||
- release
|
|
||||||
|
|
||||||
release:
|
|
||||||
stage: release
|
|
||||||
image: amazoncorretto:21-alpine
|
|
||||||
cache:
|
|
||||||
key: gradle
|
|
||||||
paths:
|
|
||||||
- .gradle/wrapper
|
|
||||||
- .gradle/caches
|
|
||||||
before_script:
|
|
||||||
- chmod +x gradlew
|
|
||||||
- apk add --no-cache docker-cli
|
|
||||||
script:
|
|
||||||
- ./gradlew bootJar -x test -PartefactVersion="$CI_COMMIT_TAG"
|
|
||||||
- JAR_FILE=$(find build/libs -name '*.jar' | head -1)
|
|
||||||
- echo "Building Docker image for tag $CI_COMMIT_TAG with JAR $JAR_FILE"
|
|
||||||
- docker build
|
|
||||||
-f Dockerfile.ci
|
|
||||||
--build-arg JAR_FILE="$JAR_FILE"
|
|
||||||
-t "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG"
|
|
||||||
.
|
|
||||||
- docker tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG" "$CI_REGISTRY_IMAGE:latest"
|
|
||||||
- echo "Pushing image $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG"
|
|
||||||
- docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD"
|
|
||||||
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG"
|
|
||||||
- docker push "$CI_REGISTRY_IMAGE:latest"
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_TAG
|
|
||||||
@@ -2,16 +2,25 @@
|
|||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
## Coding Standards
|
## Monorepo Structure
|
||||||
|
|
||||||
|
- `server/` — Java 21 + Spring Boot 3.5 + Gradle (Kotlin DSL) REST API backend
|
||||||
|
- `web/` — React 19 + Vite 8 + TypeScript + Ant Design 6 + Tailwind CSS 4 frontend
|
||||||
|
|
||||||
|
## Server
|
||||||
|
|
||||||
|
### Coding Standards
|
||||||
|
|
||||||
- **Style**: Follow the Google Java Coding Style as the foundation.
|
- **Style**: Follow the Google Java Coding Style as the foundation.
|
||||||
- **Indentation**: Use 4 spaces — no tabs.
|
- **Indentation**: Use 4 spaces — no tabs.
|
||||||
- **Line length**: Maximum 100 characters per line.
|
- **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").
|
- **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
|
### Build & Test Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cd server
|
||||||
|
|
||||||
# Build the project (skip tests)
|
# Build the project (skip tests)
|
||||||
./gradlew build -x test
|
./gradlew build -x test
|
||||||
|
|
||||||
@@ -19,10 +28,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
./gradlew test
|
./gradlew test
|
||||||
|
|
||||||
# Run a single test class
|
# Run a single test class
|
||||||
./gradlew test --tests "com.onixbyte.deltaforceguide.service.PasswordEncoderTest"
|
./gradlew test --tests "com.onixbyte.ahsarahguide.service.PasswordEncoderTest"
|
||||||
|
|
||||||
# Run a specific test method
|
|
||||||
./gradlew test --tests "com.onixbyte.deltaforceguide.service.PasswordEncoderTest.generatePassword"
|
|
||||||
|
|
||||||
# Build the full JAR
|
# Build the full JAR
|
||||||
./gradlew bootJar
|
./gradlew bootJar
|
||||||
@@ -30,14 +36,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
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.
|
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
|
### Code Architecture
|
||||||
|
|
||||||
**Delta Force Guide Server** — A REST API backend for managing Delta Force game firearm builds/modifications.
|
**Ahsarah Guide Server** — A REST API backend for managing firearm builds/modifications.
|
||||||
|
|
||||||
### Package structure
|
### Package structure
|
||||||
|
|
||||||
```
|
```
|
||||||
com.onixbyte.deltaforceguide
|
com.onixbyte.ahsarahguide
|
||||||
├── client/ # External service clients (TokenClient for JWT)
|
├── client/ # External service clients (TokenClient for JWT)
|
||||||
├── config/ # Spring beans: Security, CORS, Cache/Redis, Jackson, MyBatis, Spring Data
|
├── config/ # Spring beans: Security, CORS, Cache/Redis, Jackson, MyBatis, Spring Data
|
||||||
├── controller/ # REST controllers (Firearm, Modification, Tag, Auth)
|
├── controller/ # REST controllers (Firearm, Modification, Tag, Auth)
|
||||||
@@ -65,16 +71,9 @@ com.onixbyte.deltaforceguide
|
|||||||
- **JPA + native queries**: Most CRUD uses Spring Data JPA. Native queries (in `ModificationRepository`) handle JSONB tag filtering with Postgres `@>` operator.
|
- **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.
|
- **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`.
|
- **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.
|
- **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.
|
||||||
- **DTOs as Java records**: All request/response objects are immutable records with static `from()` factory methods for entity→DTO conversion.
|
- **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).
|
- **Flyway migrations**: SQL migrations in `src/main/resources/db/migration/`.
|
||||||
|
|
||||||
### 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
|
### API endpoints
|
||||||
|
|
||||||
@@ -94,16 +93,61 @@ com.onixbyte.deltaforceguide
|
|||||||
|
|
||||||
Conventional commits: `feat:`, `chore:`, `fix:`. Messages are in English, present tense imperative style.
|
Conventional commits: `feat:`, `chore:`, `fix:`. Messages are in English, present tense imperative style.
|
||||||
|
|
||||||
### External dependencies
|
---
|
||||||
|
|
||||||
- **DB**: PostgreSQL (via Flyway migrations), H2 in test
|
## Web
|
||||||
- **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
|
### Commands
|
||||||
|
|
||||||
- `dev`: Enables Swagger UI, connects to dev DB/Redis at `dfguide.onixbyte.cn`. Config in `config/application-dev.yaml`.
|
```bash
|
||||||
- Default profile: Used for production, no Swagger, connects to production datasource.
|
cd web
|
||||||
|
|
||||||
|
pnpm install # Install dependencies (pnpm required)
|
||||||
|
pnpm dev # Start Vite dev server
|
||||||
|
pnpm build # TypeScript check + Vite production build
|
||||||
|
pnpm build:tar # Build + tar.gz archive (used by CI)
|
||||||
|
pnpm preview # Preview production build locally
|
||||||
|
pnpm lint # ESLint
|
||||||
|
```
|
||||||
|
|
||||||
|
No test suite exists in this project.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
Chinese-language SPA for browsing and managing Ahsarah Guide (阿萨拉向导). The frontend talks to the Spring Boot backend via REST APIs.
|
||||||
|
|
||||||
|
**App shell** (`src/main.tsx`): React 19 + React Router 7 + Redux Toolkit + Ant Design 6 + Tailwind CSS 4. Wraps the router in Redux `Provider` → `PersistGate` (Redux Persist) → Ant Design `StyleProvider`/`ConfigProvider` (locale `zh_CN`).
|
||||||
|
|
||||||
|
**Routing** (`src/router/index.tsx`): Two layout groups:
|
||||||
|
- `HeroLayout` (nav header + footer) for `/`, `/firearms`, `/mod-codes`
|
||||||
|
- `EmptyLayout` (minimal) for `/login`
|
||||||
|
All page components are lazy-loaded via `createBrowserRouter` + `lazy()`.
|
||||||
|
|
||||||
|
**State** (`src/store/`): Redux Toolkit with two slices — `auth` (current user) and `firearms` (paginated firearm list). State is persisted to `localStorage` or `sessionStorage` based on the `VITE_REDUX_STORAGE` env var. Use typed hooks from `src/hooks/store.ts` (`useAppDispatch`, `useAppSelector`).
|
||||||
|
|
||||||
|
**API layer** (`src/api/`): Axios instance (`src/shared/web-client/`) with base URL from `VITE_API_BASE_URL`, 10s timeout, and credentials. API modules: `FirearmApi`, `ModificationApi`, `TagApi`, `AuthApi`.
|
||||||
|
|
||||||
|
**Pages**:
|
||||||
|
- `FirearmsPage` — paginated card grid with type filter, create/edit modals (admin-only), delete with popconfirm
|
||||||
|
- `ModCodesPage` — paginated list with tag multi-select and firearmId query param filter, create/edit modals with nested accessory/tuning form lists
|
||||||
|
- `LoginPage` — simple username/password form, dispatches `setCurrentUser` on success
|
||||||
|
|
||||||
|
**Shared form components**: `FirearmForm` and `ModificationForm` are reused by both create and edit modals. `ModificationForm` fetches all firearms for its weapon selector and supports a `lockFirearmId` prop that disables the selector (used when navigating from a specific firearm).
|
||||||
|
|
||||||
|
**Type system** (`src/types/`): `Firearm` with weapon stats, `Modification` with nested `Accessory[]` → `Tuning[]`, `Page<T>` for paginated API responses, `User` for auth.
|
||||||
|
|
||||||
|
**Vite config**: Alias `@` → `./src`. Plugins: React, Tailwind CSS 4, port checker. Build uses rolldown with manual chunk splitting for React, Redux, Ant Design, React Router, and rc-* packages.
|
||||||
|
|
||||||
|
**Styling**: Tailwind CSS 4 with CSS layers (`theme, base, antd, components, utilities`). Responsive grid for mod code cards (1→2→3→4 columns). Prettier: 100 print width, no semicolons, double quotes, trailing commas ES5.
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
```
|
||||||
|
VITE_API_BASE_URL=/api # Backend API base URL
|
||||||
|
VITE_REDUX_STORAGE=local # "local" or "session" for Redux persistence
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contributing conventions
|
||||||
|
|
||||||
|
- User-facing copy, documentation, and code comments in British English
|
||||||
|
- Commit messages use `chore:` prefix for dependency updates (per Dependabot config)
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
FROM amazoncorretto:21-alpine
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ARG JAR_FILE
|
|
||||||
COPY ${JAR_FILE} app.jar
|
|
||||||
|
|
||||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
|
||||||
@@ -1,150 +1,38 @@
|
|||||||
# Delta Force Guide Server
|
# Ahsarah Guide (阿萨拉向导)
|
||||||
|
|
||||||
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.
|
Ahsarah Guide is a full-stack web application for browsing and managing firearm modification guides.
|
||||||
|
|
||||||
|
## Monorepo Structure
|
||||||
|
|
||||||
## Tech Stack
|
| Directory | Stack | Description |
|
||||||
|
|-----------|-------|-------------|
|
||||||
| Layer | Technology |
|
| `server/` | Java 21 + Spring Boot 3.5 + Gradle | REST API backend |
|
||||||
|-----------|------------------------------------------------------------|
|
| `web/` | React 19 + Vite 8 + TypeScript + Ant Design 6 | Web frontend |
|
||||||
| 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
|
## Quick Start
|
||||||
|
|
||||||
### Prerequisites
|
### Server
|
||||||
|
|
||||||
- JDK 21 (Amazon Corretto recommended)
|
Prerequisites: JDK 21 (Amazon Corretto), PostgreSQL, Redis
|
||||||
- PostgreSQL
|
|
||||||
- Redis
|
|
||||||
|
|
||||||
### Configure
|
|
||||||
|
|
||||||
Copy and customise the development config:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp config/application-prod.yaml.example \
|
cd server
|
||||||
config/application-dev.yaml
|
cp config/application-prod.yaml.example config/application-dev.yaml
|
||||||
```
|
# Edit config/application-dev.yaml with your database and Redis credentials
|
||||||
|
|
||||||
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
|
./gradlew bootJar
|
||||||
|
java -jar build/libs/ahsarah-guide-server-*.jar
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run
|
### Web
|
||||||
|
|
||||||
|
Prerequisites: Node.js 20+, pnpm
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
SPRING_PROFILES_ACTIVE=prod java -jar build/libs/delta-force-guide-server-$version.jar
|
cd web
|
||||||
|
pnpm install
|
||||||
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
## Licence
|
||||||
|
|
||||||
This project is licensed under the [MIT Licence](LICENCE). Copyright © 2026 OnixByte.
|
MIT. See [LICENCE](LICENCE) for details.
|
||||||
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
plugins {
|
|
||||||
java
|
|
||||||
alias(libs.plugins.spring.boot)
|
|
||||||
alias(libs.plugins.spring.dependency.management)
|
|
||||||
}
|
|
||||||
|
|
||||||
val artefactVersion: 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.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.test {
|
|
||||||
useJUnitPlatform()
|
|
||||||
}
|
|
||||||
@@ -1,55 +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: 6hLFVqfGPviTYukn # Uncomment if password is necessary
|
|
||||||
|
|
||||||
logging:
|
|
||||||
pattern:
|
|
||||||
# dateformat: dd MMM yyyy HH:mm:ss.SSS # Modify this for custom date format.
|
|
||||||
|
|
||||||
app:
|
|
||||||
common:
|
|
||||||
version: 1.3.0.8-dev # Application version, you can change to any version you like, used for communication with frontend.
|
|
||||||
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
|
|
||||||
cookie: # Cookie settings.
|
|
||||||
http-only: true
|
|
||||||
secure: false
|
|
||||||
same-site: lax
|
|
||||||
path: '/'
|
|
||||||
max-age: PT2H
|
|
||||||
|
|
||||||
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).
|
|
||||||
File renamed without changes.
@@ -0,0 +1,3 @@
|
|||||||
|
/gradlew text eol=lf
|
||||||
|
*.bat text eol=crlf
|
||||||
|
*.jar binary
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
### 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
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# 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.
|
||||||
@@ -6,5 +6,5 @@ RUN chmod +x ./gradlew && \
|
|||||||
|
|
||||||
FROM amazoncorretto:21-alpine AS runner
|
FROM amazoncorretto:21-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /app/build/libs/delta-force-guide-server-1.0.0.jar app.jar
|
COPY --from=builder /app/build/libs/ahsarah-guide-server-*.jar app.jar
|
||||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
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="ahsarah-guide-server" \
|
||||||
|
org.opencontainers.image.description="REST API backend for Ahsarah Guide" \
|
||||||
|
org.opencontainers.image.version="${IMAGE_VERSION}" \
|
||||||
|
org.opencontainers.image.vendor="${IMAGE_VENDOR}" \
|
||||||
|
org.opencontainers.image.source="https://onixbyte.dev/onixbyte/ahsarah-guide" \
|
||||||
|
org.opencontainers.image.licenses="MIT" \
|
||||||
|
org.opencontainers.image.created="${BUILD_DATE}"
|
||||||
|
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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.
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
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.ahsarahguide"
|
||||||
|
version = artefactVersion
|
||||||
|
|
||||||
|
tasks.withType<JavaCompile> {
|
||||||
|
options.encoding = "UTF-8"
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// BOMs are imported as platforms, so they cannot be bundled.
|
||||||
|
implementation(platform(libs.aws.sdk.bom))
|
||||||
|
implementation(platform(libs.onixbyte.versionCatalogue))
|
||||||
|
|
||||||
|
implementation(libs.bundles.utilities)
|
||||||
|
implementation(libs.bundles.onixbyte)
|
||||||
|
implementation(libs.bundles.web)
|
||||||
|
implementation(libs.bundles.persistence)
|
||||||
|
implementation(libs.bundles.security)
|
||||||
|
implementation(libs.bundles.cache)
|
||||||
|
implementation(libs.bundles.observability)
|
||||||
|
implementation(libs.bundles.mail)
|
||||||
|
implementation(libs.bundles.aws)
|
||||||
|
implementation(libs.bundles.docs)
|
||||||
|
|
||||||
|
// Annotation processor for @ConfigurationProperties; kept out of bundles.
|
||||||
|
implementation(libs.spring.boot.configurationProcessor)
|
||||||
|
|
||||||
|
runtimeOnly(libs.postgres.driver)
|
||||||
|
|
||||||
|
testImplementation(libs.bundles.testing)
|
||||||
|
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 {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
# PostgreSQL connection URL.
|
||||||
|
url: jdbc:postgresql://localhost:5432/dfguide_dev
|
||||||
|
|
||||||
|
# PostgreSQL username.
|
||||||
|
username: postgres
|
||||||
|
|
||||||
|
# PostgreSQL password.
|
||||||
|
password: 123456
|
||||||
|
|
||||||
|
# PostgreSQL JDBC driver class name.
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
# Redis server host.
|
||||||
|
host: localhost
|
||||||
|
|
||||||
|
# Redis server port.
|
||||||
|
port: 6379
|
||||||
|
|
||||||
|
# Redis database index.
|
||||||
|
#
|
||||||
|
# Default to 0.
|
||||||
|
database: 0
|
||||||
|
|
||||||
|
# Redis server password.
|
||||||
|
#
|
||||||
|
# Uncomment if the server requires a password.
|
||||||
|
#
|
||||||
|
# password: qwerty
|
||||||
|
|
||||||
|
logging:
|
||||||
|
pattern:
|
||||||
|
# Date format used in log output. Uncomment to use a custom format.
|
||||||
|
#
|
||||||
|
# Default to the date format provided by Spring Boot.
|
||||||
|
# dateformat: dd MMM yyyy HH:mm:ss.SSS
|
||||||
|
|
||||||
|
app:
|
||||||
|
cookie:
|
||||||
|
# Whether the cookie is accessible from JavaScript. Keep `true` to mitigate XSS attacks.
|
||||||
|
http-only: true
|
||||||
|
|
||||||
|
# Whether the cookie is only sent over HTTPS. Set to `true` in production is recommended.
|
||||||
|
secure: true
|
||||||
|
|
||||||
|
# SameSite policy of the cookie.
|
||||||
|
#
|
||||||
|
# NOTE that `none` requires `secure` to be `true`.
|
||||||
|
same-site: none
|
||||||
|
|
||||||
|
# Cookie path.
|
||||||
|
path: '/'
|
||||||
|
|
||||||
|
# Cookie max age.
|
||||||
|
max-age: P1D
|
||||||
|
cors:
|
||||||
|
# Cross-origin allowed origins. Add your custom origins here.
|
||||||
|
allowed-origins: []
|
||||||
|
|
||||||
|
# Whether to allow credentials (cookies, authorisation headers) in cross-origin requests.
|
||||||
|
#
|
||||||
|
# Must be `true` since the application uses cookies for authentication. Change it only when
|
||||||
|
# the authentication mechanism no longer relies on cookies.
|
||||||
|
allow-credentials: true
|
||||||
|
|
||||||
|
# Whether to allow requests originating from private network addresses.
|
||||||
|
allow-private-network: false
|
||||||
|
|
||||||
|
# Headers allowed in cross-origin requests.
|
||||||
|
allowed-headers:
|
||||||
|
- "Content-Type"
|
||||||
|
- "Authorization"
|
||||||
|
|
||||||
|
# HTTP methods allowed in cross-origin requests.
|
||||||
|
#
|
||||||
|
# Methods are matched against Spring's HttpMethod, so they must be written in UPPER CASE.
|
||||||
|
# e.g., GET, POST, PUT, PATCH, DELETE, OPTIONS
|
||||||
|
allowed-methods:
|
||||||
|
- GET
|
||||||
|
- POST
|
||||||
|
- PUT
|
||||||
|
- PATCH
|
||||||
|
- DELETE
|
||||||
|
|
||||||
|
# Response headers exposed to the browser in cross-origin responses.
|
||||||
|
exposed-headers:
|
||||||
|
- "Content-Type"
|
||||||
|
- "Authorization"
|
||||||
|
|
||||||
|
# How long the CORS pre-flight response may be cached by the browser.
|
||||||
|
#
|
||||||
|
# Default to PT2H (2 hours).
|
||||||
|
max-age: PT2H
|
||||||
|
jwt:
|
||||||
|
# Issuer identifier, domain name is recommended.
|
||||||
|
issuer: ahsarah-guide.local
|
||||||
|
|
||||||
|
# Token signing secret, a 32-byte long or longer string is recommended.
|
||||||
|
secret: qwertyuiopasdfghjklzxcvbnm123456
|
||||||
|
|
||||||
|
# Token valid duration.
|
||||||
|
#
|
||||||
|
# Default to PT2H (2 hours).
|
||||||
|
valid-time: PT2H
|
||||||
|
|
||||||
|
# Token renew threshold.
|
||||||
|
#
|
||||||
|
# If valid time of this token is less than this value, then a new token will be delivered to
|
||||||
|
# the user.
|
||||||
|
#
|
||||||
|
# Default to PT5M (5 min).
|
||||||
|
renew-threshold: PT5M
|
||||||
|
webhook:
|
||||||
|
github:
|
||||||
|
# GitHub webhook secret. Used to verify the signature of incoming webhook requests.
|
||||||
|
secret: 123456
|
||||||
|
|
||||||
|
# GitHub usernames allowed to trigger webhook events.
|
||||||
|
allowed-users: [ octocat, onixbyte ]
|
||||||
|
|
||||||
|
springdoc:
|
||||||
|
api-docs:
|
||||||
|
# Whether to expose the OpenAPI documentation. Set to `false` if you do not need it
|
||||||
|
# (recommended in production mode).
|
||||||
|
enabled: true
|
||||||
|
swagger-ui:
|
||||||
|
# Whether to expose the Swagger UI. Set to `false` if you do not need it
|
||||||
|
# (recommended in production mode).
|
||||||
|
enabled: true
|
||||||
@@ -4,10 +4,9 @@ javaJwtVersion = "4.5.1"
|
|||||||
postgresDriverVersion = "42.7.9"
|
postgresDriverVersion = "42.7.9"
|
||||||
h2Version = "2.2.224"
|
h2Version = "2.2.224"
|
||||||
springBootVersion = "3.5.13"
|
springBootVersion = "3.5.13"
|
||||||
springSecurityVersion = "6.5.2"
|
|
||||||
reactorVersion = "3.7.8"
|
reactorVersion = "3.7.8"
|
||||||
junitPlatformVersion = "1.12.2"
|
junitPlatformVersion = "1.12.2"
|
||||||
onixbyteVersion = "3.3.0"
|
onixbyteVersion = "3.4.0"
|
||||||
onixbyteCaptcha = "1.1.0"
|
onixbyteCaptcha = "1.1.0"
|
||||||
onixbyteRegions = "2025.12.0"
|
onixbyteRegions = "2025.12.0"
|
||||||
awsSdkVersion = "2.25.48"
|
awsSdkVersion = "2.25.48"
|
||||||
@@ -32,6 +31,7 @@ jackson-jsr310 = { group = "com.fasterxml.jackson.datatype", name = "jackson-dat
|
|||||||
onixbyte-versionCatalogue = { group = "com.onixbyte", name = "version-catalogue", version.ref = "onixbyteVersion" }
|
onixbyte-versionCatalogue = { group = "com.onixbyte", name = "version-catalogue", version.ref = "onixbyteVersion" }
|
||||||
onixbyte-tuple = { group = "com.onixbyte", name = "tuple" }
|
onixbyte-tuple = { group = "com.onixbyte", name = "tuple" }
|
||||||
onixbyte-commonToolbox = { group = "com.onixbyte", name = "common-toolbox" }
|
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-mathToolbox = { group = "com.onixbyte", name = "math-toolbox" }
|
||||||
onixbyte-identityGenerator = { group = "com.onixbyte", name = "identity-generator" }
|
onixbyte-identityGenerator = { group = "com.onixbyte", name = "identity-generator" }
|
||||||
onixbyte-captcha = { group = "com.onixbyte", name = "captcha", version.ref = "onixbyteCaptcha" }
|
onixbyte-captcha = { group = "com.onixbyte", name = "captcha", version.ref = "onixbyteCaptcha" }
|
||||||
@@ -42,12 +42,10 @@ mybatis-starter-core = { group = "org.mybatis.spring.boot", name = "mybatis-spri
|
|||||||
spring-boot-starter-jpa = { group = "org.springframework.boot", name = "spring-boot-starter-data-jpa" }
|
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" }
|
hypersistence-core = { group = "io.hypersistence", name = "hypersistence-utils-hibernate-63", version.ref = "hypersistenceVersion" }
|
||||||
postgres-driver = { group = "org.postgresql", name = "postgresql", version.ref = "postgresDriverVersion" }
|
postgres-driver = { group = "org.postgresql", name = "postgresql", version.ref = "postgresDriverVersion" }
|
||||||
mysql-driver = { group = "com.mysql", name = "mysql-connector-j" }
|
|
||||||
h2-database = { group = "com.h2database", name = "h2", version.ref = "h2Version" }
|
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" }
|
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-core = { group = "org.flywaydb", name = "flyway-core" }
|
||||||
flyway-postgresql = { group = "org.flywaydb", name = "flyway-database-postgresql" }
|
flyway-postgresql = { group = "org.flywaydb", name = "flyway-database-postgresql" }
|
||||||
flyway-mysql = { group = "org.flywaydb", name = "flyway-mysql" }
|
|
||||||
|
|
||||||
# Spring Boot Core & Web
|
# Spring Boot Core & Web
|
||||||
spring-boot-starter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" }
|
spring-boot-starter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" }
|
||||||
@@ -61,6 +59,11 @@ spring-boot-actuator = { group = "org.springframework.boot", name = "spring-boot
|
|||||||
spring-boot-starter-security = { group = "org.springframework.boot", name = "spring-boot-starter-security" }
|
spring-boot-starter-security = { group = "org.springframework.boot", name = "spring-boot-starter-security" }
|
||||||
jwt-core = { group = "com.auth0", name = "java-jwt", version.ref = "javaJwtVersion" }
|
jwt-core = { group = "com.auth0", name = "java-jwt", version.ref = "javaJwtVersion" }
|
||||||
|
|
||||||
|
# Java Mail & Templating
|
||||||
|
spring-boot-starter-mail = { module = "org.springframework.boot:spring-boot-starter-mail" }
|
||||||
|
spring-boot-starter-thymeleaf = { group = "org.springframework.boot", name = "spring-boot-starter-thymeleaf" }
|
||||||
|
thymeleaf-extras-springsecurity6 = { group = "org.thymeleaf.extras", name = "thymeleaf-extras-springsecurity6" }
|
||||||
|
|
||||||
# Spring Doc
|
# Spring Doc
|
||||||
spring-boot-starter-doc = { group = "org.springdoc", name = "springdoc-openapi-starter-webmvc-ui", version.ref = "springDocVersion" }
|
spring-boot-starter-doc = { group = "org.springdoc", name = "springdoc-openapi-starter-webmvc-ui", version.ref = "springDocVersion" }
|
||||||
|
|
||||||
@@ -75,6 +78,89 @@ reactor-test = { group = "io.projectreactor", name = "reactor-test", version.ref
|
|||||||
junit-launcher = { group = "org.junit.platform", name = "junit-platform-launcher", version.ref = "junitPlatformVersion" }
|
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" }
|
mybatis-starter-test = { group = "org.mybatis.spring.boot", name = "mybatis-spring-boot-starter-test", version.ref = "mybatisVersion" }
|
||||||
|
|
||||||
|
[bundles]
|
||||||
|
# General utilities
|
||||||
|
utilities = [
|
||||||
|
"jspecify-core",
|
||||||
|
"commons-io",
|
||||||
|
"commons-lang",
|
||||||
|
"commons-collections",
|
||||||
|
"jackson-jsr310",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Onixbyte ecosystem (the version-catalogue BOM stays out of bundles, it is imported as a platform)
|
||||||
|
onixbyte = [
|
||||||
|
"onixbyte-tuple",
|
||||||
|
"onixbyte-commonToolbox",
|
||||||
|
"onixbyte-cryptoToolbox",
|
||||||
|
"onixbyte-mathToolbox",
|
||||||
|
"onixbyte-identityGenerator",
|
||||||
|
"onixbyte-captcha",
|
||||||
|
"onixbyte-regions",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Web layer
|
||||||
|
web = [
|
||||||
|
"spring-boot-starter-web",
|
||||||
|
"spring-boot-starter-webFlux",
|
||||||
|
"spring-boot-starter-validation",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Persistence and database
|
||||||
|
persistence = [
|
||||||
|
"spring-boot-starter-jpa",
|
||||||
|
"mybatis-starter-core",
|
||||||
|
"hypersistence-core",
|
||||||
|
"flyway-core",
|
||||||
|
"flyway-postgresql",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Security and authentication
|
||||||
|
security = [
|
||||||
|
"spring-boot-starter-security",
|
||||||
|
"jwt-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Caching (Redis)
|
||||||
|
cache = [
|
||||||
|
"spring-boot-starter-redis",
|
||||||
|
"spring-boot-starter-cache",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Observability
|
||||||
|
observability = [
|
||||||
|
"spring-boot-actuator",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Mail and templating
|
||||||
|
mail = [
|
||||||
|
"spring-boot-starter-mail",
|
||||||
|
"spring-boot-starter-thymeleaf",
|
||||||
|
"thymeleaf-extras-springsecurity6",
|
||||||
|
]
|
||||||
|
|
||||||
|
# AWS cloud services
|
||||||
|
aws = [
|
||||||
|
"aws-sdk-s3",
|
||||||
|
]
|
||||||
|
|
||||||
|
# API documentation
|
||||||
|
docs = [
|
||||||
|
"spring-boot-starter-doc",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
testing = [
|
||||||
|
"spring-boot-starter-test",
|
||||||
|
"spring-security-test",
|
||||||
|
"reactor-test",
|
||||||
|
"mybatis-starter-test",
|
||||||
|
]
|
||||||
|
testRuntime = [
|
||||||
|
"h2-database",
|
||||||
|
"junit-launcher",
|
||||||
|
]
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
spring-boot = { id = "org.springframework.boot", version.ref = "springBootVersion" }
|
spring-boot = { id = "org.springframework.boot", version.ref = "springBootVersion" }
|
||||||
spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "springDependencyManagementVersion" }
|
spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "springDependencyManagementVersion" }
|
||||||
File renamed without changes.
File renamed without changes.
Vendored
File renamed without changes.
File renamed without changes.
@@ -11,4 +11,4 @@ dependencyResolutionManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = "delta-force-guide-server"
|
rootProject.name = "ahsarah-guide-server"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.onixbyte.ahsarahguide;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for the Ahsarah Guide Server application.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
|
@EnableAsync
|
||||||
|
@SpringBootApplication
|
||||||
|
public class AhsarahGuideApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(AhsarahGuideApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.client;
|
||||||
|
|
||||||
|
import jakarta.mail.MessagingException;
|
||||||
|
import jakarta.mail.internet.InternetAddress;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.mail.javamail.JavaMailSender;
|
||||||
|
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.thymeleaf.TemplateEngine;
|
||||||
|
import org.thymeleaf.context.Context;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class EmailClient {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(EmailClient.class);
|
||||||
|
|
||||||
|
private final JavaMailSender mailSender;
|
||||||
|
|
||||||
|
private final TemplateEngine emailTemplateEngine;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public EmailClient(
|
||||||
|
JavaMailSender mailSender,
|
||||||
|
TemplateEngine emailTemplateEngine
|
||||||
|
) {
|
||||||
|
this.mailSender = mailSender;
|
||||||
|
this.emailTemplateEngine = emailTemplateEngine;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String fromAddress;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public void setFromAddress(@Value("${spring.mail.username}") String fromAddress) {
|
||||||
|
this.fromAddress = fromAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a template-based HTML email asynchronously.
|
||||||
|
*
|
||||||
|
* @param toRecipient recipient email address
|
||||||
|
* @param subject subject line
|
||||||
|
* @param templateName template name (without .html extension, under mail-templates/)
|
||||||
|
* @param variables context variables required by the template
|
||||||
|
*/
|
||||||
|
@Async
|
||||||
|
public void sendTemplateMail(String toRecipient, String subject, String templateName, Map<String, Object> variables) {
|
||||||
|
try {
|
||||||
|
// Render HTML content using the dedicated email template engine
|
||||||
|
var context = new Context();
|
||||||
|
if (variables != null && !variables.isEmpty()) {
|
||||||
|
context.setVariables(variables);
|
||||||
|
}
|
||||||
|
var htmlBody = emailTemplateEngine.process(templateName, context);
|
||||||
|
|
||||||
|
log.debug("htmlBody={}", htmlBody);
|
||||||
|
|
||||||
|
// Build the MIME message
|
||||||
|
var message = mailSender.createMimeMessage();
|
||||||
|
var helper = new MimeMessageHelper(
|
||||||
|
message,
|
||||||
|
MimeMessageHelper.MULTIPART_MODE_MIXED_RELATED,
|
||||||
|
StandardCharsets.UTF_8.name()
|
||||||
|
);
|
||||||
|
|
||||||
|
helper.setFrom(new InternetAddress(fromAddress, "阿萨拉向导", StandardCharsets.UTF_8.name()));
|
||||||
|
helper.setTo(toRecipient);
|
||||||
|
helper.setSubject(subject);
|
||||||
|
helper.setText(htmlBody, true);
|
||||||
|
|
||||||
|
// Dispatch the message
|
||||||
|
mailSender.send(message);
|
||||||
|
log.info("Successfully dispatched email to: [{}] with subject: [{}]", toRecipient, subject);
|
||||||
|
|
||||||
|
} catch (MessagingException | UnsupportedEncodingException ex) {
|
||||||
|
log.error("Failed to construct or send email to: [{}], subject: [{}]", toRecipient, subject, ex);
|
||||||
|
// Handle or rethrow custom business exception if required
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("Unexpected error occurred while processing email for: [{}]", toRecipient, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method specifically for sending registration/login verification codes.
|
||||||
|
*
|
||||||
|
* @param toRecipient target recipient email
|
||||||
|
* @param username display username
|
||||||
|
* @param code verification code string
|
||||||
|
* @param expireMinutes expiration time in minutes
|
||||||
|
*/
|
||||||
|
@Async
|
||||||
|
public void sendVerificationCode(String toRecipient, String username, String code, int expireMinutes) {
|
||||||
|
var subject = "【阿萨拉向导】账号注册验证码";
|
||||||
|
var variables = Map.<String, Object>of(
|
||||||
|
"username", username,
|
||||||
|
"code", code,
|
||||||
|
"expireMinutes", expireMinutes
|
||||||
|
);
|
||||||
|
|
||||||
|
sendTemplateMail(toRecipient, subject, "verification-code", variables);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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.ahsarahguide.domain.entity.User;
|
||||||
|
import com.onixbyte.ahsarahguide.properties.TokenProperties;
|
||||||
|
import com.onixbyte.ahsarahguide.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.config;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.interceptor.GitHubWebhookInterceptor;
|
||||||
|
import com.onixbyte.ahsarahguide.interceptor.TrafficInterceptor;
|
||||||
|
import com.onixbyte.ahsarahguide.properties.AppProperties;
|
||||||
|
import com.onixbyte.ahsarahguide.security.resolver.CurrentUserArgumentResolver;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||||
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableConfigurationProperties(AppProperties.class)
|
||||||
|
public class AppConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
private final TrafficInterceptor trafficInterceptor;
|
||||||
|
|
||||||
|
private final GitHubWebhookInterceptor gitHubWebhookInterceptor;
|
||||||
|
|
||||||
|
private final CurrentUserArgumentResolver currentUserArgumentResolver;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public AppConfig(
|
||||||
|
TrafficInterceptor trafficInterceptor,
|
||||||
|
GitHubWebhookInterceptor gitHubWebhookInterceptor,
|
||||||
|
CurrentUserArgumentResolver currentUserArgumentResolver
|
||||||
|
) {
|
||||||
|
this.trafficInterceptor = trafficInterceptor;
|
||||||
|
this.gitHubWebhookInterceptor = gitHubWebhookInterceptor;
|
||||||
|
this.currentUserArgumentResolver = currentUserArgumentResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
registry.addInterceptor(trafficInterceptor);
|
||||||
|
registry.addInterceptor(gitHubWebhookInterceptor)
|
||||||
|
.addPathPatterns("/webhooks/github");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
|
||||||
|
resolvers.add(currentUserArgumentResolver);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.config;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.config;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.config;
|
||||||
|
|
||||||
|
import com.onixbyte.captcha.text.TextProducer;
|
||||||
|
import com.onixbyte.captcha.text.impl.DefaultTextProducer;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.thymeleaf.TemplateEngine;
|
||||||
|
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||||
|
import org.thymeleaf.templatemode.TemplateMode;
|
||||||
|
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
|
||||||
|
import org.thymeleaf.templateresolver.ITemplateResolver;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class EmailTemplateConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public TemplateEngine emailTemplateEngine() {
|
||||||
|
var resolver = new ClassLoaderTemplateResolver();
|
||||||
|
// Template files are located under src/main/resources/mail-templates/
|
||||||
|
resolver.setPrefix("mail-templates/");
|
||||||
|
resolver.setSuffix(".html");
|
||||||
|
resolver.setTemplateMode(TemplateMode.HTML);
|
||||||
|
resolver.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||||
|
resolver.setCacheable(true); // Enable caching for the production environment
|
||||||
|
|
||||||
|
var templateEngine = new SpringTemplateEngine();
|
||||||
|
templateEngine.addTemplateResolver(resolver);
|
||||||
|
return templateEngine;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public TextProducer textProducer() {
|
||||||
|
return DefaultTextProducer.builder().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.config;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
import com.onixbyte.ahsarahguide.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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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.ahsarahguide.mapper"})
|
||||||
|
public class MyBatisConfig {
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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 = "Ahsarah Guide Server",
|
||||||
|
description = "API for Ahsarah Guide",
|
||||||
|
version = "1.4.0",
|
||||||
|
contact = @Contact(
|
||||||
|
name = "Zihlu Wang",
|
||||||
|
email = "zihlu.wang@onixbyte.com"
|
||||||
|
),
|
||||||
|
license = @License(
|
||||||
|
name = "MIT",
|
||||||
|
url = "https://onixbyte.dev/onixbyte/ahsarah-guide/raw/branch/main/LICENCE"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@Configuration
|
||||||
|
public class OpenApiConfiguration {
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.config;
|
||||||
|
|
||||||
|
import com.auth0.jwt.JWT;
|
||||||
|
import com.auth0.jwt.JWTVerifier;
|
||||||
|
import com.auth0.jwt.algorithms.Algorithm;
|
||||||
|
import com.onixbyte.ahsarahguide.exeption.ForbiddenException;
|
||||||
|
import com.onixbyte.ahsarahguide.exeption.UnauthorisedException;
|
||||||
|
import com.onixbyte.ahsarahguide.filter.TokenAuthenticationFilter;
|
||||||
|
import com.onixbyte.ahsarahguide.properties.CookieProperties;
|
||||||
|
import com.onixbyte.ahsarahguide.properties.TokenProperties;
|
||||||
|
import com.onixbyte.ahsarahguide.security.provider.UsernamePasswordAuthenticationProvider;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
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 {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the HTTP security filter chain including endpoint authorisation and JWT filter.
|
||||||
|
*
|
||||||
|
* @param httpSecurity 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()
|
||||||
|
)
|
||||||
|
.exceptionHandling(customiser -> customiser
|
||||||
|
.authenticationEntryPoint((request, response, authException) -> {
|
||||||
|
log.error("Unauthenticated request: {}", request, authException);
|
||||||
|
throw new UnauthorisedException("请先登录");
|
||||||
|
})
|
||||||
|
.accessDeniedHandler((request, response, accessDeniedException) -> {
|
||||||
|
log.error("Denied request: {}", request, accessDeniedException);
|
||||||
|
throw new ForbiddenException("权限不足");
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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.ahsarahguide.repository"})
|
||||||
|
public class SpringDataConfig {
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.config;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.properties.GitHubWebhookProperties;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableConfigurationProperties({GitHubWebhookProperties.class})
|
||||||
|
public class WebhookConfig {
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.controller;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.domain.dto.ManageRoleRequest;
|
||||||
|
import com.onixbyte.ahsarahguide.security.annotation.RequiresAuth;
|
||||||
|
import com.onixbyte.ahsarahguide.service.UserService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.Positive;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
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.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST controller for administrative user and role management.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
|
@Tag(name = "管理员", description = "管理员用户与角色管理")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/admin")
|
||||||
|
@Validated
|
||||||
|
@RequiresAuth
|
||||||
|
public class AdminController {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
public AdminController(UserService userService) {
|
||||||
|
this.userService = userService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(description = "为用户分配角色(仅超级管理员可操作)")
|
||||||
|
@PreAuthorize("hasRole('SUPER_USER')")
|
||||||
|
@PutMapping("/users/{id}/roles")
|
||||||
|
public ResponseEntity<Void> assignRole(
|
||||||
|
@PathVariable @Positive(message = "用户ID必须为正数") Long id,
|
||||||
|
@Valid @RequestBody ManageRoleRequest request
|
||||||
|
) {
|
||||||
|
userService.assignRole(id, request.role());
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(description = "移除用户角色(仅超级管理员可操作)")
|
||||||
|
@PreAuthorize("hasRole('SUPER_USER')")
|
||||||
|
@DeleteMapping("/users/{id}/roles/{role}")
|
||||||
|
public ResponseEntity<Void> removeRole(
|
||||||
|
@PathVariable @Positive(message = "用户ID必须为正数") Long id,
|
||||||
|
@PathVariable String role
|
||||||
|
) {
|
||||||
|
userService.removeRole(id, role);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.controller;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.domain.dto.*;
|
||||||
|
import com.onixbyte.ahsarahguide.security.annotation.RequiresAuth;
|
||||||
|
import com.onixbyte.ahsarahguide.service.AuthService;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST controller for user authentication endpoints (login, logout).
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
|
@Tag(name = "用户鉴权", description = "处理用户登录与退出功能")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/auth")
|
||||||
|
public class AuthController {
|
||||||
|
|
||||||
|
private final AuthService authService;
|
||||||
|
|
||||||
|
public AuthController(AuthService authService) {
|
||||||
|
this.authService = authService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(description = "用户登录")
|
||||||
|
@PostMapping("/login")
|
||||||
|
public ResponseEntity<UserResponse> login(@Validated @RequestBody LoginRequest request) {
|
||||||
|
var user = authService.login(request);
|
||||||
|
return authService.getUserResponseResponseEntity(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresAuth
|
||||||
|
@Operation(description = "退出登录")
|
||||||
|
@PostMapping("/logout")
|
||||||
|
public ResponseEntity<Void> logout() {
|
||||||
|
var expiredCookie = authService.logout();
|
||||||
|
return ResponseEntity.noContent()
|
||||||
|
.header(HttpHeaders.SET_COOKIE, expiredCookie.toString())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(description = "发送邮件验证码")
|
||||||
|
@PostMapping("/verification-code")
|
||||||
|
public SendVerificationCodeResponse sendVerificationCode(@Validated @RequestBody SendVerificationCodeRequest request) {
|
||||||
|
var verificationCodeId = authService.sendVerificationCode(request);
|
||||||
|
return new SendVerificationCodeResponse(verificationCodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(description = "用户注册")
|
||||||
|
@PostMapping("/register")
|
||||||
|
public ResponseEntity<UserResponse> register(@Validated @RequestBody RegisterRequest request) {
|
||||||
|
var user = authService.register(request);
|
||||||
|
return authService.getUserResponseResponseEntity(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.controller;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.domain.dto.DailyPasswordResponse;
|
||||||
|
import com.onixbyte.ahsarahguide.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.controller;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.domain.dto.FirearmRequest;
|
||||||
|
import com.onixbyte.ahsarahguide.domain.dto.FirearmResponse;
|
||||||
|
import com.onixbyte.ahsarahguide.domain.dto.PageResponse;
|
||||||
|
import com.onixbyte.ahsarahguide.enumeration.FirearmType;
|
||||||
|
import com.onixbyte.ahsarahguide.security.annotation.RequiresAuth;
|
||||||
|
import com.onixbyte.ahsarahguide.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.controller;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.domain.dto.ErrorResponse;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global exception handler that translates exceptions into standard error responses.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
@ExceptionHandler(ResponseStatusException.class)
|
||||||
|
public ResponseEntity<ErrorResponse> handleResponseStatusException(ResponseStatusException exception) {
|
||||||
|
var status = exception.getStatusCode();
|
||||||
|
return ResponseEntity.status(status)
|
||||||
|
.body(new ErrorResponse(exception.getReason()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(AuthenticationException.class)
|
||||||
|
public ResponseEntity<ErrorResponse> handleAuthenticationException(AuthenticationException exception) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||||
|
.body(new ErrorResponse("请先登录"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(AccessDeniedException.class)
|
||||||
|
public ResponseEntity<ErrorResponse> handleAccessDeniedException(AccessDeniedException exception) {
|
||||||
|
var authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
var httpStatus = authentication == null || !authentication.isAuthenticated()
|
||||||
|
? HttpStatus.UNAUTHORIZED
|
||||||
|
: HttpStatus.FORBIDDEN;
|
||||||
|
var message = httpStatus == HttpStatus.UNAUTHORIZED ? "请先登录" : "权限不足";
|
||||||
|
return ResponseEntity.status(httpStatus)
|
||||||
|
.body(new ErrorResponse(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.controller;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.domain.dto.ModificationBatchCreateRequest;
|
||||||
|
import com.onixbyte.ahsarahguide.domain.dto.ModificationRequest;
|
||||||
|
import com.onixbyte.ahsarahguide.domain.dto.ModificationResponse;
|
||||||
|
import com.onixbyte.ahsarahguide.domain.dto.PageResponse;
|
||||||
|
import com.onixbyte.ahsarahguide.domain.entity.User;
|
||||||
|
import com.onixbyte.ahsarahguide.security.annotation.RequiresAuth;
|
||||||
|
import com.onixbyte.ahsarahguide.security.resolver.CurrentUser;
|
||||||
|
import com.onixbyte.ahsarahguide.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(
|
||||||
|
@CurrentUser User user,
|
||||||
|
@Valid @RequestBody ModificationRequest request
|
||||||
|
) {
|
||||||
|
return modificationService.create(request, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresAuth
|
||||||
|
@Operation(description = "批量创建改装")
|
||||||
|
@PostMapping("/batch")
|
||||||
|
public List<ModificationResponse> batchCreate(
|
||||||
|
@CurrentUser User user,
|
||||||
|
@Valid @RequestBody ModificationBatchCreateRequest request
|
||||||
|
) {
|
||||||
|
return modificationService.batchCreate(request.modifications(), user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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.ahsarahguide.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.controller;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.domain.dto.BuildSummaryResponse;
|
||||||
|
import com.onixbyte.ahsarahguide.domain.dto.ChangePasswordRequest;
|
||||||
|
import com.onixbyte.ahsarahguide.domain.dto.PageResponse;
|
||||||
|
import com.onixbyte.ahsarahguide.domain.dto.UpdateProfileRequest;
|
||||||
|
import com.onixbyte.ahsarahguide.domain.dto.UserProfileResponse;
|
||||||
|
import com.onixbyte.ahsarahguide.domain.entity.User;
|
||||||
|
import com.onixbyte.ahsarahguide.security.annotation.RequiresAuth;
|
||||||
|
import com.onixbyte.ahsarahguide.security.resolver.CurrentUser;
|
||||||
|
import com.onixbyte.ahsarahguide.service.UserService;
|
||||||
|
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 org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST controller for the current user's profile management endpoints.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
|
@Tag(name = "用户个人中心", description = "当前用户的资料管理与密码修改")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/user")
|
||||||
|
@Validated
|
||||||
|
@RequiresAuth
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
public UserController(UserService userService) {
|
||||||
|
this.userService = userService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(description = "获取当前登录用户的个人资料")
|
||||||
|
@GetMapping("/me")
|
||||||
|
public UserProfileResponse profile(@CurrentUser User user) {
|
||||||
|
return userService.getProfile(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(description = "修改当前用户的个人资料(昵称与邮箱)")
|
||||||
|
@PutMapping("/me")
|
||||||
|
public UserProfileResponse updateProfile(
|
||||||
|
@CurrentUser User user,
|
||||||
|
@Valid @RequestBody UpdateProfileRequest request
|
||||||
|
) {
|
||||||
|
return userService.updateProfile(user, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(description = "修改当前用户的登录密码")
|
||||||
|
@PutMapping("/me/password")
|
||||||
|
public ResponseEntity<Void> changePassword(
|
||||||
|
@CurrentUser User user,
|
||||||
|
@Valid @RequestBody ChangePasswordRequest request
|
||||||
|
) {
|
||||||
|
userService.changePassword(user, request);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(description = "分页查询当前用户的改枪码列表")
|
||||||
|
@GetMapping("/me/builds")
|
||||||
|
public PageResponse<BuildSummaryResponse> builds(
|
||||||
|
@CurrentUser User user,
|
||||||
|
@RequestParam(defaultValue = "0") @Min(0) int page,
|
||||||
|
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int size
|
||||||
|
) {
|
||||||
|
return userService.getBuilds(user, PageRequest.of(page, size));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.controller;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.controller;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.domain.dto.GitHubIssueRequest;
|
||||||
|
import com.onixbyte.ahsarahguide.service.WebhookService;
|
||||||
|
import com.onixbyte.ahsarahguide.shared.GitHubWebhookHeader;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
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;
|
||||||
|
|
||||||
|
@Tag(name = "WebHook")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/webhooks")
|
||||||
|
public class WebhookController {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(WebhookController.class);
|
||||||
|
|
||||||
|
private final WebhookService webhookService;
|
||||||
|
|
||||||
|
public WebhookController(WebhookService webhookService) {
|
||||||
|
this.webhookService = webhookService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(description = "GitHub WebHook 接口")
|
||||||
|
@PostMapping("/github")
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.converter;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.converter;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.enumeration.ModificationStatus;
|
||||||
|
import jakarta.persistence.AttributeConverter;
|
||||||
|
import jakarta.persistence.Converter;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JPA attribute converter that maps {@link ModificationStatus} enum to/from its integer database representation.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
|
@Converter
|
||||||
|
public class ModificationStatusConverter implements AttributeConverter<ModificationStatus, Integer> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer convertToDatabaseColumn(ModificationStatus attribute) {
|
||||||
|
return Optional.ofNullable(attribute)
|
||||||
|
.map(ModificationStatus::getCode)
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ModificationStatus convertToEntityAttribute(Integer dbData) {
|
||||||
|
return ModificationStatus.fromCode(dbData);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.dto;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response record summarising a user's custom modification build.
|
||||||
|
* This is a skeleton record whose {@code from()} factory will be fully implemented
|
||||||
|
* when the CustomModification entity is introduced in a future issue.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
|
public record BuildSummaryResponse(
|
||||||
|
Long id,
|
||||||
|
String name,
|
||||||
|
String code,
|
||||||
|
String firearmName,
|
||||||
|
List<String> tags
|
||||||
|
) {
|
||||||
|
// from() factory to be implemented when CustomModification entity lands
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request record for changing the current user's password.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
|
public record ChangePasswordRequest(
|
||||||
|
@NotBlank(message = "旧密码不能为空")
|
||||||
|
String oldPassword,
|
||||||
|
|
||||||
|
@NotBlank(message = "新密码不能为空")
|
||||||
|
@Size(min = 8, max = 128, message = "密码长度必须在8-128个字符之间")
|
||||||
|
@Pattern(
|
||||||
|
regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$",
|
||||||
|
message = "密码必须包含至少一个大写字母、一个小写字母和一个数字"
|
||||||
|
)
|
||||||
|
String newPassword
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO representing a single daily-generated password for a map.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
|
public record DailyPassword(
|
||||||
|
String mapName,
|
||||||
|
String password
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO holding metadata about the daily password source and update tracking.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
|
public record DailyPasswordMetadata(
|
||||||
|
String version,
|
||||||
|
String author
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response DTO wrapping daily password data with metadata.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
|
public record DailyPasswordResponse(
|
||||||
|
String status,
|
||||||
|
String message,
|
||||||
|
DailyPasswordData data,
|
||||||
|
DailyPasswordMetadata metadata
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.dto;
|
||||||
|
|
||||||
|
public record EmailVerificationCode(
|
||||||
|
String username,
|
||||||
|
String email,
|
||||||
|
String code
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard error response body returned on API failures.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
|
public record ErrorResponse(
|
||||||
|
String message
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.dto;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.dto;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.domain.entity.Firearm;
|
||||||
|
import com.onixbyte.ahsarahguide.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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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
@@ -0,0 +1,12 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request record for managing a user's role assignment.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
|
public record ManageRoleRequest(
|
||||||
|
@NotBlank(message = "角色不能为空")
|
||||||
|
String role
|
||||||
|
) {}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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
@@ -0,0 +1,18 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.dto;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registration request containing the new user account information.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
|
@Schema(description = "注册请求")
|
||||||
|
public record RegisterRequest(
|
||||||
|
@NotBlank(message = "用户名不能为空")
|
||||||
|
@Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
String username,
|
||||||
|
@NotBlank(message = "邮箱不能为空")
|
||||||
|
@Email(message = "邮箱格式不正确")
|
||||||
|
@Schema(description = "邮箱", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
String email,
|
||||||
|
@Schema(description = "昵称")
|
||||||
|
String nickname,
|
||||||
|
@Schema(description = "头像链接")
|
||||||
|
String avatarUrl,
|
||||||
|
@NotBlank(message = "密码不能为空")
|
||||||
|
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
String password,
|
||||||
|
@NotBlank(message = "验证码 ID 不能为空") String verificationCodeId,
|
||||||
|
@NotBlank(message = "验证码不能为空") String verificationCode
|
||||||
|
) {
|
||||||
|
}
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
public record SendVerificationCodeRequest(
|
||||||
|
@NotBlank(message = "用户名不能为空") String username,
|
||||||
|
@NotBlank(message = "电子邮箱地址不能为空") String email
|
||||||
|
) {
|
||||||
|
}
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.dto;
|
||||||
|
|
||||||
|
public record SendVerificationCodeResponse(
|
||||||
|
String verificationCodeId
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.dto;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request record for updating the current user's profile information.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
|
public record UpdateProfileRequest(
|
||||||
|
@Size(min = 1, max = 64, message = "昵称长度必须在1-64个字符之间")
|
||||||
|
String nickname
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.dto;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.domain.entity.User;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response record for the current user's full profile information.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
|
public record UserProfileResponse(
|
||||||
|
Long id,
|
||||||
|
String username,
|
||||||
|
String nickname,
|
||||||
|
String email,
|
||||||
|
String avatarUrl,
|
||||||
|
Boolean enabled,
|
||||||
|
LocalDateTime createdAt,
|
||||||
|
List<String> roles
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@code UserProfileResponse} from a {@link User} entity and its assigned roles.
|
||||||
|
*
|
||||||
|
* @param user the user entity
|
||||||
|
* @param roles the list of role names assigned to the user
|
||||||
|
* @return the constructed response
|
||||||
|
*/
|
||||||
|
public static UserProfileResponse from(User user, List<String> roles) {
|
||||||
|
return new UserProfileResponse(
|
||||||
|
user.getId(),
|
||||||
|
user.getUsername(),
|
||||||
|
user.getNickname() == null || user.getNickname().isBlank()
|
||||||
|
? user.getUsername() : user.getNickname(),
|
||||||
|
user.getEmail(),
|
||||||
|
user.getAvatarUrl(),
|
||||||
|
user.getEnabled(),
|
||||||
|
user.getCreatedAt(),
|
||||||
|
roles
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.dto;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.entity;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.domain.converter.FirearmTypeConverter;
|
||||||
|
import com.onixbyte.ahsarahguide.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.entity;
|
||||||
|
|
||||||
|
import com.onixbyte.ahsarahguide.domain.converter.ModificationStatusConverter;
|
||||||
|
import com.onixbyte.ahsarahguide.enumeration.ModificationStatus;
|
||||||
|
import io.hypersistence.utils.hibernate.type.json.JsonType;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import org.hibernate.annotations.Type;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
@JoinColumn(name = "user_id")
|
||||||
|
private User user;
|
||||||
|
|
||||||
|
@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", nullable = false)
|
||||||
|
private List<String> tags = new ArrayList<>();
|
||||||
|
|
||||||
|
@Type(JsonType.class)
|
||||||
|
@Column(name = "accessories", columnDefinition = "jsonb", nullable = false)
|
||||||
|
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;
|
||||||
|
|
||||||
|
@Column(name = "status", nullable = false)
|
||||||
|
@Convert(converter = ModificationStatusConverter.class)
|
||||||
|
private ModificationStatus status = ModificationStatus.DRAFT;
|
||||||
|
|
||||||
|
@Column(name = "create_by")
|
||||||
|
private Long createBy;
|
||||||
|
|
||||||
|
@Column(name = "create_time", nullable = false)
|
||||||
|
private OffsetDateTime createTime;
|
||||||
|
|
||||||
|
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 User getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUser(User user) {
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getCreateBy() {
|
||||||
|
return createBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreateBy(Long createBy) {
|
||||||
|
this.createBy = createBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OffsetDateTime getCreateTime() {
|
||||||
|
return createTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreateTime(OffsetDateTime createTime) {
|
||||||
|
this.createTime = createTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModificationStatus getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(ModificationStatus status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private User user;
|
||||||
|
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;
|
||||||
|
private Long createBy;
|
||||||
|
private OffsetDateTime createTime;
|
||||||
|
private ModificationStatus status;
|
||||||
|
|
||||||
|
public Builder id(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder user(User user) {
|
||||||
|
this.user = user;
|
||||||
|
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 Builder createBy(Long createBy) {
|
||||||
|
this.createBy = createBy;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder createTime(OffsetDateTime createTime) {
|
||||||
|
this.createTime = createTime;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder status(ModificationStatus status) {
|
||||||
|
this.status = status;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Modification build() {
|
||||||
|
Modification modification = new Modification();
|
||||||
|
modification.id = this.id;
|
||||||
|
modification.user = this.user;
|
||||||
|
modification.firearm = this.firearm;
|
||||||
|
modification.name = this.name;
|
||||||
|
modification.code = this.code;
|
||||||
|
modification.tags = Optional.ofNullable(this.tags).orElseGet(ArrayList::new);
|
||||||
|
modification.accessories = Optional.ofNullable(this.accessories).orElseGet(ArrayList::new);
|
||||||
|
modification.note = this.note;
|
||||||
|
modification.author = this.author;
|
||||||
|
modification.videoUrl = this.videoUrl;
|
||||||
|
modification.createBy = this.createBy;
|
||||||
|
modification.createTime = Optional.ofNullable(this.createTime).orElseGet(OffsetDateTime::now);
|
||||||
|
modification.status = Optional.ofNullable(this.status).orElse(ModificationStatus.DRAFT);
|
||||||
|
return modification;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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.PreUpdate;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
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;
|
||||||
|
|
||||||
|
@Column(name = "nickname")
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
@Column(name = "avatar_url")
|
||||||
|
private String avatarUrl;
|
||||||
|
|
||||||
|
@Column(name = "enabled", nullable = false)
|
||||||
|
private Boolean enabled = true;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "updated_at", nullable = false)
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@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 String getNickname() {
|
||||||
|
return nickname;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNickname(String nickname) {
|
||||||
|
this.nickname = nickname;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAvatarUrl() {
|
||||||
|
return avatarUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAvatarUrl(String avatarUrl) {
|
||||||
|
this.avatarUrl = avatarUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(Boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getUpdatedAt() {
|
||||||
|
return updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
private void onUpdate() {
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 String nickname;
|
||||||
|
private String avatarUrl;
|
||||||
|
private Boolean enabled;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
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 nickname(String nickname) {
|
||||||
|
this.nickname = nickname;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder avatarUrl(String avatarUrl) {
|
||||||
|
this.avatarUrl = avatarUrl;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder enabled(Boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder createdAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder updatedAt(LocalDateTime updatedAt) {
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
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.nickname = this.nickname == null || this.nickname.isBlank()
|
||||||
|
? this.username : this.nickname;
|
||||||
|
user.avatarUrl = this.avatarUrl;
|
||||||
|
user.enabled = this.enabled == null || this.enabled;
|
||||||
|
user.createdAt = this.createdAt;
|
||||||
|
user.updatedAt = this.updatedAt;
|
||||||
|
user.credentials = this.credentials == null ? new ArrayList<>() : this.credentials;
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.domain.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.ForeignKey;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.MapsId;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity representing a role assignment linking a user to a security role.
|
||||||
|
* <p>
|
||||||
|
* A user owns at most one role assignment, so the user ID is the primary key.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "user_role")
|
||||||
|
public class UserRole {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "user_id")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@MapsId("userId")
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "user_id", nullable = false, foreignKey = @ForeignKey(name = "fk_user_role_user"))
|
||||||
|
private User user;
|
||||||
|
|
||||||
|
@Column(name = "role", nullable = false, length = 32)
|
||||||
|
private String role;
|
||||||
|
|
||||||
|
public Long getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(Long userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUser(User user) {
|
||||||
|
this.user = user;
|
||||||
|
if (user != null) {
|
||||||
|
this.userId = user.getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRole() {
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRole(String role) {
|
||||||
|
this.role = role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
|
||||||
|
private User user;
|
||||||
|
private Long userId;
|
||||||
|
private String role;
|
||||||
|
|
||||||
|
public Builder user(User user) {
|
||||||
|
this.user = user;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder userId(Long userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder role(String role) {
|
||||||
|
this.role = role;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserRole build() {
|
||||||
|
UserRole userRole = new UserRole();
|
||||||
|
userRole.user = this.user;
|
||||||
|
userRole.role = this.role;
|
||||||
|
if (this.user != null) {
|
||||||
|
userRole.userId = this.user.getId();
|
||||||
|
}
|
||||||
|
if (this.userId != null) {
|
||||||
|
userRole.userId = this.userId;
|
||||||
|
}
|
||||||
|
return userRole;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.enumeration;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumeration of modification statuses.
|
||||||
|
* <p>
|
||||||
|
* Each status is associated with an integer code used for database persistence.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
|
public enum ModificationStatus {
|
||||||
|
|
||||||
|
DRAFT(0),
|
||||||
|
PENDING_APPROVAL(1),
|
||||||
|
PUBLISHED(2);
|
||||||
|
|
||||||
|
private final int code;
|
||||||
|
|
||||||
|
ModificationStatus(int code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Map<Integer, ModificationStatus> CODE_MAP = Arrays.stream(values())
|
||||||
|
.collect(Collectors.toMap(ModificationStatus::getCode, Function.identity()));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a ModificationStatus from its integer code.
|
||||||
|
*
|
||||||
|
* @param code the integer code, may be null
|
||||||
|
* @return the corresponding ModificationStatus, or null if the code is null
|
||||||
|
* @throws IllegalArgumentException if the code does not match any known status
|
||||||
|
*/
|
||||||
|
public static ModificationStatus fromCode(Integer code) {
|
||||||
|
return CODE_MAP.get(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.exeption;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
public class BadRequestException extends ResponseStatusException {
|
||||||
|
|
||||||
|
public BadRequestException(String reason) {
|
||||||
|
super(HttpStatus.BAD_REQUEST, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.exeption;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
public class ConflictException extends ResponseStatusException {
|
||||||
|
|
||||||
|
public ConflictException(String reason) {
|
||||||
|
super(HttpStatus.CONFLICT, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.exeption;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
public class ForbiddenException extends ResponseStatusException {
|
||||||
|
|
||||||
|
public ForbiddenException(String reason) {
|
||||||
|
super(HttpStatus.FORBIDDEN, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package com.onixbyte.ahsarahguide.exeption;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
public class InternalServerErrorException extends ResponseStatusException {
|
||||||
|
|
||||||
|
public InternalServerErrorException(String reason) {
|
||||||
|
super(HttpStatus.INTERNAL_SERVER_ERROR, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loaded 100 of 322 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user