Compare commits
No commits in common. "main" and "v1.0.0" have entirely different histories.
315 changed files with 3031 additions and 18722 deletions
|
@ -1,20 +0,0 @@
|
||||||
frontend:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- "frontend/**"
|
|
||||||
|
|
||||||
backend:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- "backend/**"
|
|
||||||
|
|
||||||
ci:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- ".gitea/**"
|
|
||||||
|
|
||||||
docs:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- "projektdokumentation/**"
|
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
buckets:
|
|
||||||
- maxSize: 80
|
|
||||||
label: size/small
|
|
||||||
comment: null
|
|
||||||
- maxSize: 200
|
|
||||||
label: size/medium
|
|
||||||
comment: null
|
|
||||||
- maxSize: 2000
|
|
||||||
label: size/large
|
|
||||||
comment: >
|
|
||||||
👮♀️⚠️ This is a friendly reminder that the diff size of this PR is bigger than
|
|
||||||
200 lines we aim for. Please consider splitting this PR into more digestible pieces!
|
|
||||||
- maxSize: Infinity
|
|
||||||
label: size/huge
|
|
||||||
comment: >
|
|
||||||
👮♀️🛑 This PR's diff size is quite huge.
|
|
||||||
Hopefully you know what you're doing.
|
|
||||||
If you did not commit a lot of autogenerated files intentionally,
|
|
||||||
there are few good reasons for this.
|
|
|
@ -1,295 +0,0 @@
|
||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ci-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
changed_files:
|
|
||||||
name: Get Changed Files
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
backend: ${{ steps.filter.outputs.backend }}
|
|
||||||
frontend: ${{ steps.filter.outputs.frontend }}
|
|
||||||
workflow: ${{ steps.filter.outputs.workflow }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Check for file changes
|
|
||||||
uses: dorny/paths-filter@v3
|
|
||||||
id: filter
|
|
||||||
with:
|
|
||||||
filters: |
|
|
||||||
backend:
|
|
||||||
- 'backend/**'
|
|
||||||
frontend:
|
|
||||||
- 'frontend/**'
|
|
||||||
workflow:
|
|
||||||
- '.gitea/workflows/**'
|
|
||||||
|
|
||||||
test-backend:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: "Backend Tests"
|
|
||||||
needs: changed_files
|
|
||||||
if: ${{ needs.changed_files.outputs.backend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
|
|
||||||
container:
|
|
||||||
image: "cimg/openjdk:23.0-node"
|
|
||||||
steps:
|
|
||||||
- name: "Checkout"
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: "Run tests"
|
|
||||||
working-directory: ./backend
|
|
||||||
run: |
|
|
||||||
./gradlew test
|
|
||||||
|
|
||||||
- name: "Cache checkstyle results"
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: checkstyle-results
|
|
||||||
path: backend/build/reports/checkstyle
|
|
||||||
|
|
||||||
- name: "Stop Gradle"
|
|
||||||
working-directory: ./backend
|
|
||||||
run: ./gradlew --stop
|
|
||||||
|
|
||||||
checkstyle:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: "Checkstyle Main"
|
|
||||||
needs: changed_files
|
|
||||||
if: ${{ needs.changed_files.outputs.backend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
|
|
||||||
container:
|
|
||||||
image: "cimg/openjdk:23.0-node"
|
|
||||||
steps:
|
|
||||||
- name: "Checkout"
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: "Cache Gradle dependencies"
|
|
||||||
uses: https://github.com/actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.gradle/caches
|
|
||||||
~/.gradle/wrapper
|
|
||||||
backend/build/reports/checkstyle
|
|
||||||
key: gradle-${{ runner.os }}-${{ hashFiles('backend/**/*.java', 'backend/config/checkstyle/checkstyle.xml') }}
|
|
||||||
restore-keys: |
|
|
||||||
gradle-${{ runner.os }}-
|
|
||||||
- name: "Check cache for checkstyle reports"
|
|
||||||
id: check-cache
|
|
||||||
run: |
|
|
||||||
if [ -d "backend/build/reports/checkstyle" ] && [ -f "backend/build/reports/checkstyle/main.xml" ]; then
|
|
||||||
echo "Cache hit! Using cached checkstyle results"
|
|
||||||
echo "cache-hit=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "Cache miss! Running checkstyle check"
|
|
||||||
echo "cache-hit=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: "Run checkstyle"
|
|
||||||
if: steps.check-cache.outputs.cache-hit != 'true'
|
|
||||||
working-directory: ./backend
|
|
||||||
run: |
|
|
||||||
gradle checkstyleMain
|
|
||||||
|
|
||||||
- name: "Cache checkstyle results"
|
|
||||||
if: steps.check-cache.outputs.cache-hit != 'true'
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: checkstyle-results
|
|
||||||
path: backend/build/reports/checkstyle
|
|
||||||
|
|
||||||
- name: "Stop Gradle"
|
|
||||||
if: steps.check-cache.outputs.cache-hit != 'true'
|
|
||||||
working-directory: ./backend
|
|
||||||
run: gradle --stop
|
|
||||||
|
|
||||||
validate-docker-frontend:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Docker frontend validation
|
|
||||||
needs: changed_files
|
|
||||||
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
|
|
||||||
container:
|
|
||||||
image: catthehacker/ubuntu:act-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Build
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: frontend/
|
|
||||||
file: frontend/.docker/Dockerfile
|
|
||||||
push: false
|
|
||||||
|
|
||||||
validate-docker-backend:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Docker backend validation
|
|
||||||
needs: changed_files
|
|
||||||
if: ${{ needs.changed_files.outputs.backend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
|
|
||||||
container:
|
|
||||||
image: catthehacker/ubuntu:act-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Build
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: backend/
|
|
||||||
file: backend/.docker/Dockerfile
|
|
||||||
push: false
|
|
||||||
|
|
||||||
eslint:
|
|
||||||
runs-on: docker
|
|
||||||
container:
|
|
||||||
image: git.kjan.de/actions/runner-bun:latest
|
|
||||||
name: eslint
|
|
||||||
needs: changed_files
|
|
||||||
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
working-directory: ./frontend
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
frontend/node_modules/
|
|
||||||
key: ${{ runner.os }}-bun-
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
cd frontend
|
|
||||||
bun install
|
|
||||||
- name: Run Eslint
|
|
||||||
run: |
|
|
||||||
cd frontend
|
|
||||||
bun run lint
|
|
||||||
|
|
||||||
playwright:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Playwright
|
|
||||||
needs: changed_files
|
|
||||||
container:
|
|
||||||
image: git.kjan.de/actions/runner-casino-playwright:latest
|
|
||||||
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
distribution: "temurin" # See 'Supported distributions' for available options
|
|
||||||
java-version: "23"
|
|
||||||
- name: Install bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
working-directory: ./frontend
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
frontend/node_modules/
|
|
||||||
key: ${{ runner.os }}-bun-
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-
|
|
||||||
- run: bun add -g concurrently
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
cd frontend
|
|
||||||
bun install
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22.12
|
|
||||||
working-directory: ./frontend
|
|
||||||
- name: Run Playwright tests
|
|
||||||
env:
|
|
||||||
CI: true
|
|
||||||
SPRING_PROFILES_ACTIVE: inmemory
|
|
||||||
working-directory: ./frontend
|
|
||||||
run: bash -c "source $HOME/.cargo/env && bunx playwright test"
|
|
||||||
|
|
||||||
oxlint:
|
|
||||||
runs-on: docker
|
|
||||||
name: oxlint
|
|
||||||
needs: changed_files
|
|
||||||
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
|
|
||||||
container:
|
|
||||||
image: git.kjan.de/actions/runner-bun:latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
working-directory: ./frontend
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
frontend/node_modules/
|
|
||||||
key: ${{ runner.os }}-bun-
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
cd frontend
|
|
||||||
bun install
|
|
||||||
- name: Run oxlint
|
|
||||||
run: |
|
|
||||||
cd frontend
|
|
||||||
bun run oxlint
|
|
||||||
|
|
||||||
prettier:
|
|
||||||
runs-on: docker
|
|
||||||
name: prettier
|
|
||||||
needs: changed_files
|
|
||||||
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
|
|
||||||
container:
|
|
||||||
image: git.kjan.de/actions/runner-bun:latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
working-directory: ./frontend
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
frontend/node_modules/
|
|
||||||
key: ${{ runner.os }}-bun-
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
cd frontend
|
|
||||||
bun install
|
|
||||||
- name: Run prettier
|
|
||||||
run: |
|
|
||||||
cd frontend
|
|
||||||
bun run format:check
|
|
||||||
|
|
||||||
test-build:
|
|
||||||
runs-on: docker
|
|
||||||
name: test-build
|
|
||||||
needs: changed_files
|
|
||||||
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
|
|
||||||
container:
|
|
||||||
image: git.kjan.de/actions/runner-bun:latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
working-directory: ./frontend
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
frontend/node_modules/
|
|
||||||
key: ${{ runner.os }}-bun-
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
working-directory: ./frontend
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
frontend/dist/
|
|
||||||
key: ${{ runner.os }}-dist-
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-dist-
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
cd frontend
|
|
||||||
bun install
|
|
||||||
- name: Test build
|
|
||||||
run: |
|
|
||||||
cd frontend
|
|
||||||
bun run build
|
|
|
@ -1,124 +0,0 @@
|
||||||
name: Claude Gitea PR Interaction via Comment
|
|
||||||
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
claude-interact-on-comment:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: |
|
|
||||||
github.event.issue.pull_request &&
|
|
||||||
contains(github.event.comment.body, '@Claude')
|
|
||||||
steps:
|
|
||||||
- name: Check out code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0 # Required for git diff against main/master
|
|
||||||
|
|
||||||
- name: Set Tea Version
|
|
||||||
id: tea_version
|
|
||||||
run: echo "version=0.9.2" >> $GITHUB_OUTPUT # Check for the latest stable version
|
|
||||||
|
|
||||||
- name: Download Tea CLI
|
|
||||||
run: |
|
|
||||||
TEA_VERSION=$(echo "${{ steps.tea_version.outputs.version }}")
|
|
||||||
wget "https://gitea.com/gitea/tea/releases/download/v${TEA_VERSION}/tea-${TEA_VERSION}-linux-amd64" -O tea
|
|
||||||
chmod +x tea
|
|
||||||
sudo mv tea /usr/local/bin/tea
|
|
||||||
|
|
||||||
- name: Verify Tea Installation
|
|
||||||
run: tea --version
|
|
||||||
|
|
||||||
- name: Add Gitea Login
|
|
||||||
env:
|
|
||||||
GITEA_URL: ${{ secrets._GITEA_URL }}
|
|
||||||
GITEA_TOKEN: ${{ secrets._GITEA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
if [ -z "$GITEA_URL" ]; then
|
|
||||||
echo "Error: GITEA_URL secret is not set."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [ -z "$GITEA_TOKEN" ]; then
|
|
||||||
echo "Error: GITEA_TOKEN secret is not set."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
INSECURE_FLAG=""
|
|
||||||
if [[ "${GITEA_URL}" == http://* ]]; then
|
|
||||||
INSECURE_FLAG="--insecure"
|
|
||||||
fi
|
|
||||||
tea login add --name mygitea --url "$GITEA_URL" --token "$GITEA_TOKEN" $INSECURE_FLAG
|
|
||||||
|
|
||||||
- name: Install bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
|
|
||||||
- name: Install claude-code
|
|
||||||
run: bun i -g @anthropic-ai/claude-code
|
|
||||||
|
|
||||||
- name: Claude Process PR Comment
|
|
||||||
env:
|
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
||||||
PR_NUMBER: ${{ github.event.issue.number }}
|
|
||||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
|
||||||
COMMENT_AUTHOR: ${{ github.event.comment.user.login }}
|
|
||||||
GITEA_URL: ${{ secrets._GITEA_URL }}
|
|
||||||
run: |
|
|
||||||
claude --allowedTools "Bash(tea:*)" --allowedTools "Bash(git:*)" --allowedTools "Read" --allowedTools "Grep" --allowedTools "WebFetch" --allowedTools "Glob" --allowedTools "LS" -p "You are an AI assistant integrated with Gitea (at ${GITEA_URL}) via the 'tea' CLI.
|
|
||||||
You have been invoked because user '${COMMENT_AUTHOR}' left the following comment on Pull Request #${PR_NUMBER}:
|
|
||||||
---
|
|
||||||
${COMMENT_BODY}
|
|
||||||
---
|
|
||||||
|
|
||||||
Your primary task is to:
|
|
||||||
1. Carefully understand the user's request within their comment.
|
|
||||||
2. Use the 'tea' CLI to perform the requested action(s) on Pull Request #${PR_NUMBER}.
|
|
||||||
3. If the request is to review the PR, fetch the diff against the PR's base branch (e.g., 'git fetch origin main && git diff origin/main...HEAD' or similar; adapt branch name if necessary, or use 'tea pr diff ${PR_NUMBER}') and provide constructive feedback.
|
|
||||||
4. For other actions, translate the user's intent into the appropriate 'tea' command.
|
|
||||||
|
|
||||||
**How to Post Reviews and Other Feedback:**
|
|
||||||
- When you provide a review, post it as a comment using:
|
|
||||||
\`tea pr comment ${PR_NUMBER} \"Claude's Review:\n[Your detailed review, mentioning files and line numbers.]\"\`
|
|
||||||
- For other informational responses or clarifications, also use \`tea pr comment ...\`.
|
|
||||||
|
|
||||||
**Critical: Handling Approval, Rejection, or Merge Requests:**
|
|
||||||
Pull Request approval, rejection, and merging are critical actions and should not be used to 'cheat' the review process. You cannot verify Gitea user permissions.
|
|
||||||
- If a user comments asking you to directly approve (e.g., '@claude approve this'), merge, or reject a PR:
|
|
||||||
1. **Do NOT blindly execute these commands.**
|
|
||||||
2. **Approval/Merge:**
|
|
||||||
- State in a comment (using \`tea pr comment ...\`) that direct approval/merge requests via you are typically for convenience after a proper human review process has been implicitly completed or if the requester is a designated maintainer explicitly overriding.
|
|
||||||
- If the PR has not been reviewed by you yet, and the comment implies a review is also needed, perform the review FIRST and post it.
|
|
||||||
- You should only consider proceeding with a \`tea pr approve ${PR_NUMBER}\` or \`tea pr merge ${PR_NUMBER}\` command if:
|
|
||||||
a. The comment explicitly states that all necessary human reviews are complete and this is just a formal step by a trusted user.
|
|
||||||
b. OR, your own comprehensive review found no critical issues and the request seems appropriate in context.
|
|
||||||
- If in doubt, default to posting your review (if applicable) and stating that a maintainer should perform the final approval/merge. Your goal is to assist, not to bypass established review procedures.
|
|
||||||
3. **Rejection/Requesting Changes:**
|
|
||||||
- If asked to reject or request changes, you should typically base this on your own review of the PR's changes.
|
|
||||||
- First, perform a review if you haven't already.
|
|
||||||
- Then, you can use \`tea pr reject ${PR_NUMBER} \"Claude's Review Summary: [summary of reasons for rejection/changes based on your review]\"\`. Ensure your detailed review is also available as a comment.
|
|
||||||
|
|
||||||
Examples of interpreting comments and generating appropriate \`tea\` commands (keeping the above critical guidelines in mind):
|
|
||||||
- User: '@claude LGTM, approve this' -> You: First, consider if a review is implied or done. If so, and you agree, you might generate \`tea pr approve ${PR_NUMBER}\`. If not, you might generate \`tea pr comment ${PR_NUMBER} \"Claude: I can approve this if the standard review process is complete. Have maintainers reviewed this?\"\` or perform your own review and then comment.
|
|
||||||
- User: '@claude please review this PR' -> You: Get diffs, review, then generate \`tea pr comment ${PR_NUMBER} \"Claude's Review: ...\"\`.
|
|
||||||
- User: '@claude close this PR' -> You: Generate \`tea pr close ${PR_NUMBER}\` and optionally \`tea pr comment ${PR_NUMBER} \"Claude: PR #${PR_NUMBER} has been closed as requested.\"\`.
|
|
||||||
- User: '@claude add label enhancement' -> You: Generate \`tea pr label ${PR_NUMBER} --add enhancement\` and \`tea pr comment ${PR_NUMBER} \"Claude: Label 'enhancement' added to PR #${PR_NUMBER}.\"\`
|
|
||||||
- User: '@claude what are the labels on this PR?' -> You: Generate \`tea pr label ${PR_NUMBER} --list\` (this command outputs to stdout, which is fine for your internal use). Then, to inform the user, you generate: \`tea pr comment ${PR_NUMBER} \"Claude: The current labels are: [output from tea pr label --list].\"\` (You'll need to capture the output of the first command to formulate the second if the tool allows such chaining, otherwise, focus on commands that directly achieve the user's goal or report information). *Self-correction: The Bash tool can capture output. So, if you need to run a \`tea\` command to get information for yourself, do so, then use that information to formulate your \`tea pr comment ...\` to the user.*
|
|
||||||
|
|
||||||
**IMPORTANT GUIDELINES FOR YOUR OPERATION AND RESPONSE GENERATION:**
|
|
||||||
- **Your SOLE METHOD of communicating back to the user on Gitea is by generating a \`tea pr comment ${PR_NUMBER} \"...\"\` command.** This is non-negotiable. Do not output plain text messages intended for the user. Your response *is* the command.
|
|
||||||
- **Use the 'tea' CLI for ALL Gitea interactions.** This includes fetching PR details, diffs, labels, status, and posting comments, reviews, approvals, etc.
|
|
||||||
- **For PR reviews, ALWAYS analyze the diff.** Use \`tea pr diff ${PR_NUMBER}\` or git commands to get the diff. Make sure to mention specific files and line numbers in your review comment.
|
|
||||||
- **Be precise with 'tea' commands.** If a user's request is ambiguous, DO NOT GUESS. Instead, generate a \`tea pr comment ${PR_NUMBER} \"Claude Asks: [Your clarifying question]\"\` command to ask for more details.
|
|
||||||
- **Execute only necessary 'tea' command(s).** If a user asks for a review, your primary output should be the \`tea pr comment ...\` command containing the review. If they ask to add a label, your output should be \`tea pr label ...\` and then a confirmation \`tea pr comment ...\`.
|
|
||||||
- **Ensure reviews are professional, constructive, and helpful.**
|
|
||||||
- **If you need to perform an action AND then report on it, generate both \`tea\` commands sequentially.** For example, to add a label and confirm:
|
|
||||||
\`tea pr label ${PR_NUMBER} --add bug\`
|
|
||||||
\`tea pr comment ${PR_NUMBER} "Claude: I've added the 'bug' label."\`
|
|
||||||
The GitHub Actions workflow will execute these commands one after another.
|
|
||||||
- **If a user's request cannot be fulfilled using the 'tea' CLI or the allowed tools, explain this limitation by generating a \`tea pr comment ...\` command.** For example: \`tea pr comment ${PR_NUMBER} "Claude: I cannot perform that action as it's outside my current capabilities or allowed tools."\`
|
|
||||||
- **Think step-by-step.** 1. Understand request. 2. Identify necessary `tea` command(s). 3. If it's a review, get the diff. 4. Formulate the `tea` command(s) as your direct output.
|
|
||||||
|
|
||||||
**Final Check before outputting:**
|
|
||||||
"Is my entire response that's intended for the Gitea user wrapped in a \`tea pr comment ${PR_NUMBER} '...' \` command (or another appropriate \`tea\` command if it's an action like \`tea pr label ...\`)? If not, I must fix it."
|
|
||||||
|
|
||||||
You are now ready to process the comment. Remember, your output will be executed in a shell. Generate only the \`tea\` command(s) needed.
|
|
||||||
"
|
|
|
@ -1,16 +0,0 @@
|
||||||
name: Claude PR Review
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize] # Runs on new PRs and updates
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
claude-code:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Claude
|
|
||||||
uses: https://git.kjan.de/actions/claude-pr-review@v1.0.4
|
|
||||||
with:
|
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
||||||
GITEA_URL: ${{ secrets._GITEA_URL }}
|
|
||||||
GITEA_CLAUDE_TOKEN: ${{ secrets._GITEA_TOKEN }}
|
|
|
@ -1,29 +0,0 @@
|
||||||
name: Build docs
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-docs:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: git.kjan.de/actions/runner-latex:latest
|
|
||||||
env:
|
|
||||||
# Edit here with the names of your latex file and directory (can use ".")
|
|
||||||
DIR: projektdokumentation
|
|
||||||
FILE: Projektdokumentation.tex
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: LaTeX compile
|
|
||||||
working-directory: ${{ env.DIR }}
|
|
||||||
run: latexmk -pdf ${{ env.FILE }}
|
|
||||||
|
|
||||||
- name: Upload artifacts
|
|
||||||
uses: https://git.kjan.de/actions/upload-artifact@v3 # Do not upgrade
|
|
||||||
with:
|
|
||||||
name: Doku
|
|
||||||
path: projektdokumentation/Projektdokumentation.pdf
|
|
|
@ -1,14 +0,0 @@
|
||||||
name: "Pull Request Labeler"
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
labeler:
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/labeler@v5
|
|
||||||
with:
|
|
||||||
configuration-path: ".gitea/labeler.yml"
|
|
|
@ -3,11 +3,6 @@ on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "main"
|
- "main"
|
||||||
paths:
|
|
||||||
- "backend/**"
|
|
||||||
- "frontend/**"
|
|
||||||
- ".gitea/workflows/release.yml"
|
|
||||||
- "release.config.cjs"
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
@ -17,8 +12,8 @@ permissions:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Release
|
name: Release
|
||||||
|
runs-on: remote
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
issues: write
|
issues: write
|
||||||
|
@ -30,65 +25,3 @@ jobs:
|
||||||
with:
|
with:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
|
||||||
build-backend-image:
|
|
||||||
needs: release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Build Backend Image
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0 # Ensure full history is available
|
|
||||||
- name: Extract tag
|
|
||||||
run: |
|
|
||||||
TAG=$(git describe --tags --abbrev=0)
|
|
||||||
echo "TAG=$TAG" >> $GITHUB_ENV
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Login
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: git.kjan.de
|
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
|
||||||
password: ${{ secrets.DOCKER_PASS }}
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: backend/
|
|
||||||
file: backend/.docker/Dockerfile
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
git.kjan.de/szut/casino-backend:latest
|
|
||||||
git.kjan.de/szut/casino-backend:${{ env.TAG }}
|
|
||||||
|
|
||||||
build-frontend-image:
|
|
||||||
needs: release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Build Frontend Image
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0 # Ensure full history is available
|
|
||||||
- name: Extract tag
|
|
||||||
run: |
|
|
||||||
TAG=$(git describe --tags --abbrev=0)
|
|
||||||
echo "TAG=$TAG" >> $GITHUB_ENV
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Login
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: git.kjan.de
|
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
|
||||||
password: ${{ secrets.DOCKER_PASS }}
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: frontend/
|
|
||||||
file: frontend/.docker/Dockerfile
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
git.kjan.de/szut/casino-frontend:latest
|
|
||||||
git.kjan.de/szut/casino-frontend:${{ env.TAG }}
|
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
name: Label PRs based on size
|
|
||||||
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
add_pr_size_label:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Check PR size
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Label and comment PR
|
|
||||||
uses: boschresearch/pr-size-labeler@v5.0.1
|
|
||||||
with:
|
|
||||||
bucketConfigFile: ".gitea/size.yml"
|
|
|
@ -1,15 +0,0 @@
|
||||||
name: "Close stale issues and PRs"
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
- cron: "@hourly"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
stale:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/stale@v9
|
|
||||||
with:
|
|
||||||
stale-pr-message: "Will be closed in x days bc yo mom is a bitch. im not telling you when it will be closed fuckface"
|
|
||||||
days-before-pr-stale: 2
|
|
||||||
days-before-pr-close: 3
|
|
41
CLAUDE.md
41
CLAUDE.md
|
@ -1,41 +0,0 @@
|
||||||
# Casino Gaming Platform - Claude Assistant Guide
|
|
||||||
|
|
||||||
## Build Commands
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
- Build: `bun run build` or `bunx @angular/cli build`
|
|
||||||
- Start dev server: `bun run start` or `bunx @angular/cli serve --proxy-config src/proxy.conf.json`
|
|
||||||
- Format: `bun run format` or `prettier --write "src/**/*.{ts,html,css,scss}"`
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
- Build: `./gradlew build` or `./gradlew clean build`
|
|
||||||
- Run: `./gradlew bootRun`
|
|
||||||
- Generate JAR: `./gradlew bootJar`
|
|
||||||
|
|
||||||
## Lint/Test Commands
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
- Lint: `bun run lint` or `ng lint`
|
|
||||||
- Test all: `bun run test` or `bunx @angular/cli test`
|
|
||||||
- Test single file: `bunx @angular/cli test --include=path/to/test.spec.ts`
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
- Test all: `./gradlew test`
|
|
||||||
- Test single class: `./gradlew test --tests "FullyQualifiedClassName"`
|
|
||||||
- Checkstyle: `./gradlew checkstyleMain checkstyleTest`
|
|
||||||
|
|
||||||
## Code Style Guidelines
|
|
||||||
|
|
||||||
### Frontend (Angular)
|
|
||||||
- Use PascalCase for class names with suffixes (Component, Service)
|
|
||||||
- Use kebab-case for component selectors with "app-" prefix
|
|
||||||
- File naming: `name.component.ts`, `name.service.ts`
|
|
||||||
- Import order: Angular → third-party → local
|
|
||||||
- Use RxJS catchError for HTTP error handling
|
|
||||||
|
|
||||||
### Backend (Java)
|
|
||||||
- Use PascalCase for classes with descriptive suffixes (Controller, Service, Entity)
|
|
||||||
- Use camelCase for methods and variables
|
|
||||||
- Domain-driven package organization
|
|
||||||
- Prefix DTOs with domain and suffix with "Dto"
|
|
||||||
- Use Spring's global exception handling with custom exceptions
|
|
21
LICENSE
21
LICENSE
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2025 Casino Gaming Platform
|
|
||||||
|
|
||||||
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.
|
|
206
README.md
206
README.md
|
@ -1,194 +1,34 @@
|
||||||
# Casino Gaming Platform
|
# How to: Semantic Commit Messages
|
||||||
|
|
||||||
An online gaming platform offering various casino-style games with virtual currency support. This project features a modern tech stack with Angular frontend, Spring Boot backend, and complete user authentication.
|
See how a minor change to your commit message style can make you a better programmer.
|
||||||
|
|
||||||
Please refer to our [Style Guide](https://git.kjan.de/SZUT/casino/wiki/Frontend#design-system) for design guidelines and component standards.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Multiple casino games: Poker, Blackjack, Slots, Plinko, Liars Dice, and Lootboxes
|
|
||||||
- User authentication and account management via Keycloak
|
|
||||||
- Virtual currency deposit system using Stripe payments
|
|
||||||
- Transaction history tracking
|
|
||||||
- Responsive modern UI built with Angular and TailwindCSS
|
|
||||||
|
|
||||||
## Tech Stack
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
|
|
||||||
- Angular 20
|
|
||||||
- TailwindCSS
|
|
||||||
- Keycloak integration
|
|
||||||
- Stripe payment integration
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
|
|
||||||
- Spring Boot (Java)
|
|
||||||
- PostgreSQL database
|
|
||||||
- Keycloak for authentication/authorization
|
|
||||||
- Stripe API for payment processing
|
|
||||||
|
|
||||||
### Infrastructure
|
|
||||||
|
|
||||||
- Docker containerization for all services
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- [Docker](https://docs.docker.com/get-docker/)
|
|
||||||
- [Docker Compose](https://docs.docker.com/compose/install/) (included with Docker Desktop for Windows and Mac)
|
|
||||||
- Java JDK 17+
|
|
||||||
- Node.js 18+
|
|
||||||
|
|
||||||
### Setting Up the Environment
|
|
||||||
|
|
||||||
1. Clone the repository
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
cd casino
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Start the Docker services
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd docker
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
This will start:
|
|
||||||
|
|
||||||
- PostgreSQL database
|
|
||||||
- Keycloak authentication server
|
|
||||||
|
|
||||||
### Running the Backend
|
|
||||||
|
|
||||||
1. Navigate to the backend directory
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Start the Spring Boot application
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./gradlew bootRun
|
|
||||||
```
|
|
||||||
|
|
||||||
You may optionally install [watchexec](https://github.com/watchexec/watchexec?tab=readme-ov-file) and use this command to autorecompile the backend on file changes:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
watchexec -r -e java ./gradlew :bootRun
|
|
||||||
```
|
|
||||||
|
|
||||||
The backend will be available at:
|
|
||||||
|
|
||||||
- API endpoint: <http://localhost:8080>
|
|
||||||
- Swagger documentation: <http://localhost:8080/swagger>
|
|
||||||
|
|
||||||
### Running the Frontend
|
|
||||||
|
|
||||||
1. Navigate to the frontend directory
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Install dependencies
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Start the development server
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
The frontend will be available at <http://localhost:4200>
|
|
||||||
|
|
||||||
### Local Stripe integration
|
|
||||||
|
|
||||||
1. Install the Stripe CLI
|
|
||||||
<https://stripe.com/docs/stripe-cli>
|
|
||||||
|
|
||||||
2. Login to the casino stripe account
|
|
||||||
|
|
||||||
```
|
|
||||||
stripe login --api-key <casino-stripe-secret-key>
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Start webhook forwarding
|
|
||||||
|
|
||||||
```
|
|
||||||
stripe listen --forward-to localhost:8080/webhook
|
|
||||||
```
|
|
||||||
|
|
||||||
## Database Management
|
|
||||||
|
|
||||||
### Postgres Management
|
|
||||||
|
|
||||||
#### Database cleanup (if needed)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd docker
|
|
||||||
docker-compose down
|
|
||||||
docker volume rm local_lf8_starter_postgres_data
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Setting up IntelliJ Database View
|
|
||||||
|
|
||||||
1. Run the Docker container with PostgreSQL database
|
|
||||||
2. Open `application.properties` in the resources folder and copy the database URL
|
|
||||||
3. Open the Database tab in IntelliJ
|
|
||||||
4. Click on the database icon with key in the Database toolbar
|
|
||||||
5. Click the plus sign and select "Datasource from URL"
|
|
||||||
6. Paste the DB URL and select PostgreSQL driver, confirm with OK
|
|
||||||
7. Enter username `lf8_starter` and password `secret`
|
|
||||||
8. In the Schemas tab, uncheck all options and only check `lf8_starter_db` and `public`
|
|
||||||
|
|
||||||
## Authentication
|
|
||||||
|
|
||||||
The application uses Keycloak for authentication. To get a bearer token for API testing:
|
|
||||||
|
|
||||||
1. Open `requests/getBearerToken.http`
|
|
||||||
2. Click the green arrow next to the request
|
|
||||||
3. Copy the `access_token` from the response
|
|
||||||
|
|
||||||
## Development Guidelines
|
|
||||||
|
|
||||||
### Commit Message Format
|
|
||||||
|
|
||||||
We follow semantic commit messages to maintain clear project history.
|
|
||||||
|
|
||||||
Format: `<type>(<scope>): <subject>`
|
Format: `<type>(<scope>): <subject>`
|
||||||
|
|
||||||
Where `<type>` is one of:
|
`<scope>` is optional
|
||||||
|
|
||||||
- `feat`: New feature
|
## Example
|
||||||
- `fix`: Bug fix
|
|
||||||
- `docs`: Documentation changes
|
|
||||||
- `style`: Formatting, missing semicolons, etc; no code change
|
|
||||||
- `refactor`: Code refactoring
|
|
||||||
- `test`: Adding or refactoring tests
|
|
||||||
- `chore`: Updating build tasks, etc; no production code change
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
feat: add user balance display
|
feat: add hat wobble
|
||||||
fix(auth): resolve token expiration issue
|
^--^ ^------------^
|
||||||
docs: update API documentation
|
| |
|
||||||
|
| +-> Summary in present tense.
|
||||||
|
|
|
||||||
|
+-------> Type: chore, docs, feat, fix, refactor, style, or test.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
More Examples:
|
||||||
|
|
||||||
|
- `feat`: (new feature for the user, not a new feature for build script)
|
||||||
|
- `fix`: (bug fix for the user, not a fix to a build script)
|
||||||
|
- `docs`: (changes to the documentation)
|
||||||
|
- `style`: (formatting, missing semi colons, etc; no production code change)
|
||||||
|
- `refactor`: (refactoring production code, eg. renaming a variable)
|
||||||
|
- `test`: (adding missing tests, refactoring tests; no production code change)
|
||||||
|
- `chore`: (updating grunt tasks etc; no production code change)
|
||||||
|
|
||||||
References:
|
References:
|
||||||
|
|
||||||
- [Conventional Commits](https://www.conventionalcommits.org/)
|
- https://www.conventionalcommits.org/
|
||||||
- [Semantic Commit Messages](https://seesparkbox.com/foundry/semantic_commit_messages)
|
- https://seesparkbox.com/foundry/semantic_commit_messages
|
||||||
|
- http://karma-runner.github.io/1.0/dev/git-commit-msg.html
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
FROM gradle:jdk23 AS builder
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY gradlew build.gradle.kts settings.gradle.kts config ./
|
|
||||||
COPY gradle gradle
|
|
||||||
|
|
||||||
RUN chmod +x gradlew
|
|
||||||
RUN gradle dependencies
|
|
||||||
|
|
||||||
COPY src src
|
|
||||||
|
|
||||||
RUN gradle clean build -x test -x checkstyleMain -x checkstyleTest -x compileTestJava
|
|
||||||
|
|
||||||
FROM openjdk:23-jdk-slim AS runtime
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY --from=builder /app/build/libs/*.jar app.jar
|
|
||||||
|
|
||||||
EXPOSE 8080
|
|
||||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
|
|
@ -1,2 +0,0 @@
|
||||||
.gradle
|
|
||||||
build
|
|
|
@ -1,137 +1,59 @@
|
||||||
# Casino Gaming Platform - Backend API
|
# Starter für das LF08 Projekt
|
||||||
|
|
||||||
A Spring Boot backend application providing REST APIs for a casino gaming platform with multiple games, user management, authentication, and payment processing.
|
## Requirements
|
||||||
|
* Docker https://docs.docker.com/get-docker/
|
||||||
|
* Docker compose (bei Windows und Mac schon in Docker enthalten) https://docs.docker.com/compose/install/
|
||||||
|
|
||||||
## Features
|
## Endpunkt
|
||||||
|
```
|
||||||
### Games
|
http://localhost:8080
|
||||||
- **Blackjack** - Classic card game with deck management
|
```
|
||||||
- **Coinflip** - Simple heads/tails betting game
|
## Swagger
|
||||||
- **Dice** - Dice rolling game
|
```
|
||||||
- **Slots** - Slot machine with symbols and payouts
|
http://localhost:8080/swagger
|
||||||
- **Lootboxes** - Reward system with configurable prizes
|
|
||||||
|
|
||||||
### User Management
|
|
||||||
- User registration and authentication
|
|
||||||
- OAuth2 integration (Google, GitHub)
|
|
||||||
- Email verification and password reset
|
|
||||||
- Balance management and transaction history
|
|
||||||
|
|
||||||
### Payment System
|
|
||||||
- Deposit functionality with webhook support
|
|
||||||
- Transaction tracking and status management
|
|
||||||
- Balance updates and fund validation
|
|
||||||
|
|
||||||
## Tech Stack
|
|
||||||
- **Java 17** with Spring Boot
|
|
||||||
- **Spring Security** with JWT authentication
|
|
||||||
- **Spring Data JPA** with PostgreSQL
|
|
||||||
- **OAuth2** for social login
|
|
||||||
- **Email service** for notifications
|
|
||||||
- **OpenAPI/Swagger** for API documentation
|
|
||||||
|
|
||||||
## Build & Run
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
- Java 17+
|
|
||||||
- Gradle
|
|
||||||
- Docker & Docker Compose (for PostgreSQL)
|
|
||||||
|
|
||||||
### Build Commands
|
|
||||||
```bash
|
|
||||||
# Build the application
|
|
||||||
./gradlew build
|
|
||||||
|
|
||||||
# Clean build
|
|
||||||
./gradlew clean build
|
|
||||||
|
|
||||||
# Run the application
|
|
||||||
./gradlew bootRun
|
|
||||||
|
|
||||||
# Generate JAR file
|
|
||||||
./gradlew bootJar
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing
|
|
||||||
|
# Postgres
|
||||||
|
### Terminal öffnen
|
||||||
|
für alles gilt, im Terminal im Ordner docker/local sein
|
||||||
```bash
|
```bash
|
||||||
# Run all tests
|
|
||||||
./gradlew test
|
|
||||||
|
|
||||||
# Run specific test class
|
|
||||||
./gradlew test --tests "FullyQualifiedClassName"
|
|
||||||
|
|
||||||
# Run checkstyle
|
|
||||||
./gradlew checkstyleMain checkstyleTest
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
The application runs on `http://localhost:8080`
|
|
||||||
|
|
||||||
### API Documentation
|
|
||||||
- **Swagger UI**: `http://localhost:8080/swagger-ui.html`
|
|
||||||
- **OpenAPI Spec**: `http://localhost:8080/v3/api-docs`
|
|
||||||
|
|
||||||
### Main Endpoints
|
|
||||||
- `/api/auth/**` - Authentication and user management
|
|
||||||
- `/api/games/blackjack/**` - Blackjack game operations
|
|
||||||
- `/api/games/coinflip/**` - Coinflip game operations
|
|
||||||
- `/api/games/dice/**` - Dice game operations
|
|
||||||
- `/api/games/slots/**` - Slot machine operations
|
|
||||||
- `/api/lootboxes/**` - Lootbox management
|
|
||||||
- `/api/deposits/**` - Payment and deposit handling
|
|
||||||
- `/api/users/**` - User profile management
|
|
||||||
- `/api/health` - Health check endpoint
|
|
||||||
|
|
||||||
## Database Setup
|
|
||||||
|
|
||||||
### PostgreSQL with Docker
|
|
||||||
```bash
|
|
||||||
# Start PostgreSQL container
|
|
||||||
cd docker/local
|
cd docker/local
|
||||||
|
```
|
||||||
|
### Postgres starten
|
||||||
|
```bash
|
||||||
docker compose up
|
docker compose up
|
||||||
|
```
|
||||||
|
Achtung: Der Docker-Container läuft dauerhaft! Wenn er nicht mehr benötigt wird, sollten Sie ihn stoppen.
|
||||||
|
|
||||||
# Stop PostgreSQL container
|
### Postgres stoppen
|
||||||
|
```bash
|
||||||
docker compose down
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
# Reset database (if needed)
|
### Postgres Datenbank wipen, z.B. bei Problemen
|
||||||
|
```bash
|
||||||
docker compose down
|
docker compose down
|
||||||
docker volume rm local_lf8_starter_postgres_data
|
docker volume rm local_lf8_starter_postgres_data
|
||||||
docker compose up
|
docker compose up
|
||||||
```
|
```
|
||||||
|
|
||||||
### Database Configuration
|
### Intellij-Ansicht für Postgres Datenbank einrichten
|
||||||
Database connection settings are configured in `src/main/resources/application.properties`
|
```bash
|
||||||
|
1. Lasse den Docker-Container mit der PostgreSQL-Datenbank laufen
|
||||||
|
2. im Ordner resources die Datei application.properties öffnen und die URL der Datenbank kopieren
|
||||||
|
3. rechts im Fenster den Reiter Database öffnen
|
||||||
|
4. In der Database-Symbolleiste auf das Datenbanksymbol mit dem Schlüssel klicken
|
||||||
|
5. auf das Pluszeichen klicken
|
||||||
|
6. Datasource from URL auswählen
|
||||||
|
7. URL der DB einfügen und PostgreSQL-Treiber auswählen, mit OK bestätigen
|
||||||
|
8. Username lf8_starter und Passwort secret eintragen (siehe application.properties), mit Apply bestätigen
|
||||||
|
9. im Reiter Schemas alle Häkchen entfernen und lediglich vor lf8_starter_db und public Häkchen setzen
|
||||||
|
10. mit Apply und ok bestätigen
|
||||||
|
```
|
||||||
|
# Keycloak
|
||||||
|
|
||||||
### IntelliJ Database Setup
|
### Keycloak Token
|
||||||
1. Start the PostgreSQL Docker container
|
1. Auf der Projektebene [GetBearerToken.http](../GetBearerToken.http) öffnen.
|
||||||
2. Open `application.properties` and copy the database URL
|
2. Neben der Request auf den grünen Pfeil drücken
|
||||||
3. In IntelliJ, open the Database tab (right panel)
|
3. Aus dem Reponse das access_token kopieren
|
||||||
4. Click the database icon with key in the toolbar
|
|
||||||
5. Click the plus (+) icon
|
|
||||||
6. Select "Datasource from URL"
|
|
||||||
7. Paste the database URL and select PostgreSQL driver
|
|
||||||
8. Enter credentials (username: `lf8_starter`, password: `secret`)
|
|
||||||
9. In Schemas tab, uncheck all except `lf8_starter_db` and `public`
|
|
||||||
10. Apply and confirm
|
|
||||||
|
|
||||||
## Authentication
|
|
||||||
|
|
||||||
The application supports multiple authentication methods:
|
|
||||||
- JWT-based authentication
|
|
||||||
- OAuth2 (Google, GitHub)
|
|
||||||
- Email/password with verification
|
|
||||||
|
|
||||||
### Getting Bearer Token
|
|
||||||
For API testing, use the provided HTTP client file:
|
|
||||||
1. Open `GetBearerToken.http` at project root
|
|
||||||
2. Execute the request
|
|
||||||
3. Copy the `access_token` from the response
|
|
||||||
4. Use in Authorization header: `Bearer <token>`
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Key configuration files:
|
|
||||||
- `application.properties` - Main application configuration
|
|
||||||
- `SecurityConfig.java` - Security and CORS settings
|
|
||||||
- `OpenAPIConfiguration.java` - API documentation setup
|
|
|
@ -1,22 +1,7 @@
|
||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
id("org.springframework.boot") version "3.5.0"
|
id("org.springframework.boot") version "3.3.3"
|
||||||
id("io.spring.dependency-management") version "1.1.7"
|
id("io.spring.dependency-management") version "1.1.6"
|
||||||
id("checkstyle")
|
|
||||||
}
|
|
||||||
|
|
||||||
checkstyle {
|
|
||||||
configFile = file("$rootDir/config/checkstyle/checkstyle.xml")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType<Checkstyle> {
|
|
||||||
reports {
|
|
||||||
// Disable HTML report
|
|
||||||
html.required.set(false)
|
|
||||||
|
|
||||||
// Disable XML report
|
|
||||||
xml.required.set(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "de.szut"
|
group = "de.szut"
|
||||||
|
@ -24,7 +9,7 @@ version = "0.0.1-SNAPSHOT"
|
||||||
|
|
||||||
java {
|
java {
|
||||||
toolchain {
|
toolchain {
|
||||||
languageVersion = JavaLanguageVersion.of(23)
|
languageVersion = JavaLanguageVersion.of(22)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +24,6 @@ repositories {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("com.stripe:stripe-java:29.2.0")
|
|
||||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||||
compileOnly("org.projectlombok:lombok")
|
compileOnly("org.projectlombok:lombok")
|
||||||
|
@ -47,15 +31,10 @@ dependencies {
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server:3.5.0")
|
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server:3.3.3")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.5.0")
|
implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.3.3")
|
||||||
runtimeOnly("org.postgresql:postgresql")
|
runtimeOnly("org.postgresql:postgresql")
|
||||||
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9")
|
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0")
|
||||||
implementation("io.jsonwebtoken:jjwt-api:0.12.6")
|
|
||||||
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6")
|
|
||||||
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6")
|
|
||||||
implementation("org.springframework.boot:spring-boot-starter-mail")
|
|
||||||
runtimeOnly("com.h2database:h2")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<Test> {
|
tasks.withType<Test> {
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
<?xml version="1.0"?>
|
|
||||||
<!DOCTYPE module PUBLIC
|
|
||||||
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
|
|
||||||
"https://checkstyle.org/dtds/configuration_1_3.dtd"
|
|
||||||
>
|
|
||||||
<module name="Checker">
|
|
||||||
<property name="severity" value="error"/>
|
|
||||||
<property name="tabWidth" value="4"/>
|
|
||||||
|
|
||||||
<module name="LineLength">
|
|
||||||
<property name="max" value="500"/>
|
|
||||||
</module>
|
|
||||||
<module name="FileTabCharacter">
|
|
||||||
<property name="eachLine" value="true"/>
|
|
||||||
</module>
|
|
||||||
|
|
||||||
<module name="NewlineAtEndOfFile"/>
|
|
||||||
</module>
|
|
BIN
backend/gradle/wrapper/gradle-wrapper.jar
vendored
BIN
backend/gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
10
backend/gradlew
vendored
10
backend/gradlew
vendored
|
@ -15,8 +15,6 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
|
@ -86,7 +84,7 @@ done
|
||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
|
@ -114,7 +112,7 @@ case "$( uname )" in #(
|
||||||
NONSTOP* ) nonstop=true ;;
|
NONSTOP* ) nonstop=true ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH="\\\"\\\""
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
|
@ -205,7 +203,7 @@ fi
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
# Collect all arguments for the java command:
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# and any embedded shellness will be escaped.
|
# and any embedded shellness will be escaped.
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
# treated as '${Hostname}' itself on the command line.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
@ -213,7 +211,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
-classpath "$CLASSPATH" \
|
-classpath "$CLASSPATH" \
|
||||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
# Stop when "xargs" is not available.
|
||||||
|
|
6
backend/gradlew.bat
vendored
6
backend/gradlew.bat
vendored
|
@ -13,8 +13,6 @@
|
||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
@rem SPDX-License-Identifier: Apache-2.0
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
@ -70,11 +68,11 @@ goto fail
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
|
6
backend/requests/getBearerToken.http
Normal file
6
backend/requests/getBearerToken.http
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
POST http://localhost:9090/realms/LF12/protocol/openid-connect/token
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
|
||||||
|
grant_type=password&client_id=lf12&username=lf12_test_user&password=secret
|
||||||
|
|
||||||
|
> {% client.global.set("token", response.body.access_token); %}
|
1
backend/requests/healthCheck.http
Normal file
1
backend/requests/healthCheck.http
Normal file
|
@ -0,0 +1 @@
|
||||||
|
GET localhost:8080/health
|
|
@ -1,20 +1,10 @@
|
||||||
package de.szut.casino;
|
package de.szut.casino;
|
||||||
|
|
||||||
import de.szut.casino.lootboxes.LootBoxEntity;
|
|
||||||
import de.szut.casino.lootboxes.LootBoxRepository;
|
|
||||||
import de.szut.casino.lootboxes.RewardEntity;
|
|
||||||
import de.szut.casino.lootboxes.RewardRepository;
|
|
||||||
import org.springframework.boot.CommandLineRunner;
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.mail.javamail.JavaMailSenderImpl;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class CasinoApplication {
|
public class CasinoApplication {
|
||||||
|
|
||||||
|
@ -26,69 +16,4 @@ public class CasinoApplication {
|
||||||
public static RestTemplate restTemplate() {
|
public static RestTemplate restTemplate() {
|
||||||
return new RestTemplate();
|
return new RestTemplate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
public static JavaMailSenderImpl javaMailSenderImpl() {
|
|
||||||
return new JavaMailSenderImpl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public CommandLineRunner initData(LootBoxRepository lootBoxRepository, RewardRepository rewardRepository) {
|
|
||||||
return _ -> {
|
|
||||||
if (lootBoxRepository.count() == 0) {
|
|
||||||
LootBoxEntity basicLootBox = new LootBoxEntity();
|
|
||||||
basicLootBox.setName("Basic LootBox");
|
|
||||||
basicLootBox.setPrice(new BigDecimal("2"));
|
|
||||||
basicLootBox.setRewards(new ArrayList<>()); // Initialize the list
|
|
||||||
|
|
||||||
LootBoxEntity premiumLootBox = new LootBoxEntity();
|
|
||||||
premiumLootBox.setName("Premium LootBox");
|
|
||||||
premiumLootBox.setPrice(new BigDecimal("5"));
|
|
||||||
premiumLootBox.setRewards(new ArrayList<>()); // Initialize the list
|
|
||||||
|
|
||||||
lootBoxRepository.saveAll(Arrays.asList(basicLootBox, premiumLootBox));
|
|
||||||
|
|
||||||
RewardEntity commonReward = new RewardEntity();
|
|
||||||
commonReward.setValue(new BigDecimal("0.50"));
|
|
||||||
commonReward.setProbability(new BigDecimal("0.7"));
|
|
||||||
|
|
||||||
RewardEntity rareReward = new RewardEntity();
|
|
||||||
rareReward.setValue(new BigDecimal("2.00"));
|
|
||||||
rareReward.setProbability(new BigDecimal("0.25"));
|
|
||||||
|
|
||||||
RewardEntity epicReward = new RewardEntity();
|
|
||||||
epicReward.setValue(new BigDecimal("5.00"));
|
|
||||||
epicReward.setProbability(new BigDecimal("0.5"));
|
|
||||||
|
|
||||||
RewardEntity premiumCommon = new RewardEntity();
|
|
||||||
premiumCommon.setValue(new BigDecimal("2.00"));
|
|
||||||
premiumCommon.setProbability(new BigDecimal("0.6"));
|
|
||||||
|
|
||||||
RewardEntity premiumRare = new RewardEntity();
|
|
||||||
premiumRare.setValue(new BigDecimal("5.00"));
|
|
||||||
premiumRare.setProbability(new BigDecimal("0.3"));
|
|
||||||
|
|
||||||
RewardEntity legendaryReward = new RewardEntity();
|
|
||||||
legendaryReward.setValue(new BigDecimal("15.00"));
|
|
||||||
legendaryReward.setProbability(new BigDecimal("0.10"));
|
|
||||||
|
|
||||||
rewardRepository.saveAll(Arrays.asList(
|
|
||||||
commonReward, rareReward, epicReward,
|
|
||||||
premiumCommon, premiumRare, legendaryReward));
|
|
||||||
|
|
||||||
basicLootBox.getRewards().add(commonReward);
|
|
||||||
basicLootBox.getRewards().add(premiumRare);
|
|
||||||
|
|
||||||
premiumLootBox.getRewards().add(premiumCommon);
|
|
||||||
premiumLootBox.getRewards().add(premiumRare);
|
|
||||||
premiumLootBox.getRewards().add(legendaryReward);
|
|
||||||
|
|
||||||
lootBoxRepository.saveAll(Arrays.asList(basicLootBox, premiumLootBox));
|
|
||||||
|
|
||||||
System.out.println("Initial LootBoxes and rewards created successfully");
|
|
||||||
} else {
|
|
||||||
System.out.println("LootBoxes already exist, skipping initialization");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
package de.szut.casino.blackjack;
|
|
||||||
|
|
||||||
import de.szut.casino.exceptionHandling.exceptions.UserBlackJackGameMismatchException;
|
|
||||||
import de.szut.casino.shared.dto.BetDto;
|
|
||||||
import de.szut.casino.user.UserEntity;
|
|
||||||
import de.szut.casino.user.UserService;
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@RestController
|
|
||||||
public class BlackJackGameController {
|
|
||||||
|
|
||||||
private final UserService userService;
|
|
||||||
private final BlackJackService blackJackService;
|
|
||||||
|
|
||||||
public BlackJackGameController(UserService userService, BlackJackService blackJackService) {
|
|
||||||
this.blackJackService = blackJackService;
|
|
||||||
this.userService = userService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/blackjack/{id}")
|
|
||||||
public ResponseEntity<Object> getGame(@PathVariable Long id) {
|
|
||||||
BlackJackGameEntity game = getBlackJackGame(id);
|
|
||||||
|
|
||||||
return ResponseEntity.ok(game);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/blackjack/{id}/hit")
|
|
||||||
public ResponseEntity<Object> hit(@PathVariable Long id) {
|
|
||||||
BlackJackGameEntity game = getBlackJackGame(id);
|
|
||||||
|
|
||||||
return ResponseEntity.ok(blackJackService.hit(game));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/blackjack/{id}/stand")
|
|
||||||
public ResponseEntity<Object> stand(@PathVariable Long id) {
|
|
||||||
BlackJackGameEntity game = getBlackJackGame(id);
|
|
||||||
|
|
||||||
return ResponseEntity.ok(blackJackService.stand(game));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/blackjack/{id}/doubleDown")
|
|
||||||
public ResponseEntity<Object> doubleDown(@PathVariable Long id) {
|
|
||||||
BlackJackGameEntity game = getBlackJackGame(id);
|
|
||||||
|
|
||||||
return ResponseEntity.ok(blackJackService.doubleDown(game));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/blackjack/start")
|
|
||||||
public ResponseEntity<Object> createBlackJackGame(@RequestBody @Valid BetDto betDto) {
|
|
||||||
return ResponseEntity.ok(blackJackService.createBlackJackGame(betDto));
|
|
||||||
}
|
|
||||||
|
|
||||||
private BlackJackGameEntity getBlackJackGame(Long gameId) {
|
|
||||||
UserEntity user = userService.getCurrentUser();
|
|
||||||
BlackJackGameEntity game = blackJackService.getBlackJackGame(gameId);
|
|
||||||
if (game == null || !Objects.equals(game.getUserId(), user.getId())) {
|
|
||||||
throw new UserBlackJackGameMismatchException(gameId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return game;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
package de.szut.casino.blackjack;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonManagedReference;
|
|
||||||
import de.szut.casino.user.UserEntity;
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
import org.hibernate.annotations.SQLRestriction;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class BlackJackGameEntity {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "user_id", nullable = false)
|
|
||||||
@JsonIgnore
|
|
||||||
private UserEntity user;
|
|
||||||
|
|
||||||
public Long getUserId() {
|
|
||||||
return user != null ? user.getId() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
|
||||||
private BlackJackState state;
|
|
||||||
private BigDecimal bet;
|
|
||||||
|
|
||||||
@OneToMany(mappedBy = "game", cascade = CascadeType.ALL, orphanRemoval = true)
|
|
||||||
@JsonIgnore
|
|
||||||
@SQLRestriction("card_type = 'DECK'")
|
|
||||||
private List<CardEntity> deck = new ArrayList<>();
|
|
||||||
|
|
||||||
@OneToMany(mappedBy = "game", cascade = CascadeType.ALL, orphanRemoval = true)
|
|
||||||
@JsonManagedReference
|
|
||||||
@SQLRestriction("card_type = 'PLAYER'")
|
|
||||||
private List<CardEntity> playerCards = new ArrayList<>();
|
|
||||||
|
|
||||||
@OneToMany(mappedBy = "game", cascade = CascadeType.ALL, orphanRemoval = true)
|
|
||||||
@JsonManagedReference
|
|
||||||
@SQLRestriction("card_type = 'DEALER'")
|
|
||||||
private List<CardEntity> dealerCards = new ArrayList<>();
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
package de.szut.casino.blackjack;
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public interface BlackJackGameRepository extends JpaRepository<BlackJackGameEntity, Long> {
|
|
||||||
}
|
|
|
@ -1,210 +0,0 @@
|
||||||
package de.szut.casino.blackjack;
|
|
||||||
|
|
||||||
import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException;
|
|
||||||
import de.szut.casino.shared.dto.BetDto;
|
|
||||||
import de.szut.casino.shared.service.BalanceService;
|
|
||||||
import de.szut.casino.user.UserEntity;
|
|
||||||
import de.szut.casino.user.UserRepository;
|
|
||||||
import de.szut.casino.user.UserService;
|
|
||||||
import jakarta.transaction.Transactional;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class BlackJackService {
|
|
||||||
private final BlackJackGameRepository blackJackGameRepository;
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
private final BalanceService balanceService;
|
|
||||||
private final UserService userService;
|
|
||||||
private final DeckService deckService;
|
|
||||||
|
|
||||||
public BlackJackService(
|
|
||||||
BlackJackGameRepository blackJackGameRepository,
|
|
||||||
UserRepository userRepository,
|
|
||||||
BalanceService balanceService,
|
|
||||||
UserService userService,
|
|
||||||
DeckService deckService
|
|
||||||
) {
|
|
||||||
this.blackJackGameRepository = blackJackGameRepository;
|
|
||||||
this.userRepository = userRepository;
|
|
||||||
this.balanceService = balanceService;
|
|
||||||
this.userService = userService;
|
|
||||||
this.deckService = deckService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BlackJackGameEntity getBlackJackGame(Long id) {
|
|
||||||
return blackJackGameRepository.findById(id).orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public BlackJackGameEntity createBlackJackGame(BetDto betDto) {
|
|
||||||
UserEntity user = userService.getCurrentUser();
|
|
||||||
|
|
||||||
if (!this.balanceService.hasFunds(user, betDto)) {
|
|
||||||
throw new InsufficientFundsException();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.balanceService.subtractFunds(user, betDto.getBetAmount());
|
|
||||||
|
|
||||||
BlackJackGameEntity game = new BlackJackGameEntity();
|
|
||||||
game.setUser(user);
|
|
||||||
game.setBet(betDto.getBetAmount());
|
|
||||||
|
|
||||||
this.deckService.initializeDeck(game);
|
|
||||||
this.deckService.dealInitialCards(game);
|
|
||||||
|
|
||||||
game.setState(getState(game));
|
|
||||||
|
|
||||||
return processGameBasedOnState(game);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public BlackJackGameEntity hit(BlackJackGameEntity game) {
|
|
||||||
if (game.getState() != BlackJackState.IN_PROGRESS) {
|
|
||||||
return game;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.deckService.dealCardToPlayer(game);
|
|
||||||
updateGameStateAndBalance(game);
|
|
||||||
|
|
||||||
return processGameBasedOnState(game);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public BlackJackGameEntity stand(BlackJackGameEntity game) {
|
|
||||||
if (game.getState() != BlackJackState.IN_PROGRESS) {
|
|
||||||
return game;
|
|
||||||
}
|
|
||||||
|
|
||||||
dealCardsToDealerUntilMinimumScore(game);
|
|
||||||
determineWinnerAndUpdateBalance(game);
|
|
||||||
|
|
||||||
return processGameBasedOnState(game);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public BlackJackGameEntity doubleDown(BlackJackGameEntity game) {
|
|
||||||
if (game.getState() != BlackJackState.IN_PROGRESS || game.getPlayerCards().size() != 2) {
|
|
||||||
return game;
|
|
||||||
}
|
|
||||||
|
|
||||||
UserEntity user = game.getUser();
|
|
||||||
BigDecimal additionalBet = game.getBet();
|
|
||||||
|
|
||||||
this.balanceService.subtractFunds(user, additionalBet);
|
|
||||||
|
|
||||||
game.setBet(game.getBet().add(additionalBet));
|
|
||||||
|
|
||||||
this.deckService.dealCardToPlayer(game);
|
|
||||||
updateGameStateAndBalance(game);
|
|
||||||
|
|
||||||
if (game.getState() == BlackJackState.IN_PROGRESS) {
|
|
||||||
return stand(game);
|
|
||||||
}
|
|
||||||
|
|
||||||
return game;
|
|
||||||
}
|
|
||||||
|
|
||||||
private BlackJackGameEntity processGameBasedOnState(BlackJackGameEntity game) {
|
|
||||||
if (game.getState() != BlackJackState.IN_PROGRESS) {
|
|
||||||
this.blackJackGameRepository.delete(game);
|
|
||||||
return game;
|
|
||||||
}
|
|
||||||
|
|
||||||
return blackJackGameRepository.save(game);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateGameStateAndBalance(BlackJackGameEntity game) {
|
|
||||||
game.setState(getState(game));
|
|
||||||
|
|
||||||
if (game.getState() == BlackJackState.PLAYER_WON) {
|
|
||||||
updateUserBalance(game, true);
|
|
||||||
} else if (game.getState() == BlackJackState.PLAYER_LOST) {
|
|
||||||
updateUserBalance(game, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void determineWinnerAndUpdateBalance(BlackJackGameEntity game) {
|
|
||||||
int playerValue = calculateHandValue(game.getPlayerCards());
|
|
||||||
int dealerValue = calculateHandValue(game.getDealerCards());
|
|
||||||
|
|
||||||
if (dealerValue > 21 || playerValue > dealerValue) {
|
|
||||||
game.setState(BlackJackState.PLAYER_WON);
|
|
||||||
updateUserBalance(game, true);
|
|
||||||
} else if (playerValue < dealerValue) {
|
|
||||||
game.setState(BlackJackState.PLAYER_LOST);
|
|
||||||
updateUserBalance(game, false);
|
|
||||||
} else {
|
|
||||||
game.setState(BlackJackState.DRAW);
|
|
||||||
updateUserBalance(game, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void updateUserBalance(BlackJackGameEntity game, boolean isWin) {
|
|
||||||
UserEntity user = game.getUser();
|
|
||||||
BigDecimal totalBet = game.getBet();
|
|
||||||
BigDecimal balance = user.getBalance();
|
|
||||||
|
|
||||||
if (isWin) {
|
|
||||||
balance = balance.add(totalBet.multiply(BigDecimal.valueOf(2)));
|
|
||||||
} else if (game.getState() == BlackJackState.DRAW) {
|
|
||||||
balance = balance.add(totalBet);
|
|
||||||
}
|
|
||||||
|
|
||||||
user.setBalance(balance);
|
|
||||||
userRepository.save(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
private BlackJackState getState(BlackJackGameEntity game) {
|
|
||||||
int playerHandValue = calculateHandValue(game.getPlayerCards());
|
|
||||||
|
|
||||||
if (playerHandValue == 21) {
|
|
||||||
CardEntity hole = this.deckService.drawCardFromDeck(game);
|
|
||||||
hole.setCardType(CardType.DEALER);
|
|
||||||
game.getDealerCards().add(hole);
|
|
||||||
|
|
||||||
int dealerHandValue = calculateHandValue(game.getDealerCards());
|
|
||||||
|
|
||||||
if (dealerHandValue == 21) {
|
|
||||||
return BlackJackState.DRAW;
|
|
||||||
} else {
|
|
||||||
BigDecimal blackjackWinnings = game.getBet().multiply(new BigDecimal("1.5"));
|
|
||||||
UserEntity user = game.getUser();
|
|
||||||
user.setBalance(user.getBalance().add(blackjackWinnings));
|
|
||||||
return BlackJackState.PLAYER_BLACKJACK;
|
|
||||||
}
|
|
||||||
} else if (playerHandValue > 21) {
|
|
||||||
return BlackJackState.PLAYER_LOST;
|
|
||||||
}
|
|
||||||
|
|
||||||
return BlackJackState.IN_PROGRESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int calculateHandValue(List<CardEntity> hand) {
|
|
||||||
int sum = 0;
|
|
||||||
int aceCount = 0;
|
|
||||||
for (CardEntity card : hand) {
|
|
||||||
sum += card.getRank().getValue();
|
|
||||||
if (card.getRank() == Rank.ACE) {
|
|
||||||
aceCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (sum > 21 && aceCount > 0) {
|
|
||||||
sum -= 10;
|
|
||||||
aceCount--;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sum;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void dealCardsToDealerUntilMinimumScore(BlackJackGameEntity game) {
|
|
||||||
while (calculateHandValue(game.getDealerCards()) < 17) {
|
|
||||||
this.deckService.dealCardToDealer(game);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
package de.szut.casino.blackjack;
|
|
||||||
|
|
||||||
public enum BlackJackState {
|
|
||||||
IN_PROGRESS,
|
|
||||||
PLAYER_BLACKJACK,
|
|
||||||
PLAYER_LOST,
|
|
||||||
PLAYER_WON,
|
|
||||||
DRAW,
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
package de.szut.casino.blackjack;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonBackReference;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class CardEntity {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue
|
|
||||||
@JsonIgnore
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "game_id", nullable = false)
|
|
||||||
@JsonBackReference
|
|
||||||
private BlackJackGameEntity game;
|
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
|
||||||
private Suit suit;
|
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
|
||||||
private Rank rank;
|
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
|
||||||
@JsonIgnore
|
|
||||||
private CardType cardType;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum CardType {
|
|
||||||
DECK, PLAYER, DEALER
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
package de.szut.casino.blackjack;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class DeckService {
|
|
||||||
private final Random random;
|
|
||||||
|
|
||||||
public DeckService(Random random) {
|
|
||||||
this.random = random;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void initializeDeck(BlackJackGameEntity game) {
|
|
||||||
for (Suit suit : Suit.values()) {
|
|
||||||
for (Rank rank : Rank.values()) {
|
|
||||||
CardEntity card = new CardEntity();
|
|
||||||
card.setGame(game);
|
|
||||||
card.setSuit(suit);
|
|
||||||
card.setRank(rank);
|
|
||||||
card.setCardType(CardType.DECK);
|
|
||||||
game.getDeck().add(card);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
java.util.Collections.shuffle(game.getDeck(), random);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CardEntity drawCardFromDeck(BlackJackGameEntity game) {
|
|
||||||
if (game.getDeck().isEmpty()) {
|
|
||||||
throw new IllegalStateException("Deck is empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
return game.getDeck().removeFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void dealInitialCards(BlackJackGameEntity game) {
|
|
||||||
for (int i = 0; i < 2; i++) {
|
|
||||||
dealCardToPlayer(game);
|
|
||||||
}
|
|
||||||
|
|
||||||
dealCardToDealer(game);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void dealCardToPlayer(BlackJackGameEntity game) {
|
|
||||||
CardEntity card = drawCardFromDeck(game);
|
|
||||||
card.setCardType(CardType.PLAYER);
|
|
||||||
game.getPlayerCards().add(card);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void dealCardToDealer(BlackJackGameEntity game) {
|
|
||||||
CardEntity card = drawCardFromDeck(game);
|
|
||||||
card.setCardType(CardType.DEALER);
|
|
||||||
game.getDealerCards().add(card);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
package de.szut.casino.blackjack;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
public enum Rank {
|
|
||||||
TWO("2", "Two", 2),
|
|
||||||
THREE("3", "Three", 3),
|
|
||||||
FOUR("4", "Four", 4),
|
|
||||||
FIVE("5", "Five", 5),
|
|
||||||
SIX("6", "Six", 6),
|
|
||||||
SEVEN("7", "Seven", 7),
|
|
||||||
EIGHT("8", "Eight", 8),
|
|
||||||
NINE("9", "Nine", 9),
|
|
||||||
TEN("10", "Ten", 10),
|
|
||||||
JACK("J", "Jack", 10),
|
|
||||||
QUEEN("Q", "Queen", 10),
|
|
||||||
KING("K", "King", 10),
|
|
||||||
ACE("A", "Ace", 11);
|
|
||||||
|
|
||||||
private final String symbol;
|
|
||||||
private final String displayName;
|
|
||||||
private final int value;
|
|
||||||
|
|
||||||
Rank(String symbol, String displayName, int value) {
|
|
||||||
this.symbol = symbol;
|
|
||||||
this.displayName = displayName;
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
package de.szut.casino.blackjack;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
public enum Suit {
|
|
||||||
HEARTS("H", "Hearts"),
|
|
||||||
DIAMONDS("D", "Diamonds"),
|
|
||||||
CLUBS("C", "Clubs"),
|
|
||||||
SPADES("S", "Spades");
|
|
||||||
|
|
||||||
private final String symbol;
|
|
||||||
private final String displayName;
|
|
||||||
|
|
||||||
Suit(String symbol, String displayName) {
|
|
||||||
this.symbol = symbol;
|
|
||||||
this.displayName = displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
package de.szut.casino.coinflip;
|
|
||||||
|
|
||||||
public enum CoinSide {
|
|
||||||
HEAD,
|
|
||||||
TAILS;
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
package de.szut.casino.coinflip;
|
|
||||||
|
|
||||||
import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException;
|
|
||||||
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
|
|
||||||
import de.szut.casino.shared.service.BalanceService;
|
|
||||||
import de.szut.casino.user.UserEntity;
|
|
||||||
import de.szut.casino.user.UserService;
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
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.RestController;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
public class CoinflipController {
|
|
||||||
private final UserService userService;
|
|
||||||
private final BalanceService balanceService;
|
|
||||||
private final CoinflipService coinflipService;
|
|
||||||
|
|
||||||
public CoinflipController(UserService userService, BalanceService balanceService, CoinflipService coinflipService) {
|
|
||||||
this.userService = userService;
|
|
||||||
this.balanceService = balanceService;
|
|
||||||
this.coinflipService = coinflipService;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@PostMapping("/coinflip")
|
|
||||||
public ResponseEntity<Object> coinFlip(@RequestBody @Valid CoinflipDto coinflipDto) {
|
|
||||||
UserEntity user = userService.getCurrentUser();
|
|
||||||
|
|
||||||
if (!this.balanceService.hasFunds(user, coinflipDto)) {
|
|
||||||
throw new InsufficientFundsException();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResponseEntity.ok(coinflipService.play(user, coinflipDto));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
package de.szut.casino.coinflip;
|
|
||||||
|
|
||||||
import de.szut.casino.shared.dto.BetDto;
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class CoinflipDto extends BetDto {
|
|
||||||
@NotNull(message = "chosen side cannot be null")
|
|
||||||
private CoinSide coinSide;
|
|
||||||
|
|
||||||
public CoinflipDto(BigDecimal betAmount, CoinSide coinSide) {
|
|
||||||
super(betAmount);
|
|
||||||
this.coinSide = coinSide;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
package de.szut.casino.coinflip;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
@AllArgsConstructor
|
|
||||||
@Setter
|
|
||||||
@Getter
|
|
||||||
public class CoinflipResult {
|
|
||||||
private boolean isWin;
|
|
||||||
private BigDecimal payout;
|
|
||||||
private CoinSide coinSide;
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
package de.szut.casino.coinflip;
|
|
||||||
|
|
||||||
import de.szut.casino.shared.service.BalanceService;
|
|
||||||
import de.szut.casino.user.UserEntity;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class CoinflipService {
|
|
||||||
private final Random random;
|
|
||||||
private final BalanceService balanceService;
|
|
||||||
|
|
||||||
public CoinflipService(BalanceService balanceService, Random random) {
|
|
||||||
this.balanceService = balanceService;
|
|
||||||
this.random = random;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CoinflipResult play(UserEntity user, CoinflipDto coinflipDto) {
|
|
||||||
this.balanceService.subtractFunds(user, coinflipDto.getBetAmount());
|
|
||||||
|
|
||||||
CoinSide coinSide = this.random.nextBoolean() ? CoinSide.HEAD : CoinSide.TAILS;
|
|
||||||
CoinflipResult coinflipResult = new CoinflipResult(false, BigDecimal.ZERO, coinSide);
|
|
||||||
if (coinSide == coinflipDto.getCoinSide()) {
|
|
||||||
coinflipResult.setWin(true);
|
|
||||||
|
|
||||||
BigDecimal payout = coinflipDto.getBetAmount().multiply(BigDecimal.TWO);
|
|
||||||
this.balanceService.addFunds(user, payout);
|
|
||||||
coinflipResult.setPayout(payout);
|
|
||||||
}
|
|
||||||
|
|
||||||
return coinflipResult;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
package de.szut.casino.config;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class AppConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public Random random() {
|
|
||||||
return new Random();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,7 @@
|
||||||
package de.szut.casino.config;
|
package de.szut.casino.config;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.models.Components;
|
import io.swagger.v3.oas.models.Components;
|
||||||
import io.swagger.v3.oas.models.OpenAPI;
|
import io.swagger.v3.oas.models.OpenAPI;
|
||||||
import io.swagger.v3.oas.models.info.Info;
|
import io.swagger.v3.oas.models.info.Info;
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
package de.szut.casino.config;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class WebConfig {
|
|
||||||
|
|
||||||
@Value("${app.frontend-host}")
|
|
||||||
private String frontendHost;
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public WebMvcConfigurer corsConfigurer() {
|
|
||||||
return new WebMvcConfigurer() {
|
|
||||||
@Override
|
|
||||||
public void addCorsMappings(CorsRegistry registry) {
|
|
||||||
registry.addMapping("/**")
|
|
||||||
.allowedOrigins(frontendHost)
|
|
||||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
|
||||||
.allowedHeaders("*")
|
|
||||||
.exposedHeaders("*")
|
|
||||||
.allowCredentials(true)
|
|
||||||
.maxAge(3600);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
package de.szut.casino.deposit;
|
|
||||||
|
|
||||||
import com.stripe.Stripe;
|
|
||||||
import com.stripe.exception.StripeException;
|
|
||||||
import com.stripe.model.checkout.Session;
|
|
||||||
import com.stripe.param.checkout.SessionCreateParams;
|
|
||||||
import de.szut.casino.deposit.dto.AmountDto;
|
|
||||||
import de.szut.casino.deposit.dto.SessionIdDto;
|
|
||||||
import de.szut.casino.user.UserEntity;
|
|
||||||
import de.szut.casino.user.UserService;
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
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.RestController;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
public class DepositController {
|
|
||||||
|
|
||||||
@Value("${stripe.secret.key}")
|
|
||||||
private String stripeKey;
|
|
||||||
|
|
||||||
@Value("${app.frontend-host}")
|
|
||||||
private String frontendHost;
|
|
||||||
|
|
||||||
private final TransactionService transactionService;
|
|
||||||
|
|
||||||
private final UserService userService;
|
|
||||||
|
|
||||||
public DepositController(TransactionService transactionService, UserService userService) {
|
|
||||||
this.transactionService = transactionService;
|
|
||||||
this.userService = userService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/deposit/checkout")
|
|
||||||
public ResponseEntity<SessionIdDto> checkout(@RequestBody @Valid AmountDto amountDto, @RequestHeader("Authorization") String token) throws StripeException {
|
|
||||||
Stripe.apiKey = stripeKey;
|
|
||||||
|
|
||||||
UserEntity user = userService.getCurrentUser();
|
|
||||||
|
|
||||||
SessionCreateParams params = SessionCreateParams.builder()
|
|
||||||
.addLineItem(SessionCreateParams.LineItem.builder()
|
|
||||||
.setPriceData(SessionCreateParams.LineItem.PriceData.builder()
|
|
||||||
.setCurrency("EUR")
|
|
||||||
.setUnitAmount((long) amountDto.getAmount() * 100)
|
|
||||||
.setProductData(SessionCreateParams.LineItem.PriceData.ProductData.builder()
|
|
||||||
.setName("Einzahlung")
|
|
||||||
.build())
|
|
||||||
.build())
|
|
||||||
.setQuantity(1L)
|
|
||||||
.build())
|
|
||||||
.setSuccessUrl(frontendHost + "/home?success=true")
|
|
||||||
.setCancelUrl(frontendHost + "/home?success=false")
|
|
||||||
.setMode(SessionCreateParams.Mode.PAYMENT)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
Session session = Session.create(params);
|
|
||||||
|
|
||||||
transactionService.createTransaction(user, session.getId(), amountDto.getAmount());
|
|
||||||
|
|
||||||
return ResponseEntity.ok(new SessionIdDto(session.getId()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
package de.szut.casino.deposit;
|
|
||||||
|
|
||||||
import de.szut.casino.user.UserEntity;
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
@Setter
|
|
||||||
@Getter
|
|
||||||
@Entity
|
|
||||||
public class TransactionEntity {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "user_id", nullable = false)
|
|
||||||
private UserEntity user;
|
|
||||||
|
|
||||||
@Column(unique = true)
|
|
||||||
private String sessionId = null;
|
|
||||||
|
|
||||||
private double amount = 0;
|
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
|
||||||
private TransactionStatus status = TransactionStatus.PROCESSING;
|
|
||||||
|
|
||||||
@Column(name = "created_at")
|
|
||||||
private Date createdAt = new Date();
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
package de.szut.casino.deposit;
|
|
||||||
|
|
||||||
import de.szut.casino.user.UserEntity;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.data.jpa.repository.Query;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public interface TransactionRepository extends JpaRepository<TransactionEntity, Long> {
|
|
||||||
@Query("SELECT t FROM TransactionEntity t WHERE t.sessionId = ?1")
|
|
||||||
Optional<TransactionEntity> findOneBySessionID(String sessionId);
|
|
||||||
|
|
||||||
@Query("SELECT t FROM TransactionEntity t WHERE t.user = ?1")
|
|
||||||
List<TransactionEntity> findAllByUserId(UserEntity id);
|
|
||||||
|
|
||||||
@Query("SELECT t FROM TransactionEntity t WHERE t.user = ?1 ORDER BY t.createdAt DESC LIMIT ?2 OFFSET ?3")
|
|
||||||
List<TransactionEntity> findByUserIdWithLimit(UserEntity userEntity, Integer limit, Integer offset);
|
|
||||||
|
|
||||||
@Query("SELECT COUNT(t) > ?2 + ?3 FROM TransactionEntity t WHERE t.user = ?1")
|
|
||||||
Boolean hasMore(UserEntity userEntity, Integer limit, Integer offset);
|
|
||||||
}
|
|
|
@ -1,70 +0,0 @@
|
||||||
package de.szut.casino.deposit;
|
|
||||||
|
|
||||||
import com.stripe.exception.StripeException;
|
|
||||||
import com.stripe.model.checkout.Session;
|
|
||||||
import com.stripe.param.checkout.SessionRetrieveParams;
|
|
||||||
import de.szut.casino.security.service.EmailService;
|
|
||||||
import de.szut.casino.user.UserEntity;
|
|
||||||
import de.szut.casino.user.UserRepository;
|
|
||||||
import jakarta.mail.MessagingException;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class TransactionService {
|
|
||||||
private final TransactionRepository transactionRepository;
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
private final EmailService emailService;
|
|
||||||
|
|
||||||
public TransactionService(TransactionRepository transactionRepository, UserRepository userRepository, EmailService emailService) {
|
|
||||||
this.transactionRepository = transactionRepository;
|
|
||||||
this.userRepository = userRepository;
|
|
||||||
this.emailService = emailService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void createTransaction(
|
|
||||||
UserEntity user,
|
|
||||||
String sessionID,
|
|
||||||
Double amount
|
|
||||||
) {
|
|
||||||
TransactionEntity transaction = new TransactionEntity();
|
|
||||||
|
|
||||||
transaction.setUser(user);
|
|
||||||
transaction.setSessionId(sessionID);
|
|
||||||
transaction.setAmount(amount);
|
|
||||||
|
|
||||||
transactionRepository.save(transaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void fulfillCheckout(String sessionID) throws StripeException, MessagingException, IOException {
|
|
||||||
SessionRetrieveParams params = SessionRetrieveParams.builder()
|
|
||||||
.addExpand("line_items")
|
|
||||||
.build();
|
|
||||||
Session checkoutSession = Session.retrieve(sessionID, params, null);
|
|
||||||
|
|
||||||
if (!"paid".equals(checkoutSession.getPaymentStatus())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<TransactionEntity> optionalTransaction = transactionRepository.findOneBySessionID(sessionID);
|
|
||||||
if (optionalTransaction.isEmpty()) {
|
|
||||||
throw new RuntimeException("Transaction not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
TransactionEntity transaction = optionalTransaction.get();
|
|
||||||
transaction.setStatus(TransactionStatus.SUCCEEDED);
|
|
||||||
|
|
||||||
UserEntity user = transaction.getUser();
|
|
||||||
Long amountTotal = checkoutSession.getAmountTotal();
|
|
||||||
if (amountTotal != null) {
|
|
||||||
user.addBalance(BigDecimal.valueOf(amountTotal).movePointLeft(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
userRepository.save(user);
|
|
||||||
transactionRepository.save(transaction);
|
|
||||||
emailService.sendDepositEmail(transaction);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
package de.szut.casino.deposit;
|
|
||||||
|
|
||||||
public enum TransactionStatus {
|
|
||||||
PROCESSING,
|
|
||||||
SUCCEEDED,
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
package de.szut.casino.deposit;
|
|
||||||
|
|
||||||
import com.stripe.Stripe;
|
|
||||||
import com.stripe.exception.StripeException;
|
|
||||||
import com.stripe.model.Event;
|
|
||||||
import com.stripe.model.checkout.Session;
|
|
||||||
import com.stripe.net.Webhook;
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import jakarta.mail.MessagingException;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
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.RestController;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
public class WebhookController {
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(WebhookController.class);
|
|
||||||
@Value("${stripe.secret.key}")
|
|
||||||
private String stripeSecretKey;
|
|
||||||
|
|
||||||
@Value("${stripe.webhook.secret}")
|
|
||||||
private String webhookSecret;
|
|
||||||
|
|
||||||
private final TransactionService transactionService;
|
|
||||||
|
|
||||||
public WebhookController(TransactionService transactionService) {
|
|
||||||
this.transactionService = transactionService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
public void init() {
|
|
||||||
Stripe.apiKey = stripeSecretKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/webhook")
|
|
||||||
public ResponseEntity<String> webhook(@RequestBody String payload, @RequestHeader("Stripe-Signature") String sigHeader) throws StripeException, MessagingException, IOException {
|
|
||||||
Event event = Webhook.constructEvent(payload, sigHeader, webhookSecret);
|
|
||||||
|
|
||||||
if (Objects.equals(event.getType(), "checkout.session.completed") || Objects.equals(event.getType(), "checkout.session.async_payment_succeeded")) {
|
|
||||||
Session session = (Session) event.getData().getObject();
|
|
||||||
|
|
||||||
this.transactionService.fulfillCheckout(session.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResponseEntity.ok().body(null);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
package de.szut.casino.deposit.dto;
|
|
||||||
|
|
||||||
import jakarta.validation.constraints.Min;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
@Setter
|
|
||||||
@Getter
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class AmountDto {
|
|
||||||
@Min(50)
|
|
||||||
private double amount;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
package de.szut.casino.deposit.dto;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
@Setter
|
|
||||||
@Getter
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class SessionIdDto {
|
|
||||||
private String sessionId;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
package de.szut.casino.dice;
|
|
||||||
|
|
||||||
import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException;
|
|
||||||
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
|
|
||||||
import de.szut.casino.shared.service.BalanceService;
|
|
||||||
import de.szut.casino.user.UserEntity;
|
|
||||||
import de.szut.casino.user.UserService;
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
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.RestController;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
public class DiceController {
|
|
||||||
private final UserService userService;
|
|
||||||
private final BalanceService balanceService;
|
|
||||||
private final DiceService diceService;
|
|
||||||
|
|
||||||
public DiceController(UserService userService, BalanceService balanceService, DiceService diceService) {
|
|
||||||
this.userService = userService;
|
|
||||||
this.balanceService = balanceService;
|
|
||||||
this.diceService = diceService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/dice")
|
|
||||||
public ResponseEntity<Object> rollDice(@RequestBody @Valid DiceDto diceDto) {
|
|
||||||
UserEntity user = userService.getCurrentUser();
|
|
||||||
|
|
||||||
if (!this.balanceService.hasFunds(user, diceDto)) {
|
|
||||||
throw new InsufficientFundsException();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResponseEntity.ok(diceService.play(user, diceDto));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
package de.szut.casino.dice;
|
|
||||||
|
|
||||||
import de.szut.casino.shared.dto.BetDto;
|
|
||||||
import jakarta.validation.constraints.DecimalMax;
|
|
||||||
import jakarta.validation.constraints.DecimalMin;
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class DiceDto extends BetDto {
|
|
||||||
private boolean rollOver;
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@DecimalMin(value = "1.00")
|
|
||||||
@DecimalMax(value = "100")
|
|
||||||
private BigDecimal targetValue;
|
|
||||||
|
|
||||||
public DiceDto(BigDecimal betAmount, boolean rollOver, BigDecimal targetValue) {
|
|
||||||
super(betAmount);
|
|
||||||
this.rollOver = rollOver;
|
|
||||||
this.targetValue = targetValue;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
package de.szut.casino.dice;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
@Setter
|
|
||||||
@Getter
|
|
||||||
public class DiceResult {
|
|
||||||
private boolean win;
|
|
||||||
private BigDecimal payout;
|
|
||||||
private BigDecimal rolledValue;
|
|
||||||
|
|
||||||
public DiceResult(boolean win, BigDecimal payout, BigDecimal rolledValue) {
|
|
||||||
this.win = win;
|
|
||||||
this.payout = payout;
|
|
||||||
this.rolledValue = rolledValue;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
package de.szut.casino.dice;
|
|
||||||
|
|
||||||
import de.szut.casino.shared.service.BalanceService;
|
|
||||||
import de.szut.casino.user.UserEntity;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.math.RoundingMode;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class DiceService {
|
|
||||||
private static final int MAX_DICE_VALUE = 100;
|
|
||||||
private final Random random;
|
|
||||||
private final BalanceService balanceService;
|
|
||||||
|
|
||||||
public DiceService(Random random, BalanceService balanceService) {
|
|
||||||
this.random = random;
|
|
||||||
this.balanceService = balanceService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DiceResult play(UserEntity user, DiceDto diceDto) {
|
|
||||||
balanceService.subtractFunds(user, diceDto.getBetAmount());
|
|
||||||
|
|
||||||
int rolledValue = random.nextInt(MAX_DICE_VALUE) + 1;
|
|
||||||
BigDecimal rolledValueDecimal = BigDecimal.valueOf(rolledValue);
|
|
||||||
|
|
||||||
BigDecimal targetValue = diceDto.getTargetValue();
|
|
||||||
boolean isRollOver = diceDto.isRollOver();
|
|
||||||
|
|
||||||
boolean winConditionMet = isWinConditionMet(rolledValueDecimal, targetValue, isRollOver);
|
|
||||||
|
|
||||||
if (!winConditionMet) {
|
|
||||||
return new DiceResult(false, BigDecimal.ZERO, rolledValueDecimal);
|
|
||||||
}
|
|
||||||
|
|
||||||
BigDecimal winChance = calculateWinChance(targetValue, isRollOver);
|
|
||||||
BigDecimal multiplier = calculateMultiplier(winChance);
|
|
||||||
|
|
||||||
BigDecimal payout = diceDto.getBetAmount().multiply(multiplier);
|
|
||||||
balanceService.addFunds(user, payout);
|
|
||||||
|
|
||||||
return new DiceResult(true, payout, rolledValueDecimal);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isWinConditionMet(BigDecimal rolledValue, BigDecimal targetValue, boolean isRollOver) {
|
|
||||||
if (isRollOver) {
|
|
||||||
return rolledValue.compareTo(targetValue) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return rolledValue.compareTo(targetValue) < 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private BigDecimal calculateWinChance(BigDecimal targetValue, boolean isRollOver) {
|
|
||||||
if (isRollOver) {
|
|
||||||
return BigDecimal.valueOf(MAX_DICE_VALUE).subtract(targetValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
return targetValue.subtract(BigDecimal.ONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private BigDecimal calculateMultiplier(BigDecimal winChance) {
|
|
||||||
if (winChance.compareTo(BigDecimal.ZERO) > 0) {
|
|
||||||
return BigDecimal.valueOf(MAX_DICE_VALUE - 1).divide(winChance, 4, RoundingMode.HALF_UP);
|
|
||||||
}
|
|
||||||
|
|
||||||
return BigDecimal.ZERO;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +1,8 @@
|
||||||
package de.szut.casino.exceptionHandling;
|
package de.szut.casino.exceptionHandling;
|
||||||
|
|
||||||
import de.szut.casino.exceptionHandling.exceptions.EmailNotVerifiedException;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import de.szut.casino.exceptionHandling.exceptions.UserBlackJackGameMismatchException;
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
|
|
||||||
import jakarta.persistence.EntityExistsException;
|
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
|
@ -14,35 +12,17 @@ import org.springframework.web.context.request.WebRequest;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
@ControllerAdvice
|
@ControllerAdvice
|
||||||
|
@ApiResponses(value = {
|
||||||
|
@ApiResponse(responseCode = "500", description = "invalid JSON posted",
|
||||||
|
content = @Content)
|
||||||
|
})
|
||||||
public class GlobalExceptionHandler {
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
@ExceptionHandler(UserNotFoundException.class)
|
@ExceptionHandler(ResourceNotFoundException.class)
|
||||||
public ResponseEntity<?> handleUserNotFoundException(UserNotFoundException ex, WebRequest request) {
|
public ResponseEntity<?> handleHelloEntityNotFoundException(ResourceNotFoundException ex, WebRequest request) {
|
||||||
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
|
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
|
||||||
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
|
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(InsufficientFundsException.class)
|
|
||||||
public ResponseEntity<?> handleInsufficientFundsException(InsufficientFundsException ex, WebRequest request) {
|
|
||||||
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
|
|
||||||
return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExceptionHandler(EntityExistsException.class)
|
|
||||||
public ResponseEntity<?> handleEntityExistsException(EntityExistsException ex, WebRequest request) {
|
|
||||||
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
|
|
||||||
return new ResponseEntity<>(errorDetails, HttpStatus.CONFLICT);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExceptionHandler(EmailNotVerifiedException.class)
|
|
||||||
public ResponseEntity<?> handleEmailNotVerifiedException(EmailNotVerifiedException ex, WebRequest request) {
|
|
||||||
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
|
|
||||||
return new ResponseEntity<>(errorDetails, HttpStatus.UNAUTHORIZED);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExceptionHandler(UserBlackJackGameMismatchException.class)
|
|
||||||
public ResponseEntity<?> handleUserBlackJackGameMismatchException(UserBlackJackGameMismatchException ex, WebRequest request) {
|
|
||||||
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
|
|
||||||
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package de.szut.casino.exceptionHandling;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
|
||||||
|
@ResponseStatus(value = HttpStatus.NOT_FOUND)
|
||||||
|
public class ResourceNotFoundException extends RuntimeException {
|
||||||
|
public ResourceNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +0,0 @@
|
||||||
package de.szut.casino.exceptionHandling.exceptions;
|
|
||||||
|
|
||||||
public class EmailNotVerifiedException extends Exception {
|
|
||||||
public EmailNotVerifiedException() {
|
|
||||||
super("Email not verified");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
package de.szut.casino.exceptionHandling.exceptions;
|
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
|
||||||
|
|
||||||
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
|
|
||||||
public class InsufficientFundsException extends RuntimeException {
|
|
||||||
public InsufficientFundsException() {
|
|
||||||
super("insufficient funds");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
package de.szut.casino.exceptionHandling.exceptions;
|
|
||||||
|
|
||||||
import org.springframework.security.core.AuthenticationException;
|
|
||||||
|
|
||||||
public class OAuth2AuthenticationProcessingException extends AuthenticationException {
|
|
||||||
public OAuth2AuthenticationProcessingException(String msg) {
|
|
||||||
super(msg);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
package de.szut.casino.exceptionHandling.exceptions;
|
|
||||||
|
|
||||||
public class UserBlackJackGameMismatchException extends RuntimeException {
|
|
||||||
public UserBlackJackGameMismatchException(Long gameId) {
|
|
||||||
super(String.format("Blackjack game with ID %d not found or does not belong to the current user.", gameId));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
package de.szut.casino.exceptionHandling.exceptions;
|
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
|
||||||
|
|
||||||
@ResponseStatus(value = HttpStatus.NOT_FOUND)
|
|
||||||
public class UserNotFoundException extends RuntimeException {
|
|
||||||
public UserNotFoundException() {
|
|
||||||
super("User not found");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
package de.szut.casino.lootboxes;
|
|
||||||
|
|
||||||
import jakarta.validation.constraints.DecimalMin;
|
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import jakarta.validation.constraints.Size;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class CreateLootBoxDto {
|
|
||||||
@NotEmpty(message = "Loot box name cannot be empty")
|
|
||||||
@Size(min = 3, max = 50, message = "Loot box name must be between 3 and 50 characters")
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
@NotNull(message = "Price cannot be null")
|
|
||||||
@DecimalMin(value = "0.01", message = "Price must be greater than 0")
|
|
||||||
private BigDecimal price;
|
|
||||||
|
|
||||||
private List<CreateRewardDto> rewards = new ArrayList<>();
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
package de.szut.casino.lootboxes;
|
|
||||||
|
|
||||||
import jakarta.validation.constraints.DecimalMax;
|
|
||||||
import jakarta.validation.constraints.DecimalMin;
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class CreateRewardDto {
|
|
||||||
@NotNull(message = "Reward value cannot be null")
|
|
||||||
@DecimalMin(value = "0.00", message = "Reward value must be positive")
|
|
||||||
private BigDecimal value;
|
|
||||||
|
|
||||||
@NotNull(message = "Probability cannot be null")
|
|
||||||
@DecimalMin(value = "0.0", message = "Probability must be at least 0.0")
|
|
||||||
@DecimalMax(value = "1.0", message = "Probability must be at most 1.0")
|
|
||||||
private BigDecimal probability;
|
|
||||||
}
|
|
|
@ -1,85 +0,0 @@
|
||||||
package de.szut.casino.lootboxes;
|
|
||||||
|
|
||||||
import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException;
|
|
||||||
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
|
|
||||||
import de.szut.casino.user.UserEntity;
|
|
||||||
import de.szut.casino.user.UserService;
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
public class LootBoxController {
|
|
||||||
private final LootBoxRepository lootBoxRepository;
|
|
||||||
private final UserService userService;
|
|
||||||
private final LootBoxService lootBoxService;
|
|
||||||
|
|
||||||
public LootBoxController(LootBoxRepository lootBoxRepository, UserService userService, LootBoxService lootBoxService) {
|
|
||||||
this.lootBoxRepository = lootBoxRepository;
|
|
||||||
this.userService = userService;
|
|
||||||
this.lootBoxService = lootBoxService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/lootboxes")
|
|
||||||
public List<LootBoxEntity> getAllLootBoxes() {
|
|
||||||
return lootBoxRepository.findAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/lootboxes/{id}")
|
|
||||||
public ResponseEntity<Object> purchaseLootBox(@PathVariable Long id) {
|
|
||||||
Optional<LootBoxEntity> optionalLootBox = lootBoxRepository.findById(id);
|
|
||||||
if (optionalLootBox.isEmpty()) {
|
|
||||||
return ResponseEntity.notFound().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
LootBoxEntity lootBox = optionalLootBox.get();
|
|
||||||
UserEntity user = userService.getCurrentUser();
|
|
||||||
|
|
||||||
if (lootBoxService.hasSufficientBalance(user, lootBox.getPrice())) {
|
|
||||||
throw new InsufficientFundsException();
|
|
||||||
}
|
|
||||||
|
|
||||||
RewardEntity reward = lootBoxService.determineReward(lootBox);
|
|
||||||
lootBoxService.handleBalance(user, lootBox, reward);
|
|
||||||
|
|
||||||
return ResponseEntity.ok(reward);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/lootboxes")
|
|
||||||
public ResponseEntity<Object> createLootbox(@RequestBody @Valid CreateLootBoxDto createLootBoxDto) {
|
|
||||||
List<RewardEntity> rewardEntities = new ArrayList<>();
|
|
||||||
|
|
||||||
for (CreateRewardDto createRewardDto : createLootBoxDto.getRewards()) {
|
|
||||||
rewardEntities.add(new RewardEntity(createRewardDto.getValue(), createRewardDto.getProbability()));
|
|
||||||
}
|
|
||||||
|
|
||||||
LootBoxEntity lootBoxEntity = new LootBoxEntity(
|
|
||||||
createLootBoxDto.getName(),
|
|
||||||
createLootBoxDto.getPrice(),
|
|
||||||
rewardEntities
|
|
||||||
);
|
|
||||||
|
|
||||||
this.lootBoxRepository.save(lootBoxEntity);
|
|
||||||
|
|
||||||
return ResponseEntity.ok(lootBoxEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/lootboxes/{id}")
|
|
||||||
public ResponseEntity<Object> deleteLootbox(@PathVariable Long id) {
|
|
||||||
Optional<LootBoxEntity> optionalLootBox = lootBoxRepository.findById(id);
|
|
||||||
if (optionalLootBox.isEmpty()) {
|
|
||||||
return ResponseEntity.notFound().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
LootBoxEntity lootBox = optionalLootBox.get();
|
|
||||||
lootBoxRepository.delete(lootBox);
|
|
||||||
|
|
||||||
return ResponseEntity.ok(Collections.singletonMap("message", "successfully deleted lootbox"));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
package de.szut.casino.lootboxes;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class LootBoxEntity {
|
|
||||||
|
|
||||||
public LootBoxEntity(String name, BigDecimal price, List<RewardEntity> rewards) {
|
|
||||||
this.name = name;
|
|
||||||
this.price = price;
|
|
||||||
this.rewards = rewards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
@Column(precision = 19, scale = 2)
|
|
||||||
private BigDecimal price;
|
|
||||||
|
|
||||||
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
|
|
||||||
@JoinTable(
|
|
||||||
name = "lootbox_reward",
|
|
||||||
joinColumns = @JoinColumn(name = "lootbox_id"),
|
|
||||||
inverseJoinColumns = @JoinColumn(name = "reward_id")
|
|
||||||
)
|
|
||||||
private List<RewardEntity> rewards = new ArrayList<>();
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
package de.szut.casino.lootboxes;
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public interface LootBoxRepository extends JpaRepository<LootBoxEntity, Long> {
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
package de.szut.casino.lootboxes;
|
|
||||||
|
|
||||||
import de.szut.casino.user.UserEntity;
|
|
||||||
import de.szut.casino.user.UserRepository;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class LootBoxService {
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
|
|
||||||
public LootBoxService(UserRepository userRepository) {
|
|
||||||
this.userRepository = userRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasSufficientBalance(UserEntity user, BigDecimal price) {
|
|
||||||
return user.getBalance().compareTo(price) < 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public RewardEntity determineReward(LootBoxEntity lootBox) {
|
|
||||||
double randomValue = Math.random();
|
|
||||||
BigDecimal cumulativeProbability = BigDecimal.ZERO;
|
|
||||||
|
|
||||||
for (RewardEntity reward : lootBox.getRewards()) {
|
|
||||||
cumulativeProbability = cumulativeProbability.add(reward.getProbability());
|
|
||||||
if (randomValue <= cumulativeProbability.doubleValue()) {
|
|
||||||
return reward;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lootBox.getRewards().getLast();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void handleBalance(UserEntity user, LootBoxEntity lootBox, RewardEntity reward) {
|
|
||||||
user.setBalance(user.getBalance().subtract(lootBox.getPrice()));
|
|
||||||
user.setBalance(user.getBalance().add(reward.getValue()));
|
|
||||||
userRepository.save(user);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
package de.szut.casino.lootboxes;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonBackReference;
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@Entity
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class RewardEntity {
|
|
||||||
|
|
||||||
public RewardEntity(BigDecimal value, BigDecimal probability) {
|
|
||||||
this.value = value;
|
|
||||||
this.probability = probability;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(precision = 19, scale = 2, name = "rewardValue")
|
|
||||||
private BigDecimal value;
|
|
||||||
|
|
||||||
@Column(precision = 5, scale = 2)
|
|
||||||
private BigDecimal probability;
|
|
||||||
|
|
||||||
@ManyToMany(mappedBy = "rewards")
|
|
||||||
@JsonBackReference
|
|
||||||
private List<LootBoxEntity> lootBoxes = new ArrayList<>();
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
package de.szut.casino.lootboxes;
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public interface RewardRepository extends JpaRepository<RewardEntity, Long> {
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
package de.szut.casino.security;
|
|
||||||
|
|
||||||
import de.szut.casino.exceptionHandling.exceptions.EmailNotVerifiedException;
|
|
||||||
import de.szut.casino.security.dto.AuthResponseDto;
|
|
||||||
import de.szut.casino.security.dto.LoginRequestDto;
|
|
||||||
import de.szut.casino.security.dto.ResetPasswordDto;
|
|
||||||
import de.szut.casino.security.service.AuthService;
|
|
||||||
import de.szut.casino.user.dto.CreateUserDto;
|
|
||||||
import de.szut.casino.user.dto.GetUserDto;
|
|
||||||
import jakarta.mail.MessagingException;
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/auth")
|
|
||||||
public class AuthController {
|
|
||||||
|
|
||||||
|
|
||||||
private final AuthService authService;
|
|
||||||
|
|
||||||
public AuthController(AuthService authService) {
|
|
||||||
this.authService = authService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/login")
|
|
||||||
public ResponseEntity<AuthResponseDto> authenticateUser(@Valid @RequestBody LoginRequestDto loginRequest) throws EmailNotVerifiedException {
|
|
||||||
AuthResponseDto response = authService.login(loginRequest);
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/register")
|
|
||||||
public ResponseEntity<GetUserDto> registerUser(@Valid @RequestBody CreateUserDto signUpRequest) throws MessagingException, IOException {
|
|
||||||
GetUserDto response = authService.register(signUpRequest);
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/verify")
|
|
||||||
public ResponseEntity<Void> verifyEmail(@RequestParam("token") String token) throws MessagingException, IOException {
|
|
||||||
if (authService.verifyEmail(token)) {
|
|
||||||
return ResponseEntity.badRequest().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResponseEntity.ok().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/recover-password")
|
|
||||||
public ResponseEntity<Void> recoverPassword(@RequestParam("email") String email) throws MessagingException, IOException {
|
|
||||||
authService.recoverPassword(email);
|
|
||||||
return ResponseEntity.ok().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/reset-password")
|
|
||||||
public ResponseEntity<Void> resetPassword(@Valid @RequestBody ResetPasswordDto passwordDto) throws MessagingException, IOException {
|
|
||||||
authService.resetPassword(passwordDto);
|
|
||||||
return ResponseEntity.ok().build();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
package de.szut.casino.security;
|
|
||||||
|
|
||||||
import jakarta.servlet.*;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.core.Ordered;
|
|
||||||
import org.springframework.core.annotation.Order;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
|
||||||
public class CorsFilter implements Filter {
|
|
||||||
|
|
||||||
@Value("${app.frontend-host}")
|
|
||||||
private String frontendHost;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
|
|
||||||
|
|
||||||
HttpServletResponse response = (HttpServletResponse) res;
|
|
||||||
HttpServletRequest request = (HttpServletRequest) req;
|
|
||||||
|
|
||||||
response.setHeader("Access-Control-Allow-Origin", frontendHost);
|
|
||||||
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
|
|
||||||
response.setHeader("Access-Control-Allow-Headers", "*");
|
|
||||||
response.setHeader("Access-Control-Expose-Headers", "*");
|
|
||||||
response.setHeader("Access-Control-Allow-Credentials", "true");
|
|
||||||
response.setHeader("Access-Control-Max-Age", "3600");
|
|
||||||
|
|
||||||
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
|
|
||||||
response.setStatus(HttpServletResponse.SC_OK);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
chain.doFilter(req, res);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
package de.szut.casino.security;
|
|
||||||
|
|
||||||
import org.springframework.core.convert.converter.Converter;
|
|
||||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
|
||||||
import org.springframework.security.oauth2.jwt.Jwt;
|
|
||||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
|
|
||||||
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
|
|
||||||
|
|
||||||
public class CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AbstractAuthenticationToken convert(Jwt source) {
|
|
||||||
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
|
|
||||||
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
|
|
||||||
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
|
|
||||||
|
|
||||||
return converter.convert(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
public <U> Converter<Jwt, U> andThen(Converter<? super AbstractAuthenticationToken, ? extends U> after) {
|
|
||||||
return Converter.super.andThen(after);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
package de.szut.casino.security;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||||
|
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class KeycloakLogoutHandler implements LogoutHandler {
|
||||||
|
|
||||||
|
|
||||||
|
private final RestTemplate restTemplate;
|
||||||
|
|
||||||
|
public KeycloakLogoutHandler(RestTemplate restTemplate) {
|
||||||
|
this.restTemplate = restTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication auth) {
|
||||||
|
logout(request, auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void logout(HttpServletRequest request, Authentication auth) {
|
||||||
|
logoutFromKeycloak((OidcUser) auth.getPrincipal());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logoutFromKeycloak(OidcUser user) {
|
||||||
|
String endSessionEndpoint = user.getIssuer() + "/protocol/openid-connect/logout";
|
||||||
|
UriComponentsBuilder builder = UriComponentsBuilder
|
||||||
|
.fromUriString(endSessionEndpoint)
|
||||||
|
.queryParam("id_token_hint", user.getIdToken().getTokenValue());
|
||||||
|
|
||||||
|
ResponseEntity<String> logoutResponse = restTemplate.getForEntity(builder.toUriString(), String.class);
|
||||||
|
if (logoutResponse.getStatusCode().is2xxSuccessful()) {
|
||||||
|
log.info("Successfulley logged out from Keycloak");
|
||||||
|
} else {
|
||||||
|
log.error("Could not propagate logout to Keycloak");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
package de.szut.casino.security;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.session.SessionRegistry;
|
||||||
|
import org.springframework.security.core.session.SessionRegistryImpl;
|
||||||
|
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
|
||||||
|
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
|
||||||
|
import org.springframework.security.web.session.HttpSessionEventPublisher;
|
||||||
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
class KeycloakSecurityConfig {
|
||||||
|
|
||||||
|
private static final String GROUPS = "groups";
|
||||||
|
private static final String REALM_ACCESS_CLAIM = "realm_access";
|
||||||
|
private static final String ROLES_CLAIM = "roles";
|
||||||
|
|
||||||
|
private final KeycloakLogoutHandler keycloakLogoutHandler;
|
||||||
|
|
||||||
|
KeycloakSecurityConfig(KeycloakLogoutHandler keycloakLogoutHandler) {
|
||||||
|
this.keycloakLogoutHandler = keycloakLogoutHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SessionRegistry sessionRegistry() {
|
||||||
|
return new SessionRegistryImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
|
||||||
|
return new RegisterSessionAuthenticationStrategy(sessionRegistry());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public HttpSessionEventPublisher httpSessionEventPublisher() {
|
||||||
|
return new HttpSessionEventPublisher();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
|
||||||
|
http.authorizeHttpRequests(auth -> auth
|
||||||
|
.requestMatchers("/swagger", "/swagger-ui/**", "/v3/api-docs/**", "/health").permitAll()
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.oauth2ResourceServer(spec -> spec.jwt(Customizer.withDefaults()));
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public JwtAuthenticationConverter jwtAuthenticationConverter() {
|
||||||
|
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
|
||||||
|
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwt -> {
|
||||||
|
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
|
||||||
|
|
||||||
|
Map<String, Object> realmAccess = jwt.getClaim("realm_access");
|
||||||
|
if (realmAccess != null && realmAccess.containsKey("roles")) {
|
||||||
|
List<String> roles = (List<String>) realmAccess.get("roles");
|
||||||
|
for (String role : roles) {
|
||||||
|
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return grantedAuthorities;
|
||||||
|
});
|
||||||
|
return jwtAuthenticationConverter;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,94 +0,0 @@
|
||||||
package de.szut.casino.security;
|
|
||||||
|
|
||||||
import de.szut.casino.security.jwt.JwtAuthenticationFilter;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
|
||||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
|
||||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
|
||||||
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.http.SessionCreationPolicy;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
|
||||||
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.authentication.UsernamePasswordAuthenticationFilter;
|
|
||||||
import org.springframework.web.cors.CorsConfiguration;
|
|
||||||
import org.springframework.web.cors.CorsConfigurationSource;
|
|
||||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@EnableWebSecurity
|
|
||||||
@EnableMethodSecurity
|
|
||||||
public class SecurityConfig {
|
|
||||||
|
|
||||||
@Value("${app.frontend-host}")
|
|
||||||
private String frontendHost;
|
|
||||||
|
|
||||||
private final UserDetailsService userDetailsService;
|
|
||||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
|
||||||
|
|
||||||
public SecurityConfig(UserDetailsService userDetailsService, JwtAuthenticationFilter jwtAuthenticationFilter) {
|
|
||||||
this.userDetailsService = userDetailsService;
|
|
||||||
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public DaoAuthenticationProvider authenticationProvider() {
|
|
||||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
|
||||||
|
|
||||||
authProvider.setUserDetailsService(userDetailsService);
|
|
||||||
authProvider.setPasswordEncoder(passwordEncoder());
|
|
||||||
|
|
||||||
return authProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
|
|
||||||
return authConfig.getAuthenticationManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public PasswordEncoder passwordEncoder() {
|
|
||||||
return new BCryptPasswordEncoder();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
|
||||||
http
|
|
||||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
|
||||||
.csrf(csrf -> csrf.disable())
|
|
||||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
|
||||||
.authorizeHttpRequests(auth -> {
|
|
||||||
auth.requestMatchers("/auth/**", "/webhook", "/swagger/**", "/swagger-ui/**", "/health", "/error", "/oauth2/**").permitAll()
|
|
||||||
.requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll()
|
|
||||||
.anyRequest().authenticated();
|
|
||||||
})
|
|
||||||
.authenticationProvider(authenticationProvider())
|
|
||||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
|
||||||
|
|
||||||
return http.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public CorsConfigurationSource corsConfigurationSource() {
|
|
||||||
CorsConfiguration configuration = new CorsConfiguration();
|
|
||||||
configuration.setAllowedOrigins(List.of(this.frontendHost));
|
|
||||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
|
||||||
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "Accept", "Origin", "X-Requested-With", "Access-Control-Request-Method", "Access-Control-Request-Headers", "x-auth-token"));
|
|
||||||
configuration.setExposedHeaders(Arrays.asList("Authorization", "Content-Type", "x-auth-token", "Access-Control-Allow-Origin", "Access-Control-Allow-Methods", "Access-Control-Allow-Headers"));
|
|
||||||
configuration.setAllowCredentials(true);
|
|
||||||
configuration.setMaxAge(3600L);
|
|
||||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
|
||||||
source.registerCorsConfiguration("/**", configuration);
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
package de.szut.casino.security.dto;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class AuthResponseDto {
|
|
||||||
private String token;
|
|
||||||
private String tokenType = "Bearer";
|
|
||||||
|
|
||||||
public AuthResponseDto(String token) {
|
|
||||||
this.token = token;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
package de.szut.casino.security.dto;
|
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class LoginRequestDto {
|
|
||||||
@NotBlank(message = "Username or email is required")
|
|
||||||
private String usernameOrEmail;
|
|
||||||
|
|
||||||
@NotBlank(message = "Password is required")
|
|
||||||
private String password;
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
package de.szut.casino.security.dto;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
public class ResetPasswordDto {
|
|
||||||
private String token;
|
|
||||||
private String password;
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
package de.szut.casino.security.jwt;
|
|
||||||
|
|
||||||
import jakarta.servlet.FilterChain;
|
|
||||||
import jakarta.servlet.ServletException;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
|
||||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|
||||||
|
|
||||||
private final JwtUtils jwtUtils;
|
|
||||||
private final UserDetailsService userDetailsService;
|
|
||||||
|
|
||||||
public JwtAuthenticationFilter(JwtUtils jwtUtils, UserDetailsService userDetailsService) {
|
|
||||||
this.jwtUtils = jwtUtils;
|
|
||||||
this.userDetailsService = userDetailsService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
|
||||||
throws ServletException, IOException {
|
|
||||||
try {
|
|
||||||
String jwt = parseJwt(request);
|
|
||||||
if (jwt != null) {
|
|
||||||
String username = jwtUtils.extractUsername(jwt);
|
|
||||||
|
|
||||||
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
|
||||||
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
|
||||||
|
|
||||||
if (jwtUtils.validateToken(jwt, userDetails)) {
|
|
||||||
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
|
|
||||||
userDetails, null, userDetails.getAuthorities());
|
|
||||||
|
|
||||||
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
|
||||||
SecurityContextHolder.getContext().setAuthentication(authToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("Cannot set user authentication: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
filterChain.doFilter(request, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String parseJwt(HttpServletRequest request) {
|
|
||||||
String headerAuth = request.getHeader("Authorization");
|
|
||||||
|
|
||||||
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
|
|
||||||
return headerAuth.substring(7);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,109 +0,0 @@
|
||||||
package de.szut.casino.security.jwt;
|
|
||||||
|
|
||||||
import de.szut.casino.security.oauth2.UserPrincipal;
|
|
||||||
import io.jsonwebtoken.Claims;
|
|
||||||
import io.jsonwebtoken.Jwts;
|
|
||||||
import io.jsonwebtoken.SignatureAlgorithm;
|
|
||||||
import io.jsonwebtoken.security.Keys;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.security.Key;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class JwtUtils {
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);
|
|
||||||
|
|
||||||
@Value("${jwt.secret}")
|
|
||||||
private String jwtSecret;
|
|
||||||
|
|
||||||
@Value("${jwt.expiration.ms}")
|
|
||||||
private int jwtExpirationMs;
|
|
||||||
|
|
||||||
private Key getSigningKey() {
|
|
||||||
return Keys.hmacShaKeyFor(jwtSecret.getBytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
public String generateToken(Authentication authentication) {
|
|
||||||
String subject = null;
|
|
||||||
Map<String, Object> claims = new HashMap<>();
|
|
||||||
|
|
||||||
if (authentication.getPrincipal() instanceof UserPrincipal) {
|
|
||||||
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
|
|
||||||
subject = userPrincipal.getEmail();
|
|
||||||
claims.put("id", userPrincipal.getId());
|
|
||||||
claims.put("username", userPrincipal.getDisplayUsername());
|
|
||||||
logger.info("Generating token for UserPrincipal: {}", subject);
|
|
||||||
} else if (authentication.getPrincipal() instanceof OAuth2User) {
|
|
||||||
OAuth2User oauth2User = (OAuth2User) authentication.getPrincipal();
|
|
||||||
subject = (String) oauth2User.getAttributes().get("email");
|
|
||||||
logger.info("Generating token for OAuth2User: {}", subject);
|
|
||||||
} else {
|
|
||||||
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
|
|
||||||
subject = userDetails.getUsername();
|
|
||||||
logger.info("Generating token for UserDetails: {}", subject);
|
|
||||||
}
|
|
||||||
|
|
||||||
return createToken(claims, subject);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String generateToken(String username) {
|
|
||||||
Map<String, Object> claims = new HashMap<>();
|
|
||||||
return createToken(claims, username);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String createToken(Map<String, Object> claims, String subject) {
|
|
||||||
Date now = new Date();
|
|
||||||
logger.info("now: {}", now);
|
|
||||||
logger.info("jwtExpirationMs: {}", jwtExpirationMs);
|
|
||||||
logger.info("expiryDate: {}", new Date(now.getTime() + jwtExpirationMs));
|
|
||||||
Date expiryDate = new Date(now.getTime() + jwtExpirationMs);
|
|
||||||
|
|
||||||
return Jwts.builder()
|
|
||||||
.setClaims(claims)
|
|
||||||
.setSubject(subject)
|
|
||||||
.setIssuedAt(now)
|
|
||||||
.setExpiration(expiryDate)
|
|
||||||
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
|
|
||||||
.compact();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String extractUsername(String token) {
|
|
||||||
return extractClaim(token, Claims::getSubject);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Date extractExpiration(String token) {
|
|
||||||
return extractClaim(token, Claims::getExpiration);
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
|
|
||||||
final Claims claims = extractAllClaims(token);
|
|
||||||
return claimsResolver.apply(claims);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Claims extractAllClaims(String token) {
|
|
||||||
return Jwts.parser()
|
|
||||||
.setSigningKey(getSigningKey())
|
|
||||||
.build()
|
|
||||||
.parseClaimsJws(token)
|
|
||||||
.getBody();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Boolean isTokenExpired(String token) {
|
|
||||||
return extractExpiration(token).before(new Date());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean validateToken(String token, UserDetails userDetails) {
|
|
||||||
final String username = extractUsername(token);
|
|
||||||
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,106 +0,0 @@
|
||||||
package de.szut.casino.security.oauth2;
|
|
||||||
|
|
||||||
import de.szut.casino.exceptionHandling.exceptions.OAuth2AuthenticationProcessingException;
|
|
||||||
import de.szut.casino.user.AuthProvider;
|
|
||||||
import de.szut.casino.user.UserEntity;
|
|
||||||
import de.szut.casino.user.UserRepository;
|
|
||||||
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
|
||||||
import org.springframework.security.core.AuthenticationException;
|
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
||||||
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
|
|
||||||
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
|
|
||||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
|
|
||||||
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
private final PasswordEncoder oauth2PasswordEncoder;
|
|
||||||
|
|
||||||
public CustomOAuth2UserService(UserRepository userRepository, PasswordEncoder oauth2PasswordEncoder) {
|
|
||||||
this.userRepository = userRepository;
|
|
||||||
this.oauth2PasswordEncoder = oauth2PasswordEncoder;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {
|
|
||||||
OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);
|
|
||||||
|
|
||||||
try {
|
|
||||||
return processOAuth2User(oAuth2UserRequest, oAuth2User);
|
|
||||||
} catch (AuthenticationException ex) {
|
|
||||||
throw ex;
|
|
||||||
} catch (Exception ex) {
|
|
||||||
throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private OAuth2User processOAuth2User(OAuth2UserRequest oAuth2UserRequest, OAuth2User oAuth2User) {
|
|
||||||
String registrationId = oAuth2UserRequest.getClientRegistration().getRegistrationId();
|
|
||||||
OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(registrationId, oAuth2User.getAttributes());
|
|
||||||
|
|
||||||
String email = oAuth2UserInfo.getEmail();
|
|
||||||
if (StringUtils.isEmpty(email)) {
|
|
||||||
email = oAuth2UserInfo.getName() + "@github.user";
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<UserEntity> userOptional = userRepository.findByEmail(email);
|
|
||||||
UserEntity user;
|
|
||||||
|
|
||||||
if (userOptional.isPresent()) {
|
|
||||||
user = userOptional.get();
|
|
||||||
|
|
||||||
if (!user.getProvider().equals(AuthProvider.valueOf(registrationId.toUpperCase()))) {
|
|
||||||
throw new OAuth2AuthenticationProcessingException("You're signed up with " +
|
|
||||||
user.getProvider() + ". Please use your " + user.getProvider() +
|
|
||||||
" account to login.");
|
|
||||||
}
|
|
||||||
|
|
||||||
user = updateExistingUser(user, oAuth2UserInfo);
|
|
||||||
} else {
|
|
||||||
user = registerNewUser(oAuth2UserRequest, oAuth2UserInfo, email);
|
|
||||||
}
|
|
||||||
|
|
||||||
return UserPrincipal.create(user, oAuth2User.getAttributes());
|
|
||||||
}
|
|
||||||
|
|
||||||
private UserEntity registerNewUser(OAuth2UserRequest oAuth2UserRequest, OAuth2UserInfo oAuth2UserInfo, String email) {
|
|
||||||
UserEntity user = new UserEntity();
|
|
||||||
|
|
||||||
String username = oAuth2UserInfo.getName();
|
|
||||||
if (StringUtils.isEmpty(username)) {
|
|
||||||
username = "github_" + oAuth2UserInfo.getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userRepository.findByUsername(username).isPresent()) {
|
|
||||||
username = username + "_" + UUID.randomUUID().toString().substring(0, 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
user.setProvider(AuthProvider.valueOf(oAuth2UserRequest.getClientRegistration().getRegistrationId().toUpperCase()));
|
|
||||||
user.setProviderId(oAuth2UserInfo.getId());
|
|
||||||
user.setUsername(username);
|
|
||||||
user.setEmail(email);
|
|
||||||
user.setEmailVerified(true);
|
|
||||||
|
|
||||||
String randomPassword = UUID.randomUUID().toString();
|
|
||||||
user.setPassword(oauth2PasswordEncoder.encode(randomPassword));
|
|
||||||
|
|
||||||
user.setBalance(new BigDecimal("100.00")); // Starting balance
|
|
||||||
|
|
||||||
return userRepository.save(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
private UserEntity updateExistingUser(UserEntity existingUser, OAuth2UserInfo oAuth2UserInfo) {
|
|
||||||
if (!StringUtils.isEmpty(oAuth2UserInfo.getName())) {
|
|
||||||
existingUser.setUsername(oAuth2UserInfo.getName());
|
|
||||||
}
|
|
||||||
return userRepository.save(existingUser);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
package de.szut.casino.security.oauth2;
|
|
||||||
|
|
||||||
import de.szut.casino.security.jwt.JwtUtils;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(OAuth2AuthenticationSuccessHandler.class);
|
|
||||||
|
|
||||||
@Value("${app.oauth2.authorizedRedirectUris}")
|
|
||||||
private String redirectUri;
|
|
||||||
|
|
||||||
private final JwtUtils jwtUtils;
|
|
||||||
|
|
||||||
public OAuth2AuthenticationSuccessHandler(JwtUtils jwtUtils) {
|
|
||||||
this.jwtUtils = jwtUtils;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
|
||||||
throws IOException {
|
|
||||||
String targetUrl = determineTargetUrl(authentication);
|
|
||||||
|
|
||||||
logger.info("OAuth2 Authentication successful, redirecting to: {}", targetUrl);
|
|
||||||
|
|
||||||
if (response.isCommitted()) {
|
|
||||||
logger.debug("Response has already been committed. Unable to redirect to " + targetUrl);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearAuthenticationAttributes(request);
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, targetUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String determineTargetUrl(Authentication authentication) {
|
|
||||||
String token = jwtUtils.generateToken(authentication);
|
|
||||||
|
|
||||||
if (authentication.getPrincipal() instanceof UserPrincipal) {
|
|
||||||
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
|
|
||||||
logger.info("User authenticated: ID={}, Email={}", userPrincipal.getId(), userPrincipal.getEmail());
|
|
||||||
}
|
|
||||||
|
|
||||||
return UriComponentsBuilder.fromUriString(redirectUri)
|
|
||||||
.queryParam("token", token)
|
|
||||||
.build().toUriString();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
package de.szut.casino.security.oauth2;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class OAuth2Config {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public PasswordEncoder oauth2PasswordEncoder() {
|
|
||||||
return new BCryptPasswordEncoder();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
package de.szut.casino.security.oauth2;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
public abstract class OAuth2UserInfo {
|
|
||||||
protected Map<String, Object> attributes;
|
|
||||||
|
|
||||||
public OAuth2UserInfo(Map<String, Object> attributes) {
|
|
||||||
this.attributes = attributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract String getId();
|
|
||||||
|
|
||||||
public abstract String getName();
|
|
||||||
|
|
||||||
public abstract String getEmail();
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
package de.szut.casino.security.oauth2;
|
|
||||||
|
|
||||||
import de.szut.casino.exceptionHandling.exceptions.OAuth2AuthenticationProcessingException;
|
|
||||||
import de.szut.casino.security.oauth2.github.GitHubOAuth2UserInfo;
|
|
||||||
import de.szut.casino.security.oauth2.google.GoogleOAuth2UserInfo;
|
|
||||||
import de.szut.casino.user.AuthProvider;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class OAuth2UserInfoFactory {
|
|
||||||
|
|
||||||
public static OAuth2UserInfo getOAuth2UserInfo(String registrationId, Map<String, Object> attributes) {
|
|
||||||
if (registrationId.equalsIgnoreCase(AuthProvider.GITHUB.toString())) {
|
|
||||||
return new GitHubOAuth2UserInfo(attributes);
|
|
||||||
} else if (registrationId.equalsIgnoreCase(AuthProvider.GOOGLE.toString())) {
|
|
||||||
return new GoogleOAuth2UserInfo(attributes);
|
|
||||||
} else {
|
|
||||||
throw new OAuth2AuthenticationProcessingException("Sorry! Login with " + registrationId + " is not supported yet.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,102 +0,0 @@
|
||||||
package de.szut.casino.security.oauth2;
|
|
||||||
|
|
||||||
import de.szut.casino.user.UserEntity;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class UserPrincipal implements OAuth2User, UserDetails {
|
|
||||||
@Getter
|
|
||||||
private Long id;
|
|
||||||
@Getter
|
|
||||||
private String email;
|
|
||||||
private String username;
|
|
||||||
private String password;
|
|
||||||
private Collection<? extends GrantedAuthority> authorities;
|
|
||||||
@Setter
|
|
||||||
private Map<String, Object> attributes;
|
|
||||||
|
|
||||||
public UserPrincipal(Long id, String email, String username, String password, Collection<? extends GrantedAuthority> authorities) {
|
|
||||||
this.id = id;
|
|
||||||
this.email = email;
|
|
||||||
this.username = username;
|
|
||||||
this.password = password;
|
|
||||||
this.authorities = authorities;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static UserPrincipal create(UserEntity user) {
|
|
||||||
List<GrantedAuthority> authorities = Collections.
|
|
||||||
singletonList(new SimpleGrantedAuthority("ROLE_USER"));
|
|
||||||
|
|
||||||
return new UserPrincipal(
|
|
||||||
user.getId(),
|
|
||||||
user.getEmail(),
|
|
||||||
user.getUsername(),
|
|
||||||
user.getPassword(),
|
|
||||||
authorities
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static UserPrincipal create(UserEntity user, Map<String, Object> attributes) {
|
|
||||||
UserPrincipal userPrincipal = UserPrincipal.create(user);
|
|
||||||
userPrincipal.setAttributes(attributes);
|
|
||||||
return userPrincipal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getPassword() {
|
|
||||||
return password;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getUsername() {
|
|
||||||
return email;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDisplayUsername() {
|
|
||||||
return username;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isAccountNonExpired() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isAccountNonLocked() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isCredentialsNonExpired() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isEnabled() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
|
||||||
return authorities;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Map<String, Object> getAttributes() {
|
|
||||||
return attributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return String.valueOf(id);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
package de.szut.casino.security.oauth2.github;
|
|
||||||
|
|
||||||
import de.szut.casino.security.dto.AuthResponseDto;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import org.springframework.web.servlet.view.RedirectView;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/oauth2/github")
|
|
||||||
public class GitHubController {
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(GitHubController.class);
|
|
||||||
|
|
||||||
@Value("${spring.security.oauth2.client.registration.github.client-id}")
|
|
||||||
private String clientId;
|
|
||||||
|
|
||||||
@Value("${spring.security.oauth2.client.provider.github.authorization-uri}")
|
|
||||||
private String authorizationUri;
|
|
||||||
|
|
||||||
@Value("${spring.security.oauth2.client.registration.github.redirect-uri}")
|
|
||||||
private String redirectUri;
|
|
||||||
|
|
||||||
private final GitHubService githubService;
|
|
||||||
|
|
||||||
public GitHubController(GitHubService githubService) {
|
|
||||||
this.githubService = githubService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/authorize")
|
|
||||||
public RedirectView authorizeGithub() {
|
|
||||||
logger.info("Redirecting to GitHub for authorization");
|
|
||||||
|
|
||||||
String authUrl = authorizationUri +
|
|
||||||
"?client_id=" + clientId +
|
|
||||||
"&redirect_uri=" + redirectUri +
|
|
||||||
"&scope=user:email,read:user";
|
|
||||||
|
|
||||||
return new RedirectView(authUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@PostMapping("/callback")
|
|
||||||
public ResponseEntity<AuthResponseDto> githubCallback(@RequestBody GithubCallbackDto githubCallbackDto) {
|
|
||||||
String code = githubCallbackDto.getCode();
|
|
||||||
AuthResponseDto response = githubService.processGithubCode(code);
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
package de.szut.casino.security.oauth2.github;
|
|
||||||
|
|
||||||
import de.szut.casino.security.oauth2.OAuth2UserInfo;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class GitHubOAuth2UserInfo extends OAuth2UserInfo {
|
|
||||||
|
|
||||||
public GitHubOAuth2UserInfo(Map<String, Object> attributes) {
|
|
||||||
super(attributes);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getId() {
|
|
||||||
return ((Integer) attributes.get("id")).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return (String) attributes.get("name");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getEmail() {
|
|
||||||
return (String) attributes.get("email");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,172 +0,0 @@
|
||||||
package de.szut.casino.security.oauth2.github;
|
|
||||||
|
|
||||||
import de.szut.casino.deposit.TransactionEntity;
|
|
||||||
import de.szut.casino.deposit.TransactionRepository;
|
|
||||||
import de.szut.casino.deposit.TransactionStatus;
|
|
||||||
import de.szut.casino.security.dto.AuthResponseDto;
|
|
||||||
import de.szut.casino.security.jwt.JwtUtils;
|
|
||||||
import de.szut.casino.user.AuthProvider;
|
|
||||||
import de.szut.casino.user.UserEntity;
|
|
||||||
import de.szut.casino.user.UserRepository;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.http.HttpEntity;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.HttpMethod;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class GitHubService {
|
|
||||||
@Value("${spring.security.oauth2.client.registration.github.client-id}")
|
|
||||||
private String clientId;
|
|
||||||
|
|
||||||
@Value("${spring.security.oauth2.client.registration.github.client-secret}")
|
|
||||||
private String clientSecret;
|
|
||||||
|
|
||||||
private final AuthenticationManager authenticationManager;
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
private final TransactionRepository transactionRepository;
|
|
||||||
private final JwtUtils jwtUtils;
|
|
||||||
private final PasswordEncoder oauth2PasswordEncoder;
|
|
||||||
|
|
||||||
public GitHubService(AuthenticationManager authenticationManager, UserRepository userRepository, TransactionRepository transactionRepository, JwtUtils jwtUtils, PasswordEncoder oauth2PasswordEncoder) {
|
|
||||||
this.authenticationManager = authenticationManager;
|
|
||||||
this.userRepository = userRepository;
|
|
||||||
this.transactionRepository = transactionRepository;
|
|
||||||
this.jwtUtils = jwtUtils;
|
|
||||||
this.oauth2PasswordEncoder = oauth2PasswordEncoder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AuthResponseDto processGithubCode(String code) {
|
|
||||||
try {
|
|
||||||
RestTemplate restTemplate = new RestTemplate();
|
|
||||||
|
|
||||||
Map<String, String> requestBody = new HashMap<>();
|
|
||||||
requestBody.put("client_id", clientId);
|
|
||||||
requestBody.put("client_secret", clientSecret);
|
|
||||||
requestBody.put("code", code);
|
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
headers.set("Accept", "application/json");
|
|
||||||
|
|
||||||
HttpEntity<Map<String, String>> requestEntity = new HttpEntity<>(requestBody, headers);
|
|
||||||
|
|
||||||
ResponseEntity<Map> response = restTemplate.exchange(
|
|
||||||
"https://github.com/login/oauth/access_token",
|
|
||||||
HttpMethod.POST,
|
|
||||||
requestEntity,
|
|
||||||
Map.class
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, Object> responseBody = response.getBody();
|
|
||||||
|
|
||||||
if (responseBody.containsKey("error")) {
|
|
||||||
String error = (String) responseBody.get("error");
|
|
||||||
String errorDescription = (String) responseBody.get("error_description");
|
|
||||||
|
|
||||||
throw new RuntimeException("GitHub OAuth error: " + errorDescription);
|
|
||||||
}
|
|
||||||
|
|
||||||
String accessToken = (String) responseBody.get("access_token");
|
|
||||||
if (accessToken == null || accessToken.isEmpty()) {
|
|
||||||
|
|
||||||
throw new RuntimeException("Failed to receive access token from GitHub");
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpHeaders userInfoHeaders = new HttpHeaders();
|
|
||||||
userInfoHeaders.set("Authorization", "Bearer " + accessToken);
|
|
||||||
|
|
||||||
HttpEntity<String> userInfoRequestEntity = new HttpEntity<>(null, userInfoHeaders);
|
|
||||||
|
|
||||||
ResponseEntity<Map> userResponse = restTemplate.exchange(
|
|
||||||
"https://api.github.com/user",
|
|
||||||
HttpMethod.GET,
|
|
||||||
userInfoRequestEntity,
|
|
||||||
Map.class
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, Object> userAttributes = userResponse.getBody();
|
|
||||||
|
|
||||||
HttpHeaders emailsHeaders = new HttpHeaders();
|
|
||||||
emailsHeaders.set("Authorization", "Bearer " + accessToken);
|
|
||||||
|
|
||||||
HttpEntity<String> emailsRequestEntity = new HttpEntity<>(null, emailsHeaders);
|
|
||||||
|
|
||||||
ResponseEntity<List> emailsResponse = restTemplate.exchange(
|
|
||||||
"https://api.github.com/user/emails",
|
|
||||||
HttpMethod.GET,
|
|
||||||
emailsRequestEntity,
|
|
||||||
List.class
|
|
||||||
);
|
|
||||||
|
|
||||||
List<Map<String, Object>> emails = emailsResponse.getBody();
|
|
||||||
String email = null;
|
|
||||||
|
|
||||||
for (Map<String, Object> emailInfo : emails) {
|
|
||||||
Boolean primary = (Boolean) emailInfo.get("primary");
|
|
||||||
if (primary != null && primary) {
|
|
||||||
email = (String) emailInfo.get("email");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (email == null && !emails.isEmpty()) {
|
|
||||||
email = (String) emails.get(0).get("email");
|
|
||||||
}
|
|
||||||
|
|
||||||
String githubId = userAttributes.get("id").toString();
|
|
||||||
String username = (String) userAttributes.get("login");
|
|
||||||
|
|
||||||
Optional<UserEntity> userOptional = userRepository.findByProviderId(githubId);
|
|
||||||
UserEntity user;
|
|
||||||
|
|
||||||
if (userOptional.isPresent()) {
|
|
||||||
user = userOptional.get();
|
|
||||||
} else {
|
|
||||||
userOptional = userRepository.findByEmail(email);
|
|
||||||
|
|
||||||
if (userOptional.isPresent()) {
|
|
||||||
user = userOptional.get();
|
|
||||||
user.setProvider(AuthProvider.GITHUB);
|
|
||||||
user.setProviderId(githubId);
|
|
||||||
} else {
|
|
||||||
user = new UserEntity();
|
|
||||||
user.setEmail(email);
|
|
||||||
user.setUsername(username);
|
|
||||||
user.setProvider(AuthProvider.GITHUB);
|
|
||||||
user.setProviderId(githubId);
|
|
||||||
user.setEmailVerified(true);
|
|
||||||
user.setBalance(new BigDecimal("100.00"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String randomPassword = UUID.randomUUID().toString();
|
|
||||||
user.setPassword(oauth2PasswordEncoder.encode(randomPassword));
|
|
||||||
TransactionEntity transaction = new TransactionEntity();
|
|
||||||
transaction.setAmount(100L);
|
|
||||||
transaction.setUser(user);
|
|
||||||
transaction.setSessionId("signup_bonus");
|
|
||||||
transaction.setStatus(TransactionStatus.SUCCEEDED);
|
|
||||||
|
|
||||||
userRepository.save(user);
|
|
||||||
transactionRepository.save(transaction);
|
|
||||||
|
|
||||||
Authentication authentication = this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getEmail(), randomPassword));
|
|
||||||
|
|
||||||
String token = jwtUtils.generateToken(authentication);
|
|
||||||
|
|
||||||
return new AuthResponseDto(token);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException("Failed to process GitHub authentication", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
package de.szut.casino.security.oauth2.github;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class GithubCallbackDto {
|
|
||||||
private String code;
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
package de.szut.casino.security.oauth2.google;
|
|
||||||
|
|
||||||
import de.szut.casino.security.dto.AuthResponseDto;
|
|
||||||
import de.szut.casino.security.oauth2.github.GithubCallbackDto;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import org.springframework.web.servlet.view.RedirectView;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/oauth2/google")
|
|
||||||
public class GoogleController {
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(GoogleController.class);
|
|
||||||
|
|
||||||
@Value("${spring.security.oauth2.client.registration.google.client-id}")
|
|
||||||
private String clientId;
|
|
||||||
|
|
||||||
@Value("${spring.security.oauth2.client.provider.google.authorization-uri}")
|
|
||||||
private String authorizationUri;
|
|
||||||
|
|
||||||
@Value("${spring.security.oauth2.client.registration.google.redirect-uri}")
|
|
||||||
private String redirectUri;
|
|
||||||
|
|
||||||
private final GoogleService googleService;
|
|
||||||
|
|
||||||
public GoogleController(GoogleService googleService) {
|
|
||||||
this.googleService = googleService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/authorize")
|
|
||||||
public RedirectView authorizeGoogle() {
|
|
||||||
logger.info("Redirecting to Google for authorization");
|
|
||||||
|
|
||||||
String authUrl = authorizationUri +
|
|
||||||
"?client_id=" + clientId +
|
|
||||||
"&redirect_uri=" + redirectUri +
|
|
||||||
"&response_type=code" +
|
|
||||||
"&scope=email profile";
|
|
||||||
|
|
||||||
return new RedirectView(authUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/callback")
|
|
||||||
public ResponseEntity<AuthResponseDto> googleCallback(@RequestBody GithubCallbackDto callbackDto) {
|
|
||||||
String code = callbackDto.getCode();
|
|
||||||
AuthResponseDto response = googleService.processGoogleCode(code);
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
package de.szut.casino.security.oauth2.google;
|
|
||||||
|
|
||||||
import de.szut.casino.security.oauth2.OAuth2UserInfo;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class GoogleOAuth2UserInfo extends OAuth2UserInfo {
|
|
||||||
|
|
||||||
public GoogleOAuth2UserInfo(Map<String, Object> attributes) {
|
|
||||||
super(attributes);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getId() {
|
|
||||||
return (String) attributes.get("sub");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return (String) attributes.get("name");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getEmail() {
|
|
||||||
return (String) attributes.get("email");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,176 +0,0 @@
|
||||||
package de.szut.casino.security.oauth2.google;
|
|
||||||
|
|
||||||
import de.szut.casino.deposit.TransactionEntity;
|
|
||||||
import de.szut.casino.deposit.TransactionRepository;
|
|
||||||
import de.szut.casino.deposit.TransactionStatus;
|
|
||||||
import de.szut.casino.security.dto.AuthResponseDto;
|
|
||||||
import de.szut.casino.security.jwt.JwtUtils;
|
|
||||||
import de.szut.casino.user.AuthProvider;
|
|
||||||
import de.szut.casino.user.UserEntity;
|
|
||||||
import de.szut.casino.user.UserRepository;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.http.HttpEntity;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.HttpMethod;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.util.LinkedMultiValueMap;
|
|
||||||
import org.springframework.util.MultiValueMap;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class GoogleService {
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(GoogleService.class);
|
|
||||||
|
|
||||||
@Value("${spring.security.oauth2.client.registration.google.client-id}")
|
|
||||||
private String clientId;
|
|
||||||
|
|
||||||
@Value("${spring.security.oauth2.client.registration.google.client-secret}")
|
|
||||||
private String clientSecret;
|
|
||||||
|
|
||||||
@Value("${spring.security.oauth2.client.registration.google.redirect-uri}")
|
|
||||||
private String redirectUri;
|
|
||||||
|
|
||||||
@Value("${spring.security.oauth2.client.provider.google.token-uri}")
|
|
||||||
private String tokenUri;
|
|
||||||
|
|
||||||
@Value("${spring.security.oauth2.client.provider.google.user-info-uri}")
|
|
||||||
private String userInfoUri;
|
|
||||||
|
|
||||||
private final AuthenticationManager authenticationManager;
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
private final TransactionRepository transactionRepository;
|
|
||||||
private final JwtUtils jwtUtils;
|
|
||||||
private final PasswordEncoder oauth2PasswordEncoder;
|
|
||||||
|
|
||||||
public GoogleService(AuthenticationManager authenticationManager, UserRepository userRepository, TransactionRepository transactionRepository, JwtUtils jwtUtils, PasswordEncoder oauth2PasswordEncoder) {
|
|
||||||
this.authenticationManager = authenticationManager;
|
|
||||||
this.userRepository = userRepository;
|
|
||||||
this.transactionRepository = transactionRepository;
|
|
||||||
this.jwtUtils = jwtUtils;
|
|
||||||
this.oauth2PasswordEncoder = oauth2PasswordEncoder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AuthResponseDto processGoogleCode(String code) {
|
|
||||||
try {
|
|
||||||
RestTemplate restTemplate = new RestTemplate();
|
|
||||||
|
|
||||||
HttpHeaders tokenHeaders = new HttpHeaders();
|
|
||||||
tokenHeaders.set("Content-Type", "application/x-www-form-urlencoded");
|
|
||||||
|
|
||||||
MultiValueMap<String, String> tokenRequestBody = new LinkedMultiValueMap<>();
|
|
||||||
tokenRequestBody.add("client_id", clientId);
|
|
||||||
tokenRequestBody.add("client_secret", clientSecret);
|
|
||||||
tokenRequestBody.add("code", code);
|
|
||||||
tokenRequestBody.add("redirect_uri", redirectUri);
|
|
||||||
tokenRequestBody.add("grant_type", "authorization_code");
|
|
||||||
|
|
||||||
HttpEntity<MultiValueMap<String, String>> tokenRequestEntity = new HttpEntity<>(tokenRequestBody, tokenHeaders);
|
|
||||||
|
|
||||||
ResponseEntity<Map> tokenResponse = restTemplate.exchange(
|
|
||||||
tokenUri,
|
|
||||||
HttpMethod.POST,
|
|
||||||
tokenRequestEntity,
|
|
||||||
Map.class
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, Object> tokenResponseBody = tokenResponse.getBody();
|
|
||||||
|
|
||||||
if (tokenResponseBody == null || tokenResponseBody.containsKey("error")) {
|
|
||||||
String error = tokenResponseBody != null ? (String) tokenResponseBody.get("error") : "Unknown error";
|
|
||||||
throw new RuntimeException("Google OAuth error: " + error);
|
|
||||||
}
|
|
||||||
|
|
||||||
String accessToken = (String) tokenResponseBody.get("access_token");
|
|
||||||
if (accessToken == null || accessToken.isEmpty()) {
|
|
||||||
throw new RuntimeException("Failed to receive access token from Google");
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpHeaders userInfoHeaders = new HttpHeaders();
|
|
||||||
userInfoHeaders.set("Authorization", "Bearer " + accessToken);
|
|
||||||
|
|
||||||
HttpEntity<String> userInfoRequestEntity = new HttpEntity<>(null, userInfoHeaders);
|
|
||||||
|
|
||||||
ResponseEntity<Map> userResponse = restTemplate.exchange(
|
|
||||||
userInfoUri,
|
|
||||||
HttpMethod.GET,
|
|
||||||
userInfoRequestEntity,
|
|
||||||
Map.class
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, Object> userAttributes = userResponse.getBody();
|
|
||||||
if (userAttributes == null) {
|
|
||||||
throw new RuntimeException("Failed to fetch user data from Google");
|
|
||||||
}
|
|
||||||
|
|
||||||
String googleId = (String) userAttributes.get("sub");
|
|
||||||
String email = (String) userAttributes.get("email");
|
|
||||||
String name = (String) userAttributes.get("name");
|
|
||||||
Boolean emailVerified = (Boolean) userAttributes.getOrDefault("email_verified", false);
|
|
||||||
|
|
||||||
if (email == null) {
|
|
||||||
throw new RuntimeException("Google account does not have an email");
|
|
||||||
}
|
|
||||||
|
|
||||||
String username = name != null ? name.replaceAll("\\s+", "") : email.split("@")[0];
|
|
||||||
|
|
||||||
Optional<UserEntity> userOptional = userRepository.findByProviderId(googleId);
|
|
||||||
UserEntity user;
|
|
||||||
|
|
||||||
if (userOptional.isPresent()) {
|
|
||||||
user = userOptional.get();
|
|
||||||
} else {
|
|
||||||
userOptional = userRepository.findByEmail(email);
|
|
||||||
|
|
||||||
if (userOptional.isPresent()) {
|
|
||||||
user = userOptional.get();
|
|
||||||
user.setProvider(AuthProvider.GOOGLE);
|
|
||||||
user.setProviderId(googleId);
|
|
||||||
} else {
|
|
||||||
user = new UserEntity();
|
|
||||||
user.setEmail(email);
|
|
||||||
user.setUsername(username);
|
|
||||||
user.setProvider(AuthProvider.GOOGLE);
|
|
||||||
user.setProviderId(googleId);
|
|
||||||
user.setEmailVerified(emailVerified);
|
|
||||||
|
|
||||||
user.setBalance(new BigDecimal("100.00"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String randomPassword = UUID.randomUUID().toString();
|
|
||||||
user.setPassword(oauth2PasswordEncoder.encode(randomPassword));
|
|
||||||
TransactionEntity transaction = new TransactionEntity();
|
|
||||||
transaction.setAmount(100L);
|
|
||||||
transaction.setUser(user);
|
|
||||||
transaction.setSessionId("signup_bonus");
|
|
||||||
transaction.setStatus(TransactionStatus.SUCCEEDED);
|
|
||||||
|
|
||||||
userRepository.save(user);
|
|
||||||
transactionRepository.save(transaction);
|
|
||||||
|
|
||||||
Authentication authentication = authenticationManager.authenticate(
|
|
||||||
new UsernamePasswordAuthenticationToken(user.getEmail(), randomPassword)
|
|
||||||
);
|
|
||||||
|
|
||||||
String token = jwtUtils.generateToken(authentication);
|
|
||||||
|
|
||||||
return new AuthResponseDto(token);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("Failed to process Google authentication", e);
|
|
||||||
throw new RuntimeException("Failed to process Google authentication", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,108 +0,0 @@
|
||||||
package de.szut.casino.security.service;
|
|
||||||
|
|
||||||
import de.szut.casino.exceptionHandling.exceptions.EmailNotVerifiedException;
|
|
||||||
import de.szut.casino.security.dto.AuthResponseDto;
|
|
||||||
import de.szut.casino.security.dto.LoginRequestDto;
|
|
||||||
import de.szut.casino.security.dto.ResetPasswordDto;
|
|
||||||
import de.szut.casino.security.jwt.JwtUtils;
|
|
||||||
import de.szut.casino.user.UserEntity;
|
|
||||||
import de.szut.casino.user.UserService;
|
|
||||||
import de.szut.casino.user.dto.CreateUserDto;
|
|
||||||
import de.szut.casino.user.dto.GetUserDto;
|
|
||||||
import jakarta.mail.MessagingException;
|
|
||||||
import org.apache.commons.lang3.RandomStringUtils;
|
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class AuthService {
|
|
||||||
|
|
||||||
private final AuthenticationManager authenticationManager;
|
|
||||||
private final JwtUtils jwtUtils;
|
|
||||||
private final UserService userService;
|
|
||||||
private final EmailService emailService;
|
|
||||||
private final PasswordEncoder passwordEncoder;
|
|
||||||
|
|
||||||
public AuthService(AuthenticationManager authenticationManager, JwtUtils jwtUtils, UserService userService, EmailService emailService, PasswordEncoder passwordEncoder) {
|
|
||||||
this.authenticationManager = authenticationManager;
|
|
||||||
this.jwtUtils = jwtUtils;
|
|
||||||
this.userService = userService;
|
|
||||||
this.emailService = emailService;
|
|
||||||
this.passwordEncoder = passwordEncoder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AuthResponseDto login(LoginRequestDto loginRequest) throws EmailNotVerifiedException {
|
|
||||||
if (!userService.isVerified(loginRequest.getUsernameOrEmail())) {
|
|
||||||
throw new EmailNotVerifiedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
Authentication authentication = authenticationManager.authenticate(
|
|
||||||
new UsernamePasswordAuthenticationToken(
|
|
||||||
loginRequest.getUsernameOrEmail(),
|
|
||||||
loginRequest.getPassword()));
|
|
||||||
|
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
|
||||||
String jwt = jwtUtils.generateToken(authentication);
|
|
||||||
|
|
||||||
return new AuthResponseDto(jwt);
|
|
||||||
}
|
|
||||||
|
|
||||||
public GetUserDto register(CreateUserDto signUpRequest) throws MessagingException, IOException {
|
|
||||||
UserEntity user = userService.createUser(signUpRequest);
|
|
||||||
|
|
||||||
this.emailService.sendEmailVerificationEmail(user);
|
|
||||||
|
|
||||||
return new GetUserDto(
|
|
||||||
user.getId(),
|
|
||||||
user.getEmail(),
|
|
||||||
user.getUsername(),
|
|
||||||
user.getBalance()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean verifyEmail(String token) throws MessagingException, IOException {
|
|
||||||
Optional<UserEntity> optionalUser = userService.getUserByVerificationToken(token);
|
|
||||||
|
|
||||||
if (!optionalUser.isPresent()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
UserEntity user = optionalUser.get();
|
|
||||||
|
|
||||||
user.setEmailVerified(true);
|
|
||||||
user.setVerificationToken(null);
|
|
||||||
userService.saveUser(user);
|
|
||||||
this.emailService.sendWelcomeEmail(user);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void recoverPassword(String email) throws MessagingException, IOException {
|
|
||||||
Optional<UserEntity> optionalUser = userService.getUserByEmail(email);
|
|
||||||
|
|
||||||
if (optionalUser.isPresent()) {
|
|
||||||
UserEntity user = optionalUser.get();
|
|
||||||
user.setPasswordResetToken(RandomStringUtils.randomAlphanumeric(64));
|
|
||||||
userService.saveUser(user);
|
|
||||||
this.emailService.sendPasswordRecoveryEmail(user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void resetPassword(ResetPasswordDto passwordDto) {
|
|
||||||
Optional<UserEntity> optionalUser = userService.getUserByPasswordResetToken(passwordDto.getToken());
|
|
||||||
|
|
||||||
if (optionalUser.isPresent()) {
|
|
||||||
UserEntity user = optionalUser.get();
|
|
||||||
user.setPassword(passwordEncoder.encode(passwordDto.getPassword()));
|
|
||||||
user.setPasswordResetToken(null);
|
|
||||||
userService.saveUser(user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,115 +0,0 @@
|
||||||
package de.szut.casino.security.service;
|
|
||||||
|
|
||||||
import de.szut.casino.deposit.TransactionEntity;
|
|
||||||
import de.szut.casino.user.UserEntity;
|
|
||||||
import jakarta.mail.MessagingException;
|
|
||||||
import jakarta.mail.internet.MimeMessage;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.core.io.ClassPathResource;
|
|
||||||
import org.springframework.mail.javamail.JavaMailSenderImpl;
|
|
||||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.util.FileCopyUtils;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.Reader;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class EmailService {
|
|
||||||
private JavaMailSenderImpl mailSender;
|
|
||||||
private MailConfig mailConfig;
|
|
||||||
@Value("${app.frontend-host}")
|
|
||||||
private String feUrl;
|
|
||||||
|
|
||||||
public EmailService(JavaMailSenderImpl mailSender, MailConfig mailConfig) {
|
|
||||||
this.mailSender = mailSender;
|
|
||||||
this.mailConfig = mailConfig;
|
|
||||||
this.mailSender.setHost(mailConfig.host);
|
|
||||||
this.mailSender.setPort(mailConfig.port);
|
|
||||||
this.mailSender.setProtocol(mailConfig.protocol);
|
|
||||||
if (mailConfig.authenticationEnabled) {
|
|
||||||
this.mailSender.setUsername(mailConfig.username);
|
|
||||||
this.mailSender.setPassword(mailConfig.password);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendEmailVerificationEmail(UserEntity user) throws IOException, MessagingException {
|
|
||||||
String template = loadTemplate("email/verify.html");
|
|
||||||
String htmlContent = template
|
|
||||||
.replace("${username}", user.getUsername())
|
|
||||||
.replace("${feUrl}", feUrl)
|
|
||||||
.replace("${token}", user.getVerificationToken());
|
|
||||||
|
|
||||||
MimeMessage message = mailSender.createMimeMessage();
|
|
||||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
|
||||||
|
|
||||||
helper.setFrom(mailConfig.fromAddress);
|
|
||||||
helper.setTo(user.getEmailAddress());
|
|
||||||
helper.setSubject("E-Mail Bestätigung");
|
|
||||||
helper.setText(htmlContent, true);
|
|
||||||
|
|
||||||
mailSender.send(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendWelcomeEmail(UserEntity user) throws IOException, MessagingException {
|
|
||||||
String template = loadTemplate("email/welcome.html");
|
|
||||||
String htmlContent = template
|
|
||||||
.replace("${username}", user.getUsername())
|
|
||||||
.replace("${feUrl}", feUrl);
|
|
||||||
|
|
||||||
MimeMessage message = mailSender.createMimeMessage();
|
|
||||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
|
||||||
|
|
||||||
helper.setFrom(mailConfig.fromAddress);
|
|
||||||
helper.setTo(user.getEmailAddress());
|
|
||||||
helper.setSubject("Willkommen bei Trustworthy Casino©");
|
|
||||||
helper.setText(htmlContent, true);
|
|
||||||
|
|
||||||
mailSender.send(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendDepositEmail(TransactionEntity transaction) throws IOException, MessagingException {
|
|
||||||
String template = loadTemplate("email/deposit.html");
|
|
||||||
String htmlContent = template
|
|
||||||
.replace("${username}", transaction.getUser().getUsername())
|
|
||||||
.replace("${amount}", String.valueOf(transaction.getAmount()))
|
|
||||||
.replace("${feUrl}", feUrl);
|
|
||||||
|
|
||||||
MimeMessage message = mailSender.createMimeMessage();
|
|
||||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
|
||||||
|
|
||||||
helper.setFrom(mailConfig.fromAddress);
|
|
||||||
helper.setTo(transaction.getUser().getEmailAddress());
|
|
||||||
helper.setSubject("Einzahlung über ${amount}€ Erfolgreich".replace("${amount}", String.valueOf(transaction.getAmount())));
|
|
||||||
helper.setText(htmlContent, true);
|
|
||||||
|
|
||||||
mailSender.send(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendPasswordRecoveryEmail(UserEntity user) throws IOException, MessagingException {
|
|
||||||
String template = loadTemplate("email/recover-password.html");
|
|
||||||
String htmlContent = template
|
|
||||||
.replace("${username}", user.getUsername())
|
|
||||||
.replace("${resetToken}", user.getPasswordResetToken())
|
|
||||||
.replace("${feUrl}", feUrl);
|
|
||||||
|
|
||||||
MimeMessage message = mailSender.createMimeMessage();
|
|
||||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
|
||||||
|
|
||||||
helper.setFrom(mailConfig.fromAddress);
|
|
||||||
helper.setTo(user.getEmailAddress());
|
|
||||||
helper.setSubject("Zurücksetzen ihres Passworts");
|
|
||||||
helper.setText(htmlContent, true);
|
|
||||||
|
|
||||||
mailSender.send(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String loadTemplate(String templatePath) throws IOException {
|
|
||||||
ClassPathResource resource = new ClassPathResource("templates/" + templatePath);
|
|
||||||
try (Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) {
|
|
||||||
return FileCopyUtils.copyToString(reader);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
package de.szut.casino.security.service;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class MailConfig {
|
|
||||||
@Value("${app.mail.host}")
|
|
||||||
public String host;
|
|
||||||
|
|
||||||
@Value("${app.mail.port}")
|
|
||||||
public Integer port;
|
|
||||||
|
|
||||||
@Value("${app.mail.authentication}")
|
|
||||||
public Boolean authenticationEnabled;
|
|
||||||
|
|
||||||
@Value("${app.mail.username}")
|
|
||||||
public String username;
|
|
||||||
|
|
||||||
@Value("${app.mail.password}")
|
|
||||||
public String password;
|
|
||||||
|
|
||||||
@Value("${app.mail.from-address}")
|
|
||||||
public String fromAddress;
|
|
||||||
|
|
||||||
@Value("${app.mail.protocol}")
|
|
||||||
public String protocol;
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
package de.szut.casino.security.service;
|
|
||||||
|
|
||||||
import de.szut.casino.user.UserEntity;
|
|
||||||
import de.szut.casino.user.UserRepository;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class UserDetailsServiceImpl implements UserDetailsService {
|
|
||||||
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
|
|
||||||
public UserDetailsServiceImpl(UserRepository userRepository) {
|
|
||||||
this.userRepository = userRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException {
|
|
||||||
Optional<UserEntity> user = userRepository.findByUsername(usernameOrEmail);
|
|
||||||
|
|
||||||
if (user.isEmpty()) {
|
|
||||||
user = userRepository.findByEmail(usernameOrEmail);
|
|
||||||
}
|
|
||||||
|
|
||||||
UserEntity userEntity = user.orElseThrow(() ->
|
|
||||||
new UsernameNotFoundException("User not found with username or email: " + usernameOrEmail));
|
|
||||||
|
|
||||||
return new org.springframework.security.core.userdetails.User(
|
|
||||||
userEntity.getUsername(),
|
|
||||||
userEntity.getPassword(),
|
|
||||||
new ArrayList<>());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
package de.szut.casino.shared.dto;
|
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import jakarta.validation.constraints.Positive;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class BetDto {
|
|
||||||
@NotNull(message = "Bet amount cannot be null")
|
|
||||||
@Positive(message = "Bet amount must be positive")
|
|
||||||
private BigDecimal betAmount;
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
package de.szut.casino.shared.service;
|
|
||||||
|
|
||||||
import de.szut.casino.shared.dto.BetDto;
|
|
||||||
import de.szut.casino.user.UserEntity;
|
|
||||||
import de.szut.casino.user.UserRepository;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class BalanceService {
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
|
|
||||||
public BalanceService(UserRepository userRepository) {
|
|
||||||
this.userRepository = userRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasFunds(UserEntity user, BetDto betDto) {
|
|
||||||
BigDecimal balance = user.getBalance();
|
|
||||||
BigDecimal betAmount = betDto.getBetAmount();
|
|
||||||
|
|
||||||
return betAmount.compareTo(balance) <= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addFunds(UserEntity user, BigDecimal amount) {
|
|
||||||
user.addBalance(amount);
|
|
||||||
|
|
||||||
this.userRepository.save(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void subtractFunds(UserEntity user, BigDecimal amount) {
|
|
||||||
user.subtractBalance(amount);
|
|
||||||
|
|
||||||
this.userRepository.save(user);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
package de.szut.casino.slots;
|
|
||||||
|
|
||||||
import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException;
|
|
||||||
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
|
|
||||||
import de.szut.casino.shared.dto.BetDto;
|
|
||||||
import de.szut.casino.shared.service.BalanceService;
|
|
||||||
import de.szut.casino.user.UserEntity;
|
|
||||||
import de.szut.casino.user.UserService;
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
public class SlotController {
|
|
||||||
private final UserService userService;
|
|
||||||
private final BalanceService balanceService;
|
|
||||||
private final SlotService slotService;
|
|
||||||
|
|
||||||
public SlotController(UserService userService, BalanceService balanceService, SlotService slotService) {
|
|
||||||
this.userService = userService;
|
|
||||||
this.balanceService = balanceService;
|
|
||||||
this.slotService = slotService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/slots/spin")
|
|
||||||
public ResponseEntity<Object> spinSlots(@RequestBody @Valid BetDto betDto) {
|
|
||||||
UserEntity user = userService.getCurrentUser();
|
|
||||||
|
|
||||||
if (!this.balanceService.hasFunds(user, betDto)) {
|
|
||||||
throw new InsufficientFundsException();
|
|
||||||
}
|
|
||||||
|
|
||||||
SpinResult spinResult = this.slotService.spin(
|
|
||||||
betDto.getBetAmount(),
|
|
||||||
user
|
|
||||||
);
|
|
||||||
|
|
||||||
return ResponseEntity.ok(spinResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/slots/info")
|
|
||||||
public ResponseEntity<Object> spinSlots() {
|
|
||||||
Map<String, BigDecimal> info = new HashMap<>();
|
|
||||||
|
|
||||||
for (Symbol symbol : Symbol.values()) {
|
|
||||||
info.put(symbol.getDisplayName(), symbol.getPayoutMultiplier());
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResponseEntity.ok(info);
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue