Compare commits
305 commits
Author | SHA1 | Date | |
---|---|---|---|
038c6e145a |
|||
cbddfa0552 |
|||
b4df89b7b0 |
|||
|
6807d51e0c | ||
bd6dc99f30 |
|||
|
ed4753c300 | ||
4748196210 |
|||
7e861896dc |
|||
18010b28d1 |
|||
075fd97ae8 |
|||
5cedbcb004 |
|||
7296cfead8 |
|||
7714957a62 | |||
c3f2fcd431 |
|||
07bb68d864 |
|||
eb04c03208 |
|||
|
01b99c04c6 | ||
7221422d3b |
|||
d916fe4942 | |||
9e7fbd1c1e |
|||
0e9e729fdf | |||
dfcf02c1b1 |
|||
|
9c856f02cd |
||
c87a0593b3 |
|||
9ebf0d0036 | |||
ab6265b2cd |
|||
d9309cb735 | |||
9bd5a15480 |
|||
ce3af8915d |
|||
684cec7d56 |
|||
ee9dbb7577 |
|||
|
3dfe318500 | ||
a7b5c42558 |
|||
9207c070fd |
|||
4bcc6b45b4 |
|||
f754a6fc72 |
|||
96aca3b293 | |||
a411234714 |
|||
52a3e6aadc |
|||
5ea3ff73a0 |
|||
050c272226 | |||
75deee3f9f |
|||
|
4af605ef96 | ||
6737eb9e4a |
|||
4619f787f0 |
|||
3ca0b5a3c4 |
|||
7e30e191b4 |
|||
|
140bd44d66 | ||
|
1d6ac261e0 | ||
9deb92ad13 |
|||
0345df3a30 | |||
5bd3f554e2 |
|||
59bb910f05 | |||
e5155c072f |
|||
|
922c95c212 | ||
|
c5b44f3f29 | ||
fad280104d |
|||
|
9cb813bf41 | ||
56763952c0 |
|||
94eaf98250 | |||
45fe90237b |
|||
739c4f610a |
|||
9717e5c43e |
|||
cef0ca2a73 |
|||
0429406383 | |||
332f9d1ce1 |
|||
922d48e1c0 |
|||
b376307272 | |||
111c6c2a64 | |||
e828efdfa7 | |||
971519f99c |
|||
a296ae147b |
|||
|
e4173e3ade |
||
|
ed83097b6b | ||
|
68306f3893 | ||
20076d8fe0 |
|||
0c2a21c218 |
|||
|
65471d1666 | ||
801c60b793 |
|||
feb40e3f79 |
|||
761a527ef0 |
|||
e02f7357f0 |
|||
|
db93ec790e | ||
4644e5cecc |
|||
|
e35a30a606 | ||
|
81ec7122ea | ||
17774fcdb2 | |||
43e321c0d6 |
|||
|
23710a0553 | ||
|
4331484d48 | ||
790eeeb145 |
|||
0b39eacc33 |
|||
04bdb89a93 |
|||
b583eedd75 |
|||
|
1e77beb7b6 | ||
|
d4fd5e068d | ||
|
06f2ae5d86 | ||
|
b2f80dedf2 | ||
551f5bcf2e |
|||
|
d388f2a786 | ||
|
a4e12272e5 | ||
|
bd031e8658 | ||
9a7049d6ab |
|||
|
75508d9ebb | ||
c6ae4a1056 |
|||
234442dccd | |||
fc60cfb3d5 |
|||
5d600a4b21 | |||
feb67c708e |
|||
|
19cec9d7db | ||
15e8cf02f7 |
|||
f01554c9c9 |
|||
565420103f |
|||
07c4e00e33 |
|||
c89024a277 | |||
|
7b66aac33d | ||
202c5fb93f |
|||
3acd131b20 | |||
52c61c5b18 | |||
b66d39cbac | |||
16b052c0f3 | |||
99d25c0413 |
|||
d68e907061 |
|||
fb41de95b8 |
|||
02f87c177d |
|||
13ddb79d88 |
|||
4c7bff1b31 |
|||
c9a1601962 |
|||
8e4a694b24 |
|||
fa96cf8941 | |||
698370fd9b | |||
684c25b562 |
|||
674ef6e8b0 |
|||
780a360f03 |
|||
ac6c00245b |
|||
75735a0c12 |
|||
ac5c9ea912 |
|||
5d75109dcf |
|||
186a865231 | |||
39feba7d1d |
|||
9639525ddd | |||
b25f76dde8 |
|||
aa7cf00ebb |
|||
bd1d8f8339 | |||
65975fe4f8 | |||
fa67dd5ebf |
|||
b09c9c3b4f |
|||
4960b5966f |
|||
ffea4c0ec3 | |||
ed44c8b7fd |
|||
262c814df0 | |||
1de660d0e2 |
|||
5d5c27827b | |||
2b096695a3 |
|||
84a4ede026 | |||
f6b8400c0b |
|||
f991e1d6ed |
|||
c719e43ba0 | |||
5ca152007e | |||
bb1621ba49 |
|||
602166aa61 | |||
749303c3ee | |||
189fb85918 | |||
acd098225c | |||
|
3e1c15e023 | ||
|
78b8f4696c | ||
|
b7a8627bcf | ||
349378a13e |
|||
c7e364a188 | |||
e8f4ca81ba |
|||
9088adbac5 | |||
06c4df7602 |
|||
8072500faa |
|||
e418c296de | |||
db4cf796d6 |
|||
|
2a51dce565 | ||
b34b12cfc7 |
|||
|
e78fc58aaa | ||
|
3cbffba14f | ||
fe0bc9d556 |
|||
903ca20e9d | |||
9553c66f11 | |||
d5f4bcee05 |
|||
efd744261d | |||
89c6be5345 |
|||
cd43f111c4 | |||
7be0fc97bc |
|||
1f67fb3665 |
|||
92af3076b5 | |||
20b7c445e0 |
|||
52968e92a0 |
|||
|
1c45ff0058 | ||
52d9e7b688 | |||
2651e34bf1 |
|||
|
e9e2eba46f | ||
|
c277b89ffc | ||
74d812a012 |
|||
|
ce444c5e59 | ||
9859e60173 |
|||
|
e21d031b36 | ||
|
00c512bf3b | ||
4f6ca3c039 |
|||
1d1a917e33 |
|||
99d32916a3 |
|||
519c4a9038 |
|||
027bd90f4c |
|||
84412276c0 |
|||
06dd02394b |
|||
e791d56581 |
|||
b495fdbe74 |
|||
|
9101e2f5db | ||
|
5ad0740902 | ||
e72944d177 |
|||
da90a332dc |
|||
ed252696c4 |
|||
a1997537eb |
|||
1849500d74 |
|||
69af830829 |
|||
7762048ee1 | |||
c68b3f2f7e |
|||
c2e85a5516 |
|||
ba41b1e553 |
|||
dce5d1a86e |
|||
f2da3ee132 |
|||
8119db68c9 |
|||
1514f18d58 |
|||
e5f8d6ce10 |
|||
f88795f7c5 |
|||
64ee19f930 |
|||
f3ab9ffcd6 |
|||
eb1717bca1 |
|||
756beb5a4e |
|||
52de53878e |
|||
07b594fa36 |
|||
c9632d6b26 |
|||
|
26a2e0cdbf | ||
|
54e9ccf426 | ||
|
f5bae60e0f | ||
45ba7d9693 |
|||
3eea955c56 |
|||
bb5f26ab60 | |||
|
681b87383e | ||
|
e6f3f76fd6 | ||
|
c6d886b68b | ||
|
329739b103 | ||
|
f2aa81b6d2 | ||
|
b2053acdfe | ||
|
694787fe07 | ||
|
bc50261221 | ||
|
d58f24ccbf | ||
|
a62d2092b3 | ||
8b6e026e0a | |||
dae835cbfa |
|||
969e2ac0da |
|||
75de7d1370 |
|||
6f6bbe6d8b |
|||
0d9b0ad987 | |||
6f264dccf7 |
|||
0e150e9ded |
|||
2b29ef81b2 |
|||
d64b39fa69 |
|||
9770ad3d8f | |||
74798949c6 |
|||
cc1979a068 |
|||
09677effe6 | |||
e4cd62cca4 |
|||
|
1931a02369 | ||
|
1dfdedee91 | ||
41f3b506a0 | |||
61b806c048 |
|||
|
34c7c39b63 | ||
1966313a20 |
|||
816c659b5c |
|||
d02c3d24f1 | |||
0d59b63c23 |
|||
|
d670190073 | ||
|
9175c82f98 | ||
898fb41030 |
|||
876bf2f427 | |||
5c64b86bc4 |
|||
ee3a57f5b3 | |||
46c9d2b7c1 |
|||
9aab757cdf | |||
12b45957e7 |
|||
c4802546ab |
|||
a6ebf1034e |
|||
|
64a1155eed | ||
|
7c87dfb519 | ||
84feb5f080 |
|||
566ea569e1 |
|||
6d353cc202 |
|||
5809757bc9 |
|||
4f2e7fe712 |
|||
|
dd919799d6 | ||
|
9a95ad3d0f | ||
7d471b6898 |
|||
|
d049048206 | ||
|
2305e83647 | ||
|
c8f2d16f07 | ||
|
9827f81230 | ||
b41145b85c |
|||
963516a5bd |
|||
|
d7f2e72a15 | ||
d42209d1c9 |
|||
47e04567a9 |
209 changed files with 10241 additions and 1466 deletions
20
.gitea/labeler.yml
Normal file
20
.gitea/labeler.yml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
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/**"
|
||||||
|
|
19
.gitea/size.yml
Normal file
19
.gitea/size.yml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
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.
|
|
@ -29,6 +29,32 @@ jobs:
|
||||||
workflow:
|
workflow:
|
||||||
- '.gitea/workflows/**'
|
- '.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:
|
checkstyle:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: "Checkstyle Main"
|
name: "Checkstyle Main"
|
||||||
|
@ -113,17 +139,15 @@ jobs:
|
||||||
push: false
|
push: false
|
||||||
|
|
||||||
eslint:
|
eslint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: git.kjan.de/actions/runner-bun:latest
|
||||||
name: eslint
|
name: eslint
|
||||||
needs: changed_files
|
needs: changed_files
|
||||||
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
|
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
|
||||||
container:
|
|
||||||
image: catthehacker/ubuntu:act-latest
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Install bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
with:
|
with:
|
||||||
|
@ -141,18 +165,56 @@ jobs:
|
||||||
cd frontend
|
cd frontend
|
||||||
bun run lint
|
bun run lint
|
||||||
|
|
||||||
oxlint:
|
playwright:
|
||||||
runs-on: ubuntu-latest
|
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
|
name: oxlint
|
||||||
needs: changed_files
|
needs: changed_files
|
||||||
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
|
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: git.kjan.de/actions/runner-bun:latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Install bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
with:
|
with:
|
||||||
|
@ -171,17 +233,15 @@ jobs:
|
||||||
bun run oxlint
|
bun run oxlint
|
||||||
|
|
||||||
prettier:
|
prettier:
|
||||||
runs-on: ubuntu-latest
|
runs-on: docker
|
||||||
name: prettier
|
name: prettier
|
||||||
needs: changed_files
|
needs: changed_files
|
||||||
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
|
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: git.kjan.de/actions/runner-bun:latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Install bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
with:
|
with:
|
||||||
|
@ -200,17 +260,15 @@ jobs:
|
||||||
bun run format:check
|
bun run format:check
|
||||||
|
|
||||||
test-build:
|
test-build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: docker
|
||||||
name: test-build
|
name: test-build
|
||||||
needs: changed_files
|
needs: changed_files
|
||||||
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
|
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: git.kjan.de/actions/runner-bun:latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Install bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
with:
|
with:
|
||||||
|
|
124
.gitea/workflows/claude-comment.yml
Normal file
124
.gitea/workflows/claude-comment.yml
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
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.
|
||||||
|
"
|
16
.gitea/workflows/claude.yml
Normal file
16
.gitea/workflows/claude.yml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
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 }}
|
29
.gitea/workflows/docs.yml
Normal file
29
.gitea/workflows/docs.yml
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
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
|
14
.gitea/workflows/labeler.yml
Normal file
14
.gitea/workflows/labeler.yml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
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"
|
17
.gitea/workflows/size.yml
Normal file
17
.gitea/workflows/size.yml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
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"
|
15
.gitea/workflows/stale.yml
Normal file
15
.gitea/workflows/stale.yml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
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
|
40
README.md
40
README.md
|
@ -15,96 +15,113 @@ Please refer to our [Style Guide](https://git.kjan.de/SZUT/casino/wiki/Frontend#
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
- Angular 19
|
|
||||||
|
- Angular 20
|
||||||
- TailwindCSS
|
- TailwindCSS
|
||||||
- Keycloak integration
|
- Keycloak integration
|
||||||
- Stripe payment integration
|
- Stripe payment integration
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
|
|
||||||
- Spring Boot (Java)
|
- Spring Boot (Java)
|
||||||
- PostgreSQL database
|
- PostgreSQL database
|
||||||
- Keycloak for authentication/authorization
|
- Keycloak for authentication/authorization
|
||||||
- Stripe API for payment processing
|
- Stripe API for payment processing
|
||||||
|
|
||||||
### Infrastructure
|
### Infrastructure
|
||||||
|
|
||||||
- Docker containerization for all services
|
- Docker containerization for all services
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
* [Docker](https://docs.docker.com/get-docker/)
|
|
||||||
* [Docker Compose](https://docs.docker.com/compose/install/) (included with Docker Desktop for Windows and Mac)
|
- [Docker](https://docs.docker.com/get-docker/)
|
||||||
* Java JDK 17+
|
- [Docker Compose](https://docs.docker.com/compose/install/) (included with Docker Desktop for Windows and Mac)
|
||||||
* Node.js 18+
|
- Java JDK 17+
|
||||||
|
- Node.js 18+
|
||||||
|
|
||||||
### Setting Up the Environment
|
### Setting Up the Environment
|
||||||
|
|
||||||
1. Clone the repository
|
1. Clone the repository
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone <repository-url>
|
git clone <repository-url>
|
||||||
cd casino
|
cd casino
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Start the Docker services
|
2. Start the Docker services
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd docker
|
cd docker
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
This will start:
|
This will start:
|
||||||
|
|
||||||
- PostgreSQL database
|
- PostgreSQL database
|
||||||
- Keycloak authentication server
|
- Keycloak authentication server
|
||||||
|
|
||||||
### Running the Backend
|
### Running the Backend
|
||||||
|
|
||||||
1. Navigate to the backend directory
|
1. Navigate to the backend directory
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
cd backend
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Start the Spring Boot application
|
2. Start the Spring Boot application
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./gradlew bootRun
|
./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:
|
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
|
```bash
|
||||||
watchexec -r -e java ./gradlew :bootRun
|
watchexec -r -e java ./gradlew :bootRun
|
||||||
```
|
```
|
||||||
|
|
||||||
The backend will be available at:
|
The backend will be available at:
|
||||||
- API endpoint: http://localhost:8080
|
|
||||||
- Swagger documentation: http://localhost:8080/swagger
|
- API endpoint: <http://localhost:8080>
|
||||||
|
- Swagger documentation: <http://localhost:8080/swagger>
|
||||||
|
|
||||||
### Running the Frontend
|
### Running the Frontend
|
||||||
|
|
||||||
1. Navigate to the frontend directory
|
1. Navigate to the frontend directory
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Install dependencies
|
2. Install dependencies
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Start the development server
|
3. Start the development server
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
The frontend will be available at http://localhost:4200
|
The frontend will be available at <http://localhost:4200>
|
||||||
|
|
||||||
### Local Stripe integration
|
### Local Stripe integration
|
||||||
|
|
||||||
1. Install the Stripe CLI
|
1. Install the Stripe CLI
|
||||||
https://stripe.com/docs/stripe-cli
|
<https://stripe.com/docs/stripe-cli>
|
||||||
|
|
||||||
2. Login to the casino stripe account
|
2. Login to the casino stripe account
|
||||||
|
|
||||||
```
|
```
|
||||||
stripe login --api-key <casino-stripe-secret-key>
|
stripe login --api-key <casino-stripe-secret-key>
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Start webhook forwarding
|
3. Start webhook forwarding
|
||||||
|
|
||||||
```
|
```
|
||||||
stripe listen --forward-to localhost:8080/webhook
|
stripe listen --forward-to localhost:8080/webhook
|
||||||
```
|
```
|
||||||
|
@ -114,6 +131,7 @@ stripe listen --forward-to localhost:8080/webhook
|
||||||
### Postgres Management
|
### Postgres Management
|
||||||
|
|
||||||
#### Database cleanup (if needed)
|
#### Database cleanup (if needed)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd docker
|
cd docker
|
||||||
docker-compose down
|
docker-compose down
|
||||||
|
@ -122,6 +140,7 @@ docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Setting up IntelliJ Database View
|
#### Setting up IntelliJ Database View
|
||||||
|
|
||||||
1. Run the Docker container with PostgreSQL database
|
1. Run the Docker container with PostgreSQL database
|
||||||
2. Open `application.properties` in the resources folder and copy the database URL
|
2. Open `application.properties` in the resources folder and copy the database URL
|
||||||
3. Open the Database tab in IntelliJ
|
3. Open the Database tab in IntelliJ
|
||||||
|
@ -148,6 +167,7 @@ We follow semantic commit messages to maintain clear project history.
|
||||||
Format: `<type>(<scope>): <subject>`
|
Format: `<type>(<scope>): <subject>`
|
||||||
|
|
||||||
Where `<type>` is one of:
|
Where `<type>` is one of:
|
||||||
|
|
||||||
- `feat`: New feature
|
- `feat`: New feature
|
||||||
- `fix`: Bug fix
|
- `fix`: Bug fix
|
||||||
- `docs`: Documentation changes
|
- `docs`: Documentation changes
|
||||||
|
@ -157,6 +177,7 @@ Where `<type>` is one of:
|
||||||
- `chore`: Updating build tasks, etc; no production code change
|
- `chore`: Updating build tasks, etc; no production code change
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
```
|
```
|
||||||
feat: add user balance display
|
feat: add user balance display
|
||||||
fix(auth): resolve token expiration issue
|
fix(auth): resolve token expiration issue
|
||||||
|
@ -164,6 +185,7 @@ docs: update API documentation
|
||||||
```
|
```
|
||||||
|
|
||||||
References:
|
References:
|
||||||
|
|
||||||
- [Conventional Commits](https://www.conventionalcommits.org/)
|
- [Conventional Commits](https://www.conventionalcommits.org/)
|
||||||
- [Semantic Commit Messages](https://seesparkbox.com/foundry/semantic_commit_messages)
|
- [Semantic Commit Messages](https://seesparkbox.com/foundry/semantic_commit_messages)
|
||||||
|
|
||||||
|
|
|
@ -1,59 +1,137 @@
|
||||||
# Starter für das LF08 Projekt
|
# Casino Gaming Platform - Backend API
|
||||||
|
|
||||||
## Requirements
|
A Spring Boot backend application providing REST APIs for a casino gaming platform with multiple games, user management, authentication, and payment processing.
|
||||||
* Docker https://docs.docker.com/get-docker/
|
|
||||||
* Docker compose (bei Windows und Mac schon in Docker enthalten) https://docs.docker.com/compose/install/
|
|
||||||
|
|
||||||
## Endpunkt
|
## Features
|
||||||
```
|
|
||||||
http://localhost:8080
|
|
||||||
```
|
|
||||||
## Swagger
|
|
||||||
```
|
|
||||||
http://localhost:8080/swagger
|
|
||||||
```
|
|
||||||
|
|
||||||
|
### Games
|
||||||
|
- **Blackjack** - Classic card game with deck management
|
||||||
|
- **Coinflip** - Simple heads/tails betting game
|
||||||
|
- **Dice** - Dice rolling game
|
||||||
|
- **Slots** - Slot machine with symbols and payouts
|
||||||
|
- **Lootboxes** - Reward system with configurable prizes
|
||||||
|
|
||||||
# Postgres
|
### User Management
|
||||||
### Terminal öffnen
|
- User registration and authentication
|
||||||
für alles gilt, im Terminal im Ordner docker/local sein
|
- 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
|
```bash
|
||||||
|
# Build the application
|
||||||
|
./gradlew build
|
||||||
|
|
||||||
|
# Clean build
|
||||||
|
./gradlew clean build
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
./gradlew bootRun
|
||||||
|
|
||||||
|
# Generate JAR file
|
||||||
|
./gradlew bootJar
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```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.
|
|
||||||
|
|
||||||
### Postgres stoppen
|
# Stop PostgreSQL container
|
||||||
```bash
|
|
||||||
docker compose down
|
docker compose down
|
||||||
```
|
|
||||||
|
|
||||||
### Postgres Datenbank wipen, z.B. bei Problemen
|
# Reset database (if needed)
|
||||||
```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
|
||||||
```
|
```
|
||||||
|
|
||||||
### Intellij-Ansicht für Postgres Datenbank einrichten
|
### Database Configuration
|
||||||
```bash
|
Database connection settings are configured in `src/main/resources/application.properties`
|
||||||
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
|
|
||||||
|
|
||||||
### Keycloak Token
|
### IntelliJ Database Setup
|
||||||
1. Auf der Projektebene [GetBearerToken.http](../GetBearerToken.http) öffnen.
|
1. Start the PostgreSQL Docker container
|
||||||
2. Neben der Request auf den grünen Pfeil drücken
|
2. Open `application.properties` and copy the database URL
|
||||||
3. Aus dem Reponse das access_token kopieren
|
3. In IntelliJ, open the Database tab (right panel)
|
||||||
|
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,6 +1,6 @@
|
||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
id("org.springframework.boot") version "3.4.5"
|
id("org.springframework.boot") version "3.5.0"
|
||||||
id("io.spring.dependency-management") version "1.1.7"
|
id("io.spring.dependency-management") version "1.1.7"
|
||||||
id("checkstyle")
|
id("checkstyle")
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ repositories {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("com.stripe:stripe-java:29.1.0")
|
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,14 +47,15 @@ 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.4.5")
|
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server:3.5.0")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.4.5")
|
implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.5.0")
|
||||||
runtimeOnly("org.postgresql:postgresql")
|
runtimeOnly("org.postgresql:postgresql")
|
||||||
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8")
|
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9")
|
||||||
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
|
implementation("io.jsonwebtoken:jjwt-api:0.12.6")
|
||||||
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5")
|
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6")
|
||||||
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5")
|
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-mail")
|
implementation("org.springframework.boot:spring-boot-starter-mail")
|
||||||
|
runtimeOnly("com.h2database:h2")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<Test> {
|
tasks.withType<Test> {
|
||||||
|
|
|
@ -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-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
@ -8,9 +8,6 @@ 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.MailException;
|
|
||||||
import org.springframework.mail.MailSender;
|
|
||||||
import org.springframework.mail.SimpleMailMessage;
|
|
||||||
import org.springframework.mail.javamail.JavaMailSenderImpl;
|
import org.springframework.mail.javamail.JavaMailSenderImpl;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
@ -77,8 +74,7 @@ public class CasinoApplication {
|
||||||
|
|
||||||
rewardRepository.saveAll(Arrays.asList(
|
rewardRepository.saveAll(Arrays.asList(
|
||||||
commonReward, rareReward, epicReward,
|
commonReward, rareReward, epicReward,
|
||||||
premiumCommon, premiumRare, legendaryReward
|
premiumCommon, premiumRare, legendaryReward));
|
||||||
));
|
|
||||||
|
|
||||||
basicLootBox.getRewards().add(commonReward);
|
basicLootBox.getRewards().add(commonReward);
|
||||||
basicLootBox.getRewards().add(premiumRare);
|
basicLootBox.getRewards().add(premiumRare);
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
package de.szut.casino.blackjack;
|
package de.szut.casino.blackjack;
|
||||||
|
|
||||||
import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException;
|
import de.szut.casino.exceptionHandling.exceptions.UserBlackJackGameMismatchException;
|
||||||
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
|
|
||||||
import de.szut.casino.shared.dto.BetDto;
|
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.UserEntity;
|
||||||
import de.szut.casino.user.UserService;
|
import de.szut.casino.user.UserService;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
@ -12,121 +10,59 @@ import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
public class BlackJackGameController {
|
public class BlackJackGameController {
|
||||||
|
|
||||||
private final BalanceService balanceService;
|
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final BlackJackService blackJackService;
|
private final BlackJackService blackJackService;
|
||||||
|
|
||||||
public BlackJackGameController(BalanceService balanceService, UserService userService, BlackJackService blackJackService) {
|
public BlackJackGameController(UserService userService, BlackJackService blackJackService) {
|
||||||
this.balanceService = balanceService;
|
|
||||||
this.blackJackService = blackJackService;
|
this.blackJackService = blackJackService;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/blackjack/{id}")
|
@GetMapping("/blackjack/{id}")
|
||||||
public ResponseEntity<Object> getGame(@PathVariable Long id) {
|
public ResponseEntity<Object> getGame(@PathVariable Long id) {
|
||||||
Optional<UserEntity> optionalUser = userService.getCurrentUser();
|
BlackJackGameEntity game = getBlackJackGame(id);
|
||||||
|
|
||||||
if (optionalUser.isEmpty()) {
|
|
||||||
throw new UserNotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
UserEntity user = optionalUser.get();
|
|
||||||
BlackJackGameEntity game = blackJackService.getBlackJackGame(id);
|
|
||||||
if (game == null || !Objects.equals(game.getUserId(), user.getId())) {
|
|
||||||
return ResponseEntity.notFound().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResponseEntity.ok(game);
|
return ResponseEntity.ok(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/blackjack/{id}/hit")
|
@PostMapping("/blackjack/{id}/hit")
|
||||||
public ResponseEntity<Object> hit(@PathVariable Long id) {
|
public ResponseEntity<Object> hit(@PathVariable Long id) {
|
||||||
Optional<UserEntity> optionalUser = userService.getCurrentUser();
|
BlackJackGameEntity game = getBlackJackGame(id);
|
||||||
|
|
||||||
if (optionalUser.isEmpty()) {
|
|
||||||
throw new UserNotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
UserEntity user = optionalUser.get();
|
|
||||||
BlackJackGameEntity game = blackJackService.getBlackJackGame(id);
|
|
||||||
if (game == null || !Objects.equals(game.getUserId(), user.getId())) {
|
|
||||||
return ResponseEntity.notFound().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResponseEntity.ok(blackJackService.hit(game));
|
return ResponseEntity.ok(blackJackService.hit(game));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/blackjack/{id}/stand")
|
@PostMapping("/blackjack/{id}/stand")
|
||||||
public ResponseEntity<Object> stand(@PathVariable Long id) {
|
public ResponseEntity<Object> stand(@PathVariable Long id) {
|
||||||
Optional<UserEntity> optionalUser = userService.getCurrentUser();
|
BlackJackGameEntity game = getBlackJackGame(id);
|
||||||
|
|
||||||
if (optionalUser.isEmpty()) {
|
|
||||||
throw new UserNotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
UserEntity user = optionalUser.get();
|
|
||||||
BlackJackGameEntity game = blackJackService.getBlackJackGame(id);
|
|
||||||
if (game == null || !Objects.equals(game.getUserId(), user.getId())) {
|
|
||||||
return ResponseEntity.notFound().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResponseEntity.ok(blackJackService.stand(game));
|
return ResponseEntity.ok(blackJackService.stand(game));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/blackjack/{id}/doubleDown")
|
@PostMapping("/blackjack/{id}/doubleDown")
|
||||||
public ResponseEntity<Object> doubleDown(@PathVariable Long id) {
|
public ResponseEntity<Object> doubleDown(@PathVariable Long id) {
|
||||||
Optional<UserEntity> optionalUser = userService.getCurrentUser();
|
BlackJackGameEntity game = getBlackJackGame(id);
|
||||||
|
|
||||||
if (optionalUser.isEmpty()) {
|
|
||||||
throw new UserNotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
UserEntity user = optionalUser.get();
|
|
||||||
BlackJackGameEntity game = blackJackService.getBlackJackGame(id);
|
|
||||||
if (game == null || !Objects.equals(game.getUserId(), user.getId())) {
|
|
||||||
return ResponseEntity.notFound().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResponseEntity.ok(blackJackService.doubleDown(game));
|
return ResponseEntity.ok(blackJackService.doubleDown(game));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/blackjack/{id}/split")
|
|
||||||
public ResponseEntity<Object> split(@PathVariable Long id) {
|
|
||||||
Optional<UserEntity> optionalUser = userService.getCurrentUser();
|
|
||||||
|
|
||||||
if (optionalUser.isEmpty()) {
|
|
||||||
throw new UserNotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
UserEntity user = optionalUser.get();
|
|
||||||
BlackJackGameEntity game = blackJackService.getBlackJackGame(id);
|
|
||||||
if (game == null || !Objects.equals(game.getUserId(), user.getId())) {
|
|
||||||
return ResponseEntity.notFound().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResponseEntity.ok(blackJackService.split(game));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/blackjack/start")
|
@PostMapping("/blackjack/start")
|
||||||
public ResponseEntity<Object> createBlackJackGame(@RequestBody @Valid BetDto betDto) {
|
public ResponseEntity<Object> createBlackJackGame(@RequestBody @Valid BetDto betDto) {
|
||||||
Optional<UserEntity> optionalUser = userService.getCurrentUser();
|
return ResponseEntity.ok(blackJackService.createBlackJackGame(betDto));
|
||||||
|
}
|
||||||
|
|
||||||
if (optionalUser.isEmpty()) {
|
private BlackJackGameEntity getBlackJackGame(Long gameId) {
|
||||||
throw new UserNotFoundException();
|
UserEntity user = userService.getCurrentUser();
|
||||||
|
BlackJackGameEntity game = blackJackService.getBlackJackGame(gameId);
|
||||||
|
if (game == null || !Objects.equals(game.getUserId(), user.getId())) {
|
||||||
|
throw new UserBlackJackGameMismatchException(gameId);
|
||||||
}
|
}
|
||||||
|
|
||||||
UserEntity user = optionalUser.get();
|
return game;
|
||||||
|
|
||||||
if (!this.balanceService.hasFunds(user, betDto)) {
|
|
||||||
throw new InsufficientFundsException();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResponseEntity.ok(blackJackService.createBlackJackGame(user, betDto.getBetAmount()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,15 +51,4 @@ public class BlackJackGameEntity {
|
||||||
@JsonManagedReference
|
@JsonManagedReference
|
||||||
@SQLRestriction("card_type = 'DEALER'")
|
@SQLRestriction("card_type = 'DEALER'")
|
||||||
private List<CardEntity> dealerCards = new ArrayList<>();
|
private List<CardEntity> dealerCards = new ArrayList<>();
|
||||||
|
|
||||||
@OneToMany(mappedBy = "game", cascade = CascadeType.ALL, orphanRemoval = true)
|
|
||||||
@JsonManagedReference
|
|
||||||
@SQLRestriction("card_type = 'PLAYER_SPLIT'")
|
|
||||||
private List<CardEntity> playerSplitCards = new ArrayList<>();
|
|
||||||
|
|
||||||
@Column(name = "split_bet")
|
|
||||||
private BigDecimal splitBet;
|
|
||||||
|
|
||||||
@Column(name = "is_split")
|
|
||||||
private boolean isSplit;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,37 @@
|
||||||
package de.szut.casino.blackjack;
|
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.UserEntity;
|
||||||
import de.szut.casino.user.UserRepository;
|
import de.szut.casino.user.UserRepository;
|
||||||
|
import de.szut.casino.user.UserService;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class BlackJackService {
|
public class BlackJackService {
|
||||||
private final BlackJackGameRepository blackJackGameRepository;
|
private final BlackJackGameRepository blackJackGameRepository;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final Random random = new Random();
|
private final BalanceService balanceService;
|
||||||
|
private final UserService userService;
|
||||||
|
private final DeckService deckService;
|
||||||
|
|
||||||
public BlackJackService(BlackJackGameRepository blackJackGameRepository, UserRepository userRepository) {
|
public BlackJackService(
|
||||||
|
BlackJackGameRepository blackJackGameRepository,
|
||||||
|
UserRepository userRepository,
|
||||||
|
BalanceService balanceService,
|
||||||
|
UserService userService,
|
||||||
|
DeckService deckService
|
||||||
|
) {
|
||||||
this.blackJackGameRepository = blackJackGameRepository;
|
this.blackJackGameRepository = blackJackGameRepository;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
|
this.balanceService = balanceService;
|
||||||
|
this.userService = userService;
|
||||||
|
this.deckService = deckService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BlackJackGameEntity getBlackJackGame(Long id) {
|
public BlackJackGameEntity getBlackJackGame(Long id) {
|
||||||
|
@ -25,18 +39,25 @@ public class BlackJackService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public BlackJackGameEntity createBlackJackGame(UserEntity user, BigDecimal betAmount) {
|
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();
|
BlackJackGameEntity game = new BlackJackGameEntity();
|
||||||
game.setUser(user);
|
game.setUser(user);
|
||||||
game.setBet(betAmount);
|
game.setBet(betDto.getBetAmount());
|
||||||
|
|
||||||
initializeDeck(game);
|
this.deckService.initializeDeck(game);
|
||||||
dealInitialCards(game);
|
this.deckService.dealInitialCards(game);
|
||||||
|
|
||||||
game.setState(getState(game));
|
game.setState(getState(game));
|
||||||
deductBetFromBalance(user, betAmount);
|
|
||||||
|
|
||||||
return blackJackGameRepository.save(game);
|
return processGameBasedOnState(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
|
@ -45,11 +66,10 @@ public class BlackJackService {
|
||||||
return game;
|
return game;
|
||||||
}
|
}
|
||||||
|
|
||||||
dealCardToPlayer(game);
|
this.deckService.dealCardToPlayer(game);
|
||||||
|
|
||||||
updateGameStateAndBalance(game);
|
updateGameStateAndBalance(game);
|
||||||
|
|
||||||
return blackJackGameRepository.save(game);
|
return processGameBasedOnState(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
|
@ -61,7 +81,7 @@ public class BlackJackService {
|
||||||
dealCardsToDealerUntilMinimumScore(game);
|
dealCardsToDealerUntilMinimumScore(game);
|
||||||
determineWinnerAndUpdateBalance(game);
|
determineWinnerAndUpdateBalance(game);
|
||||||
|
|
||||||
return blackJackGameRepository.save(game);
|
return processGameBasedOnState(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
|
@ -70,13 +90,14 @@ public class BlackJackService {
|
||||||
return game;
|
return game;
|
||||||
}
|
}
|
||||||
|
|
||||||
UserEntity user = getUserWithFreshData(game.getUser());
|
UserEntity user = game.getUser();
|
||||||
BigDecimal additionalBet = game.getBet();
|
BigDecimal additionalBet = game.getBet();
|
||||||
|
|
||||||
deductBetFromBalance(user, additionalBet);
|
this.balanceService.subtractFunds(user, additionalBet);
|
||||||
|
|
||||||
game.setBet(game.getBet().add(additionalBet));
|
game.setBet(game.getBet().add(additionalBet));
|
||||||
|
|
||||||
dealCardToPlayer(game);
|
this.deckService.dealCardToPlayer(game);
|
||||||
updateGameStateAndBalance(game);
|
updateGameStateAndBalance(game);
|
||||||
|
|
||||||
if (game.getState() == BlackJackState.IN_PROGRESS) {
|
if (game.getState() == BlackJackState.IN_PROGRESS) {
|
||||||
|
@ -86,97 +107,22 @@ public class BlackJackService {
|
||||||
return game;
|
return game;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
private BlackJackGameEntity processGameBasedOnState(BlackJackGameEntity game) {
|
||||||
public BlackJackGameEntity split(BlackJackGameEntity game) {
|
if (game.getState() != BlackJackState.IN_PROGRESS) {
|
||||||
if (game.getState() != BlackJackState.IN_PROGRESS ||
|
this.blackJackGameRepository.delete(game);
|
||||||
game.getPlayerCards().size() != 2 ||
|
|
||||||
game.isSplit() ||
|
|
||||||
!game.getPlayerCards().get(0).getRank().equals(game.getPlayerCards().get(1).getRank())) {
|
|
||||||
return game;
|
return game;
|
||||||
}
|
}
|
||||||
|
|
||||||
UserEntity user = getUserWithFreshData(game.getUser());
|
|
||||||
BigDecimal splitBet = game.getBet();
|
|
||||||
|
|
||||||
if (user.getBalance().compareTo(splitBet) < 0) {
|
|
||||||
return game;
|
|
||||||
}
|
|
||||||
|
|
||||||
deductBetFromBalance(user, splitBet);
|
|
||||||
game.setSplitBet(splitBet);
|
|
||||||
game.setSplit(true);
|
|
||||||
|
|
||||||
CardEntity card = game.getPlayerCards().remove(1);
|
|
||||||
card.setCardType(CardType.PLAYER_SPLIT);
|
|
||||||
game.getPlayerSplitCards().add(card);
|
|
||||||
|
|
||||||
dealCardToPlayer(game);
|
|
||||||
dealCardToSplitHand(game);
|
|
||||||
|
|
||||||
return blackJackGameRepository.save(game);
|
return blackJackGameRepository.save(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
private BlackJackGameEntity refreshGameState(BlackJackGameEntity game) {
|
|
||||||
return blackJackGameRepository.findById(game.getId()).orElse(game);
|
|
||||||
}
|
|
||||||
|
|
||||||
private UserEntity getUserWithFreshData(UserEntity user) {
|
|
||||||
return userRepository.findById(user.getId()).orElse(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void dealInitialCards(BlackJackGameEntity game) {
|
|
||||||
for (int i = 0; i < 2; i++) {
|
|
||||||
dealCardToPlayer(game);
|
|
||||||
}
|
|
||||||
|
|
||||||
dealCardToDealer(game);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void dealCardToPlayer(BlackJackGameEntity game) {
|
|
||||||
CardEntity card = drawCardFromDeck(game);
|
|
||||||
card.setCardType(CardType.PLAYER);
|
|
||||||
game.getPlayerCards().add(card);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void dealCardToDealer(BlackJackGameEntity game) {
|
|
||||||
CardEntity card = drawCardFromDeck(game);
|
|
||||||
card.setCardType(CardType.DEALER);
|
|
||||||
game.getDealerCards().add(card);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void dealCardsToDealerUntilMinimumScore(BlackJackGameEntity game) {
|
|
||||||
while (calculateHandValue(game.getDealerCards()) < 17) {
|
|
||||||
dealCardToDealer(game);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void dealCardToSplitHand(BlackJackGameEntity game) {
|
|
||||||
CardEntity card = drawCardFromDeck(game);
|
|
||||||
card.setCardType(CardType.PLAYER_SPLIT);
|
|
||||||
game.getPlayerSplitCards().add(card);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateGameStateAndBalance(BlackJackGameEntity game) {
|
private void updateGameStateAndBalance(BlackJackGameEntity game) {
|
||||||
if (game.isSplit()) {
|
game.setState(getState(game));
|
||||||
int mainHandValue = calculateHandValue(game.getPlayerCards());
|
|
||||||
int splitHandValue = calculateHandValue(game.getPlayerSplitCards());
|
|
||||||
|
|
||||||
if (mainHandValue > 21 && splitHandValue > 21) {
|
if (game.getState() == BlackJackState.PLAYER_WON) {
|
||||||
game.setState(BlackJackState.PLAYER_LOST);
|
updateUserBalance(game, true);
|
||||||
updateUserBalance(game, false);
|
} else if (game.getState() == BlackJackState.PLAYER_LOST) {
|
||||||
} else if (mainHandValue <= 21 && splitHandValue <= 21) {
|
updateUserBalance(game, false);
|
||||||
game.setState(BlackJackState.IN_PROGRESS);
|
|
||||||
} else {
|
|
||||||
game.setState(BlackJackState.IN_PROGRESS);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
game.setState(getState(game));
|
|
||||||
|
|
||||||
if (game.getState() == BlackJackState.PLAYER_WON) {
|
|
||||||
updateUserBalance(game, true);
|
|
||||||
} else if (game.getState() == BlackJackState.PLAYER_LOST) {
|
|
||||||
updateUserBalance(game, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,79 +142,26 @@ public class BlackJackService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deductBetFromBalance(UserEntity user, BigDecimal betAmount) {
|
protected void updateUserBalance(BlackJackGameEntity game, boolean isWin) {
|
||||||
user.setBalance(user.getBalance().subtract(betAmount));
|
UserEntity user = game.getUser();
|
||||||
userRepository.save(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
private void updateUserBalance(BlackJackGameEntity game, boolean isWin) {
|
|
||||||
UserEntity user = getUserWithFreshData(game.getUser());
|
|
||||||
BigDecimal totalBet = game.getBet();
|
BigDecimal totalBet = game.getBet();
|
||||||
BigDecimal balance = user.getBalance();
|
BigDecimal balance = user.getBalance();
|
||||||
|
|
||||||
if (game.isSplit()) {
|
if (isWin) {
|
||||||
totalBet = totalBet.add(game.getSplitBet());
|
balance = balance.add(totalBet.multiply(BigDecimal.valueOf(2)));
|
||||||
|
} else if (game.getState() == BlackJackState.DRAW) {
|
||||||
if (isWin) {
|
balance = balance.add(totalBet);
|
||||||
int mainHandValue = calculateHandValue(game.getPlayerCards());
|
|
||||||
int splitHandValue = calculateHandValue(game.getPlayerSplitCards());
|
|
||||||
int dealerValue = calculateHandValue(game.getDealerCards());
|
|
||||||
|
|
||||||
if (mainHandValue <= 21 && (dealerValue > 21 || mainHandValue > dealerValue)) {
|
|
||||||
balance = balance.add(game.getBet().multiply(BigDecimal.valueOf(2)));
|
|
||||||
} else if (mainHandValue == dealerValue) {
|
|
||||||
balance = balance.add(game.getBet());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (splitHandValue <= 21 && (dealerValue > 21 || splitHandValue > dealerValue)) {
|
|
||||||
balance = balance.add(game.getSplitBet().multiply(BigDecimal.valueOf(2)));
|
|
||||||
} else if (splitHandValue == dealerValue) {
|
|
||||||
balance = balance.add(game.getSplitBet());
|
|
||||||
}
|
|
||||||
} else if (game.getState() == BlackJackState.DRAW) {
|
|
||||||
balance = balance.add(totalBet);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isWin) {
|
|
||||||
balance = balance.add(totalBet.multiply(BigDecimal.valueOf(2)));
|
|
||||||
} else if (game.getState() == BlackJackState.DRAW) {
|
|
||||||
balance = balance.add(totalBet);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user.setBalance(balance);
|
user.setBalance(balance);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
private 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
private CardEntity drawCardFromDeck(BlackJackGameEntity game) {
|
|
||||||
if (game.getDeck().isEmpty()) {
|
|
||||||
throw new IllegalStateException("Deck is empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
return game.getDeck().removeFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
private BlackJackState getState(BlackJackGameEntity game) {
|
private BlackJackState getState(BlackJackGameEntity game) {
|
||||||
int playerHandValue = calculateHandValue(game.getPlayerCards());
|
int playerHandValue = calculateHandValue(game.getPlayerCards());
|
||||||
|
|
||||||
if (playerHandValue == 21) {
|
if (playerHandValue == 21) {
|
||||||
CardEntity hole = drawCardFromDeck(game);
|
CardEntity hole = this.deckService.drawCardFromDeck(game);
|
||||||
hole.setCardType(CardType.DEALER);
|
hole.setCardType(CardType.DEALER);
|
||||||
game.getDealerCards().add(hole);
|
game.getDealerCards().add(hole);
|
||||||
|
|
||||||
|
@ -278,7 +171,7 @@ public class BlackJackService {
|
||||||
return BlackJackState.DRAW;
|
return BlackJackState.DRAW;
|
||||||
} else {
|
} else {
|
||||||
BigDecimal blackjackWinnings = game.getBet().multiply(new BigDecimal("1.5"));
|
BigDecimal blackjackWinnings = game.getBet().multiply(new BigDecimal("1.5"));
|
||||||
UserEntity user = getUserWithFreshData(game.getUser());
|
UserEntity user = game.getUser();
|
||||||
user.setBalance(user.getBalance().add(blackjackWinnings));
|
user.setBalance(user.getBalance().add(blackjackWinnings));
|
||||||
return BlackJackState.PLAYER_BLACKJACK;
|
return BlackJackState.PLAYER_BLACKJACK;
|
||||||
}
|
}
|
||||||
|
@ -306,4 +199,12 @@ public class BlackJackService {
|
||||||
|
|
||||||
return sum;
|
return sum;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void dealCardsToDealerUntilMinimumScore(BlackJackGameEntity game) {
|
||||||
|
while (calculateHandValue(game.getDealerCards()) < 17) {
|
||||||
|
this.deckService.dealCardToDealer(game);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -36,5 +36,5 @@ public class CardEntity {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CardType {
|
enum CardType {
|
||||||
DECK, PLAYER, DEALER, PLAYER_SPLIT
|
DECK, PLAYER, DEALER
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,13 +28,7 @@ public class CoinflipController {
|
||||||
|
|
||||||
@PostMapping("/coinflip")
|
@PostMapping("/coinflip")
|
||||||
public ResponseEntity<Object> coinFlip(@RequestBody @Valid CoinflipDto coinflipDto) {
|
public ResponseEntity<Object> coinFlip(@RequestBody @Valid CoinflipDto coinflipDto) {
|
||||||
Optional<UserEntity> optionalUser = userService.getCurrentUser();
|
UserEntity user = userService.getCurrentUser();
|
||||||
|
|
||||||
if (optionalUser.isEmpty()) {
|
|
||||||
throw new UserNotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
UserEntity user = optionalUser.get();
|
|
||||||
|
|
||||||
if (!this.balanceService.hasFunds(user, coinflipDto)) {
|
if (!this.balanceService.hasFunds(user, coinflipDto)) {
|
||||||
throw new InsufficientFundsException();
|
throw new InsufficientFundsException();
|
||||||
|
|
|
@ -2,13 +2,16 @@ package de.szut.casino.coinflip;
|
||||||
|
|
||||||
import de.szut.casino.shared.dto.BetDto;
|
import de.szut.casino.shared.dto.BetDto;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
public class CoinflipDto extends BetDto {
|
public class CoinflipDto extends BetDto {
|
||||||
@NotNull(message = "chosen side cannot be null")
|
@NotNull(message = "chosen side cannot be null")
|
||||||
private CoinSide coinSide;
|
private CoinSide coinSide;
|
||||||
|
|
|
@ -12,4 +12,5 @@ import java.math.BigDecimal;
|
||||||
public class CoinflipResult {
|
public class CoinflipResult {
|
||||||
private boolean isWin;
|
private boolean isWin;
|
||||||
private BigDecimal payout;
|
private BigDecimal payout;
|
||||||
|
private CoinSide coinSide;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,18 +9,19 @@ import java.util.Random;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class CoinflipService {
|
public class CoinflipService {
|
||||||
private final Random random = new Random();
|
private final Random random;
|
||||||
private final BalanceService balanceService;
|
private final BalanceService balanceService;
|
||||||
|
|
||||||
public CoinflipService(BalanceService balanceService) {
|
public CoinflipService(BalanceService balanceService, Random random) {
|
||||||
this.balanceService = balanceService;
|
this.balanceService = balanceService;
|
||||||
|
this.random = random;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CoinflipResult play(UserEntity user, CoinflipDto coinflipDto) {
|
public CoinflipResult play(UserEntity user, CoinflipDto coinflipDto) {
|
||||||
this.balanceService.subtractFunds(user, coinflipDto.getBetAmount());
|
this.balanceService.subtractFunds(user, coinflipDto.getBetAmount());
|
||||||
|
|
||||||
CoinflipResult coinflipResult = new CoinflipResult(false, BigDecimal.ZERO);
|
|
||||||
CoinSide coinSide = this.random.nextBoolean() ? CoinSide.HEAD : CoinSide.TAILS;
|
CoinSide coinSide = this.random.nextBoolean() ? CoinSide.HEAD : CoinSide.TAILS;
|
||||||
|
CoinflipResult coinflipResult = new CoinflipResult(false, BigDecimal.ZERO, coinSide);
|
||||||
if (coinSide == coinflipDto.getCoinSide()) {
|
if (coinSide == coinflipDto.getCoinSide()) {
|
||||||
coinflipResult.setWin(true);
|
coinflipResult.setWin(true);
|
||||||
|
|
||||||
|
|
15
backend/src/main/java/de/szut/casino/config/AppConfig.java
Normal file
15
backend/src/main/java/de/szut/casino/config/AppConfig.java
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
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,7 +1,6 @@
|
||||||
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;
|
||||||
|
|
|
@ -18,12 +18,12 @@ public class WebConfig {
|
||||||
@Override
|
@Override
|
||||||
public void addCorsMappings(CorsRegistry registry) {
|
public void addCorsMappings(CorsRegistry registry) {
|
||||||
registry.addMapping("/**")
|
registry.addMapping("/**")
|
||||||
.allowedOrigins(frontendHost)
|
.allowedOrigins(frontendHost)
|
||||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||||
.allowedHeaders("*")
|
.allowedHeaders("*")
|
||||||
.exposedHeaders("*")
|
.exposedHeaders("*")
|
||||||
.allowCredentials(true)
|
.allowCredentials(true)
|
||||||
.maxAge(3600);
|
.maxAge(3600);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,8 +16,6 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestHeader;
|
import org.springframework.web.bind.annotation.RequestHeader;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
public class DepositController {
|
public class DepositController {
|
||||||
|
|
||||||
|
@ -29,7 +27,7 @@ public class DepositController {
|
||||||
|
|
||||||
private final TransactionService transactionService;
|
private final TransactionService transactionService;
|
||||||
|
|
||||||
private UserService userService;
|
private final UserService userService;
|
||||||
|
|
||||||
public DepositController(TransactionService transactionService, UserService userService) {
|
public DepositController(TransactionService transactionService, UserService userService) {
|
||||||
this.transactionService = transactionService;
|
this.transactionService = transactionService;
|
||||||
|
@ -40,7 +38,7 @@ public class DepositController {
|
||||||
public ResponseEntity<SessionIdDto> checkout(@RequestBody @Valid AmountDto amountDto, @RequestHeader("Authorization") String token) throws StripeException {
|
public ResponseEntity<SessionIdDto> checkout(@RequestBody @Valid AmountDto amountDto, @RequestHeader("Authorization") String token) throws StripeException {
|
||||||
Stripe.apiKey = stripeKey;
|
Stripe.apiKey = stripeKey;
|
||||||
|
|
||||||
Optional<UserEntity> optionalUserEntity = this.userService.getCurrentUser();
|
UserEntity user = userService.getCurrentUser();
|
||||||
|
|
||||||
SessionCreateParams params = SessionCreateParams.builder()
|
SessionCreateParams params = SessionCreateParams.builder()
|
||||||
.addLineItem(SessionCreateParams.LineItem.builder()
|
.addLineItem(SessionCreateParams.LineItem.builder()
|
||||||
|
@ -53,18 +51,14 @@ public class DepositController {
|
||||||
.build())
|
.build())
|
||||||
.setQuantity(1L)
|
.setQuantity(1L)
|
||||||
.build())
|
.build())
|
||||||
.setSuccessUrl(frontendHost+"/home?success=true")
|
.setSuccessUrl(frontendHost + "/home?success=true")
|
||||||
.setCancelUrl(frontendHost+"/home?success=false")
|
.setCancelUrl(frontendHost + "/home?success=false")
|
||||||
.setMode(SessionCreateParams.Mode.PAYMENT)
|
.setMode(SessionCreateParams.Mode.PAYMENT)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
Session session = Session.create(params);
|
Session session = Session.create(params);
|
||||||
|
|
||||||
if (optionalUserEntity.isEmpty()) {
|
transactionService.createTransaction(user, session.getId(), amountDto.getAmount());
|
||||||
throw new RuntimeException("User doesnt exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
transactionService.createTransaction(optionalUserEntity.get(), session.getId(), amountDto.getAmount());
|
|
||||||
|
|
||||||
return ResponseEntity.ok(new SessionIdDto(session.getId()));
|
return ResponseEntity.ok(new SessionIdDto(session.getId()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
29
backend/src/main/java/de/szut/casino/dice/DiceDto.java
Normal file
29
backend/src/main/java/de/szut/casino/dice/DiceDto.java
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
20
backend/src/main/java/de/szut/casino/dice/DiceResult.java
Normal file
20
backend/src/main/java/de/szut/casino/dice/DiceResult.java
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
69
backend/src/main/java/de/szut/casino/dice/DiceService.java
Normal file
69
backend/src/main/java/de/szut/casino/dice/DiceService.java
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package de.szut.casino.exceptionHandling;
|
||||||
|
|
||||||
import de.szut.casino.exceptionHandling.exceptions.EmailNotVerifiedException;
|
import de.szut.casino.exceptionHandling.exceptions.EmailNotVerifiedException;
|
||||||
import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException;
|
import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException;
|
||||||
|
import de.szut.casino.exceptionHandling.exceptions.UserBlackJackGameMismatchException;
|
||||||
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
|
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
|
||||||
import jakarta.persistence.EntityExistsException;
|
import jakarta.persistence.EntityExistsException;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
@ -38,4 +39,10 @@ public class GlobalExceptionHandler {
|
||||||
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.UNAUTHORIZED);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package de.szut.casino.exceptionHandling.exceptions;
|
package de.szut.casino.exceptionHandling.exceptions;
|
||||||
|
|
||||||
import de.szut.casino.security.service.EmailService;
|
|
||||||
|
|
||||||
public class EmailNotVerifiedException extends Exception {
|
public class EmailNotVerifiedException extends Exception {
|
||||||
public EmailNotVerifiedException() {
|
public EmailNotVerifiedException() {
|
||||||
super("Email not verified");
|
super("Email not verified");
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
package de.szut.casino.exceptionHandling.exceptions;
|
||||||
|
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
|
||||||
|
public class OAuth2AuthenticationProcessingException extends AuthenticationException {
|
||||||
|
public OAuth2AuthenticationProcessingException(String msg) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,13 +38,7 @@ public class LootBoxController {
|
||||||
}
|
}
|
||||||
|
|
||||||
LootBoxEntity lootBox = optionalLootBox.get();
|
LootBoxEntity lootBox = optionalLootBox.get();
|
||||||
|
UserEntity user = userService.getCurrentUser();
|
||||||
Optional<UserEntity> optionalUser = userService.getCurrentUser();
|
|
||||||
if (optionalUser.isEmpty()) {
|
|
||||||
throw new UserNotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
UserEntity user = optionalUser.get();
|
|
||||||
|
|
||||||
if (lootBoxService.hasSufficientBalance(user, lootBox.getPrice())) {
|
if (lootBoxService.hasSufficientBalance(user, lootBox.getPrice())) {
|
||||||
throw new InsufficientFundsException();
|
throw new InsufficientFundsException();
|
||||||
|
|
|
@ -25,7 +25,7 @@ public class RewardEntity {
|
||||||
@GeneratedValue
|
@GeneratedValue
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@Column(precision = 19, scale = 2)
|
@Column(precision = 19, scale = 2, name = "rewardValue")
|
||||||
private BigDecimal value;
|
private BigDecimal value;
|
||||||
|
|
||||||
@Column(precision = 5, scale = 2)
|
@Column(precision = 5, scale = 2)
|
||||||
|
|
|
@ -1,27 +1,29 @@
|
||||||
package de.szut.casino.security;
|
package de.szut.casino.security;
|
||||||
|
|
||||||
import de.szut.casino.exceptionHandling.ErrorDetails;
|
|
||||||
import de.szut.casino.exceptionHandling.exceptions.EmailNotVerifiedException;
|
import de.szut.casino.exceptionHandling.exceptions.EmailNotVerifiedException;
|
||||||
import de.szut.casino.security.dto.AuthResponseDto;
|
import de.szut.casino.security.dto.AuthResponseDto;
|
||||||
import de.szut.casino.security.dto.LoginRequestDto;
|
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.security.service.AuthService;
|
||||||
import de.szut.casino.user.dto.CreateUserDto;
|
import de.szut.casino.user.dto.CreateUserDto;
|
||||||
import de.szut.casino.user.dto.GetUserDto;
|
import de.szut.casino.user.dto.GetUserDto;
|
||||||
import jakarta.mail.MessagingException;
|
import jakarta.mail.MessagingException;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/auth")
|
@RequestMapping("/auth")
|
||||||
public class AuthController {
|
public class AuthController {
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private AuthService authService;
|
private final AuthService authService;
|
||||||
|
|
||||||
|
public AuthController(AuthService authService) {
|
||||||
|
this.authService = authService;
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public ResponseEntity<AuthResponseDto> authenticateUser(@Valid @RequestBody LoginRequestDto loginRequest) throws EmailNotVerifiedException {
|
public ResponseEntity<AuthResponseDto> authenticateUser(@Valid @RequestBody LoginRequestDto loginRequest) throws EmailNotVerifiedException {
|
||||||
|
@ -37,10 +39,22 @@ public class AuthController {
|
||||||
|
|
||||||
@PostMapping("/verify")
|
@PostMapping("/verify")
|
||||||
public ResponseEntity<Void> verifyEmail(@RequestParam("token") String token) throws MessagingException, IOException {
|
public ResponseEntity<Void> verifyEmail(@RequestParam("token") String token) throws MessagingException, IOException {
|
||||||
if (authService.verifyEmail(token)) {
|
if (authService.verifyEmail(token)) {
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok().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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,6 @@ public class CorsFilter implements Filter {
|
||||||
HttpServletResponse response = (HttpServletResponse) res;
|
HttpServletResponse response = (HttpServletResponse) res;
|
||||||
HttpServletRequest request = (HttpServletRequest) req;
|
HttpServletRequest request = (HttpServletRequest) req;
|
||||||
|
|
||||||
// Allow requests from the frontend
|
|
||||||
response.setHeader("Access-Control-Allow-Origin", frontendHost);
|
response.setHeader("Access-Control-Allow-Origin", frontendHost);
|
||||||
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
|
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
|
||||||
response.setHeader("Access-Control-Allow-Headers", "*");
|
response.setHeader("Access-Control-Allow-Headers", "*");
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package de.szut.casino.security;
|
package de.szut.casino.security;
|
||||||
|
|
||||||
import de.szut.casino.security.jwt.JwtAuthenticationFilter;
|
import de.szut.casino.security.jwt.JwtAuthenticationFilter;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
@ -32,11 +31,14 @@ public class SecurityConfig {
|
||||||
@Value("${app.frontend-host}")
|
@Value("${app.frontend-host}")
|
||||||
private String frontendHost;
|
private String frontendHost;
|
||||||
|
|
||||||
@Autowired
|
private final UserDetailsService userDetailsService;
|
||||||
private UserDetailsService userDetailsService;
|
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||||
|
|
||||||
|
public SecurityConfig(UserDetailsService userDetailsService, JwtAuthenticationFilter jwtAuthenticationFilter) {
|
||||||
|
this.userDetailsService = userDetailsService;
|
||||||
|
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
|
||||||
|
}
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private JwtAuthenticationFilter jwtAuthenticationFilter;
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public DaoAuthenticationProvider authenticationProvider() {
|
public DaoAuthenticationProvider authenticationProvider() {
|
||||||
|
@ -61,16 +63,16 @@ public class SecurityConfig {
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
http
|
http
|
||||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||||
.csrf(csrf -> csrf.disable())
|
.csrf(csrf -> csrf.disable())
|
||||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.authorizeHttpRequests(auth -> {
|
.authorizeHttpRequests(auth -> {
|
||||||
auth.requestMatchers("/auth/**", "/webhook", "/swagger/**", "/swagger-ui/**", "/health", "/error").permitAll()
|
auth.requestMatchers("/auth/**", "/webhook", "/swagger/**", "/swagger-ui/**", "/health", "/error", "/oauth2/**").permitAll()
|
||||||
.requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll()
|
.requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll()
|
||||||
.anyRequest().authenticated();
|
.anyRequest().authenticated();
|
||||||
})
|
})
|
||||||
.authenticationProvider(authenticationProvider())
|
.authenticationProvider(authenticationProvider())
|
||||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
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;
|
||||||
|
}
|
|
@ -4,7 +4,6 @@ import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
@ -19,11 +18,13 @@ import java.io.IOException;
|
||||||
@Component
|
@Component
|
||||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
@Autowired
|
private final JwtUtils jwtUtils;
|
||||||
private JwtUtils jwtUtils;
|
private final UserDetailsService userDetailsService;
|
||||||
|
|
||||||
@Autowired
|
public JwtAuthenticationFilter(JwtUtils jwtUtils, UserDetailsService userDetailsService) {
|
||||||
private UserDetailsService userDetailsService;
|
this.jwtUtils = jwtUtils;
|
||||||
|
this.userDetailsService = userDetailsService;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
package de.szut.casino.security.jwt;
|
package de.szut.casino.security.jwt;
|
||||||
|
|
||||||
|
import de.szut.casino.security.oauth2.UserPrincipal;
|
||||||
import io.jsonwebtoken.Claims;
|
import io.jsonwebtoken.Claims;
|
||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
import io.jsonwebtoken.SignatureAlgorithm;
|
import io.jsonwebtoken.SignatureAlgorithm;
|
||||||
import io.jsonwebtoken.security.Keys;
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
|
@ -17,6 +21,7 @@ import java.util.function.Function;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class JwtUtils {
|
public class JwtUtils {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);
|
||||||
|
|
||||||
@Value("${jwt.secret}")
|
@Value("${jwt.secret}")
|
||||||
private String jwtSecret;
|
private String jwtSecret;
|
||||||
|
@ -29,8 +34,26 @@ public class JwtUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String generateToken(Authentication authentication) {
|
public String generateToken(Authentication authentication) {
|
||||||
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
|
String subject = null;
|
||||||
return generateToken(userDetails.getUsername());
|
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) {
|
public String generateToken(String username) {
|
||||||
|
@ -40,6 +63,9 @@ public class JwtUtils {
|
||||||
|
|
||||||
private String createToken(Map<String, Object> claims, String subject) {
|
private String createToken(Map<String, Object> claims, String subject) {
|
||||||
Date now = new Date();
|
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);
|
Date expiryDate = new Date(now.getTime() + jwtExpirationMs);
|
||||||
|
|
||||||
return Jwts.builder()
|
return Jwts.builder()
|
||||||
|
@ -65,7 +91,7 @@ public class JwtUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Claims extractAllClaims(String token) {
|
private Claims extractAllClaims(String token) {
|
||||||
return Jwts.parserBuilder()
|
return Jwts.parser()
|
||||||
.setSigningKey(getSigningKey())
|
.setSigningKey(getSigningKey())
|
||||||
.build()
|
.build()
|
||||||
.parseClaimsJws(token)
|
.parseClaimsJws(token)
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
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();
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,172 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package de.szut.casino.security.oauth2.github;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class GithubCallbackDto {
|
||||||
|
private String code;
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,176 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,17 +3,19 @@ package de.szut.casino.security.service;
|
||||||
import de.szut.casino.exceptionHandling.exceptions.EmailNotVerifiedException;
|
import de.szut.casino.exceptionHandling.exceptions.EmailNotVerifiedException;
|
||||||
import de.szut.casino.security.dto.AuthResponseDto;
|
import de.szut.casino.security.dto.AuthResponseDto;
|
||||||
import de.szut.casino.security.dto.LoginRequestDto;
|
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.security.jwt.JwtUtils;
|
||||||
import de.szut.casino.user.UserEntity;
|
import de.szut.casino.user.UserEntity;
|
||||||
import de.szut.casino.user.UserService;
|
import de.szut.casino.user.UserService;
|
||||||
import de.szut.casino.user.dto.CreateUserDto;
|
import de.szut.casino.user.dto.CreateUserDto;
|
||||||
import de.szut.casino.user.dto.GetUserDto;
|
import de.szut.casino.user.dto.GetUserDto;
|
||||||
import jakarta.mail.MessagingException;
|
import jakarta.mail.MessagingException;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -22,17 +24,19 @@ import java.util.Optional;
|
||||||
@Service
|
@Service
|
||||||
public class AuthService {
|
public class AuthService {
|
||||||
|
|
||||||
@Autowired
|
private final AuthenticationManager authenticationManager;
|
||||||
private AuthenticationManager authenticationManager;
|
private final JwtUtils jwtUtils;
|
||||||
|
private final UserService userService;
|
||||||
|
private final EmailService emailService;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
@Autowired
|
public AuthService(AuthenticationManager authenticationManager, JwtUtils jwtUtils, UserService userService, EmailService emailService, PasswordEncoder passwordEncoder) {
|
||||||
private JwtUtils jwtUtils;
|
this.authenticationManager = authenticationManager;
|
||||||
|
this.jwtUtils = jwtUtils;
|
||||||
@Autowired
|
this.userService = userService;
|
||||||
private UserService userService;
|
this.emailService = emailService;
|
||||||
|
this.passwordEncoder = passwordEncoder;
|
||||||
@Autowired
|
}
|
||||||
private EmailService emailService;
|
|
||||||
|
|
||||||
public AuthResponseDto login(LoginRequestDto loginRequest) throws EmailNotVerifiedException {
|
public AuthResponseDto login(LoginRequestDto loginRequest) throws EmailNotVerifiedException {
|
||||||
if (!userService.isVerified(loginRequest.getUsernameOrEmail())) {
|
if (!userService.isVerified(loginRequest.getUsernameOrEmail())) {
|
||||||
|
@ -66,7 +70,7 @@ public class AuthService {
|
||||||
public Boolean verifyEmail(String token) throws MessagingException, IOException {
|
public Boolean verifyEmail(String token) throws MessagingException, IOException {
|
||||||
Optional<UserEntity> optionalUser = userService.getUserByVerificationToken(token);
|
Optional<UserEntity> optionalUser = userService.getUserByVerificationToken(token);
|
||||||
|
|
||||||
if(!optionalUser.isPresent()) {
|
if (!optionalUser.isPresent()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,4 +83,26 @@ public class AuthService {
|
||||||
|
|
||||||
return true;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ public class EmailService {
|
||||||
this.mailConfig = mailConfig;
|
this.mailConfig = mailConfig;
|
||||||
this.mailSender.setHost(mailConfig.host);
|
this.mailSender.setHost(mailConfig.host);
|
||||||
this.mailSender.setPort(mailConfig.port);
|
this.mailSender.setPort(mailConfig.port);
|
||||||
|
this.mailSender.setProtocol(mailConfig.protocol);
|
||||||
if (mailConfig.authenticationEnabled) {
|
if (mailConfig.authenticationEnabled) {
|
||||||
this.mailSender.setUsername(mailConfig.username);
|
this.mailSender.setUsername(mailConfig.username);
|
||||||
this.mailSender.setPassword(mailConfig.password);
|
this.mailSender.setPassword(mailConfig.password);
|
||||||
|
@ -87,6 +88,24 @@ public class EmailService {
|
||||||
mailSender.send(message);
|
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 {
|
private String loadTemplate(String templatePath) throws IOException {
|
||||||
ClassPathResource resource = new ClassPathResource("templates/" + templatePath);
|
ClassPathResource resource = new ClassPathResource("templates/" + templatePath);
|
||||||
try (Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) {
|
try (Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) {
|
||||||
|
|
|
@ -22,4 +22,7 @@ public class MailConfig {
|
||||||
|
|
||||||
@Value("${app.mail.from-address}")
|
@Value("${app.mail.from-address}")
|
||||||
public String fromAddress;
|
public String fromAddress;
|
||||||
|
|
||||||
|
@Value("${app.mail.protocol}")
|
||||||
|
public String protocol;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package de.szut.casino.security.service;
|
||||||
|
|
||||||
import de.szut.casino.user.UserEntity;
|
import de.szut.casino.user.UserEntity;
|
||||||
import de.szut.casino.user.UserRepository;
|
import de.szut.casino.user.UserRepository;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
|
@ -14,8 +13,11 @@ import java.util.Optional;
|
||||||
@Service
|
@Service
|
||||||
public class UserDetailsServiceImpl implements UserDetailsService {
|
public class UserDetailsServiceImpl implements UserDetailsService {
|
||||||
|
|
||||||
@Autowired
|
private final UserRepository userRepository;
|
||||||
private UserRepository userRepository;
|
|
||||||
|
public UserDetailsServiceImpl(UserRepository userRepository) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException {
|
public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Positive;
|
import jakarta.validation.constraints.Positive;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
@ -11,6 +12,7 @@ import java.math.BigDecimal;
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
public class BetDto {
|
public class BetDto {
|
||||||
@NotNull(message = "Bet amount cannot be null")
|
@NotNull(message = "Bet amount cannot be null")
|
||||||
@Positive(message = "Bet amount must be positive")
|
@Positive(message = "Bet amount must be positive")
|
||||||
|
|
|
@ -32,13 +32,7 @@ public class SlotController {
|
||||||
|
|
||||||
@PostMapping("/slots/spin")
|
@PostMapping("/slots/spin")
|
||||||
public ResponseEntity<Object> spinSlots(@RequestBody @Valid BetDto betDto) {
|
public ResponseEntity<Object> spinSlots(@RequestBody @Valid BetDto betDto) {
|
||||||
Optional<UserEntity> optionalUser = userService.getCurrentUser();
|
UserEntity user = userService.getCurrentUser();
|
||||||
|
|
||||||
if (optionalUser.isEmpty()) {
|
|
||||||
throw new UserNotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
UserEntity user = optionalUser.get();
|
|
||||||
|
|
||||||
if (!this.balanceService.hasFunds(user, betDto)) {
|
if (!this.balanceService.hasFunds(user, betDto)) {
|
||||||
throw new InsufficientFundsException();
|
throw new InsufficientFundsException();
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
package de.szut.casino.user;
|
||||||
|
|
||||||
|
public enum AuthProvider {
|
||||||
|
LOCAL,
|
||||||
|
GITHUB,
|
||||||
|
GOOGLE
|
||||||
|
}
|
|
@ -2,7 +2,6 @@ package de.szut.casino.user;
|
||||||
|
|
||||||
import de.szut.casino.user.dto.GetUserDto;
|
import de.szut.casino.user.dto.GetUserDto;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
@ -15,14 +14,17 @@ import org.springframework.web.bind.annotation.RestController;
|
||||||
@RequestMapping("/users")
|
@RequestMapping("/users")
|
||||||
public class UserController {
|
public class UserController {
|
||||||
|
|
||||||
@Autowired
|
private final UserService userService;
|
||||||
private UserService userService;
|
|
||||||
|
|
||||||
@Autowired
|
private final UserMappingService userMappingService;
|
||||||
private UserMappingService userMappingService;
|
|
||||||
|
public UserController(UserService userService, UserMappingService userMappingService) {
|
||||||
|
this.userService = userService;
|
||||||
|
this.userMappingService = userMappingService;
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/me")
|
@GetMapping("/me")
|
||||||
public ResponseEntity<GetUserDto> getCurrentUser() {
|
public ResponseEntity<GetUserDto> getCurrentUser() {
|
||||||
return ResponseEntity.ok(userMappingService.mapToGetUserDto(userService.getCurrentUser().orElseThrow()));
|
return ResponseEntity.ok(userMappingService.mapToGetUserDto(userService.getCurrentUser()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
package de.szut.casino.user;
|
package de.szut.casino.user;
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.*;
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.GeneratedValue;
|
|
||||||
import jakarta.persistence.Id;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
@ -19,6 +16,9 @@ public class UserEntity {
|
||||||
@GeneratedValue
|
@GeneratedValue
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Version
|
||||||
|
private Long version;
|
||||||
|
|
||||||
@Column(unique = true)
|
@Column(unique = true)
|
||||||
private String email;
|
private String email;
|
||||||
|
|
||||||
|
@ -34,6 +34,13 @@ public class UserEntity {
|
||||||
|
|
||||||
private String verificationToken;
|
private String verificationToken;
|
||||||
|
|
||||||
|
private String passwordResetToken;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private AuthProvider provider = AuthProvider.LOCAL;
|
||||||
|
|
||||||
|
private String providerId;
|
||||||
|
|
||||||
public UserEntity(String email, String username, String password, BigDecimal balance, String verificationToken) {
|
public UserEntity(String email, String username, String password, BigDecimal balance, String verificationToken) {
|
||||||
this.email = email;
|
this.email = email;
|
||||||
this.username = username;
|
this.username = username;
|
||||||
|
@ -42,6 +49,15 @@ public class UserEntity {
|
||||||
this.verificationToken = verificationToken;
|
this.verificationToken = verificationToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UserEntity(String email, String username, AuthProvider provider, String providerId, BigDecimal balance) {
|
||||||
|
this.email = email;
|
||||||
|
this.username = username;
|
||||||
|
this.provider = provider;
|
||||||
|
this.providerId = providerId;
|
||||||
|
this.balance = balance;
|
||||||
|
this.emailVerified = true; // OAuth providers verify emails
|
||||||
|
}
|
||||||
|
|
||||||
public void addBalance(BigDecimal amountToAdd) {
|
public void addBalance(BigDecimal amountToAdd) {
|
||||||
if (amountToAdd == null || amountToAdd.compareTo(BigDecimal.ZERO) <= 0) {
|
if (amountToAdd == null || amountToAdd.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -12,6 +12,8 @@ public interface UserRepository extends JpaRepository<UserEntity, Long> {
|
||||||
|
|
||||||
Optional<UserEntity> findByEmail(String email);
|
Optional<UserEntity> findByEmail(String email);
|
||||||
|
|
||||||
|
Optional<UserEntity> findByProviderId(String providerId);
|
||||||
|
|
||||||
boolean existsByUsername(String username);
|
boolean existsByUsername(String username);
|
||||||
|
|
||||||
boolean existsByEmail(String email);
|
boolean existsByEmail(String email);
|
||||||
|
@ -21,4 +23,7 @@ public interface UserRepository extends JpaRepository<UserEntity, Long> {
|
||||||
|
|
||||||
@Query("SELECT u FROM UserEntity u WHERE u.username = ?1 OR u.email = ?1")
|
@Query("SELECT u FROM UserEntity u WHERE u.username = ?1 OR u.email = ?1")
|
||||||
Optional<UserEntity> findOneByUsernameOrEmail(String usernameOrEmail);
|
Optional<UserEntity> findOneByUsernameOrEmail(String usernameOrEmail);
|
||||||
|
|
||||||
|
@Query("SELECT u FROM UserEntity u WHERE u.passwordResetToken = ?1")
|
||||||
|
Optional<UserEntity> findOneByPasswordResetToken(String token);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package de.szut.casino.user;
|
package de.szut.casino.user;
|
||||||
|
|
||||||
|
import de.szut.casino.deposit.TransactionEntity;
|
||||||
|
import de.szut.casino.deposit.TransactionStatus;
|
||||||
|
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
|
||||||
import de.szut.casino.user.dto.CreateUserDto;
|
import de.szut.casino.user.dto.CreateUserDto;
|
||||||
import jakarta.persistence.EntityExistsException;
|
import jakarta.persistence.EntityExistsException;
|
||||||
import org.apache.commons.lang3.RandomStringUtils;
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
@ -13,11 +15,13 @@ import java.util.Optional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class UserService {
|
public class UserService {
|
||||||
@Autowired
|
private final UserRepository userRepository;
|
||||||
private UserRepository userRepository;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
@Autowired
|
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
|
||||||
private PasswordEncoder passwordEncoder;
|
this.userRepository = userRepository;
|
||||||
|
this.passwordEncoder = passwordEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
public UserEntity createUser(CreateUserDto createUserDto) {
|
public UserEntity createUser(CreateUserDto createUserDto) {
|
||||||
if (userRepository.existsByUsername(createUserDto.getUsername())) {
|
if (userRepository.existsByUsername(createUserDto.getUsername())) {
|
||||||
|
@ -29,20 +33,31 @@ public class UserService {
|
||||||
}
|
}
|
||||||
|
|
||||||
UserEntity user = new UserEntity(
|
UserEntity user = new UserEntity(
|
||||||
createUserDto.getEmail(),
|
createUserDto.getEmail(),
|
||||||
createUserDto.getUsername(),
|
createUserDto.getUsername(),
|
||||||
passwordEncoder.encode(createUserDto.getPassword()),
|
passwordEncoder.encode(createUserDto.getPassword()),
|
||||||
BigDecimal.valueOf(100),
|
BigDecimal.valueOf(100),
|
||||||
RandomStringUtils.randomAlphanumeric(64)
|
RandomStringUtils.randomAlphanumeric(64)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TransactionEntity transaction = new TransactionEntity();
|
||||||
|
transaction.setAmount(100L);
|
||||||
|
transaction.setUser(user);
|
||||||
|
transaction.setSessionId("signup_bonus");
|
||||||
|
transaction.setStatus(TransactionStatus.SUCCEEDED);
|
||||||
|
|
||||||
return userRepository.save(user);
|
return userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<UserEntity> getCurrentUser() {
|
public UserEntity getCurrentUser() {
|
||||||
String username = SecurityContextHolder.getContext().getAuthentication().getName();
|
String username = SecurityContextHolder.getContext().getAuthentication().getName();
|
||||||
|
|
||||||
return userRepository.findByUsername(username);
|
Optional<UserEntity> optionalUser = userRepository.findByUsername(username);
|
||||||
|
if (optionalUser.isEmpty()) {
|
||||||
|
throw new UserNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return optionalUser.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<UserEntity> getUserByVerificationToken(String token) {
|
public Optional<UserEntity> getUserByVerificationToken(String token) {
|
||||||
|
@ -50,7 +65,7 @@ public class UserService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveUser(UserEntity user) {
|
public void saveUser(UserEntity user) {
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isVerified(String usernameOrEmail) {
|
public boolean isVerified(String usernameOrEmail) {
|
||||||
|
@ -62,4 +77,12 @@ public class UserService {
|
||||||
|
|
||||||
return optionalUser.get().getEmailVerified();
|
return optionalUser.get().getEmailVerified();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<UserEntity> getUserByEmail(String email) {
|
||||||
|
return userRepository.findByEmail(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<UserEntity> getUserByPasswordResetToken(String token) {
|
||||||
|
return this.userRepository.findOneByPasswordResetToken(token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,31 +6,29 @@ import de.szut.casino.user.UserEntity;
|
||||||
import de.szut.casino.user.UserService;
|
import de.szut.casino.user.UserService;
|
||||||
import de.szut.casino.user.transaction.dto.GetTransactionDto;
|
import de.szut.casino.user.transaction.dto.GetTransactionDto;
|
||||||
import de.szut.casino.user.transaction.dto.UserTransactionsDto;
|
import de.szut.casino.user.transaction.dto.UserTransactionsDto;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class GetTransactionService {
|
public class GetTransactionService {
|
||||||
|
|
||||||
@Autowired
|
private final UserService userService;
|
||||||
private UserService userService;
|
|
||||||
|
|
||||||
@Autowired
|
private final TransactionRepository transactionRepository;
|
||||||
private TransactionRepository transactionRepository;
|
|
||||||
|
|
||||||
public UserTransactionsDto getUserTransactionsDto(String authToken, Integer limit, Integer offset) {
|
public GetTransactionService(UserService userService, TransactionRepository transactionRepository) {
|
||||||
Optional<UserEntity> user = this.userService.getCurrentUser();
|
this.userService = userService;
|
||||||
if (user.isPresent()) {
|
this.transactionRepository = transactionRepository;
|
||||||
List<TransactionEntity> transactionEntities = this.transactionRepository.findByUserIdWithLimit(user.get(), limit, offset);
|
}
|
||||||
Boolean hasMore = this.transactionRepository.hasMore(user.get(), limit, offset);
|
|
||||||
|
|
||||||
return new UserTransactionsDto(mapTransactionsToDtos(transactionEntities), hasMore);
|
public UserTransactionsDto getUserTransactionsDto(Integer limit, Integer offset) {
|
||||||
}
|
UserEntity user = userService.getCurrentUser();
|
||||||
|
|
||||||
return new UserTransactionsDto(List.of(), false);
|
List<TransactionEntity> transactionEntities = this.transactionRepository.findByUserIdWithLimit(user, limit, offset);
|
||||||
|
Boolean hasMore = this.transactionRepository.hasMore(user, limit, offset);
|
||||||
|
|
||||||
|
return new UserTransactionsDto(mapTransactionsToDtos(transactionEntities), hasMore);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<GetTransactionDto> mapTransactionsToDtos(List<TransactionEntity> transactions) {
|
public List<GetTransactionDto> mapTransactionsToDtos(List<TransactionEntity> transactions) {
|
||||||
|
|
|
@ -1,26 +1,26 @@
|
||||||
package de.szut.casino.user.transaction;
|
package de.szut.casino.user.transaction;
|
||||||
|
|
||||||
import de.szut.casino.user.transaction.dto.UserTransactionsDto;
|
import de.szut.casino.user.transaction.dto.UserTransactionsDto;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestHeader;
|
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
public class TransactionController {
|
public class TransactionController {
|
||||||
|
|
||||||
@Autowired
|
private final GetTransactionService transactionService;
|
||||||
private GetTransactionService transactionService;
|
|
||||||
|
public TransactionController(GetTransactionService transactionService) {
|
||||||
|
this.transactionService = transactionService;
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/user/transactions")
|
@GetMapping("/user/transactions")
|
||||||
public ResponseEntity<UserTransactionsDto> getUserTransactions(
|
public ResponseEntity<UserTransactionsDto> getUserTransactions(
|
||||||
@RequestHeader("Authorization") String authToken,
|
|
||||||
@RequestParam(value = "limit", required = false) Integer limit,
|
@RequestParam(value = "limit", required = false) Integer limit,
|
||||||
@RequestParam(value = "offset", required = false) Integer offset
|
@RequestParam(value = "offset", required = false) Integer offset
|
||||||
) {
|
) {
|
||||||
UserTransactionsDto transactionEntities = this.transactionService.getUserTransactionsDto(authToken, limit, offset);
|
UserTransactionsDto transactionEntities = this.transactionService.getUserTransactionsDto(limit, offset);
|
||||||
|
|
||||||
return ResponseEntity.ok(transactionEntities);
|
return ResponseEntity.ok(transactionEntities);
|
||||||
}
|
}
|
||||||
|
|
58
backend/src/main/resources/application-inmemory.properties
Normal file
58
backend/src/main/resources/application-inmemory.properties
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
|
||||||
|
spring.datasource.driverClassName=org.h2.Driver
|
||||||
|
spring.datasource.username=sa
|
||||||
|
spring.datasource.password=
|
||||||
|
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
||||||
|
|
||||||
|
spring.jpa.hibernate.ddl-auto=create-drop
|
||||||
|
|
||||||
|
server.port=${HTTP_PORT:8080}
|
||||||
|
stripe.secret.key=${STRIPE_SECRET_KEY:sk_test_51QrePYIvCfqz7ANgqam8rEwWcMeKiLOof3j6SCMgu2sl4sESP45DJxca16mWcYo1sQaiBv32CMR6Z4AAAGQPCJo300ubuZKO8I}
|
||||||
|
stripe.webhook.secret=${STRIPE_WEBHOOK_SECRET:whsec_746b6a488665f6057118bdb4a2b32f4916f16c277109eeaed5e8f8e8b81b8c15}
|
||||||
|
|
||||||
|
app.frontend-host=${FE_URL:http://localhost:4200}
|
||||||
|
|
||||||
|
app.mail.authentication=${MAIL_AUTHENTICATION:false}
|
||||||
|
app.mail.host=${MAIL_HOST:localhost}
|
||||||
|
app.mail.port=${MAIL_PORT:1025}
|
||||||
|
app.mail.username=${MAIL_USER:null}
|
||||||
|
app.mail.password=${MAIL_PASS:null}
|
||||||
|
app.mail.from-address=${MAIL_FROM:casino@localhost}
|
||||||
|
app.mail.protocol=${MAIL_PROTOCOL:smtp}
|
||||||
|
|
||||||
|
spring.application.name=casino
|
||||||
|
|
||||||
|
# JWT Configuration
|
||||||
|
jwt.secret=${JWT_SECRET:5367566B59703373367639792F423F4528482B4D6251655468576D5A71347437}
|
||||||
|
jwt.expiration.ms=${JWT_EXPIRATION_MS:86400000}
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
logging.level.org.springframework.security=DEBUG
|
||||||
|
|
||||||
|
# Swagger
|
||||||
|
springdoc.swagger-ui.path=swagger
|
||||||
|
springdoc.swagger-ui.try-it-out-enabled=true
|
||||||
|
|
||||||
|
# GitHub OAuth2 Configuration
|
||||||
|
spring.security.oauth2.client.registration.github.client-id=${GITHUB_CLIENT_ID:Ov23lingzZsPn1wwACoK}
|
||||||
|
spring.security.oauth2.client.registration.github.client-secret=${GITHUB_CLIENT_SECRET:4b327fb3b1ab67584a03bcb9d53fa6439fbccad7}
|
||||||
|
spring.security.oauth2.client.registration.github.redirect-uri=${app.frontend-host}/oauth2/callback/github
|
||||||
|
spring.security.oauth2.client.registration.github.scope=user:email,read:user
|
||||||
|
spring.security.oauth2.client.provider.github.authorization-uri=https://github.com/login/oauth/authorize
|
||||||
|
spring.security.oauth2.client.provider.github.token-uri=https://github.com/login/oauth/access_token
|
||||||
|
spring.security.oauth2.client.provider.github.user-info-uri=https://api.github.com/user
|
||||||
|
spring.security.oauth2.client.provider.github.user-name-attribute=login
|
||||||
|
|
||||||
|
# OAuth Success and Failure URLs
|
||||||
|
app.oauth2.authorizedRedirectUris=${app.frontend-host}/auth/oauth2/callback
|
||||||
|
|
||||||
|
# Google OAuth2 Configuration
|
||||||
|
spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID:350791038883-c1r7v4o793itq8a0rh7dut7itm7uneam.apps.googleusercontent.com}
|
||||||
|
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET: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}
|
||||||
|
spring.security.oauth2.client.registration.google.redirect-uri=${app.frontend-host}/oauth2/callback/google
|
||||||
|
spring.security.oauth2.client.registration.google.scope=email,profile
|
||||||
|
spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/v2/auth
|
||||||
|
spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token
|
||||||
|
spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo
|
||||||
|
spring.security.oauth2.client.provider.google.user-name-attribute=sub
|
||||||
|
|
|
@ -14,6 +14,7 @@ app.mail.port=${MAIL_PORT:1025}
|
||||||
app.mail.username=${MAIL_USER:null}
|
app.mail.username=${MAIL_USER:null}
|
||||||
app.mail.password=${MAIL_PASS:null}
|
app.mail.password=${MAIL_PASS:null}
|
||||||
app.mail.from-address=${MAIL_FROM:casino@localhost}
|
app.mail.from-address=${MAIL_FROM:casino@localhost}
|
||||||
|
app.mail.protocol=${MAIL_PROTOCOL:smtp}
|
||||||
|
|
||||||
spring.application.name=casino
|
spring.application.name=casino
|
||||||
|
|
||||||
|
@ -28,3 +29,26 @@ logging.level.org.springframework.security=DEBUG
|
||||||
springdoc.swagger-ui.path=swagger
|
springdoc.swagger-ui.path=swagger
|
||||||
springdoc.swagger-ui.try-it-out-enabled=true
|
springdoc.swagger-ui.try-it-out-enabled=true
|
||||||
|
|
||||||
|
# GitHub OAuth2 Configuration
|
||||||
|
spring.security.oauth2.client.registration.github.client-id=${GITHUB_CLIENT_ID:Ov23lingzZsPn1wwACoK}
|
||||||
|
spring.security.oauth2.client.registration.github.client-secret=${GITHUB_CLIENT_SECRET:4b327fb3b1ab67584a03bcb9d53fa6439fbccad7}
|
||||||
|
spring.security.oauth2.client.registration.github.redirect-uri=${app.frontend-host}/oauth2/callback/github
|
||||||
|
spring.security.oauth2.client.registration.github.scope=user:email,read:user
|
||||||
|
spring.security.oauth2.client.provider.github.authorization-uri=https://github.com/login/oauth/authorize
|
||||||
|
spring.security.oauth2.client.provider.github.token-uri=https://github.com/login/oauth/access_token
|
||||||
|
spring.security.oauth2.client.provider.github.user-info-uri=https://api.github.com/user
|
||||||
|
spring.security.oauth2.client.provider.github.user-name-attribute=login
|
||||||
|
|
||||||
|
# OAuth Success and Failure URLs
|
||||||
|
app.oauth2.authorizedRedirectUris=${app.frontend-host}/auth/oauth2/callback
|
||||||
|
|
||||||
|
# Google OAuth2 Configuration
|
||||||
|
spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID:350791038883-c1r7v4o793itq8a0rh7dut7itm7uneam.apps.googleusercontent.com}
|
||||||
|
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET: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}
|
||||||
|
spring.security.oauth2.client.registration.google.redirect-uri=${app.frontend-host}/oauth2/callback/google
|
||||||
|
spring.security.oauth2.client.registration.google.scope=email,profile
|
||||||
|
spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/v2/auth
|
||||||
|
spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token
|
||||||
|
spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo
|
||||||
|
spring.security.oauth2.client.provider.google.user-name-attribute=sub
|
||||||
|
|
||||||
|
|
156
backend/src/main/resources/templates/email/recover-password.html
Normal file
156
backend/src/main/resources/templates/email/recover-password.html
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Passwort zurücksetzen - Trustworthy Casino©</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
color: #64748b;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background-color: #0a1219;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background-color: #1a2835;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
background-color: #121e27;
|
||||||
|
padding: 30px;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
background-color: #1a2835;
|
||||||
|
color: #94a3b8;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #10b981;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 12px 24px;
|
||||||
|
margin: 20px 0;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
.button:hover {
|
||||||
|
background-color: #059669;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
color: #ffffff;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
padding-left: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
li::marker {
|
||||||
|
color: #34d399;
|
||||||
|
}
|
||||||
|
.highlight {
|
||||||
|
color: #10b981;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
background-color: #1a2835;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
.recovery-code {
|
||||||
|
background-color: #1a2835;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
letter-spacing: 5px;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
.info-box {
|
||||||
|
background-color: #1a2835;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
color: #f59e0b;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.security-info {
|
||||||
|
background-color: #1a2835;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>Trustworthy Casino</h1>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h2>Hallo <span class="highlight">${username}</span>,</h2>
|
||||||
|
|
||||||
|
<p>wir haben eine Anfrage zum Zurücksetzen Ihres Passworts für Ihr Trustworthy Casino Konto erhalten. Um Ihr Passwort zurückzusetzen, klicken Sie bitte auf den folgenden Button:</p>
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="${feUrl}/reset-password?token=${resetToken}" class="button">Passwort zurücksetzen</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<p><span class="warning">Hinweis:</span> Dieser Link und Code sind aus Sicherheitsgründen vielleicht nur <span class="highlight">60 Minuten</span> gültig.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="security-info">
|
||||||
|
<p>Falls Sie diese Anfrage nicht gestellt haben, ignorieren Sie diese E-Mail bitte. In diesem Fall empfehlen wir Ihnen, Ihr Passwort zu ändern und unseren Kundenservice zu kontaktieren, um die Sicherheit Ihres Kontos zu gewährleisten.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<p>Bei Fragen steht Ihnen unser Support-Team nicht zur Verfügung.</p>
|
||||||
|
|
||||||
|
<p>Mit freundlichen Grüßen,<br>
|
||||||
|
Ihr <span style="color: #10b981;">Trustworthy Casino</span> Team</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>2025 Trustworthy Casino - Keine Rechte vorbehalten</p>
|
||||||
|
<p>Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht darauf.</p>
|
||||||
|
<p>Für Fragen zur Sicherheit Ihres Kontos kontaktieren Sie uns bitte nicht.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -123,7 +123,7 @@
|
||||||
<p>Klicken Sie auf den folgenden Button, um Ihre E-Mail-Adresse zu bestätigen:</p>
|
<p>Klicken Sie auf den folgenden Button, um Ihre E-Mail-Adresse zu bestätigen:</p>
|
||||||
|
|
||||||
<div style="text-align: center;">
|
<div style="text-align: center;">
|
||||||
<a href="${feUrl}/verify?token=${token}" class="button">E-Mail bestätigen</a>
|
<a href="${feUrl}/verify?email-token=${token}" class="button">E-Mail bestätigen</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
package de.szut.casino;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
|
|
||||||
@SpringBootTest
|
|
||||||
class Lf8StarterApplicationTests {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void contextLoads() {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
package de.szut.casino.coinflip;
|
||||||
|
|
||||||
|
import de.szut.casino.shared.service.BalanceService;
|
||||||
|
import de.szut.casino.user.UserEntity;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
class CoinflipServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private BalanceService balanceService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private Random random;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private CoinflipService coinflipService;
|
||||||
|
|
||||||
|
private UserEntity user;
|
||||||
|
private CoinflipDto coinflipDto;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
MockitoAnnotations.openMocks(this);
|
||||||
|
user = new UserEntity();
|
||||||
|
user.setBalance(BigDecimal.valueOf(100));
|
||||||
|
coinflipDto = new CoinflipDto(BigDecimal.valueOf(10), CoinSide.HEAD);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPlay_userWins() {
|
||||||
|
when(random.nextBoolean()).thenReturn(true);
|
||||||
|
|
||||||
|
CoinflipResult result = coinflipService.play(user, coinflipDto);
|
||||||
|
|
||||||
|
assertTrue(result.isWin());
|
||||||
|
assertEquals(BigDecimal.valueOf(20), result.getPayout());
|
||||||
|
assertEquals(CoinSide.HEAD, result.getCoinSide());
|
||||||
|
verify(balanceService, times(1)).subtractFunds(user, BigDecimal.valueOf(10));
|
||||||
|
verify(balanceService, times(1)).addFunds(user, BigDecimal.valueOf(20));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPlay_userLoses() {
|
||||||
|
when(random.nextBoolean()).thenReturn(false);
|
||||||
|
|
||||||
|
CoinflipResult result = coinflipService.play(user, coinflipDto);
|
||||||
|
|
||||||
|
assertFalse(result.isWin());
|
||||||
|
assertEquals(BigDecimal.ZERO, result.getPayout());
|
||||||
|
assertEquals(CoinSide.TAILS, result.getCoinSide());
|
||||||
|
verify(balanceService, times(1)).subtractFunds(user, BigDecimal.valueOf(10));
|
||||||
|
verify(balanceService, never()).addFunds(any(), any());
|
||||||
|
}
|
||||||
|
}
|
251
backend/src/test/java/de/szut/casino/dice/DiceServiceTest.java
Normal file
251
backend/src/test/java/de/szut/casino/dice/DiceServiceTest.java
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
package de.szut.casino.dice;
|
||||||
|
|
||||||
|
import de.szut.casino.shared.service.BalanceService;
|
||||||
|
import de.szut.casino.user.UserEntity;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class DiceServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private BalanceService balanceService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private Random random;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private DiceService diceService;
|
||||||
|
|
||||||
|
private UserEntity user;
|
||||||
|
private DiceDto diceDto;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
user = new UserEntity();
|
||||||
|
user.setId(1L);
|
||||||
|
user.setBalance(BigDecimal.valueOf(1000));
|
||||||
|
|
||||||
|
diceDto = new DiceDto();
|
||||||
|
diceDto.setBetAmount(BigDecimal.valueOf(10));
|
||||||
|
diceDto.setTargetValue(BigDecimal.valueOf(50));
|
||||||
|
diceDto.setRollOver(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void play_rollOver_win() {
|
||||||
|
diceDto.setRollOver(true);
|
||||||
|
diceDto.setTargetValue(BigDecimal.valueOf(50));
|
||||||
|
when(random.nextInt(anyInt())).thenReturn(55);
|
||||||
|
|
||||||
|
DiceResult result = diceService.play(user, diceDto);
|
||||||
|
|
||||||
|
assertTrue(result.isWin());
|
||||||
|
assertEquals(BigDecimal.valueOf(56), result.getRolledValue());
|
||||||
|
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||||
|
verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void play_rollOver_lose() {
|
||||||
|
diceDto.setRollOver(true);
|
||||||
|
diceDto.setTargetValue(BigDecimal.valueOf(50));
|
||||||
|
when(random.nextInt(anyInt())).thenReturn(49);
|
||||||
|
|
||||||
|
DiceResult result = diceService.play(user, diceDto);
|
||||||
|
|
||||||
|
assertFalse(result.isWin());
|
||||||
|
assertEquals(BigDecimal.valueOf(50), result.getRolledValue());
|
||||||
|
assertEquals(BigDecimal.ZERO, result.getPayout());
|
||||||
|
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||||
|
verify(balanceService, never()).addFunds(eq(user), any(BigDecimal.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void play_rollUnder_win() {
|
||||||
|
diceDto.setRollOver(false);
|
||||||
|
diceDto.setTargetValue(BigDecimal.valueOf(50));
|
||||||
|
when(random.nextInt(anyInt())).thenReturn(48);
|
||||||
|
|
||||||
|
DiceResult result = diceService.play(user, diceDto);
|
||||||
|
|
||||||
|
assertTrue(result.isWin());
|
||||||
|
assertEquals(BigDecimal.valueOf(49), result.getRolledValue());
|
||||||
|
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||||
|
verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void play_rollUnder_lose() {
|
||||||
|
diceDto.setRollOver(false);
|
||||||
|
diceDto.setTargetValue(BigDecimal.valueOf(50));
|
||||||
|
when(random.nextInt(anyInt())).thenReturn(50);
|
||||||
|
|
||||||
|
DiceResult result = diceService.play(user, diceDto);
|
||||||
|
|
||||||
|
assertFalse(result.isWin());
|
||||||
|
assertEquals(BigDecimal.valueOf(51), result.getRolledValue());
|
||||||
|
assertEquals(BigDecimal.ZERO, result.getPayout());
|
||||||
|
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||||
|
verify(balanceService, never()).addFunds(eq(user), any(BigDecimal.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void play_rollOver_targetValueOne_rolledOne_lose() {
|
||||||
|
diceDto.setRollOver(true);
|
||||||
|
diceDto.setTargetValue(BigDecimal.valueOf(1));
|
||||||
|
when(random.nextInt(anyInt())).thenReturn(0);
|
||||||
|
|
||||||
|
DiceResult result = diceService.play(user, diceDto);
|
||||||
|
|
||||||
|
assertFalse(result.isWin());
|
||||||
|
assertEquals(BigDecimal.valueOf(1), result.getRolledValue());
|
||||||
|
assertEquals(BigDecimal.ZERO, result.getPayout());
|
||||||
|
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||||
|
verify(balanceService, never()).addFunds(eq(user), any(BigDecimal.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void play_rollOver_targetValueOne_rolledTwo_win() {
|
||||||
|
diceDto.setRollOver(true);
|
||||||
|
diceDto.setTargetValue(BigDecimal.valueOf(1));
|
||||||
|
when(random.nextInt(anyInt())).thenReturn(1);
|
||||||
|
|
||||||
|
DiceResult result = diceService.play(user, diceDto);
|
||||||
|
|
||||||
|
assertTrue(result.isWin());
|
||||||
|
assertEquals(BigDecimal.valueOf(2), result.getRolledValue());
|
||||||
|
// Win chance for target 1 (roll over) is 99. Multiplier = (100-1)/99 = 1
|
||||||
|
assertEquals(diceDto.getBetAmount().stripTrailingZeros(), result.getPayout().stripTrailingZeros());
|
||||||
|
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||||
|
verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void play_rollUnder_targetValueOne_alwaysLose_winChanceZero() {
|
||||||
|
diceDto.setRollOver(false);
|
||||||
|
diceDto.setTargetValue(BigDecimal.valueOf(1));
|
||||||
|
when(random.nextInt(anyInt())).thenReturn(0);
|
||||||
|
|
||||||
|
DiceResult result = diceService.play(user, diceDto);
|
||||||
|
|
||||||
|
assertFalse(result.isWin());
|
||||||
|
assertEquals(BigDecimal.valueOf(1), result.getRolledValue());
|
||||||
|
assertEquals(BigDecimal.ZERO, result.getPayout());
|
||||||
|
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||||
|
verify(balanceService, never()).addFunds(eq(user), any(BigDecimal.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void play_rollOver_targetValueNinetyNine_rolledHundred_win() {
|
||||||
|
diceDto.setRollOver(true);
|
||||||
|
diceDto.setTargetValue(BigDecimal.valueOf(99));
|
||||||
|
when(random.nextInt(anyInt())).thenReturn(99);
|
||||||
|
|
||||||
|
DiceResult result = diceService.play(user, diceDto);
|
||||||
|
|
||||||
|
assertTrue(result.isWin());
|
||||||
|
assertEquals(BigDecimal.valueOf(100), result.getRolledValue());
|
||||||
|
// Win chance for target 99 (roll over) is 1. Multiplier = (100-1)/1 = 99
|
||||||
|
assertEquals(diceDto.getBetAmount().multiply(BigDecimal.valueOf(99)).stripTrailingZeros(), result.getPayout().stripTrailingZeros());
|
||||||
|
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||||
|
verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void play_rollUnder_targetValueNinetyNine_rolledNinetyEight_win() {
|
||||||
|
diceDto.setRollOver(false);
|
||||||
|
diceDto.setTargetValue(BigDecimal.valueOf(99));
|
||||||
|
when(random.nextInt(anyInt())).thenReturn(97);
|
||||||
|
|
||||||
|
DiceResult result = diceService.play(user, diceDto);
|
||||||
|
|
||||||
|
assertTrue(result.isWin());
|
||||||
|
assertEquals(BigDecimal.valueOf(98), result.getRolledValue());
|
||||||
|
// Win chance for target 99 (roll under) is 98. Multiplier = (100-1)/98 = 99/98
|
||||||
|
assertEquals(diceDto.getBetAmount().multiply(BigDecimal.valueOf(99).divide(BigDecimal.valueOf(98), 4, RoundingMode.HALF_UP)), result.getPayout());
|
||||||
|
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||||
|
verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void play_rollOver_targetValueOneHundred_alwaysLose_winChanceZero() {
|
||||||
|
diceDto.setRollOver(true);
|
||||||
|
diceDto.setTargetValue(BigDecimal.valueOf(100));
|
||||||
|
when(random.nextInt(anyInt())).thenReturn(99);
|
||||||
|
|
||||||
|
DiceResult result = diceService.play(user, diceDto);
|
||||||
|
|
||||||
|
assertFalse(result.isWin());
|
||||||
|
assertEquals(BigDecimal.valueOf(100), result.getRolledValue());
|
||||||
|
assertEquals(BigDecimal.ZERO, result.getPayout());
|
||||||
|
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||||
|
verify(balanceService, never()).addFunds(eq(user), any(BigDecimal.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void play_rollUnder_targetValueOneHundred_rolledNinetyNine_win() {
|
||||||
|
diceDto.setRollOver(false);
|
||||||
|
diceDto.setTargetValue(BigDecimal.valueOf(100));
|
||||||
|
when(random.nextInt(anyInt())).thenReturn(98);
|
||||||
|
|
||||||
|
DiceResult result = diceService.play(user, diceDto);
|
||||||
|
|
||||||
|
assertTrue(result.isWin());
|
||||||
|
assertEquals(BigDecimal.valueOf(99), result.getRolledValue());
|
||||||
|
// Win chance for target 100 (roll under) is 99. Multiplier = (100-1)/99 = 1
|
||||||
|
assertEquals(diceDto.getBetAmount().stripTrailingZeros(), result.getPayout().stripTrailingZeros());
|
||||||
|
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||||
|
verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void play_payoutCalculationCorrect() {
|
||||||
|
diceDto.setRollOver(true);
|
||||||
|
diceDto.setTargetValue(BigDecimal.valueOf(75));
|
||||||
|
when(random.nextInt(anyInt())).thenReturn(75);
|
||||||
|
|
||||||
|
// Multiplier for win chance 25: (100-1)/25 = 99/25 = 3.96
|
||||||
|
// Payout: 10 * 3.96 = 39.6
|
||||||
|
|
||||||
|
DiceResult result = diceService.play(user, diceDto);
|
||||||
|
|
||||||
|
assertTrue(result.isWin());
|
||||||
|
assertEquals(BigDecimal.valueOf(39.6).stripTrailingZeros(), result.getPayout().stripTrailingZeros());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void play_payoutCalculationCorrect_rollUnder() {
|
||||||
|
diceDto.setRollOver(false);
|
||||||
|
diceDto.setTargetValue(BigDecimal.valueOf(25));
|
||||||
|
when(random.nextInt(anyInt())).thenReturn(0);
|
||||||
|
|
||||||
|
// Multiplier for win chance 24: (100-1)/24 = 99/24 = 4.125
|
||||||
|
// Payout: 10 * 4.125 = 41.25
|
||||||
|
|
||||||
|
DiceResult result = diceService.play(user, diceDto);
|
||||||
|
|
||||||
|
assertTrue(result.isWin());
|
||||||
|
assertEquals(BigDecimal.valueOf(41.25).stripTrailingZeros(), result.getPayout().stripTrailingZeros());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void play_betAmountSubtracted() {
|
||||||
|
when(random.nextInt(anyInt())).thenReturn(50);
|
||||||
|
|
||||||
|
diceService.play(user, diceDto);
|
||||||
|
|
||||||
|
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,26 +0,0 @@
|
||||||
package de.szut.casino.health;
|
|
||||||
|
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
|
||||||
|
|
||||||
@WebMvcTest(HealthController.class)
|
|
||||||
@AutoConfigureMockMvc(addFilters = false)
|
|
||||||
public class HealthControllerTest {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MockMvc mockMvc;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void healthCheckReturnsUpStatus() throws Exception {
|
|
||||||
mockMvc.perform(get("/health"))
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath("$.status").value("UP"));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
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.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
class BalanceServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private UserRepository userRepository;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private BalanceService balanceService;
|
||||||
|
|
||||||
|
private UserEntity user;
|
||||||
|
private BetDto betDto;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
MockitoAnnotations.openMocks(this);
|
||||||
|
user = new UserEntity();
|
||||||
|
user.setBalance(BigDecimal.valueOf(100));
|
||||||
|
betDto = new BetDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testHasFunds_sufficientFunds() {
|
||||||
|
betDto.setBetAmount(BigDecimal.valueOf(50));
|
||||||
|
assertTrue(balanceService.hasFunds(user, betDto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testHasFunds_insufficientFunds() {
|
||||||
|
betDto.setBetAmount(BigDecimal.valueOf(150));
|
||||||
|
assertFalse(balanceService.hasFunds(user, betDto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testHasFunds_exactFunds() {
|
||||||
|
betDto.setBetAmount(BigDecimal.valueOf(100));
|
||||||
|
assertTrue(balanceService.hasFunds(user, betDto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAddFunds() {
|
||||||
|
BigDecimal amountToAdd = BigDecimal.valueOf(50);
|
||||||
|
balanceService.addFunds(user, amountToAdd);
|
||||||
|
assertEquals(BigDecimal.valueOf(150), user.getBalance());
|
||||||
|
verify(userRepository, times(1)).save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSubtractFunds_sufficientFunds() {
|
||||||
|
BigDecimal amountToSubtract = BigDecimal.valueOf(50);
|
||||||
|
balanceService.subtractFunds(user, amountToSubtract);
|
||||||
|
assertEquals(BigDecimal.valueOf(50), user.getBalance());
|
||||||
|
verify(userRepository, times(1)).save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSubtractFunds_insufficientFunds() {
|
||||||
|
BigDecimal amountToSubtract = BigDecimal.valueOf(150);
|
||||||
|
assertThrows(IllegalStateException.class, () -> balanceService.subtractFunds(user, amountToSubtract));
|
||||||
|
verify(userRepository, never()).save(user);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,122 +0,0 @@
|
||||||
package de.szut.casino.user;
|
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import de.szut.casino.user.dto.CreateUserDto;
|
|
||||||
import de.szut.casino.user.dto.GetUserDto;
|
|
||||||
|
|
||||||
@WebMvcTest(UserController.class)
|
|
||||||
@AutoConfigureMockMvc(addFilters = false)
|
|
||||||
public class UserControllerTest {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MockMvc mockMvc;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
@MockBean
|
|
||||||
private UserService userService;
|
|
||||||
|
|
||||||
private GetUserDto getUserDto;
|
|
||||||
private CreateUserDto createUserDto;
|
|
||||||
private UserEntity testUser;
|
|
||||||
private final String TEST_ID = "test-id-123";
|
|
||||||
private final String AUTH_TOKEN = "Bearer test-token";
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
getUserDto = new GetUserDto();
|
|
||||||
getUserDto.setAuthentikId(TEST_ID);
|
|
||||||
getUserDto.setUsername("testuser");
|
|
||||||
|
|
||||||
testUser = new UserEntity();
|
|
||||||
testUser.setAuthentikId(TEST_ID);
|
|
||||||
testUser.setUsername("testuser");
|
|
||||||
|
|
||||||
createUserDto = new CreateUserDto();
|
|
||||||
createUserDto.setAuthentikId(TEST_ID);
|
|
||||||
createUserDto.setUsername("testuser");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getUserByIdSuccess() throws Exception {
|
|
||||||
when(userService.exists(TEST_ID)).thenReturn(true);
|
|
||||||
when(userService.getUser(TEST_ID)).thenReturn(getUserDto);
|
|
||||||
|
|
||||||
mockMvc.perform(get("/user/" + TEST_ID))
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath("$.authentikId").value(TEST_ID))
|
|
||||||
.andExpect(jsonPath("$.username").value("testuser"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getUserByIdNotFound() throws Exception {
|
|
||||||
when(userService.exists(TEST_ID)).thenReturn(false);
|
|
||||||
|
|
||||||
mockMvc.perform(get("/user/" + TEST_ID))
|
|
||||||
.andExpect(status().isNotFound());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createUserSuccess() throws Exception {
|
|
||||||
when(userService.exists(TEST_ID)).thenReturn(false);
|
|
||||||
when(userService.createUser(any(CreateUserDto.class))).thenReturn(testUser);
|
|
||||||
|
|
||||||
mockMvc.perform(post("/user")
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.content(objectMapper.writeValueAsString(createUserDto)))
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath("$.authentikId").value(TEST_ID))
|
|
||||||
.andExpect(jsonPath("$.username").value("testuser"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createUserAlreadyExists() throws Exception {
|
|
||||||
when(userService.exists(TEST_ID)).thenReturn(true);
|
|
||||||
|
|
||||||
mockMvc.perform(post("/user")
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.content(objectMapper.writeValueAsString(createUserDto)))
|
|
||||||
.andExpect(status().isFound())
|
|
||||||
.andExpect(header().string("Location", "/user/" + TEST_ID));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getCurrentUserSuccess() throws Exception {
|
|
||||||
when(userService.getCurrentUser(AUTH_TOKEN)).thenReturn(getUserDto);
|
|
||||||
|
|
||||||
mockMvc.perform(get("/user")
|
|
||||||
.header("Authorization", AUTH_TOKEN))
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath("$.authentikId").value(TEST_ID))
|
|
||||||
.andExpect(jsonPath("$.username").value("testuser"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getCurrentUserNotFound() throws Exception {
|
|
||||||
when(userService.getCurrentUser(anyString())).thenReturn(null);
|
|
||||||
|
|
||||||
mockMvc.perform(get("/user")
|
|
||||||
.header("Authorization", AUTH_TOKEN))
|
|
||||||
.andExpect(status().isNotFound());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,12 @@
|
||||||
FROM oven/bun:debian AS build
|
FROM oven/bun:debian AS build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apt-get update -y && apt-get install nodejs -y
|
RUN apt-get update -y && \
|
||||||
|
apt-get install -y --no-install-recommends curl ca-certificates gnupg && \
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
||||||
|
apt-get install -y --no-install-recommends nodejs && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
|
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
|
@ -29,6 +29,9 @@ yarn-error.log
|
||||||
.history/*
|
.history/*
|
||||||
|
|
||||||
# Miscellaneous
|
# Miscellaneous
|
||||||
|
/.claude
|
||||||
|
/test-results
|
||||||
|
/playwright-report
|
||||||
/.angular/cache
|
/.angular/cache
|
||||||
.sass-cache/
|
.sass-cache/
|
||||||
/connect.lock
|
/connect.lock
|
||||||
|
|
|
@ -1,18 +1,106 @@
|
||||||
# Casino Gaming Platform - Frontend
|
# Casino Gaming Platform - Frontend
|
||||||
|
|
||||||
This is the frontend application for the Casino Gaming Platform. It's built with Angular 18 and TailwindCSS, providing a responsive and modern UI for the casino gaming experience.
|
A modern Angular 20 casino gaming platform featuring multiple games including Blackjack, Coinflip, Dice, Slots, and Lootboxes. Built with Angular 20, TailwindCSS 4, and powered by Bun for fast development.
|
||||||
|
|
||||||
## Development
|
## 🎮 Features
|
||||||
|
|
||||||
### Commands
|
- **Multiple Games**: Blackjack, Coinflip, Dice, Slots, Lootboxes
|
||||||
|
- **User Authentication**: OAuth2, email verification, password recovery
|
||||||
|
- **Real-time Gaming**: Interactive game mechanics with animations
|
||||||
|
- **Payment Integration**: Stripe integration for deposits
|
||||||
|
- **Responsive Design**: Mobile-first design with TailwindCSS
|
||||||
|
- **Audio Experience**: Game sounds and audio feedback
|
||||||
|
- **Transaction History**: Complete betting and transaction tracking
|
||||||
|
|
||||||
- **Build**: `bun run build` or `bunx @angular/cli build`
|
## 🚀 Getting Started
|
||||||
- **Start Dev Server**: `bun run start` or `bunx @angular/cli serve --proxy-config src/proxy.conf.json`
|
|
||||||
- **Format Code**: `bun run format` or `prettier --write "src/**/*.{ts,html,css,scss}"`
|
### Prerequisites
|
||||||
- **Lint**: `bun run lint` or `ng lint`
|
|
||||||
- **Test**: `bun run test` or `bunx @angular/cli test`
|
- [Bun](https://bun.sh/) (recommended) or Node.js 18+
|
||||||
|
- Angular CLI 20+
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
bun install
|
||||||
|
|
||||||
|
# Start development server
|
||||||
|
bun run start
|
||||||
|
```
|
||||||
|
|
||||||
|
The app will be available at `http://localhost:4200`
|
||||||
|
|
||||||
|
## 📋 Commands
|
||||||
|
|
||||||
|
### Development
|
||||||
|
- **Start Dev Server**: `bun run start` - Starts dev server with proxy configuration
|
||||||
|
- **Build**: `bun run build` - Production build
|
||||||
|
- **Watch Build**: `bun run watch` - Development build with file watching
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
- **Format**: `bun run format` - Format code with Prettier
|
||||||
|
- **Format Check**: `bun run format:check` - Check code formatting
|
||||||
|
- **Lint**: `bun run lint` - Run ESLint
|
||||||
|
- **OxLint**: `bun run oxlint` - Run OxLint with strict warnings
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- **Test All**: `bun run test` - Run all tests with Karma/Jasmine
|
||||||
- **Test Single File**: `bunx @angular/cli test --include=path/to/test.spec.ts`
|
- **Test Single File**: `bunx @angular/cli test --include=path/to/test.spec.ts`
|
||||||
|
|
||||||
|
## 🛠️ Technology Stack
|
||||||
|
|
||||||
|
### Core
|
||||||
|
- **Angular 20**: Latest Angular framework with standalone components
|
||||||
|
- **TypeScript 5.8**: Strongly typed JavaScript
|
||||||
|
- **RxJS 7.8**: Reactive programming for HTTP and state management
|
||||||
|
|
||||||
|
### Styling & UI
|
||||||
|
- **TailwindCSS 4**: Utility-first CSS framework
|
||||||
|
- **PostCSS**: CSS processing and optimization
|
||||||
|
- **FontAwesome**: Icon library with Angular integration
|
||||||
|
|
||||||
|
### Animation & Interaction
|
||||||
|
- **GSAP**: High-performance animations
|
||||||
|
- **CountUp.js**: Number animation effects
|
||||||
|
- **Custom Audio Service**: Game sound effects and feedback
|
||||||
|
|
||||||
|
### Development Tools
|
||||||
|
- **Bun**: Fast JavaScript runtime and package manager
|
||||||
|
- **ESLint + Angular ESLint**: Code linting with Angular-specific rules
|
||||||
|
- **OxLint**: Fast Rust-based linter
|
||||||
|
- **Prettier**: Code formatting
|
||||||
|
- **Karma + Jasmine**: Testing framework
|
||||||
|
|
||||||
|
### Payment & APIs
|
||||||
|
- **Stripe**: Payment processing integration
|
||||||
|
- **Custom HTTP Interceptors**: API communication and error handling
|
||||||
|
|
||||||
|
## 🏗️ Architecture
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── app/
|
||||||
|
│ ├── feature/ # Feature modules
|
||||||
|
│ │ ├── auth/ # Authentication (login, register, OAuth2)
|
||||||
|
│ │ ├── game/ # Game modules (blackjack, coinflip, dice, slots)
|
||||||
|
│ │ ├── lootboxes/ # Lootbox system
|
||||||
|
│ │ └── deposit/ # Payment and deposits
|
||||||
|
│ ├── model/ # Data models and interfaces
|
||||||
|
│ ├── service/ # Core services (auth, user, transaction)
|
||||||
|
│ └── shared/ # Shared components, directives, services
|
||||||
|
├── environments/ # Environment configurations
|
||||||
|
└── public/ # Static assets (images, sounds)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Components
|
||||||
|
- **Game Components**: Modular game implementations with services
|
||||||
|
- **Shared Components**: Reusable UI components (navbar, footer, modals)
|
||||||
|
- **Services**: Business logic and API communication
|
||||||
|
- **Guards**: Route protection and authentication
|
||||||
|
- **Interceptors**: HTTP request/response handling
|
||||||
|
|
||||||
## Style Guide
|
## Style Guide
|
||||||
|
|
||||||
### Color Palette
|
### Color Palette
|
||||||
|
|
|
@ -21,7 +21,8 @@
|
||||||
{
|
{
|
||||||
"glob": "**/*",
|
"glob": "**/*",
|
||||||
"input": "public"
|
"input": "public"
|
||||||
}
|
},
|
||||||
|
"src/assets"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.css"
|
"src/styles.css"
|
||||||
|
|
File diff suppressed because it is too large
Load diff
8
frontend/e2e/backend.spec.ts
Normal file
8
frontend/e2e/backend.spec.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('backend works', async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:8080/health');
|
||||||
|
const response = await page.textContent('body');
|
||||||
|
expect(response).toBeTruthy();
|
||||||
|
expect(page.getByText('{"status":"UP"}')).toBeVisible();
|
||||||
|
});
|
36
frontend/e2e/homepage.spec.ts
Normal file
36
frontend/e2e/homepage.spec.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('home page loads correctly', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
await expect(page).toHaveTitle(/Casino/);
|
||||||
|
await expect(page.getByRole('heading', { name: 'Willkommensbonus' })).toBeVisible();
|
||||||
|
await expect(page.getByText('von bis zu €')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('registration popup should open and close', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.getByRole('navigation').getByRole('button', { name: 'Jetzt registrieren' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByText('Konto erstellenE-')).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Dialog schließen' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByText('Konto erstellenE-')).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('registration should work', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.getByRole('navigation').getByRole('button', { name: 'Jetzt registrieren' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('textbox', { name: 'E-Mail' }).fill('test@kjan.email');
|
||||||
|
await page.getByRole('textbox', { name: 'Benutzername' }).fill('test-playwright');
|
||||||
|
await page.getByRole('textbox', { name: 'Passwort' }).fill('BananaMan123');
|
||||||
|
await page.locator('form').getByRole('button', { name: 'Registrieren' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Dialog schließen' }).click();
|
||||||
|
await page.getByRole('navigation').getByRole('button', { name: 'Anmelden' }).click();
|
||||||
|
await page.getByRole('textbox', { name: 'Benutzername oder E-Mail' }).fill('test@kjan.email');
|
||||||
|
await page.getByRole('textbox', { name: 'Passwort' }).fill('BananaMan123');
|
||||||
|
await page.locator('form').getByRole('button', { name: 'Anmelden' }).click();
|
||||||
|
await expect(page.getByText('Email not verified')).toBeVisible();
|
||||||
|
});
|
|
@ -14,16 +14,16 @@
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^19.0.0",
|
"@angular/animations": "^20.0.0",
|
||||||
"@angular/cdk": "~19.2.0",
|
"@angular/cdk": "~20.0.0",
|
||||||
"@angular/common": "^19.0.0",
|
"@angular/common": "^20.0.0",
|
||||||
"@angular/compiler": "^19.2.4",
|
"@angular/compiler": "^20.0.0",
|
||||||
"@angular/core": "^19.0.0",
|
"@angular/core": "^20.0.0",
|
||||||
"@angular/forms": "^19.0.0",
|
"@angular/forms": "^20.0.0",
|
||||||
"@angular/platform-browser": "^19.0.0",
|
"@angular/platform-browser": "^20.0.0",
|
||||||
"@angular/platform-browser-dynamic": "^19.0.0",
|
"@angular/platform-browser-dynamic": "^20.0.0",
|
||||||
"@angular/router": "^19.0.0",
|
"@angular/router": "^20.0.0",
|
||||||
"@fortawesome/angular-fontawesome": "^1.0.0",
|
"@fortawesome/angular-fontawesome": "^2.0.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||||
|
@ -39,13 +39,14 @@
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^19.0.0",
|
"@angular-devkit/build-angular": "^20.0.0",
|
||||||
"@angular/cli": "^19.2.5",
|
"@angular/cli": "^20.0.0",
|
||||||
"@angular/compiler-cli": "^19.0.0",
|
"@angular/compiler-cli": "^20.0.0",
|
||||||
|
"@playwright/test": "^1.52.0",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"angular-eslint": "19.4.0",
|
"angular-eslint": "20.0.0",
|
||||||
"eslint": "^9.25.1",
|
"eslint": "^9.28.0",
|
||||||
"jasmine-core": "~5.7.0",
|
"jasmine-core": "~5.8.0",
|
||||||
"karma": "~6.4.0",
|
"karma": "~6.4.0",
|
||||||
"karma-chrome-launcher": "~3.2.0",
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
"karma-coverage": "~2.2.0",
|
"karma-coverage": "~2.2.0",
|
||||||
|
@ -53,6 +54,6 @@
|
||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"typescript": "~5.8.0",
|
"typescript": "~5.8.0",
|
||||||
"typescript-eslint": "8.32.1"
|
"typescript-eslint": "8.34.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
49
frontend/playwright.config.ts
Normal file
49
frontend/playwright.config.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// playwright.config.ts (or .js)
|
||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e',
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
reporter: 'html',
|
||||||
|
use: {
|
||||||
|
// This baseURL is for your frontend tests.
|
||||||
|
// Tests hitting the backend directly will use absolute URLs.
|
||||||
|
baseURL: 'http://localhost:4200',
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'firefox',
|
||||||
|
use: { ...devices['Desktop Firefox'] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
webServer: {
|
||||||
|
command:
|
||||||
|
'cd .. && conc -n "frontend,backend" "cd frontend && bun run start" "cd backend/ && watchexec -r -e java ./gradlew :bootRun"',
|
||||||
|
// **IMPORTANT CHANGE HERE:**
|
||||||
|
// Point to your backend's health check endpoint.
|
||||||
|
// If your Spring Boot app uses Actuator, it might be /actuator/health
|
||||||
|
// Verify the correct health endpoint for your backend.
|
||||||
|
url: 'http://localhost:8080/health', // Or "http://localhost:8080/actuator/health"
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
// **INCREASE TIMEOUT SIGNIFICANTLY**
|
||||||
|
// Gradle + Spring Boot can take a while, especially on first run or in CI.
|
||||||
|
// Adjust as needed, e.g., 3-5 minutes.
|
||||||
|
timeout: 300 * 1000, // 300 seconds = 5 minutes
|
||||||
|
stdout: 'pipe', // Good for capturing logs in CI reports
|
||||||
|
stderr: 'pipe',
|
||||||
|
// Optional: If your server needs specific environment variables
|
||||||
|
// env: {
|
||||||
|
// SPRING_PROFILES_ACTIVE: 'test', // Example for Spring Boot
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
});
|
BIN
frontend/public/coinflip.png
Normal file
BIN
frontend/public/coinflip.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 MiB |
Binary file not shown.
Before Width: | Height: | Size: 60 KiB |
BIN
frontend/public/sounds/bet.mp3
Normal file
BIN
frontend/public/sounds/bet.mp3
Normal file
Binary file not shown.
BIN
frontend/public/sounds/coinflip.mp3
Normal file
BIN
frontend/public/sounds/coinflip.mp3
Normal file
Binary file not shown.
BIN
frontend/public/sounds/drag.mp3
Normal file
BIN
frontend/public/sounds/drag.mp3
Normal file
Binary file not shown.
BIN
frontend/public/sounds/win.mp3
Normal file
BIN
frontend/public/sounds/win.mp3
Normal file
Binary file not shown.
|
@ -5,8 +5,8 @@
|
||||||
</main>
|
</main>
|
||||||
<app-footer></app-footer>
|
<app-footer></app-footer>
|
||||||
|
|
||||||
Auth Forms Overlay -->
|
<!-- Auth Forms Overlay -->
|
||||||
@if (showLogin() || showRegister()) {
|
@if (showLogin() || showRegister() || showRecoverPassword()) {
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 bg-black/50 z-40"
|
class="fixed inset-0 bg-black/50 z-40"
|
||||||
(click)="hideAuthForms()"
|
(click)="hideAuthForms()"
|
||||||
|
@ -18,7 +18,11 @@
|
||||||
<div class="fixed inset-0 flex items-center justify-center z-50 p-4" role="presentation">
|
<div class="fixed inset-0 flex items-center justify-center z-50 p-4" role="presentation">
|
||||||
<div class="relative" role="dialog" aria-modal="true">
|
<div class="relative" role="dialog" aria-modal="true">
|
||||||
@if (showLogin()) {
|
@if (showLogin()) {
|
||||||
<app-login (switchForm)="showRegisterForm()" (closeDialog)="hideAuthForms()"></app-login>
|
<app-login
|
||||||
|
(switchForm)="showRegisterForm()"
|
||||||
|
(closeDialog)="hideAuthForms()"
|
||||||
|
(forgotPassword)="showRecoverPasswordForm()"
|
||||||
|
></app-login>
|
||||||
}
|
}
|
||||||
@if (showRegister()) {
|
@if (showRegister()) {
|
||||||
<app-register
|
<app-register
|
||||||
|
@ -26,6 +30,12 @@
|
||||||
(closeDialog)="hideAuthForms()"
|
(closeDialog)="hideAuthForms()"
|
||||||
></app-register>
|
></app-register>
|
||||||
}
|
}
|
||||||
|
@if (showRecoverPassword()) {
|
||||||
|
<app-recover-password
|
||||||
|
(closeDialog)="hideAuthForms()"
|
||||||
|
(switchToLogin)="showLoginForm()"
|
||||||
|
></app-recover-password>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,37 @@
|
||||||
import { Component, HostListener, signal } from '@angular/core';
|
import { Component, HostListener, inject, signal } from '@angular/core';
|
||||||
import { RouterOutlet } from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
import { NavbarComponent } from './shared/components/navbar/navbar.component';
|
import { NavbarComponent } from '@shared/components/navbar/navbar.component';
|
||||||
import { FooterComponent } from './shared/components/footer/footer.component';
|
import { FooterComponent } from '@shared/components/footer/footer.component';
|
||||||
import { LoginComponent } from './feature/auth/login/login.component';
|
import { LoginComponent } from './feature/auth/login/login.component';
|
||||||
import { RegisterComponent } from './feature/auth/register/register.component';
|
import { RegisterComponent } from './feature/auth/register/register.component';
|
||||||
|
import RecoverPasswordComponent from './feature/auth/recover-password/recover-password.component';
|
||||||
|
import { PlaySoundDirective } from '@shared/directives/play-sound.directive';
|
||||||
|
import { SoundInitializerService } from '@shared/services/sound-initializer.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [RouterOutlet, NavbarComponent, FooterComponent, LoginComponent, RegisterComponent],
|
imports: [
|
||||||
|
RouterOutlet,
|
||||||
|
NavbarComponent,
|
||||||
|
FooterComponent,
|
||||||
|
LoginComponent,
|
||||||
|
RegisterComponent,
|
||||||
|
RecoverPasswordComponent,
|
||||||
|
],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
|
hostDirectives: [PlaySoundDirective],
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
|
private soundInitializer = inject(SoundInitializerService);
|
||||||
|
|
||||||
showLogin = signal(false);
|
showLogin = signal(false);
|
||||||
showRegister = signal(false);
|
showRegister = signal(false);
|
||||||
|
showRecoverPassword = signal(false);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.soundInitializer.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
@HostListener('document:keydown.escape')
|
@HostListener('document:keydown.escape')
|
||||||
handleEscapeKey() {
|
handleEscapeKey() {
|
||||||
|
@ -23,18 +41,28 @@ export class AppComponent {
|
||||||
showLoginForm() {
|
showLoginForm() {
|
||||||
this.showLogin.set(true);
|
this.showLogin.set(true);
|
||||||
this.showRegister.set(false);
|
this.showRegister.set(false);
|
||||||
|
this.showRecoverPassword.set(false);
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
showRegisterForm() {
|
showRegisterForm() {
|
||||||
this.showRegister.set(true);
|
this.showRegister.set(true);
|
||||||
this.showLogin.set(false);
|
this.showLogin.set(false);
|
||||||
|
this.showRecoverPassword.set(false);
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
showRecoverPasswordForm() {
|
||||||
|
this.showRecoverPassword.set(true);
|
||||||
|
this.showLogin.set(false);
|
||||||
|
this.showRegister.set(false);
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
hideAuthForms() {
|
hideAuthForms() {
|
||||||
this.showLogin.set(false);
|
this.showLogin.set(false);
|
||||||
this.showRegister.set(false);
|
this.showRegister.set(false);
|
||||||
|
this.showRecoverPassword.set(false);
|
||||||
document.body.style.overflow = 'auto';
|
document.body.style.overflow = 'auto';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ApplicationConfig, provideExperimentalZonelessChangeDetection } from '@angular/core';
|
import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ export const appConfig: ApplicationConfig = {
|
||||||
provideRouter(routes),
|
provideRouter(routes),
|
||||||
FontAwesomeModule,
|
FontAwesomeModule,
|
||||||
provideHttpClient(withInterceptors([httpInterceptor])),
|
provideHttpClient(withInterceptors([httpInterceptor])),
|
||||||
provideExperimentalZonelessChangeDetection(),
|
provideZonelessChangeDetection(),
|
||||||
provideAnimationsAsync(),
|
provideAnimationsAsync(),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,30 +14,66 @@ export const routes: Routes = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'verify',
|
path: 'verify',
|
||||||
loadComponent: () =>
|
loadComponent: () => import('./feature/auth/verify-email/verify-email.component'),
|
||||||
import('./feature/auth/verify-email/verify-email.component').then(
|
|
||||||
(m) => m.VerifyEmailComponent
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'game/blackjack',
|
path: 'recover-password',
|
||||||
loadComponent: () => import('./feature/game/blackjack/blackjack.component'),
|
loadComponent: () => import('./feature/auth/recover-password/recover-password.component'),
|
||||||
canActivate: [authGuard],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'game/slots',
|
path: 'reset-password',
|
||||||
loadComponent: () => import('./feature/game/slots/slots.component'),
|
loadComponent: () => import('./feature/auth/recover-password/recover-password.component'),
|
||||||
canActivate: [authGuard],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'game/lootboxes',
|
path: 'oauth2/callback',
|
||||||
loadComponent: () =>
|
children: [
|
||||||
import('./feature/lootboxes/lootbox-selection/lootbox-selection.component'),
|
{
|
||||||
canActivate: [authGuard],
|
path: 'github',
|
||||||
|
loadComponent: () => import('./feature/auth/oauth2/oauth2-callback.component'),
|
||||||
|
data: { provider: 'github' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'google',
|
||||||
|
loadComponent: () => import('./feature/auth/oauth2/oauth2-callback.component'),
|
||||||
|
data: { provider: 'google' },
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'game/lootboxes/open/:id',
|
path: 'game',
|
||||||
loadComponent: () => import('./feature/lootboxes/lootbox-opening/lootbox-opening.component'),
|
children: [
|
||||||
canActivate: [authGuard],
|
{
|
||||||
|
path: 'blackjack',
|
||||||
|
loadComponent: () => import('./feature/game/blackjack/blackjack.component'),
|
||||||
|
canActivate: [authGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'coinflip',
|
||||||
|
loadComponent: () => import('./feature/game/coinflip/coinflip.component'),
|
||||||
|
canActivate: [authGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'slots',
|
||||||
|
loadComponent: () => import('./feature/game/slots/slots.component'),
|
||||||
|
canActivate: [authGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'lootboxes',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('./feature/lootboxes/lootbox-selection/lootbox-selection.component'),
|
||||||
|
canActivate: [authGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'lootboxes/open/:id',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('./feature/lootboxes/lootbox-opening/lootbox-opening.component'),
|
||||||
|
canActivate: [authGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'dice',
|
||||||
|
loadComponent: () => import('./feature/game/dice/dice.component'),
|
||||||
|
canActivate: [authGuard],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -83,6 +83,68 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div class="my-4 flex items-center">
|
||||||
|
<div class="flex-grow h-px bg-deep-blue-light/30"></div>
|
||||||
|
<span class="px-3 text-xs text-text-secondary">ODER</span>
|
||||||
|
<div class="flex-grow h-px bg-deep-blue-light/30"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3 mb-4">
|
||||||
|
<button
|
||||||
|
(click)="loginWithGithub()"
|
||||||
|
class="w-full py-2.5 px-4 rounded flex items-center justify-center bg-gray-800 hover:bg-gray-700 text-white transition-colors"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="h-5 w-5 mr-2"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Mit GitHub anmelden
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
(click)="loginWithGoogle()"
|
||||||
|
class="w-full py-2.5 px-4 rounded flex items-center justify-center bg-white hover:bg-gray-100 transition-colors !text-black"
|
||||||
|
>
|
||||||
|
<svg class="h-5 w-5 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="#EA4335"
|
||||||
|
d="M5.266 9.765A7.077 7.077 0 0 1 12 4.909c1.69 0 3.218.6 4.418 1.582L19.91 3C17.782 1.145 15.055 0 12 0 7.27 0 3.198 2.698 1.24 6.65l4.026 3.115Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#34A853"
|
||||||
|
d="M16.04 18.013c-1.09.703-2.474 1.078-4.04 1.078a7.077 7.077 0 0 1-6.723-4.823l-4.04 3.067A11.965 11.965 0 0 0 12 24c2.933 0 5.735-1.043 7.834-3l-3.793-2.987Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#4A90E2"
|
||||||
|
d="M19.834 21c2.195-2.048 3.62-5.096 3.62-9 0-.71-.109-1.473-.272-2.182H12v4.637h6.436c-.317 1.559-1.17 2.766-2.395 3.558L19.834 21Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#FBBC05"
|
||||||
|
d="M5.277 14.268A7.12 7.12 0 0 1 4.909 12c0-.782.125-1.533.357-2.235L1.24 6.65A11.934 11.934 0 0 0 0 12c0 1.92.445 3.73 1.237 5.335l4.04-3.067Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Mit Google anmelden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-sm text-text-secondary">
|
||||||
|
Passwort vergessen?
|
||||||
|
<button
|
||||||
|
(click)="switchToForgotPassword()"
|
||||||
|
class="font-medium text-emerald hover:text-emerald-light transition-all duration-200"
|
||||||
|
>
|
||||||
|
Passwort zurücksetzen
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 text-center">
|
<div class="mt-6 text-center">
|
||||||
<p class="text-sm text-text-secondary">
|
<p class="text-sm text-text-secondary">
|
||||||
Noch kein Konto?
|
Noch kein Konto?
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { Component, EventEmitter, Output, signal } from '@angular/core';
|
import { Component, EventEmitter, Output, signal, inject } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { LoginRequest } from '../../../model/auth/LoginRequest';
|
import { LoginRequest } from '../../../model/auth/LoginRequest';
|
||||||
import { AuthService } from '@service/auth.service';
|
import { AuthService } from '@service/auth.service';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { environment } from '@environments/environment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-login',
|
selector: 'app-login',
|
||||||
|
@ -17,12 +18,13 @@ export class LoginComponent {
|
||||||
isLoading = signal(false);
|
isLoading = signal(false);
|
||||||
@Output() switchForm = new EventEmitter<void>();
|
@Output() switchForm = new EventEmitter<void>();
|
||||||
@Output() closeDialog = new EventEmitter<void>();
|
@Output() closeDialog = new EventEmitter<void>();
|
||||||
|
@Output() forgotPassword = new EventEmitter<void>();
|
||||||
|
|
||||||
constructor(
|
private fb = inject(FormBuilder);
|
||||||
private fb: FormBuilder,
|
private authService = inject(AuthService);
|
||||||
private authService: AuthService,
|
private router = inject(Router);
|
||||||
private router: Router
|
|
||||||
) {
|
constructor() {
|
||||||
this.loginForm = this.fb.group({
|
this.loginForm = this.fb.group({
|
||||||
usernameOrEmail: ['', [Validators.required]],
|
usernameOrEmail: ['', [Validators.required]],
|
||||||
password: ['', [Validators.required]],
|
password: ['', [Validators.required]],
|
||||||
|
@ -63,4 +65,18 @@ export class LoginComponent {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loginWithGithub(): void {
|
||||||
|
this.isLoading.set(true);
|
||||||
|
window.location.href = `${environment.apiUrl}/oauth2/github/authorize`;
|
||||||
|
}
|
||||||
|
|
||||||
|
loginWithGoogle(): void {
|
||||||
|
this.isLoading.set(true);
|
||||||
|
window.location.href = `${environment.apiUrl}/oauth2/google/authorize`;
|
||||||
|
}
|
||||||
|
|
||||||
|
switchToForgotPassword() {
|
||||||
|
this.forgotPassword.emit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue