Compare commits

...

340 commits
v1.5.0 ... main

Author SHA1 Message Date
9ec6d8f413
Merge pull request 'chore(deps): update dependency typescript-eslint to v8.29.1' (!126) from renovate/devdependencies-(non-major) into main
All checks were successful
Release / Release (push) Successful in 58s
Reviewed-on: #126
Reviewed-by: Jan K9f <jan@kjan.email>
2025-04-07 20:25:36 +00:00
f680a8829c
chore(deps): update dependency typescript-eslint to v8.29.1
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / Checkstyle Main (pull_request) Has been skipped
CI / prettier (pull_request) Successful in 41s
CI / eslint (pull_request) Successful in 1m37s
CI / test-build (pull_request) Successful in 1m42s
2025-04-07 18:02:33 +00:00
b803055307
Merge pull request 'feat: add authentik for authentication' (!58) from feature/authentik into main
All checks were successful
Release / Release (push) Successful in 1m7s
Reviewed-on: #58
Reviewed-by: Phan Huy Tran <ptran@noreply.localhost>
2025-04-04 13:26:02 +00:00
c942b4bb39
style: remove unnecessary blank line in config file
All checks were successful
CI / Get Changed Files (pull_request) Successful in 27s
CI / prettier (pull_request) Successful in 23s
CI / Checkstyle Main (pull_request) Successful in 48s
CI / eslint (pull_request) Successful in 1m11s
CI / test-build (pull_request) Successful in 1m34s
2025-04-03 12:02:59 +02:00
c765ef87e3
refactor: remove unused storage factory code and comments
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / eslint (pull_request) Successful in 24s
CI / test-build (pull_request) Successful in 31s
CI / prettier (pull_request) Failing after 56s
CI / Checkstyle Main (pull_request) Successful in 1m30s
2025-04-03 11:58:58 +02:00
02453449cd
refactor(user): clean up comments and rename variables
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / eslint (pull_request) Successful in 25s
CI / test-build (pull_request) Successful in 31s
CI / prettier (pull_request) Successful in 56s
CI / Checkstyle Main (pull_request) Successful in 2m12s
2025-04-03 11:51:55 +02:00
25c68e230d
Merge branch 'main' into feature/authentik
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / prettier (pull_request) Successful in 55s
CI / Checkstyle Main (pull_request) Successful in 1m1s
CI / eslint (pull_request) Successful in 1m43s
CI / test-build (pull_request) Successful in 1m55s
2025-04-03 11:44:49 +02:00
87c822dbd7
Merge pull request 'feat(blackjack): add animated number component and usage' (!123) from task/CAS-50/add_rest_blackjack_logic_with_frontend_animations into main
All checks were successful
Release / Release (push) Successful in 1m4s
Reviewed-on: #123
Reviewed-by: Jan K9f <jan@kjan.email>
2025-04-03 08:07:07 +00:00
b5a6582905
style(blackjack): format code and adjust whitespace
All checks were successful
CI / Get Changed Files (pull_request) Successful in 7s
CI / Checkstyle Main (pull_request) Has been skipped
CI / eslint (pull_request) Successful in 36s
CI / prettier (pull_request) Successful in 40s
CI / test-build (pull_request) Successful in 51s
2025-04-03 10:05:31 +02:00
28f7b15d4c
refactor: remove unnecessary comments and variables
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / Checkstyle Main (pull_request) Has been skipped
CI / prettier (pull_request) Failing after 23s
CI / eslint (pull_request) Successful in 1m41s
CI / test-build (pull_request) Successful in 1m53s
2025-04-03 10:04:28 +02:00
4b70a4ac4a
feat(blackjack): add animated number component and usage 2025-04-03 10:04:28 +02:00
64f701c651
refactor(login-success): remove unnecessary blank line
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / prettier (pull_request) Successful in 23s
CI / Checkstyle Main (pull_request) Successful in 31s
CI / eslint (pull_request) Successful in 1m20s
CI / test-build (pull_request) Successful in 1m37s
2025-04-02 16:36:26 +02:00
0e1946d190
refactor(auth): clean up login and logout logic
Some checks failed
CI / Get Changed Files (pull_request) Successful in 7s
CI / eslint (pull_request) Successful in 25s
CI / test-build (pull_request) Successful in 32s
CI / prettier (pull_request) Failing after 59s
CI / Checkstyle Main (pull_request) Successful in 1m29s
2025-04-02 16:33:28 +02:00
9de08ab233
refactor: remove debug logs from auth components
Some checks failed
CI / Get Changed Files (pull_request) Successful in 7s
CI / prettier (pull_request) Failing after 23s
CI / eslint (pull_request) Failing after 28s
CI / test-build (pull_request) Successful in 1m16s
CI / Checkstyle Main (pull_request) Successful in 1m22s
2025-04-02 16:27:35 +02:00
2e76446328
feat(auth): improve logout functionality and token management
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / eslint (pull_request) Successful in 26s
CI / test-build (pull_request) Successful in 33s
CI / prettier (pull_request) Failing after 50s
CI / Checkstyle Main (pull_request) Successful in 1m7s
2025-04-02 16:24:40 +02:00
47f4a4d558
style(user.service.ts): format code for clarity and consistency
All checks were successful
CI / Get Changed Files (pull_request) Successful in 7s
CI / prettier (pull_request) Successful in 26s
CI / Checkstyle Main (pull_request) Successful in 41s
CI / test-build (pull_request) Successful in 41s
CI / eslint (pull_request) Successful in 51s
2025-04-02 16:21:56 +02:00
e37dcecd3f
refactor: update imports and type definitions in services
Some checks failed
CI / Get Changed Files (pull_request) Successful in 7s
CI / eslint (pull_request) Successful in 26s
CI / test-build (pull_request) Successful in 31s
CI / prettier (pull_request) Failing after 54s
CI / Checkstyle Main (pull_request) Successful in 1m57s
2025-04-02 16:20:37 +02:00
d3b7e7d5e7
refactor: improve type annotations in services and config
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / prettier (pull_request) Successful in 23s
CI / Checkstyle Main (pull_request) Successful in 31s
CI / eslint (pull_request) Successful in 1m30s
CI / test-build (pull_request) Failing after 1m43s
2025-04-02 16:15:31 +02:00
617654caeb
style: Fix formatting and spacing in multiple files
Some checks failed
CI / Get Changed Files (pull_request) Successful in 7s
CI / prettier (pull_request) Successful in 22s
CI / Checkstyle Main (pull_request) Successful in 53s
CI / eslint (pull_request) Failing after 1m8s
CI / test-build (pull_request) Successful in 1m35s
2025-04-02 16:11:53 +02:00
fa09a8533f
refactor(deposit, user): rename Keycloak to Authentik user info
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / eslint (pull_request) Failing after 25s
CI / test-build (pull_request) Successful in 33s
CI / prettier (pull_request) Failing after 56s
CI / Checkstyle Main (pull_request) Successful in 2m0s
2025-04-02 16:09:34 +02:00
d7fe0e3965
Merge branch 'main' into feature/authentik
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / prettier (pull_request) Failing after 22s
CI / Checkstyle Main (pull_request) Failing after 35s
CI / eslint (pull_request) Failing after 1m41s
CI / test-build (pull_request) Successful in 1m48s
2025-04-02 16:00:01 +02:00
80d5c1e413
Merge pull request 'refactor: rename keycloakId to authentikId in codebase' (!122) from fix-authentik into feature/authentik
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / eslint (pull_request) Failing after 20s
CI / test-build (pull_request) Failing after 24s
CI / prettier (pull_request) Failing after 51s
CI / Checkstyle Main (pull_request) Successful in 2m11s
Reviewed-on: #122
2025-04-02 13:52:17 +00:00
8317349507
refactor: rename keycloakId to authentikId in codebase
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / prettier (pull_request) Failing after 46s
CI / Checkstyle Main (pull_request) Successful in 49s
CI / eslint (pull_request) Failing after 1m2s
CI / test-build (pull_request) Failing after 1m9s
2025-04-02 15:49:58 +02:00
a2f1a40931
Merge pull request 'task/CAS-50/add_rest_blackjack_logic_with_frontend_animations' (!121) from task/CAS-50/add_rest_blackjack_logic_with_frontend_animations into main
All checks were successful
Release / Release (push) Successful in 51s
Reviewed-on: #121
2025-04-02 11:09:07 +00:00
csimonis
7eebd12699 feat(login): log user info on successful login success page
Some checks failed
CI / Get Changed Files (pull_request) Successful in 8s
CI / eslint (pull_request) Failing after 24s
CI / test-build (pull_request) Failing after 30s
CI / prettier (pull_request) Failing after 50s
CI / Checkstyle Main (pull_request) Successful in 1m19s
2025-04-02 13:07:10 +02:00
3d7ee92cf2
refactor(debt-dialog): update timerSubscription type to Subscription
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / eslint (pull_request) Successful in 22s
CI / test-build (pull_request) Successful in 36s
CI / prettier (pull_request) Successful in 43s
CI / Checkstyle Main (pull_request) Successful in 1m46s
2025-04-02 13:07:01 +02:00
7bec17dd52
style: Format code for readability and consistency
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / eslint (pull_request) Failing after 20s
CI / prettier (pull_request) Successful in 43s
CI / test-build (pull_request) Successful in 32s
CI / Checkstyle Main (pull_request) Successful in 1m52s
2025-04-02 13:04:22 +02:00
775205b54c
style(blackjack): remove commented modal sections from HTML 2025-04-02 13:04:19 +02:00
eb5b94c7bb
Merge pull request 'fix(deps): update dependency ajv-formats to v3' (!120) from renovate/major-dependencies-(major-and-minor) into main
All checks were successful
Release / Release (push) Successful in 58s
Reviewed-on: #120
Reviewed-by: Jan K9f <jan@kjan.email>
2025-04-02 11:03:59 +00:00
40c402ae36
feat: add hand value display to dealer and player hands
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / prettier (pull_request) Failing after 20s
CI / test-build (pull_request) Successful in 35s
CI / eslint (pull_request) Failing after 44s
CI / Checkstyle Main (pull_request) Successful in 51s
2025-04-02 13:03:04 +02:00
801edfe89e
feat(blackjack): add balance display to game result component 2025-04-02 12:58:12 +02:00
68a226b677
feat(debt-dialog): add debt warning dialog for negative balance 2025-04-02 12:50:51 +02:00
faa0a1495b
fix(deps): update dependency ajv-formats to v3
All checks were successful
CI / Get Changed Files (pull_request) Successful in 8s
CI / Checkstyle Main (pull_request) Has been skipped
CI / prettier (pull_request) Successful in 22s
CI / eslint (pull_request) Successful in 1m36s
CI / test-build (pull_request) Successful in 1m42s
2025-04-02 10:42:58 +00:00
823cb88807
Merge pull request 'Update the stripe api' (!119) from fix-renovate into main
All checks were successful
Release / Release (push) Successful in 56s
Reviewed-on: #119
Reviewed-by: Phan Huy Tran <ptran@noreply.localhost>
2025-04-02 10:21:33 +00:00
0aa7ad1031
style: Fix missing newline at end of files
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / eslint (pull_request) Successful in 1m3s
CI / prettier (pull_request) Successful in 51s
CI / test-build (pull_request) Successful in 1m23s
CI / Checkstyle Main (pull_request) Successful in 3m40s
2025-04-02 12:17:35 +02:00
b1b8c939a6
Merge branch 'main' into fix-renovate
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / prettier (pull_request) Successful in 23s
CI / Checkstyle Main (pull_request) Failing after 38s
CI / eslint (pull_request) Successful in 1m1s
CI / test-build (pull_request) Successful in 1m30s
2025-04-02 12:15:23 +02:00
6182ff717f
feat(deposit): enhance payment session handling and error logging
Some checks failed
CI / Get Changed Files (pull_request) Successful in 7s
CI / prettier (pull_request) Successful in 27s
CI / Checkstyle Main (pull_request) Failing after 1m0s
CI / eslint (pull_request) Successful in 1m54s
CI / test-build (pull_request) Successful in 2m2s
2025-04-02 12:12:13 +02:00
4c3f42d347
Merge pull request 'fix(deps): update dependency ajv to v8.17.1' (!117) from renovate/dependencies-(major-and-minor) into main
All checks were successful
Release / Release (push) Successful in 1m10s
Reviewed-on: #117
Reviewed-by: Jan K9f <jan@kjan.email>
2025-04-02 10:03:53 +00:00
9981ebc9d1
fix(deps): update dependencies (major and minor)
Some checks failed
CI / Get Changed Files (pull_request) Successful in 7s
CI / eslint (pull_request) Successful in 30s
CI / test-build (pull_request) Successful in 37s
CI / prettier (pull_request) Successful in 1m2s
CI / Checkstyle Main (pull_request) Failing after 2m20s
2025-04-02 10:01:44 +00:00
ab80f5d285
fix(deps): update dependency ajv to v8.17.1
All checks were successful
CI / Get Changed Files (pull_request) Successful in 7s
CI / Checkstyle Main (pull_request) Has been skipped
CI / eslint (pull_request) Successful in 31s
CI / test-build (pull_request) Successful in 40s
CI / prettier (pull_request) Successful in 45s
2025-04-02 10:01:37 +00:00
ac6e3be52c
Merge pull request 'Add all the renovate changes' (!115) from renovate-test into main
All checks were successful
Release / Release (push) Successful in 58s
Reviewed-on: #115
2025-04-02 10:00:20 +00:00
3c64b9ec8b
build: update stripe-java dependency version to 20.136.0
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / Checkstyle Main (pull_request) Has been skipped
CI / prettier (pull_request) Successful in 23s
CI / eslint (pull_request) Successful in 1m26s
CI / test-build (pull_request) Successful in 1m50s
2025-04-02 11:54:06 +02:00
6b3a8a41fd
ci: remove debug job from CI workflow
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / eslint (pull_request) Successful in 29s
CI / test-build (pull_request) Successful in 35s
CI / prettier (pull_request) Successful in 1m0s
CI / Checkstyle Main (pull_request) Failing after 1m52s
2025-04-02 11:45:35 +02:00
06318a8b57
build(frontend): update ajv and ajv-formats versions
Some checks failed
CI / Get Changed Files (pull_request) Successful in 7s
CI / prettier (pull_request) Successful in 24s
CI / Checkstyle Main (pull_request) Failing after 33s
CI / eslint (pull_request) Failing after 1m38s
CI / test-build (pull_request) Successful in 1m51s
2025-04-02 11:42:53 +02:00
5d15f40a30
Merge pull request 'feat(blackjack): add split functionality to the game' (!116) from task/CAS-50/add_rest_blackjack_logic_with_frontend_animations into main
All checks were successful
Release / Release (push) Successful in 48s
Reviewed-on: #116
Reviewed-by: Jan K9f <jan@kjan.email>
2025-04-02 09:41:18 +00:00
4c83419a31
Merge branch 'main' into task/CAS-50/add_rest_blackjack_logic_with_frontend_animations
All checks were successful
CI / Get Changed Files (pull_request) Successful in 8s
CI / eslint (pull_request) Successful in 21s
CI / test-build (pull_request) Successful in 28s
CI / prettier (pull_request) Successful in 42s
CI / Checkstyle Main (pull_request) Successful in 1m47s
2025-04-02 09:38:43 +00:00
d22c4c243f
feat(game-info): add placeholder and disable bet input
All checks were successful
CI / Get Changed Files (pull_request) Successful in 7s
CI / eslint (pull_request) Successful in 25s
CI / prettier (pull_request) Successful in 26s
CI / Checkstyle Main (pull_request) Successful in 41s
CI / test-build (pull_request) Successful in 42s
2025-04-02 11:38:14 +02:00
f60ce73e65
build(frontend): add ajv-formats dependency to package.json
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / prettier (pull_request) Successful in 24s
CI / Checkstyle Main (pull_request) Failing after 32s
CI / eslint (pull_request) Failing after 1m35s
CI / test-build (pull_request) Failing after 1m45s
2025-04-02 11:36:55 +02:00
bd82262049
ci: add debug step to CI workflow for better logging
Some checks failed
CI / Get Changed Files (pull_request) Successful in 7s
CI / eslint (pull_request) Failing after 24s
CI / test-build (pull_request) Failing after 27s
CI / prettier (pull_request) Successful in 1m3s
CI / Checkstyle Main (pull_request) Failing after 2m1s
2025-04-02 11:34:46 +02:00
6b4adfca0a
build(frontend): update angular compiler version to 19.2.4
Some checks failed
CI / Get Changed Files (pull_request) Successful in 10s
CI / eslint (pull_request) Failing after 35s
CI / prettier (pull_request) Successful in 39s
CI / test-build (pull_request) Failing after 42s
CI / Checkstyle Main (pull_request) Failing after 51s
2025-04-02 11:31:39 +02:00
1fc62af66d
build(frontend): update rxjs version to ~7.8.2
Some checks failed
CI / Get Changed Files (pull_request) Successful in 7s
CI / prettier (pull_request) Successful in 26s
CI / Checkstyle Main (pull_request) Failing after 36s
CI / eslint (pull_request) Failing after 1m26s
CI / test-build (pull_request) Failing after 1m24s
2025-04-02 11:31:08 +02:00
e9c4d4fbed
refactor(BlackJackService): simplify card dealing logic
All checks were successful
CI / Get Changed Files (pull_request) Successful in 5s
CI / prettier (pull_request) Successful in 21s
CI / test-build (pull_request) Successful in 40s
CI / eslint (pull_request) Successful in 48s
CI / Checkstyle Main (pull_request) Successful in 52s
2025-04-02 11:29:16 +02:00
1916c04f4a
chore: update lint command in package.json
Some checks failed
CI / Get Changed Files (pull_request) Successful in 7s
CI / eslint (pull_request) Failing after 20s
CI / test-build (pull_request) Failing after 25s
CI / prettier (pull_request) Successful in 1m0s
CI / Checkstyle Main (pull_request) Failing after 2m12s
2025-04-02 11:28:01 +02:00
575a6651d6
build(frontend): update Angular CLI version in package.json
Some checks failed
CI / Get Changed Files (pull_request) Successful in 9s
CI / eslint (pull_request) Failing after 33s
CI / prettier (pull_request) Successful in 34s
CI / Checkstyle Main (pull_request) Failing after 51s
CI / test-build (pull_request) Failing after 23s
2025-04-02 11:25:24 +02:00
3ef5530e77
feat(blackjack): add split functionality to the game
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / prettier (pull_request) Successful in 19s
CI / Checkstyle Main (pull_request) Successful in 50s
CI / eslint (pull_request) Successful in 53s
CI / test-build (pull_request) Successful in 1m12s
2025-04-02 11:25:03 +02:00
942a47c1b2
build(frontend): update Angular dependencies to v19.0.0
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / prettier (pull_request) Successful in 23s
CI / Checkstyle Main (pull_request) Failing after 33s
CI / eslint (pull_request) Failing after 1m17s
CI / test-build (pull_request) Failing after 1m23s
2025-04-02 11:20:36 +02:00
ff98cab6ac
Merge pull request 'feat: Create api routes for lootboxes (CAS-43)' (!110) from feat/lootboxes into main
All checks were successful
Release / Release (push) Successful in 1m22s
Reviewed-on: #110
Reviewed-by: Jan K9f <jan@kjan.email>
Reviewed-by: lziemke <lea.z4@schule.bremen.de>
2025-04-02 09:18:54 +00:00
c64fccb236
Merge pull request 'chore(deps): update devdependencies (non-major)' (!114) from renovate/devdependencies-(non-major) into renovate-test
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / prettier (pull_request) Successful in 18s
CI / Checkstyle Main (pull_request) Failing after 31s
CI / eslint (pull_request) Successful in 59s
CI / test-build (pull_request) Failing after 58s
Reviewed-on: #114
2025-04-02 09:17:38 +00:00
210500783f
Merge pull request 'fix(deps): update dependencies (major and minor)' (!113) from renovate/major-dependencies-(major-and-minor) into renovate-test
Reviewed-on: #113
2025-04-02 09:16:18 +00:00
ec994616ee
Merge branch 'main' into feat/lootboxes
All checks were successful
CI / Get Changed Files (pull_request) Successful in 7s
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Successful in 38s
2025-04-02 09:15:31 +00:00
Phan Huy Tran
948240ba1e fix: fix wrong reward getting returned, refactor to service
All checks were successful
CI / Get Changed Files (pull_request) Successful in 7s
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Successful in 39s
2025-04-02 11:15:08 +02:00
Phan Huy Tran
b963595ab4 feat: manage balance
All checks were successful
CI / Get Changed Files (pull_request) Successful in 7s
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Successful in 1m29s
2025-04-02 11:04:20 +02:00
aa613a95e3
fix(deps): update dependencies (major and minor)
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / prettier (pull_request) Successful in 19s
CI / Checkstyle Main (pull_request) Failing after 31s
CI / eslint (pull_request) Successful in 55s
CI / test-build (pull_request) Failing after 52s
2025-04-02 09:02:01 +00:00
38f4617fb0
chore(deps): update devdependencies (non-major)
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / Checkstyle Main (pull_request) Has been skipped
CI / eslint (pull_request) Successful in 20s
CI / test-build (pull_request) Failing after 21s
CI / prettier (pull_request) Successful in 34s
2025-04-02 09:01:53 +00:00
626e28ab65
Merge pull request 'feat: add stand and get game features to blackjack game with animations' (!105) from task/CAS-50/add_rest_blackjack_logic_with_frontend_animations into main
All checks were successful
Release / Release (push) Successful in 1m1s
Reviewed-on: #105
Reviewed-by: Jan K9f <jan@kjan.email>
2025-04-02 08:28:43 +00:00
4a7c54eab8
style: format code for consistency and readability
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / eslint (pull_request) Successful in 19s
CI / test-build (pull_request) Successful in 27s
CI / prettier (pull_request) Successful in 40s
CI / Checkstyle Main (pull_request) Successful in 1m28s
2025-04-02 10:26:11 +02:00
e983b21e07
feat(blackjack): refresh user balance on game state change 2025-04-02 10:25:43 +02:00
Phan Huy Tran
8a6bc95c92 feat: add route to get all lootboxes 2025-04-02 10:18:51 +02:00
08b12d238e
feat(blackjack): add PLAYER_WON state to BlackJackState
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / eslint (pull_request) Successful in 18s
CI / test-build (pull_request) Successful in 28s
CI / prettier (pull_request) Successful in 59s
CI / Checkstyle Main (pull_request) Successful in 2m18s
2025-04-02 10:14:19 +02:00
d400986c34
refactor(blackjack): update import paths for modules 2025-04-02 10:12:29 +02:00
5d803e4b8b
feat(landing): add NavbarComponent to landing page component 2025-04-02 10:12:29 +02:00
6508a233b2
style(game-controls): fix formatting in constructor method 2025-04-02 10:12:29 +02:00
889863aad6
refactor(game-controls): update import paths for consistency 2025-04-02 10:12:29 +02:00
ba854be5db
style: fix formatting in constructor definitions 2025-04-02 10:12:29 +02:00
1d9eec4546
refactor(blackjack): update import paths for components 2025-04-02 10:12:29 +02:00
56c63d48f6
style(tsconfig): update path mappings to array syntax 2025-04-02 10:12:29 +02:00
db3d2e7b82
style(tsconfig): fix path configuration formatting 2025-04-02 10:12:29 +02:00
2a11675fb6
style: format constructor style in components 2025-04-02 10:12:28 +02:00
2405c00f49
refactor: update imports to use absolute paths 2025-04-02 10:12:28 +02:00
b4caf70ffe
refactor: update import paths for better readability 2025-04-02 10:12:28 +02:00
defe26d0c1
refactor(blackjack): move GameState to feature folder 2025-04-02 10:12:28 +02:00
2d8b137e69
style: format code and add missing newlines 2025-04-02 10:12:28 +02:00
4b569157aa
feat: add game state enum and refactor game components 2025-04-02 10:12:28 +02:00
349e4ce1ec
refactor(blackjack): remove unnecessary comments and clean code 2025-04-02 10:12:28 +02:00
deac128935
style(blackjack): format code for better readability 2025-04-02 10:12:28 +02:00
acdbea5a99
feat(blackjack): add action indicators and loading states 2025-04-02 10:12:28 +02:00
d2b22b561d
feat: add double down feature to blackjack game 2025-04-02 10:12:19 +02:00
d90fcdcf1e
feat: add stand and get game features to blackjack game 2025-04-02 10:11:26 +02:00
Phan Huy Tran
e4bcd9d791 refactor: use many to many relation for lootboxes and rewards 2025-04-02 10:08:23 +02:00
Phan Huy Tran
084d478cd9 feat: create repositories
All checks were successful
CI / Get Changed Files (pull_request) Successful in 7s
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Successful in 1m28s
2025-04-02 09:18:17 +02:00
Phan Huy Tran
1878ed8fe4 feat: create lootbox and rewards entity 2025-04-02 09:15:37 +02:00
4e8530c861 Merge pull request 'refactor: refactor state logic' (!106) from refactor/state-logic into main
All checks were successful
Release / Release (push) Successful in 49s
Reviewed-on: #106
2025-03-27 17:25:00 +00:00
Phan Huy Tran
cb6c1550b7 refactor: refactor state logic
All checks were successful
CI / Get Changed Files (pull_request) Successful in 9s
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Successful in 54s
2025-03-27 15:46:07 +01:00
ed6071a0ba Merge pull request 'feat: add balance when player wins with blackjack in first round' (!104) from feat/blackjack-win-on-start into main
All checks were successful
Release / Release (push) Successful in 1m12s
Reviewed-on: #104
Reviewed-by: Jan K9f <jan@kjan.email>
2025-03-27 13:34:38 +00:00
Phan Huy Tran
ffd651d74b feat: add balance when player wins gets blackjack in first round
All checks were successful
CI / Get Changed Files (pull_request) Successful in 30s
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Successful in 1m19s
2025-03-27 14:18:03 +01:00
4fa7b63b04 Merge pull request 'feat: validate game state on hit (CAS-51)' (!103) from feat/hit-state-validation into main
All checks were successful
Release / Release (push) Successful in 41s
Reviewed-on: #103
Reviewed-by: Jan K9f <jan@kjan.email>
2025-03-27 13:07:37 +00:00
4b4de32e1d Merge pull request 'feat(blackjack): add player lost state in game logic (CAS-51)' (!102) from update-state-on-hit into main
Some checks failed
Release / Release (push) Has been cancelled
Reviewed-on: #102
Reviewed-by: Phan Huy Tran <ptran@noreply.localhost>
2025-03-27 13:07:03 +00:00
Phan Huy Tran
4764c12909 feat: validate game state on hit
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Successful in 1m45s
2025-03-27 14:05:13 +01:00
e9a8267208
feat(blackjack): add player lost state in game logic
All checks were successful
CI / Get Changed Files (pull_request) Successful in 8s
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Successful in 1m1s
2025-03-27 14:03:33 +01:00
d7690e630e Merge pull request 'chore(deps): update plugin org.springframework.boot to v3.4.4' (!84) from renovate/all-minor-patch into main
All checks were successful
Release / Release (push) Successful in 40s
Reviewed-on: #84
Reviewed-by: Jan K9f <jan@kjan.email>
2025-03-27 12:53:46 +00:00
8ec97d7f2f Merge pull request 'fix(deps): update dependencies (major and minor)' (!85) from renovate/dependencies-(major-and-minor) into main
Some checks failed
Release / Release (push) Has been cancelled
Reviewed-on: #85
Reviewed-by: Jan K9f <jan@kjan.email>
2025-03-27 12:53:27 +00:00
36f2d80d5c Merge pull request 'feat(blackjack): add hit endpoint for blackjack game (CAS-51)' (!97) from add-hitting into main
Some checks failed
Release / Release (push) Has been cancelled
Reviewed-on: #97
Reviewed-by: Phan Huy Tran <ptran@noreply.localhost>
2025-03-27 12:52:41 +00:00
Phan Huy Tran
7a7d24c8ea fix: fix playercards, dealercards and deck referencing the same database entity
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Successful in 34s
2025-03-27 13:50:17 +01:00
2b8d45f6ec Merge pull request 'Update README.md' (!99) from jank-patch-1 into main
Reviewed-on: #99
2025-03-27 12:02:02 +00:00
aca389a9b0 fix(deps): update dependencies (major and minor)
All checks were successful
CI / Get Changed Files (pull_request) Successful in 7s
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Successful in 1m36s
2025-03-27 12:01:53 +00:00
bbb991a25b Update README.md
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / Checkstyle Main (pull_request) Has been skipped
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
2025-03-27 11:59:49 +00:00
500a76dd7a player gets way to many cards and I dont know why pls help
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Successful in 35s
2025-03-27 12:50:59 +01:00
59562e5b64 fix(BlackJackGameController): improve game existence check 2025-03-27 12:50:59 +01:00
bd5539bb42 feat(blackjack): implement hit action for blackjack game 2025-03-27 12:50:59 +01:00
ed5960877d feat(blackjack): add hit endpoint for blackjack game 2025-03-27 12:50:55 +01:00
deb5b6525f Merge pull request 'feat: Implement player winning state at startup' (!98) from feat/state-enum into main
All checks were successful
Release / Release (push) Successful in 48s
Reviewed-on: #98
Reviewed-by: Jan K9f <jan@kjan.email>
2025-03-27 11:49:37 +00:00
Phan Huy Tran
90abd13a2c feat: handle player drawing blackjack at the start of the game
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Successful in 36s
2025-03-27 12:45:52 +01:00
Phan Huy Tran
caa210a80e refactor: add blackjack state enum 2025-03-27 12:08:57 +01:00
4b23f3c67f Merge pull request 'feat: add deck to blackjack game instead of random cards' (!96) from feat/deck into main
All checks were successful
Release / Release (push) Successful in 50s
Reviewed-on: #96
Reviewed-by: Jan K9f <jan@kjan.email>
2025-03-27 10:54:49 +00:00
4f6add087b Merge pull request 'docs: add optional watchexec command to README.md' (!95) from add-some-docs into main
Reviewed-on: #95
Reviewed-by: Phan Huy Tran <ptran@noreply.localhost>
2025-03-27 10:42:27 +00:00
Phan Huy Tran
55daca72c0 feat: add deck to blackjack game instead of random cards
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Successful in 1m24s
2025-03-27 11:40:50 +01:00
71cda97dab
docs: add optional watchexec command to README.md
All checks were successful
CI / Get Changed Files (pull_request) Successful in 25s
CI / Checkstyle Main (pull_request) Has been skipped
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
2025-03-27 10:47:46 +01:00
cc92f234d7 Merge pull request 'feat(blackjack): implement game start and controls functionality' (!94) from task/CAS-50/add_frontend_game_start into main
All checks were successful
Release / Release (push) Successful in 48s
Reviewed-on: #94
2025-03-26 14:39:15 +00:00
99f9f8d3c3
feat(blackjack): implement game start and controls functionality
All checks were successful
CI / Get Changed Files (pull_request) Successful in 7s
CI / eslint (pull_request) Successful in 20s
CI / test-build (pull_request) Successful in 28s
CI / prettier (pull_request) Successful in 34s
CI / Checkstyle Main (pull_request) Successful in 1m1s
2025-03-26 15:30:55 +01:00
d0ba0eb71d Merge pull request 'feat: Implement starting a Blackjack game (CAS-50)' (!89) from feat/blackjack into main
All checks were successful
Release / Release (push) Successful in 50s
Reviewed-on: #89
Reviewed-by: Jan K9f <jan@kjan.email>
2025-03-26 13:47:13 +00:00
Phan Huy Tran
ca87c684b1 refactor: Extract game creation logic to service
All checks were successful
CI / Get Changed Files (pull_request) Successful in 29s
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Successful in 1m19s
2025-03-26 14:31:32 +01:00
Phan Huy Tran
1b9bc90920 feat: Adjust json responses 2025-03-26 14:20:31 +01:00
Phan Huy Tran
85d2b218aa feat: Add user relation to black jack game entity 2025-03-26 13:03:06 +00:00
Phan Huy Tran
64e41b663e feat: Subtract user balance, persist game 2025-03-26 13:03:06 +00:00
Phan Huy Tran
b2b0bb2f44 feat: Validate bet amount 2025-03-26 13:03:06 +00:00
Phan Huy Tran
1a4b3f073f fix: add openid scope to get bearer token request 2025-03-26 13:03:06 +00:00
8cb045fcbe feat(blackjack): add BlackJack game creation functionality 2025-03-26 13:03:06 +00:00
Phan Huy Tran
24ea51318f feat: Add card class 2025-03-26 13:03:06 +00:00
3df8926e8a Merge pull request 'chore: update Hibernate ddl-auto setting to 'update'' (#93) from fix/db-deletion into main
All checks were successful
Release / Release (push) Successful in 31s
Reviewed-on: #93
Reviewed-by: Jan K9f <jan@kjan.email>
2025-03-26 12:41:12 +00:00
ad9fb7e735 chore: update Hibernate ddl-auto setting to 'update'
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Successful in 26s
2025-03-26 12:39:40 +00:00
67b83bcf63 Merge pull request 'feat(game): add blackjack game component and routing' (#92) from task/CAS-49/add_base_styling_for_blackjack into main
All checks were successful
Release / Release (push) Successful in 48s
Reviewed-on: #92
2025-03-26 12:36:20 +00:00
03ce527087
refactor(blackjack): rename event emitters for clarity
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / Checkstyle Main (pull_request) Has been skipped
CI / prettier (pull_request) Successful in 37s
CI / eslint (pull_request) Successful in 39s
CI / test-build (pull_request) Successful in 46s
2025-03-26 13:35:07 +01:00
5e5fe603f9
style: update card background colors in templates 2025-03-26 13:35:07 +01:00
a639888a33 Merge branch 'main' into task/CAS-49/add_base_styling_for_blackjack
Some checks failed
CI / Get Changed Files (pull_request) Successful in 7s
CI / Checkstyle Main (pull_request) Has been skipped
CI / prettier (pull_request) Successful in 24s
CI / test-build (pull_request) Successful in 37s
CI / eslint (pull_request) Failing after 50s
2025-03-26 12:27:47 +00:00
3da534f3ae
feat(security): add CORS support and update security config
Some checks failed
CI / Get Changed Files (pull_request) Successful in 9s
CI / eslint (pull_request) Failing after 29s
CI / prettier (pull_request) Failing after 32s
CI / test-build (pull_request) Failing after 58s
CI / Checkstyle Main (pull_request) Successful in 1m24s
2025-03-26 13:27:42 +01:00
9a9447961f Merge pull request 'style(navbar): update text from 'Balance' to 'Guthaben'' (#91) from translation into main
All checks were successful
Release / Release (push) Successful in 59s
Reviewed-on: #91
2025-03-26 12:27:38 +00:00
eb153f4459
feat(game): add blackjack game component and routing
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / Checkstyle Main (pull_request) Has been skipped
CI / prettier (pull_request) Successful in 16s
CI / eslint (pull_request) Failing after 43s
CI / test-build (pull_request) Successful in 46s
2025-03-26 13:26:38 +01:00
cd84d0c949
style(navbar): update text from 'Balance' to 'Guthaben'
All checks were successful
CI / Get Changed Files (pull_request) Successful in 5s
CI / Checkstyle Main (pull_request) Has been skipped
CI / eslint (pull_request) Successful in 37s
CI / prettier (pull_request) Successful in 41s
CI / test-build (pull_request) Successful in 41s
2025-03-26 13:21:29 +01:00
csimonis
242b72ca45
idek man
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / prettier (pull_request) Failing after 23s
CI / Checkstyle Main (pull_request) Successful in 44s
CI / test-build (pull_request) Failing after 55s
CI / eslint (pull_request) Failing after 57s
2025-03-26 11:10:19 +01:00
csimonis
e848b548b5
wip 2025-03-26 11:10:19 +01:00
144f033beb
chore: remove keycloak 2025-03-26 11:10:19 +01:00
f547d05f64
wip 2025-03-26 11:10:18 +01:00
33683f565f
wip 2025-03-26 11:09:28 +01:00
374175d386 Merge branch 'main' into renovate/all-minor-patch
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / eslint (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Successful in 41s
2025-03-26 10:07:06 +00:00
32aa753452 Merge pull request 'feat(home): add router to clear query parameters' (#88) from task/remove_success_url_parameter into main
All checks were successful
Release / Release (push) Successful in 50s
CI / Get Changed Files (pull_request) Successful in 6s
CI / eslint (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
Reviewed-on: #88
Reviewed-by: Constantin Simonis <constantin@simonis.lol>
Reviewed-by: Phan Huy Tran <ptran@noreply.localhost>
2025-03-26 10:04:23 +00:00
ce17741e72
style(home): format constructor for better readability
All checks were successful
CI / Get Changed Files (pull_request) Successful in 9s
CI / Checkstyle Main (pull_request) Has been skipped
CI / prettier (pull_request) Successful in 20s
CI / test-build (pull_request) Successful in 31s
CI / eslint (pull_request) Successful in 36s
2025-03-26 11:03:28 +01:00
cafa2c148f chore(deps): update plugin org.springframework.boot to v3.4.4
All checks were successful
CI / Get Changed Files (pull_request) Successful in 7s
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Successful in 2m50s
2025-03-26 10:01:43 +00:00
af90108b3b
refactor(home): remove unnecessary comment from code
Some checks failed
CI / Get Changed Files (pull_request) Successful in 17s
CI / Checkstyle Main (pull_request) Has been skipped
CI / prettier (pull_request) Failing after 39s
CI / eslint (pull_request) Successful in 42s
CI / test-build (pull_request) Successful in 38s
2025-03-26 11:00:43 +01:00
cf42e725cc
feat(home): add router to clear query parameters
Some checks failed
CI / Get Changed Files (pull_request) Successful in 41s
CI / Checkstyle Main (pull_request) Has been skipped
CI / prettier (pull_request) Failing after 42s
CI / eslint (pull_request) Successful in 56s
CI / test-build (pull_request) Successful in 1m13s
2025-03-26 10:58:25 +01:00
c42c5577cf Merge pull request 'docs: Add docs for local stripe development' (#87) from docs/stripe-dev into main
Reviewed-on: #87
Reviewed-by: jleibl <jleibl@proton.me>
2025-03-26 09:14:13 +00:00
Phan Huy Tran
75d9a4e2fb docs: Add docs for local stripe development
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / Checkstyle Main (pull_request) Has been skipped
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
2025-03-26 09:40:57 +01:00
6349ad0bab Merge pull request 'refactor: Switch stripe tokens to use the casino stripe account' (#86) from refactor/stripe-account-swtich into main
All checks were successful
Release / Release (push) Successful in 56s
Reviewed-on: #86
Reviewed-by: Constantin Simonis <constantin@simonis.lol>
2025-03-26 08:16:22 +00:00
Phan Huy Tran
f8013d2438 refactor: Switch stripe tokens to use the casino stripe account
All checks were successful
CI / Get Changed Files (pull_request) Successful in 25s
CI / prettier (pull_request) Successful in 52s
CI / Checkstyle Main (pull_request) Successful in 59s
CI / eslint (pull_request) Successful in 1m12s
CI / test-build (pull_request) Successful in 1m16s
2025-03-26 09:14:07 +01:00
c97cb51c6a Merge pull request 'chore(deps): update devdependencies (non-major)' (#80) from renovate/devdependencies-(non-major) into main
All checks were successful
Release / Release (push) Successful in 54s
Reviewed-on: #80
Reviewed-by: Jan Gleytenhoover <jan@kjan.email>
2025-03-16 08:10:24 +00:00
c590cfaec5 chore(package.json): downgrade typescript version to 5.5.0
All checks were successful
CI / Get Changed Files (pull_request) Successful in 7s
CI / Checkstyle Main (pull_request) Has been skipped
CI / prettier (pull_request) Successful in 16s
CI / eslint (pull_request) Successful in 41s
CI / test-build (pull_request) Successful in 46s
2025-03-16 09:06:30 +01:00
814cf0e9fa chore: update typescript version in package.json
Some checks failed
CI / Get Changed Files (pull_request) Successful in 27s
CI / Checkstyle Main (pull_request) Has been skipped
CI / prettier (pull_request) Successful in 37s
CI / test-build (pull_request) Failing after 48s
CI / eslint (pull_request) Successful in 32s
2025-03-16 09:04:27 +01:00
1df00211d5 chore(deps): update devdependencies (non-major)
Some checks failed
CI / Get Changed Files (pull_request) Successful in 29s
CI / Checkstyle Main (pull_request) Has been skipped
CI / prettier (pull_request) Successful in 34s
CI / eslint (pull_request) Successful in 51s
CI / test-build (pull_request) Failing after 54s
2025-03-13 15:02:53 +00:00
fbaa612980 Merge pull request 'feat: Save balance after successful payment' (#81) from feature/save-balance into main
All checks were successful
Release / Release (push) Successful in 45s
Reviewed-on: #81
Reviewed-by: Jan Gleytenhoover <jan@kjan.email>
2025-03-13 14:18:31 +00:00
Phan Huy Tran
07a5cee330 checkstyle
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / prettier (pull_request) Successful in 19s
CI / eslint (pull_request) Successful in 34s
CI / Checkstyle Main (pull_request) Successful in 38s
CI / test-build (pull_request) Successful in 41s
2025-03-13 15:10:16 +01:00
Phan Huy Tran
d25b894f38 feat: Save balance after successful payment
Some checks failed
CI / Get Changed Files (pull_request) Successful in 9s
CI / eslint (pull_request) Successful in 39s
CI / prettier (pull_request) Successful in 37s
CI / test-build (pull_request) Successful in 42s
CI / Checkstyle Main (pull_request) Failing after 1m22s
2025-03-13 15:02:32 +01:00
ce7c30b9ae Merge pull request 'chore(deps): update dorny/paths-filter action to v3' (#75) from renovate/dorny-paths-filter-3.x into main
Reviewed-on: #75
Reviewed-by: Jan Gleytenhoover <jan@kjan.email>
2025-03-13 10:08:35 +00:00
5c3b74bede chore(deps): update dorny/paths-filter action to v3
All checks were successful
CI / Get Changed Files (pull_request) Successful in 10s
CI / prettier (pull_request) Successful in 24s
CI / eslint (pull_request) Successful in 43s
CI / test-build (pull_request) Successful in 43s
CI / Checkstyle Main (pull_request) Successful in 1m49s
2025-03-13 10:02:48 +00:00
d81cf1f68f Merge pull request 'fix(deps): update dependencies (major and minor)' (#62) from renovate/dependencies-(major-and-minor) into main
All checks were successful
Release / Release (push) Successful in 48s
Reviewed-on: #62
Reviewed-by: Jan Gleytenhoover <jan@kjan.email>
2025-03-13 09:49:25 +00:00
ec9468943a Merge pull request 'chore: remove unused file a from the backend directory' (#74) from fix-a into main
All checks were successful
Release / Release (push) Successful in 48s
Reviewed-on: #74
2025-03-13 09:10:54 +00:00
61bc89e62f
chore: remove unused file a from the backend directory
All checks were successful
CI / Get Changed Files (pull_request) Successful in 27s
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Successful in 49s
2025-03-13 10:07:29 +01:00
a0968e784a Merge pull request 'ci: update CI workflows to include path filters' (#73) from pipeline-rules into main
All checks were successful
Release / Release (push) Successful in 45s
Reviewed-on: #73
2025-03-13 09:04:46 +00:00
ebd16b6503 fix(deps): update dependencies (major and minor)
All checks were successful
CI / eslint (pull_request) Successful in 28s
CI / prettier (pull_request) Successful in 36s
CI / test-build (pull_request) Successful in 43s
CI / Checkstyle Main (pull_request) Successful in 1m42s
2025-03-13 07:02:20 +00:00
70fe8d8c88 Merge pull request 'chore(deps): update postgres docker tag to v17' (#68) from renovate/postgres-17.x into main
All checks were successful
Release / Release (push) Successful in 43s
Reviewed-on: #68
Reviewed-by: Jan Gleytenhoover <jan@kjan.email>
2025-03-13 06:50:38 +00:00
d722f0af76 chore(deps): update postgres docker tag to v17
All checks were successful
CI / eslint (pull_request) Successful in 28s
CI / prettier (pull_request) Successful in 25s
CI / test-build (pull_request) Successful in 44s
CI / Checkstyle Main (pull_request) Successful in 1m32s
2025-03-13 06:44:49 +00:00
0227146da7
ci: improve checkstyle report caching in CI workflow
All checks were successful
CI / Get Changed Files (pull_request) Successful in 7s
CI / prettier (pull_request) Successful in 19s
CI / eslint (pull_request) Successful in 21s
CI / test-build (pull_request) Successful in 28s
CI / Checkstyle Main (pull_request) Successful in 37s
2025-03-12 21:08:49 +01:00
fd7c92ebb1
ci: update CI workflow to check for changed files
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / eslint (pull_request) Successful in 20s
CI / prettier (pull_request) Successful in 26s
CI / test-build (pull_request) Successful in 27s
CI / Checkstyle Main (pull_request) Successful in 1m57s
2025-03-12 21:06:25 +01:00
db37f0de35
chore: add new file a to the backend directory
All checks were successful
CI / Checkstyle Main (pull_request) Has been skipped
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
2025-03-12 21:05:03 +01:00
763afdc546
ci: update CI workflows to include path filters
All checks were successful
CI / Checkstyle Main (pull_request) Has been skipped
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
2025-03-12 21:03:40 +01:00
dc5275d043 Merge pull request 'chore(deps): update all non-major dependencies' (#59) from renovate/all-minor-patch into main
All checks were successful
Release / Release (push) Successful in 47s
Reviewed-on: #59
Reviewed-by: Jan Gleytenhoover <jan@kjan.email>
2025-03-12 19:26:53 +00:00
7cc33c69e5 Merge pull request 'Add styleguide based on current design' (#57) from styleguide into main
All checks were successful
Release / Release (push) Successful in 52s
Reviewed-on: #57
Reviewed-by: jleibl <jleibl@proton.me>
2025-03-12 19:08:46 +00:00
3092f4a688 chore(deps): update all non-major dependencies
All checks were successful
CI / prettier (pull_request) Successful in 24s
CI / eslint (pull_request) Successful in 42s
CI / test-build (pull_request) Successful in 51s
CI / Checkstyle Main (pull_request) Successful in 2m7s
2025-03-12 19:02:41 +00:00
cd098f717c Merge branch 'main' into styleguide
All checks were successful
CI / prettier (pull_request) Successful in 46s
CI / eslint (pull_request) Successful in 54s
CI / Checkstyle Main (pull_request) Successful in 1m4s
CI / test-build (pull_request) Successful in 1m3s
2025-03-12 18:53:16 +00:00
09fdd83192 Merge pull request 'Rename variables to non dom element names' (#71) from fix/rename-to-not-standard-dom-elements into main
All checks were successful
Release / Release (push) Successful in 1m44s
Reviewed-on: #71
2025-03-12 18:52:30 +00:00
8c9d7c498b
style(home): format HTML and TypeScript code for clarity
All checks were successful
CI / prettier (pull_request) Successful in 22s
CI / Checkstyle Main (pull_request) Successful in 31s
CI / eslint (pull_request) Successful in 37s
CI / test-build (pull_request) Successful in 44s
2025-03-12 19:26:07 +01:00
abc4277e84
refactor(deposit): rename close event emitter for clarity 2025-03-12 19:25:11 +01:00
a7d98e1150
feat: rename event emitter for confirmation modal closure
Some checks failed
CI / prettier (pull_request) Failing after 20s
CI / Checkstyle Main (pull_request) Successful in 31s
CI / eslint (pull_request) Successful in 37s
CI / test-build (pull_request) Successful in 42s
2025-03-12 19:20:51 +01:00
7bbabc46f3 Merge pull request 'chore(deps): update https://github.com/actions/cache action to v4' (#67) from renovate/https-github.com-actions-cache-4.x into main
All checks were successful
Release / Release (push) Successful in 1m8s
Reviewed-on: #67
Reviewed-by: Jan Gleytenhoover <jan@kjan.email>
2025-03-12 18:01:55 +00:00
ba1415e8b0 Merge pull request 'chore(deps): update actions/checkout action to v4' (#64) from renovate/actions-checkout-4.x into main
All checks were successful
Release / Release (push) Successful in 1m10s
Reviewed-on: #64
Reviewed-by: Jan Gleytenhoover <jan@kjan.email>
2025-03-12 17:46:48 +00:00
f7c344faa6 chore(deps): update https://github.com/actions/cache action to v4
All checks were successful
CI / eslint (pull_request) Successful in 19s
CI / test-build (pull_request) Successful in 27s
CI / prettier (pull_request) Successful in 57s
CI / Checkstyle Main (pull_request) Successful in 1m20s
2025-03-12 16:04:42 +00:00
910895ed82 chore(deps): update actions/checkout action to v4
All checks were successful
CI / eslint (pull_request) Successful in 27s
CI / prettier (pull_request) Successful in 40s
CI / test-build (pull_request) Successful in 1m25s
CI / Checkstyle Main (pull_request) Successful in 2m20s
2025-03-12 16:04:02 +00:00
1ad34b1ae1 Merge pull request 'chore(deps): update actions/cache action to v4' (#63) from renovate/actions-cache-4.x into main
All checks were successful
Release / Release (push) Successful in 1m8s
Reviewed-on: #63
Reviewed-by: Jan Gleytenhoover <jan@kjan.email>
2025-03-12 15:43:04 +00:00
79b83fc4c8 chore(deps): update actions/cache action to v4
All checks were successful
CI / Checkstyle Main (pull_request) Successful in 46s
CI / eslint (pull_request) Successful in 50s
CI / prettier (pull_request) Successful in 1m3s
CI / test-build (pull_request) Successful in 1m9s
2025-03-12 15:03:14 +00:00
e8944c46a4
chore: remove unused Tailwind CSS configuration file
All checks were successful
CI / prettier (pull_request) Successful in 18s
CI / Checkstyle Main (pull_request) Successful in 27s
CI / eslint (pull_request) Successful in 35s
CI / test-build (pull_request) Successful in 43s
2025-03-12 14:49:11 +01:00
a3da0f9a4d
style: Update color theme in styles.css file
All checks were successful
CI / eslint (pull_request) Successful in 21s
CI / test-build (pull_request) Successful in 28s
CI / prettier (pull_request) Successful in 40s
CI / Checkstyle Main (pull_request) Successful in 1m12s
2025-03-12 14:45:20 +01:00
e0afebb397
style: update Tailwind CSS import in styles.css
Some checks failed
CI / prettier (pull_request) Successful in 19s
CI / Checkstyle Main (pull_request) Successful in 27s
CI / eslint (pull_request) Successful in 37s
CI / test-build (pull_request) Failing after 42s
2025-03-12 14:38:08 +01:00
8012112f6c
style(tailwind): update color keys for consistency
Some checks failed
CI / prettier (pull_request) Successful in 19s
CI / Checkstyle Main (pull_request) Successful in 27s
CI / eslint (pull_request) Successful in 46s
CI / test-build (pull_request) Failing after 51s
2025-03-12 14:31:55 +01:00
e9159abf3d
feat: add MIT License and update README for clarity
Some checks failed
CI / prettier (pull_request) Successful in 37s
CI / eslint (pull_request) Successful in 41s
CI / test-build (pull_request) Failing after 51s
CI / Checkstyle Main (pull_request) Successful in 56s
2025-03-12 14:29:21 +01:00
520c8f8343
docs: update README with style guide and license details
All checks were successful
CI / prettier (pull_request) Successful in 18s
CI / Checkstyle Main (pull_request) Successful in 27s
CI / eslint (pull_request) Successful in 35s
CI / test-build (pull_request) Successful in 42s
2025-03-12 14:21:12 +01:00
f826e1e203
docs: add style guide for casino gaming platform 2025-03-12 14:18:42 +01:00
8b2d8f7e05 Merge pull request 'update-readme' (#56) from update-readme into main
All checks were successful
Release / Release (push) Successful in 45s
Reviewed-on: #56
2025-03-12 13:11:19 +00:00
877ce2a6dd Merge pull request 'chore: Configure Renovate' (#55) from renovate/configure into main
All checks were successful
Release / Release (push) Successful in 50s
Reviewed-on: #55
Reviewed-by: Jan Gleytenhoover <jan@kjan.email>
2025-03-12 13:09:14 +00:00
c73a1a3af2
docs: add Claude Assistant Guide to documentation
All checks were successful
CI / eslint (pull_request) Successful in 20s
CI / prettier (pull_request) Successful in 26s
CI / test-build (pull_request) Successful in 27s
CI / Checkstyle Main (pull_request) Successful in 51s
2025-03-12 14:07:53 +01:00
9fbb339261
docs: update README for Casino Gaming Platform details 2025-03-12 14:04:10 +01:00
730a076710 chore(deps): add renovate.json
All checks were successful
CI / prettier (pull_request) Successful in 20s
CI / eslint (pull_request) Successful in 36s
CI / test-build (pull_request) Successful in 43s
CI / Checkstyle Main (pull_request) Successful in 47s
2025-03-12 13:01:23 +00:00
efef0aad42 Merge pull request 'fix pipes' (#54) from migr into main
All checks were successful
Release / Release (push) Successful in 1m8s
Reviewed-on: #54
2025-03-12 12:36:19 +00:00
319db88e24 Update .gitea/workflows/ci.yml
All checks were successful
CI / eslint (pull_request) Successful in 35s
CI / prettier (pull_request) Successful in 45s
CI / test-build (pull_request) Successful in 48s
CI / Checkstyle Main (pull_request) Successful in 1m3s
2025-03-12 12:30:26 +00:00
77549bbdfd Update .gitea/workflows/ci.yml
Some checks failed
CI / eslint (pull_request) Successful in 25s
CI / prettier (pull_request) Successful in 27s
CI / Checkstyle Main (pull_request) Successful in 37s
CI / test-build (pull_request) Successful in 36s
CI / Test (pull_request) Failing after 1m33s
2025-03-12 12:27:31 +00:00
b69640a1bd Update .gitea/workflows/ci.yml
Some checks failed
CI / eslint (pull_request) Successful in 25s
CI / prettier (pull_request) Successful in 27s
CI / test-build (pull_request) Successful in 38s
CI / Test (pull_request) Failing after 1m59s
CI / Checkstyle Main (pull_request) Successful in 4m22s
2025-03-12 12:25:30 +00:00
e3182ecaae Update .gitea/workflows/ci.yml
Some checks failed
CI / prettier (pull_request) Successful in 24s
CI / eslint (pull_request) Successful in 26s
CI / test-build (pull_request) Successful in 31s
CI / Test (pull_request) Failing after 1m45s
CI / Checkstyle Main (pull_request) Failing after 14s
2025-03-12 12:17:34 +00:00
f33e58cacf Update .gitea/workflows/ci.yml
Some checks failed
CI / prettier (pull_request) Successful in 22s
CI / test-build (pull_request) Successful in 38s
CI / Test (pull_request) Failing after 1m1s
CI / eslint (pull_request) Successful in 1m44s
CI / Checkstyle Main (pull_request) Successful in 6m30s
2025-03-12 12:13:04 +00:00
77c636968b Update .gitea/workflows/ci.yml
Some checks failed
CI / prettier (pull_request) Successful in 46s
CI / Test (pull_request) Failing after 53s
CI / Checkstyle Main (pull_request) Failing after 1m27s
CI / eslint (pull_request) Successful in 1m30s
CI / test-build (pull_request) Successful in 1m37s
2025-03-12 12:10:21 +00:00
49d908060a Update .gitea/workflows/release.yml
Some checks failed
CI / prettier (pull_request) Successful in 24s
CI / test-build (pull_request) Successful in 40s
CI / Test (pull_request) Failing after 1m4s
CI / eslint (pull_request) Successful in 1m10s
CI / Checkstyle Main (pull_request) Failing after 1m26s
2025-03-12 12:02:59 +00:00
2d8510c6a4 Update .gitea/workflows/ci.yml
Some checks failed
CI / eslint (pull_request) Has been cancelled
CI / Checkstyle Main (pull_request) Has been cancelled
CI / test-build (pull_request) Has been cancelled
CI / prettier (pull_request) Has been cancelled
CI / Test (pull_request) Has been cancelled
2025-03-12 12:02:33 +00:00
564cc84c79 Update .gitea/workflows/ci.yml
Some checks failed
CI / Test (pull_request) Has been cancelled
CI / eslint (pull_request) Has been cancelled
CI / prettier (pull_request) Has been cancelled
CI / test-build (pull_request) Has been cancelled
CI / Checkstyle Main (pull_request) Has been cancelled
2025-03-12 12:01:04 +00:00
27a225fffc Update release.config.cjs
Some checks failed
CI / Checkstyle Main (pull_request) Has been cancelled
CI / Test (pull_request) Has been cancelled
CI / eslint (pull_request) Has been cancelled
CI / prettier (pull_request) Has been cancelled
CI / test-build (pull_request) Has been cancelled
2025-03-12 12:00:22 +00:00
Hop In, I Have Puppies AND WiFi
f855dc521d Merge pull request 'Remove unused imports in security config and user entity (CAS-0)' (!53) from task/CAS-0/RemoveUnusedImports into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/53
Reviewed-by: Huy <ptran@noreply@simonis.lol>
2025-03-06 11:51:23 +00:00
f2bd399c05 Merge pull request 'add test for backend' (!42) from tests/backend into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/42
Reviewed-by: Huy <ptran@noreply@simonis.lol>
2025-03-06 11:50:06 +00:00
Hop In, I Have Puppies AND WiFi
9a8543cbfe Merge branch 'main' into task/CAS-0/RemoveUnusedImports 2025-03-06 11:49:59 +00:00
Hop In, I Have Puppies AND WiFi
259ada7523 Merge pull request 'Correct spelling of "Klarna" in footer text (CAS-0)' (!52) from task/CAS-0/FixKlarnaTextInFooter into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/52
Reviewed-by: Klan Jattenhoff <jan@kjan.email>
2025-03-06 11:48:26 +00:00
1d7d84706d
refactor: remove unused imports in security config and user entity 2025-03-06 12:45:24 +01:00
bb7bac9a67
style(footer): correct spelling of "Klarna" in footer text 2025-03-06 12:42:18 +01:00
Hop In, I Have Puppies AND WiFi
e53a08441b Merge pull request 'Improve card styling and transitions in HTML (CAS-0)' (!51) from task/CAS-0/UpdateGameGalleryStyling into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/51
2025-03-06 11:41:31 +00:00
e5ac810a67
style(home): Improve card styling and transitions in HTML 2025-03-06 12:37:51 +01:00
Hop In, I Have Puppies AND WiFi
bd4e7521c0 Merge pull request 'Implement modal animations with GSAP (CAS-0)' (!50) from task/CAS-0/adjust-animations-for-dialogs into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/50
Reviewed-by: Constantin Simonis <constantin@simonis.lol>
Reviewed-by: Huy <ptran@noreply@simonis.lol>
2025-03-06 11:31:59 +00:00
9c12ccd716
style: format code for better readability and consistency 2025-03-06 11:56:00 +01:00
1569922fda
refactor(deposit): remove unnecessary comments in code 2025-03-06 11:55:01 +01:00
08a1a5e877
feat(deposit): implement modal animations with GSAP 2025-03-06 11:52:31 +01:00
Hop In, I Have Puppies AND WiFi
c651337d30 Merge pull request 'Add current user balance display in navbar (CAS-26)' (!49) from task/CAS-26/add-balance-to-nav into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/49
Reviewed-by: Klan Jattenhoff <jan@kjan.email>
Reviewed-by: lziemke <lea.z4@schule.bremen.de>
2025-03-05 11:15:47 +00:00
d61ef4df7e Merge pull request 'feature/confirmation-modal-after-deposit (CAS-29)' (!48) from feature/confirmation-modal-after-deposit into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/48
Reviewed-by: Klan Jattenhoff <jan@kjan.email>
Reviewed-by: Hop In, I Have Puppies AND WiFi <jleibl@noreply@simonis.lol>
2025-03-05 11:11:12 +00:00
1768cf6536
style(user.service): fix string formatting in getCurrentUser method 2025-03-05 12:08:06 +01:00
454e99f812
feat(navbar): update balance display and use signal for state 2025-03-05 12:07:20 +01:00
564601f7bc
feat(navbar): add current user balance display in navbar 2025-03-05 12:05:46 +01:00
Lea
15a92b984c style: linter and prettier 2025-03-05 12:04:48 +01:00
Lea
212bee3bd2 feat: implemented confirmation modal for depositing money 2025-03-05 12:04:48 +01:00
f172e00d0a chore: add success bool to success / cancel url 2025-03-05 12:04:48 +01:00
Lea
95889fc937 fix: closing of deposit confirmation modal 2025-03-05 12:04:48 +01:00
Lea
caf2794489 comfirmation modal for deposit and refactoring 2025-03-05 12:04:48 +01:00
Hop In, I Have Puppies AND WiFi
63db07b6ae Merge pull request 'Update Java language version to 23' (!46) from task/CAS-0/UpdateJavaVersion into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/46
Reviewed-by: Constantin Simonis <constantin@simonis.lol>
2025-03-05 10:09:20 +00:00
Hop In, I Have Puppies AND WiFi
a1850a9705 Merge branch 'main' into task/CAS-0/UpdateJavaVersion 2025-03-05 10:03:50 +00:00
4d251a21c3
build(ci): update Java version in CI workflow 2025-03-05 10:21:55 +01:00
909dab2876
build: update Java language version to 23 2025-03-05 10:17:11 +01:00
6ff6c1eb5e Merge pull request 'fix: add correct redirect url after deposit' (!45) from bugfix/deposit into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/45
Reviewed-by: lziemke <lea.z4@schule.bremen.de>
Reviewed-by: Klan Jattenhoff <jan@kjan.email>
2025-03-05 08:19:53 +00:00
bba33135d5
fix: change redirect route 2025-03-05 09:14:10 +01:00
c102f6ea65
fix: typo 2025-03-05 09:00:41 +01:00
f011ade4a8
fix: add frontend host to application properties 2025-03-05 08:59:34 +01:00
204970856b
fix: add correct redirect url after deposit 2025-03-05 08:57:14 +01:00
925a9d540d Merge pull request 'feature: removed angular materials and implemented new deposit modal' (!44) from feature/deposit-modal into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/44
Reviewed-by: Huy <ptran@noreply@simonis.lol>
Reviewed-by: Klan Jattenhoff <jan@kjan.email>
2025-02-26 12:31:14 +00:00
Lea
65ed49df2a style: removed angular material lines 2025-02-26 13:26:51 +01:00
Lea
a6e3ae1a33 style: removed missed angular material lines 2025-02-26 13:22:52 +01:00
Lea
6c6e2b5cb5 style:fixed prettier and eslint 2025-02-26 13:13:44 +01:00
Lea
25492f3e68 style: fixed prettier and hopefully eslint 2025-02-26 13:13:44 +01:00
Lea
604d593fdc feature: removed angular materials and implemented new deposit modal 2025-02-26 13:13:44 +01:00
Huy
0100df89f5 Merge pull request 'refactor: Change balance type to BigDecimal for better precision' (!43) from refactor/money-precision into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/43
Reviewed-by: Constantin Simonis <constantin@simonis.lol>
Reviewed-by: Kjan Jattenhoff <jan@kjan.email>
2025-02-26 11:10:25 +00:00
Phan Huy Tran
aa6ec4397f revert: revert 2025-02-26 11:06:40 +01:00
Phan Huy Tran
aef0f09d09 refactor: Change balance type to bigdecimal for better precision 2025-02-26 11:05:06 +01:00
74f598a24c
remove comment 2025-02-26 11:01:26 +01:00
39a9ca1831 add test to ci 2025-02-26 09:57:54 +00:00
834cb559ef add test for health controller 2025-02-26 09:57:54 +00:00
cf569386ad add test for user controller 2025-02-26 09:57:54 +00:00
Huy
9c5e05f29d Merge pull request 'refactor: refactor and clean up of home.component.ts' (!41) from refactor/home into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/41
Reviewed-by: Kjan Jattenhoff <jan@kjan.email>
Reviewed-by: Huy <ptran@noreply@simonis.lol>
2025-02-26 09:46:14 +00:00
Lea
cfd59d876d prettier 2025-02-26 10:43:32 +01:00
Lea
f09935564d refactor: refactor and clean up of home.component.ts 2025-02-26 10:18:56 +01:00
Huy
9104c6eb2c Merge pull request 'fix: Redirect to homepage after login' (!40) from fix/home-redirect into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/40
Reviewed-by: Constantin Simonis <constantin@simonis.lol>
Reviewed-by: lziemke <lea.z4@schule.bremen.de>
2025-02-26 09:10:10 +00:00
33f962a02c Merge branch 'main' into fix/home-redirect 2025-02-26 09:06:06 +00:00
Phan Huy Tran
3b1a97c08a fix: Redirect to homepage after login 2025-02-26 10:03:34 +01:00
Huy
11213ec5f0 Merge pull request 'chore: Add http requests for user' (!39) from chore/add-user-requests into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/39
Reviewed-by: We ball <jan@kjan.email>
Reviewed-by: Constantin Simonis <constantin@simonis.lol>
2025-02-26 09:02:28 +00:00
Phan Huy Tran
24be28af1b chore: Add http requests for user 2025-02-26 09:58:33 +01:00
cc8f553d66 Merge pull request 'feat: add homepage ui and images' (!38) from feature/homepage-games-preview into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/38
Reviewed-by: We ball <jan@kjan.email>
Reviewed-by: Huy <ptran@noreply@simonis.lol>
2025-02-19 12:06:35 +00:00
Lea
428c33b000 formatted 2025-02-19 13:02:13 +01:00
Lea
09ccad479e added images for the games 2025-02-19 13:02:13 +01:00
Lea
a933f0b397 implemented better ui and pseudo data 2025-02-19 13:02:13 +01:00
Hop In, I Have Puppies AND WiFi
b9ce80a28a Merge pull request 'feat: add user managment' (!32) from feat/user-managment into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/32
Reviewed-by: Huy <ptran@noreply@simonis.lol>
Reviewed-by: Hop In, I Have Puppies AND WiFi <jleibl@noreply@simonis.lol>
2025-02-19 11:57:30 +00:00
8303c906aa Merge pull request 'Add cache to other pipelines' (!36) from feature/custom-pipeline-images into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/36
Reviewed-by: Huy <ptran@noreply@simonis.lol>
2025-02-19 11:55:50 +00:00
Hop In, I Have Puppies AND WiFi
219cbfca0e Merge pull request 'refactor(routes): change home route to lazy loading' (!37) from task/lazy-load-homepage into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/37
Reviewed-by: We ball <jan@kjan.email>
Reviewed-by: Constantin Simonis <constantin@simonis.lol>
2025-02-19 11:55:12 +00:00
11c6634d6c
fix: build 2025-02-19 12:50:13 +01:00
356a599dd5
chore: remove unused Dockerfile from bunPipeline 2025-02-19 12:48:40 +01:00
0c7c2ae9fa
chore: remove obsolete Docker build workflow file 2025-02-19 12:48:03 +01:00
c75c62b396 Merge pull request 'Fix caching of pipeline' (!34) from fix/improve-checkstyle-pipeline into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/34
Reviewed-by: Hop In, I Have Puppies AND WiFi <jleibl@noreply@simonis.lol>
Reviewed-by: Huy <ptran@noreply@simonis.lol>
2025-02-19 11:47:50 +00:00
8547bd1fa3
ci: remove lint job from CI configuration 2025-02-19 12:36:22 +01:00
da047eef70
style: prettier 2025-02-19 12:31:07 +01:00
da50d19f9d
ci: update CI workflow to use Super-linter for linting 2025-02-19 12:30:07 +01:00
5cb08ca5f8
ci: update build command in CI workflow 2025-02-19 12:27:29 +01:00
8da1ff8acd
ci: update CI runner to vps-4 from ubuntu-latest 2025-02-19 12:24:55 +01:00
7bd06dee62
refactor(user): reorganize imports and code structure 2025-02-19 12:23:45 +01:00
72f56928d4
ci: add linelint job to CI workflow 2025-02-19 12:23:32 +01:00
6c025cc8d2
fix: lazy load login success component 2025-02-19 12:18:12 +01:00
642f5727e5
ci: update build output directory in CI workflow 2025-02-19 12:16:50 +01:00
8d4901601f
chore: adjust login success view 2025-02-19 12:09:53 +01:00
355d1b0c06
ci: update frontend build process in CI workflow 2025-02-19 12:09:22 +01:00
877b6f77b2
chore: move classes add balance fix routes 2025-02-19 12:06:49 +01:00
0590a2f9ee
build(ci): add caching for frontend dependencies and dist 2025-02-19 12:05:59 +01:00
ef8111cc7e
ci: update node_modules path in CI workflow config 2025-02-19 12:01:14 +01:00
a091387c1c
fix: 500 when loggin in 2025-02-19 11:59:36 +01:00
2392dac519
ci: update path in CI workflow configuration 2025-02-19 11:58:56 +01:00
fadedb0bcd
ci: Fix node_modules path in CI workflow configuration 2025-02-19 11:57:08 +01:00
59263dca71
ci: update path for node_modules in CI workflow 2025-02-19 11:54:35 +01:00
a19ddeed47
ci: update cache paths in CI configuration 2025-02-19 11:50:07 +01:00
bd26ded681
ci: update CI workflow for bun installation and caching 2025-02-19 11:45:20 +01:00
5afdbad461
refactor 2025-02-19 11:43:53 +01:00
2326d41a96
satisfy quality tools 2025-02-19 11:43:53 +01:00
df9fa9f275
fix: fix some stuff 2025-02-19 11:43:53 +01:00
793f3f6834
feat: add user creation on login (wip) 2025-02-19 11:43:49 +01:00
c55fcd9ea0
ci: update CI workflow to install dependencies correctly 2025-02-19 11:40:53 +01:00
7533f1139c
ci: update workflow trigger for bun.yml file 2025-02-19 11:38:18 +01:00
6fb3f2bef2
ci: remove time command from CI workflow script 2025-02-19 11:36:51 +01:00
bb24af241c
ci: add timing to node_modules symlink creation step 2025-02-19 11:35:44 +01:00
c258f1014d
ci: update dependency installation to use symlink 2025-02-19 11:30:20 +01:00
0b61ce11e1
build(ci): update container image for CI jobs 2025-02-19 11:27:01 +01:00
876d174f8f
ci: update CI workflow to remove bun installation step 2025-02-19 11:24:45 +01:00
c0e7f5f7f8
ci: add bun install to CI workflow for frontend setup 2025-02-19 11:21:29 +01:00
c7acd8271a
ci: update CI workflow to copy node_modules instead of bun 2025-02-19 11:19:48 +01:00
aaff4a543b
ci: update path in CI workflow for proper execution 2025-02-19 11:18:14 +01:00
82a3f4d195
ci: update CI workflow command to change directory first 2025-02-19 11:09:08 +01:00
483446cdd9
build: update Dockerfile to include CMD instruction 2025-02-19 11:08:43 +01:00
562a93bbf1
refactor(home): change HomeComponent to default export 2025-02-19 11:07:46 +01:00
840d6b5bfd
refactor(routes): change home route to lazy loading 2025-02-19 11:06:57 +01:00
36be142de1
ci: simplify command in CI workflow configuration 2025-02-19 11:04:31 +01:00
50d0782e02
ci: update CI workflow for frontend directory listing 2025-02-19 10:45:29 +01:00
4ce4f86419
ci: update checkout step in CI workflow to change dir 2025-02-19 10:44:04 +01:00
313950e998
ci: add command to list files in CI workflow 2025-02-19 10:42:43 +01:00
aa2bb187ce
ci: update CI container image for test-build job 2025-02-19 10:37:19 +01:00
c0ad17490e
build(Dockerfile): update Dockerfile for bun installation 2025-02-19 10:34:55 +01:00
4c376f8375
build: update Dockerfile and workflow context path 2025-02-19 10:33:12 +01:00
3433641025
ci: update paths in bun.yml for build triggers 2025-02-19 10:30:05 +01:00
9d8509731f
ci: update Docker password variable in workflow file 2025-02-19 10:27:06 +01:00
ba776f0ec1
ci: update Docker login action to version 3 2025-02-19 10:25:16 +01:00
f8db9221a6
ci: update login-action reference in workflow file 2025-02-19 10:24:36 +01:00
0d67c0e305
ci: add Docker.io login step to workflow configuration 2025-02-19 10:22:40 +01:00
586044a23d
chore: clean up Dockerfile by removing unused jobs section 2025-02-19 10:18:38 +01:00
a50b9be463
ci: update runner to vps-4 in workflow configuration 2025-02-19 10:16:43 +01:00
0d45d9659a
chore: rename bun-image.yml to bun.yml 2025-02-19 10:12:45 +01:00
ecdcea4994
ci: remove main branch restriction from workflow 2025-02-19 10:12:16 +01:00
a8a7d4296a
feat: add Dockerfile and CI workflow for image build 2025-02-19 10:11:18 +01:00
13245cdab6
ci: update CI workflow by removing unnecessary caching steps 2025-02-19 09:54:41 +01:00
9d4fb96daf
ci: update tree command in CI workflow 2025-02-19 09:52:09 +01:00
1bc2ca5f9a
ci: add tree command to CI workflow 2025-02-19 09:47:55 +01:00
Hop In, I Have Puppies AND WiFi
44c7d8be57 Merge pull request 'navbar: update navbar text to German language' (!35) from task/translate-navbar into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/35
Reviewed-by: We ball <jan@kjan.email>
2025-02-19 08:44:56 +00:00
aa39a42df6
docs(navbar): update navbar text to German language 2025-02-19 09:40:14 +01:00
455ebdbe91
ci: add caching for Gradle build outputs 2025-02-19 09:36:57 +01:00
204b205b44
ci: add caching for Gradle dependencies in CI workflow 2025-02-19 09:26:38 +01:00
3cd1f63dba Merge pull request 'chore(docker): remove version from docker-compose file' (!33) from task/adjust-docker-compose into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/33
Reviewed-by: We ball <jan@kjan.email>
2025-02-19 08:22:48 +00:00
1c5f5524fa
ci: remove caching step from CI workflow 2025-02-19 09:20:43 +01:00
0868ef0776
chore(docker): remove version from docker-compose file 2025-02-19 09:07:08 +01:00
Huy
695a1073a9 Merge pull request 'fix: Remove material css' (!31) from chore/remove-material-styles into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/31
2025-02-19 07:25:08 +00:00
Phan Huy Tran
47ca56deb4 fix: Remove material css 2025-02-13 13:19:12 +01:00
108 changed files with 4979 additions and 1040 deletions

View file

@ -4,42 +4,79 @@ on:
pull_request:
jobs:
changed_files:
name: Get Changed Files
runs-on: ubuntu-latest
outputs:
backend: ${{ steps.filter.outputs.backend }}
frontend: ${{ steps.filter.outputs.frontend }}
workflow: ${{ steps.filter.outputs.workflow }}
steps:
- uses: actions/checkout@v4
- name: Check for file changes
uses: dorny/paths-filter@v3
id: filter
with:
filters: |
backend:
- 'backend/**'
frontend:
- 'frontend/**'
workflow:
- '.gitea/workflows/**'
checkstyle:
name: "Checkstyle Main"
runs-on: "vps-4"
needs: changed_files
if: ${{ needs.changed_files.outputs.backend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
container:
image: "cimg/openjdk:22.0-node"
image: "cimg/openjdk:23.0-node"
steps:
- name: "Checkout"
uses: actions/checkout@v3
- name: Setup Java 22
uses: actions/setup-java@v3
with:
distribution: "temurin"
java-version: "22"
- uses: actions/cache@v3
working-directory: ./backend
uses: actions/checkout@v4
- name: "Cache Gradle dependencies"
uses: https://github.com/actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
backend/build/reports/checkstyle
key: gradle-${{ runner.os }}-${{ hashFiles('backend/**/*.java', 'backend/config/checkstyle/checkstyle.xml') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: "Prepare Gradle"
gradle-${{ runner.os }}-
- name: "Check cache for checkstyle reports"
id: check-cache
run: |
if [ -d "backend/build/reports/checkstyle" ] && [ -f "backend/build/reports/checkstyle/main.xml" ]; then
echo "Cache hit! Using cached checkstyle results"
echo "cache-hit=true" >> $GITHUB_OUTPUT
else
echo "Cache miss! Running checkstyle check"
echo "cache-hit=false" >> $GITHUB_OUTPUT
fi
- name: "Run checkstyle"
if: steps.check-cache.outputs.cache-hit != 'true'
working-directory: ./backend
run: gradle clean
- name: "Check"
working-directory: ./backend
run: gradle checkstyleMain
run: |
gradle checkstyleMain
- name: "Cache checkstyle results"
if: steps.check-cache.outputs.cache-hit != 'true'
uses: actions/upload-artifact@v4
with:
name: checkstyle-results
path: backend/build/reports/checkstyle
- name: "Stop Gradle"
if: steps.check-cache.outputs.cache-hit != 'true'
working-directory: ./backend
run: gradle --stop
eslint:
name: eslint
runs-on: vps-4
needs: changed_files
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
container:
image: catthehacker/ubuntu:act-latest
steps:
@ -47,6 +84,14 @@ jobs:
uses: actions/checkout@v4
- 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-
- name: Install dependencies
run: |
cd frontend
@ -58,7 +103,8 @@ jobs:
prettier:
name: prettier
runs-on: vps-4
needs: changed_files
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
container:
image: catthehacker/ubuntu:act-latest
steps:
@ -66,6 +112,14 @@ jobs:
uses: actions/checkout@v4
- 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-
- name: Install dependencies
run: |
cd frontend
@ -77,7 +131,8 @@ jobs:
test-build:
name: test-build
runs-on: vps-4
needs: changed_files
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
container:
image: catthehacker/ubuntu:act-latest
steps:
@ -85,6 +140,22 @@ jobs:
uses: actions/checkout@v4
- 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-
- uses: actions/cache@v4
working-directory: ./frontend
with:
path: |
frontend/dist/
key: ${{ runner.os }}-dist-
restore-keys: |
${{ runner.os }}-dist-
- name: Install dependencies
run: |
cd frontend

View file

@ -3,6 +3,11 @@ on:
push:
branches:
- "main"
paths:
- 'backend/**'
- 'frontend/**'
- '.gitea/workflows/release.yml'
- 'release.config.cjs'
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
@ -13,7 +18,6 @@ permissions:
jobs:
release:
name: Release
runs-on: vps-4
permissions:
contents: write
issues: write

41
CLAUDE.md Normal file
View file

@ -0,0 +1,41 @@
# Casino Gaming Platform - Claude Assistant Guide
## Build Commands
### Frontend
- Build: `bun run build` or `bunx @angular/cli build`
- Start dev server: `bun run start` or `bunx @angular/cli serve --proxy-config src/proxy.conf.json`
- Format: `bun run format` or `prettier --write "src/**/*.{ts,html,css,scss}"`
### Backend
- Build: `./gradlew build` or `./gradlew clean build`
- Run: `./gradlew bootRun`
- Generate JAR: `./gradlew bootJar`
## Lint/Test Commands
### Frontend
- Lint: `bun run lint` or `ng lint`
- Test all: `bun run test` or `bunx @angular/cli test`
- Test single file: `bunx @angular/cli test --include=path/to/test.spec.ts`
### Backend
- Test all: `./gradlew test`
- Test single class: `./gradlew test --tests "FullyQualifiedClassName"`
- Checkstyle: `./gradlew checkstyleMain checkstyleTest`
## Code Style Guidelines
### Frontend (Angular)
- Use PascalCase for class names with suffixes (Component, Service)
- Use kebab-case for component selectors with "app-" prefix
- File naming: `name.component.ts`, `name.service.ts`
- Import order: Angular → third-party → local
- Use RxJS catchError for HTTP error handling
### Backend (Java)
- Use PascalCase for classes with descriptive suffixes (Controller, Service, Entity)
- Use camelCase for methods and variables
- Domain-driven package organization
- Prefix DTOs with domain and suffix with "Dto"
- Use Spring's global exception handling with custom exceptions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Casino Gaming Platform
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

186
README.md
View file

@ -1,34 +1,172 @@
# How to: Semantic Commit Messages
# Casino Gaming Platform
See how a minor change to your commit message style can make you a better programmer.
An online gaming platform offering various casino-style games with virtual currency support. This project features a modern tech stack with Angular frontend, Spring Boot backend, and complete user authentication.
Please refer to our [Style Guide](https://git.kjan.de/SZUT/casino/wiki/Frontend#design-system) for design guidelines and component standards.
## Features
- Multiple casino games: Poker, Blackjack, Slots, Plinko, Liars Dice, and Lootboxes
- User authentication and account management via Keycloak
- Virtual currency deposit system using Stripe payments
- Transaction history tracking
- Responsive modern UI built with Angular and TailwindCSS
## Tech Stack
### Frontend
- Angular 18
- TailwindCSS
- Keycloak integration
- Stripe payment integration
### Backend
- Spring Boot (Java)
- PostgreSQL database
- Keycloak for authentication/authorization
- Stripe API for payment processing
### Infrastructure
- Docker containerization for all services
## Getting Started
### Prerequisites
* [Docker](https://docs.docker.com/get-docker/)
* [Docker Compose](https://docs.docker.com/compose/install/) (included with Docker Desktop for Windows and Mac)
* Java JDK 17+
* Node.js 18+
### Setting Up the Environment
1. Clone the repository
```bash
git clone <repository-url>
cd casino
```
2. Start the Docker services
```bash
cd docker
docker-compose up -d
```
This will start:
- PostgreSQL database
- Keycloak authentication server
### Running the Backend
1. Navigate to the backend directory
```bash
cd backend
```
2. Start the Spring Boot application
```bash
./gradlew bootRun
```
You may optionally install [watchexec](https://github.com/watchexec/watchexec?tab=readme-ov-file) and use this command to autorecompile the backend on file changes:
```bash
watchexec -r -e java ./gradlew :bootRun
```
The backend will be available at:
- API endpoint: http://localhost:8080
- Swagger documentation: http://localhost:8080/swagger
### Running the Frontend
1. Navigate to the frontend directory
```bash
cd frontend
```
2. Install dependencies
```bash
npm install
```
3. Start the development server
```bash
npm run dev
```
The frontend will be available at http://localhost:4200
### Local Stripe integration
1. Install the Stripe CLI
https://stripe.com/docs/stripe-cli
2. Login to the casino stripe account
```
stripe login --api-key <casino-stripe-secret-key>
```
3. Start webhook forwarding
```
stripe listen --forward-to localhost:8080/webhook
```
## Database Management
### Postgres Management
#### Database cleanup (if needed)
```bash
cd docker
docker-compose down
docker volume rm local_lf8_starter_postgres_data
docker-compose up -d
```
#### Setting up IntelliJ Database View
1. Run the Docker container with PostgreSQL database
2. Open `application.properties` in the resources folder and copy the database URL
3. Open the Database tab in IntelliJ
4. Click on the database icon with key in the Database toolbar
5. Click the plus sign and select "Datasource from URL"
6. Paste the DB URL and select PostgreSQL driver, confirm with OK
7. Enter username `lf8_starter` and password `secret`
8. In the Schemas tab, uncheck all options and only check `lf8_starter_db` and `public`
## Authentication
The application uses Keycloak for authentication. To get a bearer token for API testing:
1. Open `requests/getBearerToken.http`
2. Click the green arrow next to the request
3. Copy the `access_token` from the response
## Development Guidelines
### Commit Message Format
We follow semantic commit messages to maintain clear project history.
Format: `<type>(<scope>): <subject>`
`<scope>` is optional
## Example
Where `<type>` is one of:
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation changes
- `style`: Formatting, missing semicolons, etc; no code change
- `refactor`: Code refactoring
- `test`: Adding or refactoring tests
- `chore`: Updating build tasks, etc; no production code change
Examples:
```
feat: add hat wobble
^--^ ^------------^
| |
| +-> Summary in present tense.
|
+-------> Type: chore, docs, feat, fix, refactor, style, or test.
feat: add user balance display
fix(auth): resolve token expiration issue
docs: update API documentation
```
More Examples:
- `feat`: (new feature for the user, not a new feature for build script)
- `fix`: (bug fix for the user, not a fix to a build script)
- `docs`: (changes to the documentation)
- `style`: (formatting, missing semi colons, etc; no production code change)
- `refactor`: (refactoring production code, eg. renaming a variable)
- `test`: (adding missing tests, refactoring tests; no production code change)
- `chore`: (updating grunt tasks etc; no production code change)
References:
- [Conventional Commits](https://www.conventionalcommits.org/)
- [Semantic Commit Messages](https://seesparkbox.com/foundry/semantic_commit_messages)
- https://www.conventionalcommits.org/
- https://seesparkbox.com/foundry/semantic_commit_messages
- http://karma-runner.github.io/1.0/dev/git-commit-msg.html
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

View file

@ -1,7 +1,7 @@
plugins {
java
id("org.springframework.boot") version "3.3.3"
id("io.spring.dependency-management") version "1.1.6"
id("org.springframework.boot") version "3.4.4"
id("io.spring.dependency-management") version "1.1.7"
id("checkstyle")
}
@ -24,7 +24,7 @@ version = "0.0.1-SNAPSHOT"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(22)
languageVersion = JavaLanguageVersion.of(23)
}
}
@ -39,7 +39,7 @@ repositories {
}
dependencies {
implementation("com.stripe:stripe-java:20.79.0")
implementation("com.stripe:stripe-java:29.0.0")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
compileOnly("org.projectlombok:lombok")
@ -47,10 +47,10 @@ dependencies {
testImplementation("org.springframework.boot:spring-boot-starter-test")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server:3.3.3")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.3.3")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server:3.4.4")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.4.4")
runtimeOnly("org.postgresql:postgresql")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6")
}
tasks.withType<Test> {

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View file

@ -0,0 +1,20 @@
POST http://localhost:8080/blackjack/start
Authorization: Bearer {{token}}
Content-Type: application/json
{
"betAmount": 1.01
}
###
POST http://localhost:8080/blackjack/54/hit
Authorization: Bearer {{token}}
###
POST http://localhost:8080/blackjack/202/stand
Authorization: Bearer {{token}}
###
GET http://localhost:8080/blackjack/202
Authorization: Bearer {{token}}

View file

@ -1,6 +1,6 @@
POST http://localhost:9090/realms/LF12/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=password&client_id=lf12&username=lf12_test_user&password=secret
grant_type=password&client_id=lf12&username=lf12_test_user&password=secret&scope=openid
> {% client.global.set("token", response.body.access_token); %}

View file

@ -0,0 +1,9 @@
GET http://localhost:8080/lootboxes
Authorization: Bearer {{token}}
Content-Type: application/json
###
POST http://localhost:8080/lootboxes/2
Authorization: Bearer {{token}}
Content-Type: application/json

View file

@ -0,0 +1,27 @@
### Get User by ID
GET http://localhost:8080/user/52cc0208-a3bd-4367-94c5-0404b016a003
Authorization: Bearer {{token}}
### Get current user with token
GET http://localhost:8080/user
Authorization: Bearer {{token}}
### Create User
POST http://localhost:8080/user
Content-Type: application/json
Authorization: Bearer {{token}}
{
"authentikId": "52cc0208-a3bd-4367-94c5-0404b016a003",
"username": "john.doe"
}
### Deposit
POST http://localhost:8080/deposit/checkout
Content-Type: application/json
Origin: http://localhost:8080
Authorization: Bearer {{token}}
{
"amount": 60.12
}

View file

@ -0,0 +1 @@
POST localhost:8080/webhook

View file

@ -1,10 +1,20 @@
package de.szut.casino;
import de.szut.casino.lootboxes.LootBoxEntity;
import de.szut.casino.lootboxes.LootBoxRepository;
import de.szut.casino.lootboxes.RewardEntity;
import de.szut.casino.lootboxes.RewardRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@SpringBootApplication
public class CasinoApplication {
@ -16,4 +26,65 @@ public class CasinoApplication {
public static RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
public CommandLineRunner initData(LootBoxRepository lootBoxRepository, RewardRepository rewardRepository) {
return _ -> {
if (lootBoxRepository.count() == 0) {
LootBoxEntity basicLootBox = new LootBoxEntity();
basicLootBox.setName("Basic LootBox");
basicLootBox.setPrice(new BigDecimal("2"));
basicLootBox.setRewards(new ArrayList<>()); // Initialize the list
LootBoxEntity premiumLootBox = new LootBoxEntity();
premiumLootBox.setName("Premium LootBox");
premiumLootBox.setPrice(new BigDecimal("5"));
premiumLootBox.setRewards(new ArrayList<>()); // Initialize the list
lootBoxRepository.saveAll(Arrays.asList(basicLootBox, premiumLootBox));
RewardEntity commonReward = new RewardEntity();
commonReward.setValue(new BigDecimal("0.50"));
commonReward.setProbability(new BigDecimal("0.7"));
RewardEntity rareReward = new RewardEntity();
rareReward.setValue(new BigDecimal("2.00"));
rareReward.setProbability(new BigDecimal("0.25"));
RewardEntity epicReward = new RewardEntity();
epicReward.setValue(new BigDecimal("5.00"));
epicReward.setProbability(new BigDecimal("0.5"));
RewardEntity premiumCommon = new RewardEntity();
premiumCommon.setValue(new BigDecimal("2.00"));
premiumCommon.setProbability(new BigDecimal("0.6"));
RewardEntity premiumRare = new RewardEntity();
premiumRare.setValue(new BigDecimal("5.00"));
premiumRare.setProbability(new BigDecimal("0.3"));
RewardEntity legendaryReward = new RewardEntity();
legendaryReward.setValue(new BigDecimal("15.00"));
legendaryReward.setProbability(new BigDecimal("0.10"));
rewardRepository.saveAll(Arrays.asList(
commonReward, rareReward, epicReward,
premiumCommon, premiumRare, legendaryReward
));
basicLootBox.getRewards().add(commonReward);
basicLootBox.getRewards().add(premiumRare);
premiumLootBox.getRewards().add(premiumCommon);
premiumLootBox.getRewards().add(premiumRare);
premiumLootBox.getRewards().add(legendaryReward);
lootBoxRepository.saveAll(Arrays.asList(basicLootBox, premiumLootBox));
System.out.println("Initial LootBoxes and rewards created successfully");
} else {
System.out.println("LootBoxes already exist, skipping initialization");
}
};
}
}

View file

@ -0,0 +1,140 @@
package de.szut.casino.blackjack;
import de.szut.casino.blackjack.dto.CreateBlackJackGameDto;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserService;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@Slf4j
@RestController
public class BlackJackGameController {
private final UserService userService;
private final BlackJackService blackJackService;
public BlackJackGameController(UserService userService, BlackJackService blackJackService) {
this.blackJackService = blackJackService;
this.userService = userService;
}
@GetMapping("/blackjack/{id}")
public ResponseEntity<Object> getGame(@PathVariable Long id, @RequestHeader("Authorization") String token) {
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
if (optionalUser.isEmpty()) {
return ResponseEntity.notFound().build();
}
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);
}
@PostMapping("/blackjack/{id}/hit")
public ResponseEntity<Object> hit(@PathVariable Long id, @RequestHeader("Authorization") String token) {
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
if (optionalUser.isEmpty()) {
return ResponseEntity.notFound().build();
}
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));
}
@PostMapping("/blackjack/{id}/stand")
public ResponseEntity<Object> stand(@PathVariable Long id, @RequestHeader("Authorization") String token) {
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
if (optionalUser.isEmpty()) {
return ResponseEntity.notFound().build();
}
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));
}
@PostMapping("/blackjack/{id}/doubleDown")
public ResponseEntity<Object> doubleDown(@PathVariable Long id, @RequestHeader("Authorization") String token) {
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
if (optionalUser.isEmpty()) {
return ResponseEntity.notFound().build();
}
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));
}
@PostMapping("/blackjack/{id}/split")
public ResponseEntity<Object> split(@PathVariable Long id, @RequestHeader("Authorization") String token) {
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
if (optionalUser.isEmpty()) {
return ResponseEntity.notFound().build();
}
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")
public ResponseEntity<Object> createBlackJackGame(@RequestBody @Valid CreateBlackJackGameDto createBlackJackGameDto, @RequestHeader("Authorization") String token) {
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
if (optionalUser.isEmpty()) {
return ResponseEntity.notFound().build();
}
UserEntity user = optionalUser.get();
BigDecimal balance = user.getBalance();
BigDecimal betAmount = createBlackJackGameDto.getBetAmount();
if (betAmount.compareTo(BigDecimal.ZERO) <= 0) {
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("error", "Invalid bet amount");
return ResponseEntity.badRequest().body(errorResponse);
}
if (betAmount.compareTo(balance) > 0) {
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("error", "Insufficient funds");
return ResponseEntity.badRequest().body(errorResponse);
}
return ResponseEntity.ok(blackJackService.createBlackJackGame(user, betAmount));
}
}

View file

@ -0,0 +1,65 @@
package de.szut.casino.blackjack;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import de.szut.casino.user.UserEntity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.SQLRestriction;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class BlackJackGameEntity {
@Id
@GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
@JsonIgnore
private UserEntity user;
public Long getUserId() {
return user != null ? user.getId() : null;
}
@Enumerated(EnumType.STRING)
private BlackJackState state;
private BigDecimal bet;
@OneToMany(mappedBy = "game", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore
@SQLRestriction("card_type = 'DECK'")
private List<CardEntity> deck = new ArrayList<>();
@OneToMany(mappedBy = "game", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference
@SQLRestriction("card_type = 'PLAYER'")
private List<CardEntity> playerCards = new ArrayList<>();
@OneToMany(mappedBy = "game", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference
@SQLRestriction("card_type = 'DEALER'")
private List<CardEntity> dealerCards = new ArrayList<>();
@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;
}

View file

@ -0,0 +1,12 @@
package de.szut.casino.blackjack;
import de.szut.casino.user.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public interface BlackJackGameRepository extends JpaRepository<BlackJackGameEntity, Long> {
}

View file

@ -0,0 +1,310 @@
package de.szut.casino.blackjack;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
import java.util.Random;
@Service
public class BlackJackService {
private final BlackJackGameRepository blackJackGameRepository;
private final UserRepository userRepository;
private final Random random = new Random();
public BlackJackService(BlackJackGameRepository blackJackGameRepository, UserRepository userRepository) {
this.blackJackGameRepository = blackJackGameRepository;
this.userRepository = userRepository;
}
public BlackJackGameEntity getBlackJackGame(Long id) {
return blackJackGameRepository.findById(id).orElse(null);
}
@Transactional
public BlackJackGameEntity createBlackJackGame(UserEntity user, BigDecimal betAmount) {
BlackJackGameEntity game = new BlackJackGameEntity();
game.setUser(user);
game.setBet(betAmount);
initializeDeck(game);
dealInitialCards(game);
game.setState(getState(game));
deductBetFromBalance(user, betAmount);
return blackJackGameRepository.save(game);
}
@Transactional
public BlackJackGameEntity hit(BlackJackGameEntity game) {
if (game.getState() != BlackJackState.IN_PROGRESS) {
return game;
}
dealCardToPlayer(game);
updateGameStateAndBalance(game);
return blackJackGameRepository.save(game);
}
@Transactional
public BlackJackGameEntity stand(BlackJackGameEntity game) {
if (game.getState() != BlackJackState.IN_PROGRESS) {
return game;
}
dealCardsToDealerUntilMinimumScore(game);
determineWinnerAndUpdateBalance(game);
return blackJackGameRepository.save(game);
}
@Transactional
public BlackJackGameEntity doubleDown(BlackJackGameEntity game) {
if (game.getState() != BlackJackState.IN_PROGRESS || game.getPlayerCards().size() != 2) {
return game;
}
UserEntity user = getUserWithFreshData(game.getUser());
BigDecimal additionalBet = game.getBet();
deductBetFromBalance(user, additionalBet);
game.setBet(game.getBet().add(additionalBet));
dealCardToPlayer(game);
updateGameStateAndBalance(game);
if (game.getState() == BlackJackState.IN_PROGRESS) {
return stand(game);
}
return game;
}
@Transactional
public BlackJackGameEntity split(BlackJackGameEntity game) {
if (game.getState() != BlackJackState.IN_PROGRESS ||
game.getPlayerCards().size() != 2 ||
game.isSplit() ||
!game.getPlayerCards().get(0).getRank().equals(game.getPlayerCards().get(1).getRank())) {
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);
}
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) {
if (game.isSplit()) {
int mainHandValue = calculateHandValue(game.getPlayerCards());
int splitHandValue = calculateHandValue(game.getPlayerSplitCards());
if (mainHandValue > 21 && splitHandValue > 21) {
game.setState(BlackJackState.PLAYER_LOST);
updateUserBalance(game, false);
} else if (mainHandValue <= 21 && splitHandValue <= 21) {
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);
}
}
}
private void determineWinnerAndUpdateBalance(BlackJackGameEntity game) {
int playerValue = calculateHandValue(game.getPlayerCards());
int dealerValue = calculateHandValue(game.getDealerCards());
if (dealerValue > 21 || playerValue > dealerValue) {
game.setState(BlackJackState.PLAYER_WON);
updateUserBalance(game, true);
} else if (playerValue < dealerValue) {
game.setState(BlackJackState.PLAYER_LOST);
updateUserBalance(game, false);
} else {
game.setState(BlackJackState.DRAW);
updateUserBalance(game, false);
}
}
private void deductBetFromBalance(UserEntity user, BigDecimal betAmount) {
user.setBalance(user.getBalance().subtract(betAmount));
userRepository.save(user);
}
@Transactional
private void updateUserBalance(BlackJackGameEntity game, boolean isWin) {
UserEntity user = getUserWithFreshData(game.getUser());
BigDecimal totalBet = game.getBet();
BigDecimal balance = user.getBalance();
if (game.isSplit()) {
totalBet = totalBet.add(game.getSplitBet());
if (isWin) {
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);
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) {
int playerHandValue = calculateHandValue(game.getPlayerCards());
if (playerHandValue == 21) {
CardEntity hole = drawCardFromDeck(game);
hole.setCardType(CardType.DEALER);
game.getDealerCards().add(hole);
int dealerHandValue = calculateHandValue(game.getDealerCards());
if (dealerHandValue == 21) {
return BlackJackState.DRAW;
} else {
BigDecimal blackjackWinnings = game.getBet().multiply(new BigDecimal("1.5"));
UserEntity user = getUserWithFreshData(game.getUser());
user.setBalance(user.getBalance().add(blackjackWinnings));
return BlackJackState.PLAYER_BLACKJACK;
}
} else if (playerHandValue > 21) {
return BlackJackState.PLAYER_LOST;
}
return BlackJackState.IN_PROGRESS;
}
private int calculateHandValue(List<CardEntity> hand) {
int sum = 0;
int aceCount = 0;
for (CardEntity card : hand) {
sum += card.getRank().getValue();
if (card.getRank() == Rank.ACE) {
aceCount++;
}
}
while (sum > 21 && aceCount > 0) {
sum -= 10;
aceCount--;
}
return sum;
}
}

View file

@ -0,0 +1,9 @@
package de.szut.casino.blackjack;
public enum BlackJackState {
IN_PROGRESS,
PLAYER_BLACKJACK,
PLAYER_LOST,
PLAYER_WON,
DRAW,
}

View file

@ -0,0 +1,40 @@
package de.szut.casino.blackjack;
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class CardEntity {
@Id
@GeneratedValue
@JsonIgnore
private Long id;
@ManyToOne
@JoinColumn(name = "game_id", nullable = false)
@JsonBackReference
private BlackJackGameEntity game;
@Enumerated(EnumType.STRING)
private Suit suit;
@Enumerated(EnumType.STRING)
private Rank rank;
@Enumerated(EnumType.STRING)
@JsonIgnore
private CardType cardType;
}
enum CardType {
DECK, PLAYER, DEALER, PLAYER_SPLIT
}

View file

@ -0,0 +1,31 @@
package de.szut.casino.blackjack;
import lombok.Getter;
@Getter
public enum Rank {
TWO("2", "Two", 2),
THREE("3", "Three", 3),
FOUR("4", "Four", 4),
FIVE("5", "Five", 5),
SIX("6", "Six", 6),
SEVEN("7", "Seven", 7),
EIGHT("8", "Eight", 8),
NINE("9", "Nine", 9),
TEN("10", "Ten", 10),
JACK("J", "Jack", 10),
QUEEN("Q", "Queen", 10),
KING("K", "King", 10),
ACE("A", "Ace", 11);
private final String symbol;
private final String displayName;
private final int value;
Rank(String symbol, String displayName, int value) {
this.symbol = symbol;
this.displayName = displayName;
this.value = value;
}
}

View file

@ -0,0 +1,20 @@
package de.szut.casino.blackjack;
import lombok.Getter;
@Getter
public enum Suit {
HEARTS("H", "Hearts"),
DIAMONDS("D", "Diamonds"),
CLUBS("C", "Clubs"),
SPADES("S", "Spades");
private final String symbol;
private final String displayName;
Suit(String symbol, String displayName) {
this.symbol = symbol;
this.displayName = displayName;
}
}

View file

@ -0,0 +1,16 @@
package de.szut.casino.blackjack.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.math.BigDecimal;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class CreateBlackJackGameDto {
private BigDecimal betAmount;
}

View file

@ -3,20 +3,23 @@ package de.szut.casino.deposit;
import com.stripe.Stripe;
import com.stripe.exception.StripeException;
import com.stripe.model.checkout.Session;
import com.stripe.param.InvoiceItemCreateParams;
import com.stripe.param.PriceCreateParams;
import com.stripe.param.checkout.SessionCreateParams;
import de.szut.casino.deposit.dto.AmountDto;
import de.szut.casino.deposit.dto.SessionIdDto;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserRepository;
import de.szut.casino.user.UserService;
import de.szut.casino.user.dto.KeycloakUserDto;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.cassandra.CassandraProperties;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.util.Optional;
@RestController
public class DepositController {
@ -24,27 +27,62 @@ public class DepositController {
@Value("${stripe.secret.key}")
private String stripeKey;
@Value("${app.frontend-host}")
private String frontendHost;
private final TransactionService transactionService;
private final RestTemplate restTemplate;
private final UserRepository userRepository;
public DepositController(TransactionService transactionService, RestTemplate restTemplate, UserRepository userRepository) {
this.transactionService = transactionService;
this.restTemplate = restTemplate;
this.userRepository = userRepository;
}
@PostMapping("/deposit/checkout")
public ResponseEntity<SessionIdDto> checkout(
@RequestBody @Valid AmountDto amountDto,
@RequestHeader("Origin") String origin
) throws StripeException {
public ResponseEntity<SessionIdDto> checkout(@RequestBody @Valid AmountDto amountDto, @RequestHeader("Authorization") String token) throws StripeException {
Stripe.apiKey = stripeKey;
KeycloakUserDto userData = getAuthentikUserInfo(token);
Optional<UserEntity> optionalUserEntity = this.userRepository.findOneByAuthentikId(userData.getSub());
SessionCreateParams params = SessionCreateParams.builder()
.addLineItem(SessionCreateParams.LineItem.builder()
.setAmount((long) amountDto.getAmount() * 100)
.setCurrency("EUR")
.setPriceData(SessionCreateParams.LineItem.PriceData.builder()
.setCurrency("EUR")
.setUnitAmount((long) amountDto.getAmount() * 100)
.setProductData(SessionCreateParams.LineItem.PriceData.ProductData.builder()
.setName("Einzahlung")
.build())
.build())
.setQuantity(1L)
.setName("Einzahlung")
.build())
.setSuccessUrl(origin+"/deposit/success")
.setSuccessUrl(frontendHost+"/home?success=true")
.setCancelUrl(frontendHost+"/home?success=false")
.setMode(SessionCreateParams.Mode.PAYMENT)
.build();
Session session = Session.create(params);
if (optionalUserEntity.isEmpty()) {
throw new RuntimeException("User doesnt exist");
}
transactionService.createTransaction(optionalUserEntity.get(), session.getId(), amountDto.getAmount());
return ResponseEntity.ok(new SessionIdDto(session.getId()));
}
private KeycloakUserDto getAuthentikUserInfo(String token) {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", token);
ResponseEntity<KeycloakUserDto> response = this.restTemplate.exchange("https://oauth.simonis.lol/application/o/userinfo/", HttpMethod.GET, new HttpEntity<>(headers), KeycloakUserDto.class);
return response.getBody();
}
}

View file

@ -0,0 +1,29 @@
package de.szut.casino.deposit;
import de.szut.casino.user.UserEntity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
@Setter
@Getter
@Entity
public class TransactionEntity {
@Id
@GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private UserEntity user;
@Column(unique = true)
private String sessionId = null;
private double amount = 0;
@Enumerated(EnumType.STRING)
private TransactionStatus status = TransactionStatus.PROCESSING;
}

View file

@ -0,0 +1,14 @@
package de.szut.casino.deposit;
import de.szut.casino.user.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public interface TransactionRepository extends JpaRepository<TransactionEntity, Long> {
@Query("SELECT t FROM TransactionEntity t WHERE t.sessionId = ?1")
Optional<TransactionEntity> findOneBySessionID(String sessionId);
}

View file

@ -0,0 +1,64 @@
package de.szut.casino.deposit;
import com.stripe.exception.StripeException;
import com.stripe.model.checkout.Session;
import com.stripe.param.checkout.SessionRetrieveParams;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserRepository;
import org.springframework.stereotype.Service;
import java.util.Objects;
import java.util.Optional;
@Service
public class TransactionService {
private final TransactionRepository transactionRepository;
private final UserRepository userRepository;
public TransactionService(TransactionRepository transactionRepository, UserRepository userRepository) {
this.transactionRepository = transactionRepository;
this.userRepository = userRepository;
}
public void createTransaction(
UserEntity user,
String sessionID,
Double amount
) {
TransactionEntity transaction = new TransactionEntity();
transaction.setUser(user);
transaction.setSessionId(sessionID);
transaction.setAmount(amount);
transactionRepository.save(transaction);
}
public void fulfillCheckout(String sessionID) throws StripeException {
SessionRetrieveParams params = SessionRetrieveParams.builder()
.addExpand("line_items")
.build();
Session checkoutSession = Session.retrieve(sessionID, params, null);
if (!"paid".equals(checkoutSession.getPaymentStatus())) {
return;
}
Optional<TransactionEntity> optionalTransaction = transactionRepository.findOneBySessionID(sessionID);
if (optionalTransaction.isEmpty()) {
throw new RuntimeException("Transaction not found");
}
TransactionEntity transaction = optionalTransaction.get();
transaction.setStatus(TransactionStatus.SUCCEEDED);
UserEntity user = transaction.getUser();
Long amountTotal = checkoutSession.getAmountTotal();
if (amountTotal != null) {
user.addBalance(amountTotal);
}
userRepository.save(user);
transactionRepository.save(transaction);
}
}

View file

@ -0,0 +1,6 @@
package de.szut.casino.deposit;
public enum TransactionStatus {
PROCESSING,
SUCCEEDED,
}

View file

@ -0,0 +1,70 @@
package de.szut.casino.deposit;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.stripe.Stripe;
import com.stripe.exception.SignatureVerificationException;
import com.stripe.exception.StripeException;
import com.stripe.model.*;
import com.stripe.model.checkout.Session;
import com.stripe.net.Webhook;
import com.stripe.param.checkout.SessionRetrieveParams;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserRepository;
import jakarta.annotation.PostConstruct;
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 java.math.BigDecimal;
import java.util.Objects;
import java.util.Optional;
@RestController
public class WebhookController {
private static final Logger logger = LoggerFactory.getLogger(WebhookController.class);
@Value("${stripe.secret.key}")
private String stripeSecretKey;
@Value("${stripe.webhook.secret}")
private String webhookSecret;
private final TransactionService transactionService;
public WebhookController(TransactionService transactionService) {
this.transactionService = transactionService;
}
@PostConstruct
public void init() {
Stripe.apiKey = stripeSecretKey;
}
@PostMapping("/webhook")
public ResponseEntity<String> webhook(@RequestBody String payload, @RequestHeader("Stripe-Signature") String sigHeader) throws StripeException {
Event event = Webhook.constructEvent(payload, sigHeader, webhookSecret);
System.out.println(event.getType());
switch (event.getType()) {
case "checkout.session.completed":
case "checkout.session.async_payment_succeeded":
EventDataObjectDeserializer dataObjectDeserializer = event.getDataObjectDeserializer();
if (dataObjectDeserializer.getObject().isPresent()) {
Session session = (Session) dataObjectDeserializer.getObject().get();
this.transactionService.fulfillCheckout(session.getId());
} else {
logger.error("Failed to deserialize webhook event data");
}
break;
default:
// No action needed for other event types
break;
}
return ResponseEntity.ok().body(null);
}
}

View file

@ -0,0 +1,58 @@
package de.szut.casino.lootboxes;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserRepository;
import de.szut.casino.user.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@RestController
public class LootBoxController {
private final LootBoxRepository lootBoxRepository;
private final UserService userService;
private final LootBoxService lootBoxService;
public LootBoxController(LootBoxRepository lootBoxRepository, UserRepository userRepository, UserService userService, LootBoxService lootBoxService) {
this.lootBoxRepository = lootBoxRepository;
this.userService = userService;
this.lootBoxService = lootBoxService;
}
@GetMapping("/lootboxes")
public List<LootBoxEntity> getAllLootBoxes() {
return lootBoxRepository.findAll();
}
@PostMapping("/lootboxes/{id}")
public ResponseEntity<Object> purchaseLootBox(@PathVariable Long id, @RequestHeader("Authorization") String token) {
Optional<LootBoxEntity> optionalLootBox = lootBoxRepository.findById(id);
if (optionalLootBox.isEmpty()) {
return ResponseEntity.notFound().build();
}
LootBoxEntity lootBox = optionalLootBox.get();
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
if (optionalUser.isEmpty()) {
return ResponseEntity.notFound().build();
}
UserEntity user = optionalUser.get();
if (lootBoxService.hasSufficientBalance(user, lootBox.getPrice())) {
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("error", "Insufficient balance");
return ResponseEntity.badRequest().body(errorResponse);
}
RewardEntity reward = lootBoxService.determineReward(lootBox);
lootBoxService.handleBalance(user, lootBox, reward);
return ResponseEntity.ok(reward);
}
}

View file

@ -0,0 +1,40 @@
package de.szut.casino.lootboxes;
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import de.szut.casino.blackjack.CardEntity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.SQLRestriction;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class LootBoxEntity {
@Id
@GeneratedValue
private Long id;
private String name;
@Column(precision = 19, scale = 2)
private BigDecimal price;
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinTable(
name = "lootbox_reward",
joinColumns = @JoinColumn(name = "lootbox_id"),
inverseJoinColumns = @JoinColumn(name = "reward_id")
)
private List<RewardEntity> rewards = new ArrayList<>();
}

View file

@ -0,0 +1,8 @@
package de.szut.casino.lootboxes;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Service;
@Service
public interface LootBoxRepository extends JpaRepository<LootBoxEntity, Long> {
}

View file

@ -0,0 +1,40 @@
package de.szut.casino.lootboxes;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserRepository;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class LootBoxService {
private final UserRepository userRepository;
public LootBoxService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public boolean hasSufficientBalance(UserEntity user, BigDecimal price) {
return user.getBalance().compareTo(price) < 0;
}
public RewardEntity determineReward(LootBoxEntity lootBox) {
double randomValue = Math.random();
BigDecimal cumulativeProbability = BigDecimal.ZERO;
for (RewardEntity reward : lootBox.getRewards()) {
cumulativeProbability = cumulativeProbability.add(reward.getProbability());
if (randomValue <= cumulativeProbability.doubleValue()) {
return reward;
}
}
return lootBox.getRewards().getLast();
}
public void handleBalance(UserEntity user, LootBoxEntity lootBox, RewardEntity reward) {
user.setBalance(user.getBalance().subtract(lootBox.getPrice()));
user.setBalance(user.getBalance().add(reward.getValue()));
userRepository.save(user);
}
}

View file

@ -0,0 +1,30 @@
package de.szut.casino.lootboxes;
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
@Entity
public class RewardEntity {
@Id
@GeneratedValue
private Long id;
@Column(precision = 19, scale = 2)
private BigDecimal value;
@Column(precision = 5, scale = 2)
private BigDecimal probability;
@ManyToMany(mappedBy = "rewards")
@JsonBackReference
private List<LootBoxEntity> lootBoxes = new ArrayList<>();
}

View file

@ -0,0 +1,8 @@
package de.szut.casino.lootboxes;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Service;
@Service
public interface RewardRepository extends JpaRepository<RewardEntity, Long> {
}

View file

@ -0,0 +1,24 @@
package de.szut.casino.security;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
public class CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
@Override
public AbstractAuthenticationToken convert(Jwt source) {
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return converter.convert(source);
}
public <U> Converter<Jwt, U> andThen(Converter<? super AbstractAuthenticationToken, ? extends U> after) {
return Converter.super.andThen(after);
}
}

View file

@ -1,48 +0,0 @@
package de.szut.casino.security;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
@Slf4j
@Component
public class KeycloakLogoutHandler implements LogoutHandler {
private final RestTemplate restTemplate;
public KeycloakLogoutHandler(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication auth) {
logout(request, auth);
}
public void logout(HttpServletRequest request, Authentication auth) {
logoutFromKeycloak((OidcUser) auth.getPrincipal());
}
private void logoutFromKeycloak(OidcUser user) {
String endSessionEndpoint = user.getIssuer() + "/protocol/openid-connect/logout";
UriComponentsBuilder builder = UriComponentsBuilder
.fromUriString(endSessionEndpoint)
.queryParam("id_token_hint", user.getIdToken().getTokenValue());
ResponseEntity<String> logoutResponse = restTemplate.getForEntity(builder.toUriString(), String.class);
if (logoutResponse.getStatusCode().is2xxSuccessful()) {
log.info("Successfulley logged out from Keycloak");
} else {
log.error("Could not propagate logout to Keycloak");
}
}
}

View file

@ -1,82 +0,0 @@
package de.szut.casino.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Configuration
@EnableWebSecurity
class KeycloakSecurityConfig {
private static final String GROUPS = "groups";
private static final String REALM_ACCESS_CLAIM = "realm_access";
private static final String ROLES_CLAIM = "roles";
private final KeycloakLogoutHandler keycloakLogoutHandler;
KeycloakSecurityConfig(KeycloakLogoutHandler keycloakLogoutHandler) {
this.keycloakLogoutHandler = keycloakLogoutHandler;
}
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(sessionRegistry());
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
@Bean
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/swagger", "/swagger-ui/**", "/v3/api-docs/**", "/health").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(spec -> spec.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwt -> {
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
Map<String, Object> realmAccess = jwt.getClaim("realm_access");
if (realmAccess != null && realmAccess.containsKey("roles")) {
List<String> roles = (List<String>) realmAccess.get("roles");
for (String role : roles) {
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}
}
return grantedAuthorities;
});
return jwtAuthenticationConverter;
}
}

View file

@ -0,0 +1,49 @@
package de.szut.casino.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
import java.util.List;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/swagger/**", "/swagger-ui/**", "/health").permitAll()
.anyRequest().authenticated();
})
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt ->
jwt.jwtAuthenticationConverter(new CustomJwtAuthenticationConverter())
));
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://localhost:4200"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type", "x-auth-token", "Access-Control-Allow-Origin"));
configuration.setExposedHeaders(List.of("x-auth-token"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

View file

@ -0,0 +1,47 @@
package de.szut.casino.user;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import de.szut.casino.user.dto.CreateUserDto;
import de.szut.casino.user.dto.GetUserDto;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RestController
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/user")
public ResponseEntity<?> createUser(@RequestBody @Valid CreateUserDto userData) {
if (userService.exists(userData.getAuthentikId())) {
HttpHeaders headers = new HttpHeaders();
headers.add("Location", "/user");
return new ResponseEntity<>(headers, HttpStatus.FOUND);
}
return ResponseEntity.ok(userService.createUser(userData));
}
@GetMapping("/user")
public ResponseEntity<GetUserDto> getCurrentUser(@RequestHeader("Authorization") String token) {
GetUserDto userData = userService.getCurrentUserAsDto(token);
if (userData == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(userData);
}
}

View file

@ -0,0 +1,43 @@
package de.szut.casino.user;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.math.BigDecimal;
@Setter
@Getter
@Entity
@NoArgsConstructor
public class UserEntity {
@Id
@GeneratedValue
private Long id;
@Column(unique = true)
private String authentikId;
private String username;
@Column(precision = 19, scale = 2)
private BigDecimal balance;
public UserEntity(String authentikId, String username, BigDecimal balance) {
this.authentikId = authentikId;
this.username = username;
this.balance = balance;
}
public void addBalance(long amountInCents) {
BigDecimal amountToAdd = BigDecimal.valueOf(amountInCents).movePointLeft(2);
if (this.balance == null) {
this.balance = amountToAdd;
} else {
this.balance = this.balance.add(amountToAdd);
}
}
}

View file

@ -0,0 +1,19 @@
package de.szut.casino.user;
import de.szut.casino.user.dto.CreateUserDto;
import de.szut.casino.user.dto.GetUserDto;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class UserMappingService {
public GetUserDto mapToGetUserDto(UserEntity user) {
return new GetUserDto(user.getAuthentikId(), user.getUsername(), user.getBalance());
}
public UserEntity mapToUserEntity(CreateUserDto createUserDto) {
return new UserEntity(createUserDto.getAuthentikId(), createUserDto.getUsername(), BigDecimal.ZERO);
}
}

View file

@ -0,0 +1,15 @@
package de.szut.casino.user;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public interface UserRepository extends JpaRepository<UserEntity, Long> {
@Query("SELECT u FROM UserEntity u WHERE u.authentikId = ?1")
Optional<UserEntity> findOneByAuthentikId(String authentikId);
boolean existsByAuthentikId(String authentikId);
}

View file

@ -0,0 +1,81 @@
package de.szut.casino.user;
import de.szut.casino.user.dto.CreateUserDto;
import de.szut.casino.user.dto.GetUserDto;
import de.szut.casino.user.dto.KeycloakUserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Optional;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private RestTemplate http;
@Autowired
private UserMappingService mappingService;
public UserEntity createUser(CreateUserDto createUserDto) {
UserEntity user = mappingService.mapToUserEntity(createUserDto);
userRepository.save(user);
return user;
}
public GetUserDto getUser(String authentikId) {
Optional<UserEntity> user = this.userRepository.findOneByAuthentikId(authentikId);
return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null);
}
public GetUserDto getCurrentUserAsDto(String token) {
KeycloakUserDto userData = getAuthentikUserInfo(token);
if (userData == null) {
return null;
}
Optional<UserEntity> user = this.userRepository.findOneByAuthentikId(userData.getSub());
return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null);
}
public Optional<UserEntity> getCurrentUser(String token) {
KeycloakUserDto userData = getAuthentikUserInfo(token);
if (userData == null) {
return Optional.empty();
}
return this.userRepository.findOneByAuthentikId(userData.getSub());
}
private KeycloakUserDto getAuthentikUserInfo(String token) {
try {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", token);
ResponseEntity<KeycloakUserDto> response = this.http.exchange(
"https://oauth.simonis.lol/application/o/userinfo/",
HttpMethod.GET,
new HttpEntity<>(headers),
KeycloakUserDto.class
);
return response.getBody();
} catch (Exception e) {
System.err.println("Error fetching user info from Authentik: " + e.getMessage());
return null;
}
}
public boolean exists(String authentikId) {
return userRepository.existsByAuthentikId(authentikId);
}
}

View file

@ -0,0 +1,15 @@
package de.szut.casino.user.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class CreateUserDto {
private String authentikId;
private String username;
}

View file

@ -0,0 +1,18 @@
package de.szut.casino.user.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.math.BigDecimal;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class GetUserDto {
private String authentikId;
private String username;
private BigDecimal balance;
}

View file

@ -0,0 +1,15 @@
package de.szut.casino.user.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class KeycloakUserDto {
private String sub;
private String preferred_username;
}

View file

@ -2,21 +2,38 @@ spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:5432/postgresdb
spring.datasource.username=postgres_user
spring.datasource.password=postgres_pass
server.port=8080
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.hibernate.ddl-auto=update
stripe.secret.key=${STRIPE_SECRET_KEY:sk_test_51QrePYIvCfqz7ANgqam8rEwWcMeKiLOof3j6SCMgu2sl4sESP45DJxca16mWcYo1sQaiBv32CMR6Z4AAAGQPCJo300ubuZKO8I}
stripe.webhook.secret=whsec_746b6a488665f6057118bdb4a2b32f4916f16c277109eeaed5e8f8e8b81b8c15
app.frontend-host=http://localhost:4200
spring.application.name=lf12_starter
#client registration configuration
spring.security.oauth2.client.registration.keycloak.client-id=lf12
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.keycloak.scope=openid
spring.security.oauth2.client.registration.authentik.client-id=MDqjm1kcWKuZfqHJXjxwAV20i44aT7m4VhhTL3Nm
spring.security.oauth2.client.registration.authentik.client-secret=GY2F8te6iAVYt1TNAUVLzWZEXb6JoMNp6chbjqaXNq4gS5xTDL54HqBiAlV1jFKarN28LQ7FUsYX4SbwjfEhZhgeoKuBnZKjR9eiu7RawnGgxIK9ffvUfMkjRxnmiGI5
spring.security.oauth2.client.registration.authentik.provider=authentik
spring.security.oauth2.client.registration.authentik.client-name=Authentik
spring.security.oauth2.client.registration.authentik.scope=openid,email,profile
spring.security.oauth2.client.registration.authentik.client-authentication-method=client_secret_basic
spring.security.oauth2.client.registration.authentik.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.authentik.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
# Provider settings
spring.security.oauth2.client.provider.authentik.issuer-uri=https://oauth.simonis.lol/application/o/casino-dev/
spring.security.oauth2.client.provider.authentik.authorization-uri=https://oauth.simonis.lol/application/o/authorize/
spring.security.oauth2.client.provider.authentik.token-uri=https://oauth.simonis.lol/application/o/token/
spring.security.oauth2.client.provider.authentik.user-info-uri=https://oauth.simonis.lol/application/o/userinfo/
spring.security.oauth2.client.provider.authentik.jwk-set-uri=https://oauth.simonis.lol/application/o/casino-dev/jwks/
spring.security.oauth2.client.provider.authentik.user-name-attribute=preferred_username
# Resource server config
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://oauth.simonis.lol/application/o/casino-dev/
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://oauth.simonis.lol/application/o/casino-dev/jwks/
#OIDC provider configuration:
spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:9090/realms/LF12
spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username
logging.level.org.springframework.security=DEBUG
#validating JWT token against our Keycloak server
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9090/realms/LF12
#validating JWT token against our Authentik server
springdoc.swagger-ui.path=swagger
springdoc.swagger-ui.try-it-out-enabled=true

View file

@ -0,0 +1,26 @@
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"));
}
}

View file

@ -0,0 +1,122 @@
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());
}
}

View file

@ -1,5 +1,3 @@
version: '3'
volumes:
keycloak_data:
postgres_data_keycloak_db:
@ -30,7 +28,7 @@ services:
condition: service_healthy
keycloakdb_svr:
image: postgres:14.2
image: postgres:17.4
volumes:
- postgres_data_keycloak_db:/var/lib/postgresql/data
environment:
@ -43,7 +41,7 @@ services:
- "9433:5432"
postgres_db:
image: postgres:16.4
image: postgres:17.4
volumes:
- postgres_data:/var/lib/postgresql/data
environment:

View file

@ -1,3 +1,136 @@
# Lf12Starter 2024
# 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.
## Development
### Commands
- **Build**: `bun run build` or `bunx @angular/cli build`
- **Start Dev Server**: `bun run start` or `bunx @angular/cli serve --proxy-config src/proxy.conf.json`
- **Format Code**: `bun run format` or `prettier --write "src/**/*.{ts,html,css,scss}"`
- **Lint**: `bun run lint` or `ng lint`
- **Test**: `bun run test` or `bunx @angular/cli test`
- **Test Single File**: `bunx @angular/cli test --include=path/to/test.spec.ts`
## Style Guide
### Color Palette
#### Primary Colors
- Deep Blue: `#0a1219` (background)
- Deep Blue Light: `#121e27` (secondary background)
- Deep Blue Contrast: `#1a2835` (cards, elements)
#### Accent Colors
- Emerald: `#10b981` (primary buttons)
- Emerald Dark: `#059669` (button hover)
- Emerald Light: `#34d399` (highlights)
#### Text Colors
- Primary Text: `#ffffff` (white)
- Secondary Text: `#94a3b8` (light gray)
- Tertiary Text: `#64748b` (darker gray)
#### Additional Accents
- Yellow: `#fbbf24`
- Red: `#ef4444`
- Purple: `#8b5cf6`
### Typography
#### Font Sizes
- Extra Small: Text-xs (footer disclaimers)
- Small: Text-sm (navigation links, footer links)
- Base: Text-base (general text)
- Large: Text-lg (section headings)
- Extra Large: Text-xl (stat numbers, game headings)
- Display: Text-4xl/5xl/7xl (welcome bonus text)
#### Font Weights
- Normal: General text
- Medium: Labels
- Semibold: Navigation brand
- Bold: Headings, stats
- Extrabold: Welcome bonus text
### Components
#### Buttons
- Primary: Emerald background with hover state
- Secondary: Deep blue light background with hover state
- All buttons have active scale effect (95%)
- Transition duration: 200-300ms
#### Cards
- Background: Deep blue contrast
- Rounded corners (lg)
- Shadow effects with hover transition
- Consistent padding (p-4)
#### Navigation
- Desktop: Horizontal links with hover effects
- Mobile: Collapsible menu with toggle
- Links have color and background transitions
#### Modals
- Backdrop blur with dark overlay
- Card-style container with emerald focus rings
- Consistent form styling with transitions
### Forms
#### Inputs
- Dark background with border
- Focus states with emerald accent
- Consistent padding and rounded corners
- Clear label positioning
### Layout
#### Grid System
- Mobile-first responsive grid
- Breakpoints: sm, md, lg
- Grid columns: 1 (mobile), 2 (tablet), 3 (desktop)
- Consistent gap spacing (gap-4)
#### Spacing
- Consistent margin/padding scale
- Mobile-responsive spacing adjustments
### Animation
- Transitions: 200-500ms duration
- Hover/active state animations
- Scale transformations (95-110%)
- Opacity transitions for navigation elements
### Components & Classes
#### Common UI Elements
- `.card` - Base card container
- `.button-primary` - Main CTA buttons
- `.button-secondary` - Alternative action buttons
- `.section-heading` - Section titles
- `.nav-link` - Navigation links
- `.modal-card` - Modal container
#### Game Elements
- `.game-card-content` - Game information container
- `.game-heading-sm` - Small game titles
- `.game-heading-xl` - Large game titles
- `.game-text` - Game descriptions
- `.slider-container` - Game carousel container
### Responsive Design
- Mobile-first approach
- Tailwind breakpoints (sm, md, lg)
- Different layouts based on screen size
- Responsive text sizing and spacing
- Hidden/visible elements using responsive classes
### CSS Framework
- Tailwind CSS for utility classes
- Custom utility classes with @apply directive
- CSS variables for theming
- Component-based styling approach

View file

@ -24,7 +24,6 @@
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.css"
],
"scripts": []
@ -79,7 +78,6 @@
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.css"
],
"scripts": []

File diff suppressed because it is too large Load diff

View file

@ -7,7 +7,7 @@ volumes:
services:
postgres-employee:
container_name: postgres_employee
image: postgres:13.3
image: postgres:17.4
volumes:
- employee_postgres_data:/var/lib/postgresql/data
environment:
@ -19,7 +19,7 @@ services:
employee:
container_name: employee
image: berndheidemann/employee-management-service:1.0.4
image: berndheidemann/employee-management-service:1.1.3
# image: berndheidemann/employee-management-service_without_keycloak:1.1
environment:
spring.datasource.url: jdbc:postgresql://postgres-employee:5432/employee_db

View file

@ -9,48 +9,52 @@
"test": "bunx @angular/cli test",
"format": "prettier --write \"src/**/*.{ts,html,css,scss}\"",
"format:check": "prettier --check \"src/**/*.{ts,html,css,scss}\"",
"lint": "ng lint"
"lint": "bunx @angular/cli lint"
},
"private": true,
"dependencies": {
"@angular/animations": "^18.2.0",
"@angular/cdk": "~18.2.14",
"@angular/common": "^18.2.0",
"@angular/compiler": "^18.2.0",
"@angular/core": "^18.2.0",
"@angular/forms": "^18.2.0",
"@angular/material": "~18.2.14",
"@angular/platform-browser": "^18.2.0",
"@angular/platform-browser-dynamic": "^18.2.0",
"@angular/router": "^18.2.0",
"@angular/animations": "^19.0.0",
"@angular/cdk": "~19.2.0",
"@angular/common": "^19.0.0",
"@angular/compiler": "^19.2.4",
"@angular/core": "^19.0.0",
"@angular/forms": "^19.0.0",
"@angular/platform-browser": "^19.0.0",
"@angular/platform-browser-dynamic": "^19.0.0",
"@angular/router": "^19.0.0",
"@fortawesome/angular-fontawesome": "^1.0.0",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@stripe/stripe-js": "^5.6.0",
"@stripe/stripe-js": "^7.0.0",
"@tailwindcss/postcss": "^4.0.3",
"keycloak-angular": "^16.0.1",
"keycloak-js": "^25.0.5",
"ajv": "8.17.1",
"ajv-formats": "3.0.1",
"countup.js": "^2.8.0",
"gsap": "^3.12.7",
"angular-oauth2-oidc": "^19.0.0",
"keycloak-angular": "^19.0.0",
"keycloak-js": "^26.0.0",
"postcss": "^8.5.1",
"rxjs": "~7.8.0",
"rxjs": "~7.8.2",
"tailwindcss": "^4.0.3",
"tslib": "^2.3.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^18.2.2",
"@angular/cli": "^18.2.2",
"@angular/compiler-cli": "^18.2.0",
"@angular-devkit/build-angular": "^19.0.0",
"@angular/cli": "^19.2.5",
"@angular/compiler-cli": "^19.0.0",
"@types/jasmine": "~5.1.0",
"angular-eslint": "19.1.0",
"angular-eslint": "19.3.0",
"eslint": "^9.20.0",
"jasmine-core": "~5.2.0",
"jasmine-core": "~5.6.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"prettier": "^3.4.2",
"typescript": "~5.5.2",
"typescript-eslint": "8.23.0"
"typescript": "~5.8.0",
"typescript-eslint": "8.29.1"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

BIN
frontend/public/plinko.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
frontend/public/poker.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
frontend/public/slots.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

View file

@ -1,13 +1,12 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { KeycloakAngularModule } from 'keycloak-angular';
import { FooterComponent } from './shared/components/footer/footer.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, KeycloakAngularModule, FooterComponent],
imports: [CommonModule, RouterOutlet, FooterComponent],
providers: [],
templateUrl: './app.component.html',
styleUrl: './app.component.css',

View file

@ -1,59 +1,24 @@
import {
APP_INITIALIZER,
ApplicationConfig,
provideExperimentalZonelessChangeDetection,
} from '@angular/core';
import { ApplicationConfig, provideExperimentalZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { routes } from './app.routes';
import {
KeycloakAngularModule,
KeycloakBearerInterceptor,
KeycloakService,
} from 'keycloak-angular';
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
export const initializeKeycloak = (keycloak: KeycloakService) => async () =>
keycloak.init({
config: {
url: 'http://localhost:9090',
realm: 'LF12',
clientId: 'lf12',
},
loadUserProfileAtStartUp: true,
initOptions: {
onLoad: 'check-sso',
silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
checkLoginIframe: false,
redirectUri: 'http://localhost:4200',
},
});
function initializeApp(keycloak: KeycloakService): () => Promise<boolean> {
return () => initializeKeycloak(keycloak)();
}
import { OAuthStorage, provideOAuthClient } from 'angular-oauth2-oidc';
import { httpInterceptor } from './shared/interceptor/http.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
KeycloakAngularModule,
FontAwesomeModule,
{
provide: APP_INITIALIZER,
useFactory: initializeApp,
multi: true,
deps: [KeycloakService],
},
KeycloakService,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClient(withInterceptors([httpInterceptor])),
provideExperimentalZonelessChangeDetection(),
{
provide: HTTP_INTERCEPTORS,
useClass: KeycloakBearerInterceptor,
multi: true,
},
provideAnimationsAsync(),
provideOAuthClient(),
{
provide: OAuthStorage,
useFactory: () => localStorage,
},
],
};

View file

@ -1,6 +1,5 @@
import { Routes } from '@angular/router';
import { LandingComponent } from './feature/landing/landing.component';
import { HomeComponent } from './feature/home/home.component';
import { authGuard } from './auth.guard';
export const routes: Routes = [
@ -8,9 +7,18 @@ export const routes: Routes = [
path: '',
component: LandingComponent,
},
{
path: 'auth/callback',
loadComponent: () => import('./feature/login-success/login-success.component'),
},
{
path: 'home',
component: HomeComponent,
loadComponent: () => import('./feature/home/home.component'),
canActivate: [authGuard],
},
{
path: 'game/blackjack',
loadComponent: () => import('./feature/game/blackjack/blackjack.component'),
canActivate: [authGuard],
},
];

View file

@ -1,23 +1,16 @@
import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router';
import { CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
import { AuthService } from './service/auth.service';
export const authGuard: CanActivateFn = async (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
) => {
const keycloakService = inject(KeycloakService);
const isLoggedIn = keycloakService.isLoggedIn();
export const authGuard: CanActivateFn = async () => {
const authService = inject(AuthService);
const router = inject(Router);
if (isLoggedIn) {
if (authService.isLoggedIn()) {
return true;
}
const baseurl = window.location.origin;
keycloakService.login({
redirectUri: `${baseurl}${state.url}`,
});
router.navigate(['']);
return false;
};

View file

@ -1,21 +1,25 @@
<h2 mat-dialog-title class="text-xl font-semibold">Guthaben aufladen</h2>
<mat-dialog-content>
<form [formGroup]="form">
<div *ngIf="errorMsg">
{{ errorMsg }}
@if (isOpen) {
<div #modalBg class="modal-bg">
<div #modalCard class="modal-card">
<h2 class="modal-heading">Guthaben aufladen</h2>
<form [formGroup]="form">
@if (errorMsg) {
{{ errorMsg }}
}
<div class="mb-2">
<label for="amount">Betrag</label>
<input
type="number"
id="amount"
formControlName="amount"
class="w-full px-2 py-1 bg-deep-blue-light text-white rounded my-1"
/>
</div>
</form>
<div class="my-1">
<button (click)="closeModal()" class="button-secondary">Abbrechen</button>
<button (click)="submit()" class="button-primary w-full py-2">Einzahlen</button>
</div>
</div>
<div class="mb-2">
<label for="amount">Betrag</label>
<input
type="number"
id="amount"
formControlName="amount"
class="w-full px-2 py-1 bg-white text-black"
/>
</div>
</form>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-flat-button (click)="closeDialog()">Abbrechen</button>
<button mat-flat-button (click)="submit()">Einzahlen</button>
</mat-dialog-actions>
</div>
}

View file

@ -1,38 +1,46 @@
import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
inject,
Input,
OnInit,
Output,
ViewChild,
AfterViewInit,
OnDestroy,
OnChanges,
SimpleChanges,
ChangeDetectorRef,
} from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { loadStripe, Stripe } from '@stripe/stripe-js';
import { DepositService } from '../../service/deposit.service';
import { debounceTime } from 'rxjs';
import { environment } from '../../../environments/environment';
import { NgIf } from '@angular/common';
import {
MatDialogActions,
MatDialogContent,
MatDialogRef,
MatDialogTitle,
} from '@angular/material/dialog';
import { MatButton } from '@angular/material/button';
import { CommonModule } from '@angular/common';
import gsap from 'gsap';
import { DepositService } from '@service/deposit.service';
import { environment } from '@environments/environment';
import { ModalAnimationService } from '@shared/services/modal-animation.service';
@Component({
selector: 'app-deposit',
standalone: true,
imports: [
ReactiveFormsModule,
NgIf,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatButton,
],
imports: [ReactiveFormsModule, CommonModule],
templateUrl: './deposit.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DepositComponent implements OnInit {
export class DepositComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges {
@Input() isOpen = false;
@Output() closeModalEmitter = new EventEmitter<void>();
@ViewChild('modalBg') modalBg!: ElementRef;
@ViewChild('modalCard') modalCard!: ElementRef;
protected form!: FormGroup;
protected errorMsg = '';
private stripe: Stripe | null = null;
private service: DepositService = inject(DepositService);
public dialogRef: MatDialogRef<DepositComponent> = inject(MatDialogRef<DepositComponent>);
private modalAnimationService: ModalAnimationService = inject(ModalAnimationService);
private cdr: ChangeDetectorRef = inject(ChangeDetectorRef);
async ngOnInit() {
this.form = new FormGroup({
@ -48,6 +56,40 @@ export class DepositComponent implements OnInit {
this.stripe = await loadStripe(environment.STRIPE_KEY);
}
ngAfterViewInit() {
if (this.isOpen) {
this.openModal();
}
}
ngOnChanges(changes: SimpleChanges) {
if (changes['isOpen']) {
this.cdr.detectChanges();
setTimeout(() => {
if (this.modalBg?.nativeElement && this.modalCard?.nativeElement) {
if (changes['isOpen'].currentValue) {
this.openModal();
} else {
this.closeModal();
}
}
}, 0);
}
}
ngOnDestroy() {
gsap.killTweensOf([this.modalBg?.nativeElement, this.modalCard?.nativeElement]);
}
private openModal() {
if (this.modalBg?.nativeElement && this.modalCard?.nativeElement) {
this.modalAnimationService.openModal(
this.modalCard.nativeElement,
this.modalBg.nativeElement
);
}
}
submit() {
if (!this.stripe) {
this.errorMsg = 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.';
@ -63,7 +105,13 @@ export class DepositComponent implements OnInit {
});
}
public closeDialog(): void {
this.dialogRef.close();
public closeModal() {
if (this.modalBg?.nativeElement && this.modalCard?.nativeElement) {
this.modalAnimationService.closeModal(
this.modalCard.nativeElement,
this.modalBg.nativeElement,
() => this.closeModalEmitter.emit()
);
}
}
}

View file

@ -0,0 +1,46 @@
<app-navbar></app-navbar>
<div class="container mx-auto px-4 py-6 space-y-8">
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
<div class="lg:col-span-3 space-y-6 flex flex-col gap-4">
<app-dealer-hand [cards]="dealerCards()"></app-dealer-hand>
<app-player-hand [cards]="playerCards()"></app-player-hand>
@if (gameInProgress()) {
<app-game-controls
[playerCards]="playerCards()"
[gameState]="gameState()"
[isActionInProgress]="isActionInProgress()"
(hit)="onHit()"
(stand)="onStand()"
(doubleDown)="onDoubleDown()"
(leave)="leaveGame()"
></app-game-controls>
}
</div>
<div class="lg:col-span-1 space-y-6">
<app-game-info
[balance]="balance()"
[currentBet]="currentBet()"
[gameInProgress]="gameInProgress()"
[isActionInProgress]="isActionInProgress()"
(newGame)="onNewGame($event)"
></app-game-info>
</div>
</div>
</div>
<app-game-result
[gameState]="gameState()"
[amount]="currentBet()"
[balance]="balance()"
[show]="showGameResult()"
(gameResultClosed)="onCloseGameResult()"
></app-game-result>
<app-debt-dialog
[amount]="debtAmount()"
[show]="showDebtDialog()"
(dialogClosed)="onCloseDebtDialog()"
></app-debt-dialog>

View file

@ -0,0 +1,226 @@
import { ChangeDetectionStrategy, Component, inject, signal, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router } from '@angular/router';
import { PlayingCardComponent } from './components/playing-card/playing-card.component';
import { DealerHandComponent } from './components/dealer-hand/dealer-hand.component';
import { PlayerHandComponent } from './components/player-hand/player-hand.component';
import { GameControlsComponent } from './components/game-controls/game-controls.component';
import { GameInfoComponent } from './components/game-info/game-info.component';
import { Card, BlackjackGame } from '@blackjack/models/blackjack.model';
import { BlackjackService } from '@blackjack/services/blackjack.service';
import { HttpErrorResponse } from '@angular/common/http';
import { GameResultComponent } from '@blackjack/components/game-result/game-result.component';
import { GameState } from '@blackjack/enum/gameState';
import { NavbarComponent } from '@shared/components/navbar/navbar.component';
import { UserService } from '@service/user.service';
import { timer } from 'rxjs';
import { DebtDialogComponent } from '@shared/components/debt-dialog/debt-dialog.component';
@Component({
selector: 'app-blackjack',
standalone: true,
imports: [
CommonModule,
NavbarComponent,
PlayingCardComponent,
DealerHandComponent,
PlayerHandComponent,
GameControlsComponent,
GameInfoComponent,
GameResultComponent,
DebtDialogComponent,
],
templateUrl: './blackjack.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class BlackjackComponent implements OnInit {
private router = inject(Router);
private userService = inject(UserService);
private blackjackService = inject(BlackjackService);
dealerCards = signal<Card[]>([]);
playerCards = signal<Card[]>([]);
currentBet = signal(0);
balance = signal(0);
currentGameId = signal<number | undefined>(undefined);
gameInProgress = signal(false);
gameState = signal<GameState>(GameState.IN_PROGRESS);
showGameResult = signal(false);
isActionInProgress = signal(false);
showDebtDialog = signal(false);
debtAmount = signal(0);
ngOnInit(): void {
this.userService.currentUser$.subscribe((user) => {
if (user) {
this.balance.set(user.balance);
}
});
}
private updateGameState(game: BlackjackGame) {
console.log('Game state update:', game);
this.currentGameId.set(game.id);
this.currentBet.set(game.bet);
this.gameInProgress.set(game.state === GameState.IN_PROGRESS);
this.gameState.set(game.state as GameState);
const isGameOver = game.state !== GameState.IN_PROGRESS;
this.dealerCards.set(
game.dealerCards.map((card, index) => ({
...card,
hidden: !isGameOver && index === 1 && game.state === GameState.IN_PROGRESS,
}))
);
this.playerCards.set(
game.playerCards.map((card) => ({
...card,
hidden: false,
}))
);
if (isGameOver) {
console.log('Game is over, state:', game.state);
this.userService.refreshCurrentUser();
timer(1500).subscribe(() => {
this.showGameResult.set(true);
console.log('Game result dialog shown after delay');
});
}
}
onNewGame(bet: number): void {
this.isActionInProgress.set(true);
this.blackjackService.startGame(bet).subscribe({
next: (game) => {
this.updateGameState(game);
this.userService.refreshCurrentUser();
this.isActionInProgress.set(false);
},
error: (error) => {
console.error('Failed to start game:', error);
this.isActionInProgress.set(false);
},
});
}
onHit(): void {
if (!this.currentGameId() || this.isActionInProgress()) return;
this.isActionInProgress.set(true);
this.blackjackService.hit(this.currentGameId()!).subscribe({
next: (game) => {
this.updateGameState(game);
if (game.state !== 'IN_PROGRESS') {
this.userService.refreshCurrentUser();
}
this.isActionInProgress.set(false);
},
error: (error) => {
console.error('Failed to hit:', error);
this.handleGameError(error);
this.isActionInProgress.set(false);
},
});
}
onStand(): void {
if (!this.currentGameId() || this.isActionInProgress()) return;
if (this.gameState() !== GameState.IN_PROGRESS) {
console.log('Cannot stand: game is not in progress');
return;
}
this.isActionInProgress.set(true);
this.blackjackService.stand(this.currentGameId()!).subscribe({
next: (game) => {
this.updateGameState(game);
this.userService.refreshCurrentUser();
this.isActionInProgress.set(false);
},
error: (error) => {
console.error('Failed to stand:', error);
this.handleGameError(error);
this.isActionInProgress.set(false);
},
});
}
onDoubleDown(): void {
if (!this.currentGameId() || this.isActionInProgress()) return;
if (this.gameState() !== GameState.IN_PROGRESS || this.playerCards().length !== 2) {
console.log('Cannot double down: game is not in progress or more than 2 cards');
return;
}
this.isActionInProgress.set(true);
this.blackjackService.doubleDown(this.currentGameId()!).subscribe({
next: (game) => {
this.updateGameState(game);
this.userService.getCurrentUser().subscribe((user) => {
if (user && user.balance < 0) {
this.debtAmount.set(Math.abs(user.balance));
this.showDebtDialog.set(true);
}
});
this.isActionInProgress.set(false);
},
error: (error) => {
console.error('Failed to double down:', error);
this.handleGameError(error);
this.isActionInProgress.set(false);
},
});
}
onCloseGameResult(): void {
console.log('Closing game result dialog');
this.showGameResult.set(false);
this.userService.refreshCurrentUser();
}
onCloseDebtDialog(): void {
this.showDebtDialog.set(false);
}
private handleGameError(error: HttpErrorResponse): void {
if (error instanceof HttpErrorResponse) {
if (error.status === 400 && error.error?.error === 'Invalid state') {
this.gameInProgress.set(false);
this.userService.refreshCurrentUser();
} else if (error.status === 500) {
console.log('Server error occurred. The game may have been updated in another session.');
this.gameInProgress.set(false);
this.userService.refreshCurrentUser();
if (this.currentGameId()) {
this.refreshGameState(this.currentGameId()!);
}
}
}
}
private refreshGameState(gameId: number): void {
this.blackjackService.getGame(gameId).subscribe({
next: (game) => {
this.updateGameState(game);
},
error: (err) => {
console.error('Failed to refresh game state:', err);
},
});
}
leaveGame(): void {
this.router.navigate(['/home']);
}
}

View file

@ -0,0 +1,83 @@
import {
ChangeDetectionStrategy,
Component,
Input,
OnChanges,
SimpleChanges,
ElementRef,
ViewChild,
AfterViewInit,
} from '@angular/core';
import { CommonModule, CurrencyPipe } from '@angular/common';
import { CountUp } from 'countup.js';
@Component({
selector: 'app-animated-number',
standalone: true,
imports: [CommonModule, CurrencyPipe],
template: ` <span #numberElement>{{ formattedValue }}</span> `,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnimatedNumberComponent implements OnChanges, AfterViewInit {
@Input() value = 0;
@Input() duration = 1;
@Input() ease = 'power1.out';
@ViewChild('numberElement') numberElement!: ElementRef;
private countUp: CountUp | null = null;
private previousValue = 0;
formattedValue = '0,00 €';
ngAfterViewInit(): void {
this.initializeCountUp();
if (this.countUp && this.value !== 0) {
this.countUp.start(() => {
this.previousValue = this.value;
});
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['value']) {
if (this.countUp) {
const endVal = this.value;
this.countUp.update(endVal);
this.previousValue = endVal;
} else {
this.formattedValue = new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(this.value);
}
}
}
private initializeCountUp(): void {
if (this.numberElement) {
this.countUp = new CountUp(this.numberElement.nativeElement, this.value, {
startVal: this.previousValue,
duration: this.duration,
easingFn: (t, b, c, d) => {
if (this.ease === 'power1.out') {
return c * (1 - Math.pow(1 - t / d, 1)) + b;
}
return c * (t / d) + b;
},
formattingFn: (value) => {
const formatted = new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value);
this.formattedValue = formatted;
return formatted;
},
});
}
}
}

View file

@ -0,0 +1,73 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Card } from '@blackjack/models/blackjack.model';
import { PlayingCardComponent } from '../playing-card/playing-card.component';
import { GameControlsService } from '@blackjack/services/game-controls.service';
@Component({
selector: 'app-dealer-hand',
standalone: true,
imports: [CommonModule, PlayingCardComponent],
template: `
<div class="space-y-4">
<div class="flex justify-between items-center">
<h3 class="section-heading text-2xl">Dealer's Karten</h3>
<div class="flex items-center gap-2">
<div class="text-text-secondary">Punkte:</div>
<div class="text-xl font-bold text-accent-red">
{{ gameControlsService.calculateHandValue(cards) }}
</div>
</div>
</div>
<div class="card p-6 !bg-accent-red">
<div class="flex justify-center gap-4 min-h-[160px] p-4 border-2 border-red-400 rounded-lg">
@if (cards.length > 0) {
@for (card of cardsWithState; track card.id) {
<app-playing-card
[rank]="card.rank"
[suit]="card.suit"
[hidden]="card.hidden"
[isNew]="card.isNew"
></app-playing-card>
}
} @else {
<div class="flex items-center justify-center text-white/70 text-lg font-medium">
Warte auf Spielstart...
</div>
}
</div>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DealerHandComponent implements OnChanges {
@Input() cards: Card[] = [];
cardsWithState: (Card & { isNew: boolean; id: string })[] = [];
private lastCardCount = 0;
constructor(protected gameControlsService: GameControlsService) {}
ngOnChanges(changes: SimpleChanges): void {
if (changes['cards']) {
this.updateCardsWithState();
}
}
private updateCardsWithState(): void {
const newCards = this.cards.length > this.lastCardCount;
this.cardsWithState = this.cards.map((card, index) => {
const isNew = newCards && index >= this.lastCardCount;
return {
...card,
isNew,
id: `${card.suit}-${card.rank}-${index}`,
};
});
this.lastCardCount = this.cards.length;
}
}

View file

@ -0,0 +1,81 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { GameState } from '@blackjack/enum/gameState';
import { Card } from '@blackjack/models/blackjack.model';
import { GameControlsService } from '@blackjack/services/game-controls.service';
@Component({
selector: 'app-game-controls',
standalone: true,
imports: [CommonModule],
template: `
<div class="flex flex-col gap-4">
<div class="flex justify-center text-lg mb-5">
<div class="card p-4">
<div class="text-emerald font-bold mb-1">
Deine Punkte: {{ gameControlsService.calculateHandValue(playerCards) }}
</div>
<div class="text-text-secondary">
Status:
<span [class]="gameControlsService.getStatusClass(gameState)">{{
gameControlsService.getStatusText(gameState)
}}</span>
</div>
</div>
</div>
<div class="flex justify-center gap-4">
<button
(click)="hit.emit()"
class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative"
[disabled]="gameState !== GameState.IN_PROGRESS || isActionInProgress"
>
<span>Ziehen</span>
</button>
<button
(click)="stand.emit()"
class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative"
[disabled]="gameState !== GameState.IN_PROGRESS || isActionInProgress"
>
<span>Halten</span>
</button>
<button
(click)="doubleDown.emit()"
class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative"
[disabled]="!canDoubleDown || isActionInProgress"
>
<span>Verdoppeln</span>
</button>
<button
(click)="leave.emit()"
class="bg-accent-red hover:bg-accent-red/80 px-8 py-4 rounded text-lg font-medium min-w-[120px] transition-all duration-300"
[disabled]="isActionInProgress"
>
Abbrechen
</button>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GameControlsComponent {
@Input() playerCards: Card[] = [];
@Input() gameState: GameState = GameState.IN_PROGRESS;
@Input() isActionInProgress = false;
@Output() hit = new EventEmitter<void>();
@Output() stand = new EventEmitter<void>();
@Output() doubleDown = new EventEmitter<void>();
@Output() leave = new EventEmitter<void>();
protected readonly GameState = GameState;
constructor(protected gameControlsService: GameControlsService) {}
get canDoubleDown(): boolean {
return (
this.gameState === GameState.IN_PROGRESS &&
this.playerCards.length === 2 &&
!this.isActionInProgress
);
}
}

View file

@ -0,0 +1,150 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnChanges,
Output,
SimpleChanges,
signal,
} from '@angular/core';
import { CommonModule, CurrencyPipe } from '@angular/common';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { BettingService } from '@blackjack/services/betting.service';
import { AnimatedNumberComponent } from '../animated-number/animated-number.component';
@Component({
selector: 'app-game-info',
standalone: true,
imports: [CommonModule, CurrencyPipe, ReactiveFormsModule, AnimatedNumberComponent],
template: `
<div class="card p-4">
<h3 class="section-heading text-xl mb-4">Spiel Informationen</h3>
<div class="space-y-4">
<div class="flex justify-between items-center">
<span class="text-text-secondary">Aktuelle Wette:</span>
<span [class]="currentBet > 0 ? 'text-accent-red' : 'text-text-secondary'">
<app-animated-number [value]="currentBet" [duration]="0.5"></app-animated-number>
</span>
</div>
@if (!gameInProgress) {
<div class="grid grid-cols-2 gap-2 mb-4">
<button
(click)="setBetAmount(0.1)"
class="button-primary py-2 text-sm"
[disabled]="gameInProgress"
>
10%
</button>
<button
(click)="setBetAmount(0.25)"
class="button-primary py-2 text-sm"
[disabled]="gameInProgress"
>
25%
</button>
<button
(click)="setBetAmount(0.5)"
class="button-primary py-2 text-sm"
[disabled]="gameInProgress"
>
50%
</button>
<button
(click)="setBetAmount(1)"
class="button-primary py-2 text-sm"
[disabled]="gameInProgress"
>
100%
</button>
</div>
}
<form [formGroup]="betForm" (ngSubmit)="onSubmit()" class="space-y-2">
<div class="space-y-1">
<label for="bet" class="text-sm text-text-secondary">Einsatz</label>
<input
type="number"
id="bet"
formControlName="bet"
class="w-full px-3 py-2 bg-deep-blue-light text-white rounded focus:outline-none focus:ring-2 focus:ring-emerald disabled:opacity-50"
[min]="1"
[max]="balance"
step="0.01"
[disabled]="gameInProgress || isActionInProgress"
[placeholder]="balance | currency: 'EUR'"
/>
@if (betForm.get('bet')?.errors?.['required'] && betForm.get('bet')?.touched) {
<span class="text-xs text-accent-red">Bitte geben Sie einen Einsatz ein</span>
}
@if (betForm.get('bet')?.errors?.['min'] && betForm.get('bet')?.touched) {
<span class="text-xs text-accent-red">Mindestens 1 setzen</span>
}
@if (betForm.get('bet')?.errors?.['max'] && betForm.get('bet')?.touched) {
<span class="text-xs text-accent-red">Nicht genügend Guthaben</span>
}
</div>
<button
type="submit"
class="button-primary w-full py-2 relative"
[disabled]="!betForm.valid || gameInProgress || isActionInProgress"
>
<span [class.invisible]="isActionInProgress">Neues Spiel</span>
@if (isActionInProgress) {
<div class="absolute inset-0 flex items-center justify-center">
<div
class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"
></div>
</div>
}
</button>
</form>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GameInfoComponent implements OnChanges {
@Input() set balance(value: number) {
this._balance.set(value);
}
get balance() {
return this._balance();
}
private _balance = signal(0);
@Input() currentBet = 0;
@Input() gameInProgress = false;
@Input() isActionInProgress = false;
@Output() newGame = new EventEmitter<number>();
betForm: FormGroup;
constructor(private bettingService: BettingService) {
this.betForm = this.bettingService.createBetForm();
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['balance']) {
this.bettingService.updateBetFormValidators(this.betForm, this.balance);
}
}
setBetAmount(percentage: number) {
const betAmount = this.bettingService.calculateBetAmount(this.balance, percentage);
if (this.bettingService.isValidBet(betAmount, this.balance)) {
this.betForm.patchValue({ bet: betAmount });
}
}
onSubmit() {
if (this.betForm.valid) {
const betAmount = parseFloat(this.betForm.value.bet);
if (this.bettingService.isValidBet(betAmount, this.balance)) {
this.newGame.emit(betAmount);
this.betForm.reset();
}
}
}
}

View file

@ -0,0 +1,135 @@
import { ChangeDetectionStrategy, Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule, CurrencyPipe } from '@angular/common';
import { animate, style, transition, trigger } from '@angular/animations';
import { GameState } from '../../enum/gameState';
import { AnimatedNumberComponent } from '../animated-number/animated-number.component';
@Component({
selector: 'app-game-result',
standalone: true,
imports: [CommonModule, CurrencyPipe, AnimatedNumberComponent],
template: `
<div *ngIf="visible" [@fadeInOut] class="modal-bg" style="z-index: 1000; position: fixed;">
<div class="modal-card" [@cardAnimation]>
<h2 class="modal-heading" [class]="getResultClass()">{{ getResultTitle() }}</h2>
<p class="py-2 text-text-secondary mb-4">{{ getResultMessage() }}</p>
<div
class="bg-deep-blue-light/50 rounded-lg p-5 mb-6 shadow-inner border border-deep-blue-light/30"
>
<div class="grid grid-cols-2 gap-4">
<div class="text-text-secondary">Einsatz:</div>
<div class="font-medium text-right">
<app-animated-number [value]="amount" [duration]="0.5"></app-animated-number>
</div>
<div class="text-text-secondary">
{{ isDraw ? 'Zurückgegeben:' : isWin ? 'Gewonnen:' : 'Verloren:' }}
</div>
<div
class="font-medium text-right"
[ngClass]="{
'text-emerald': isWin,
'text-accent-red': isLoss,
'text-yellow-400': isDraw,
}"
>
{{ isLoss ? '-' : '+' }}
<app-animated-number
[value]="isWin ? amount * 2 : amount"
[duration]="0.5"
></app-animated-number>
<div *ngIf="isWin" class="text-xs text-text-secondary">
(Einsatz
<app-animated-number [value]="amount" [duration]="0.5"></app-animated-number> × 2)
</div>
</div>
<div class="text-text-secondary border-t border-text-secondary/20 pt-3 font-medium">
Kontostand:
</div>
<div class="font-medium text-right border-t border-text-secondary/20 pt-3">
<app-animated-number [value]="balance" [duration]="0.5"></app-animated-number>
</div>
</div>
</div>
<button type="button" (click)="closeDialog()" class="button-primary w-full py-2">
Verstanden
</button>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
trigger('fadeInOut', [
transition(':enter', [
style({ opacity: 0 }),
animate('150ms ease-out', style({ opacity: 1 })),
]),
transition(':leave', [animate('150ms ease-in', style({ opacity: 0 }))]),
]),
trigger('cardAnimation', [
transition(':enter', [
style({ opacity: 0, transform: 'scale(0.95)' }),
animate('200ms ease-out', style({ opacity: 1, transform: 'scale(1)' })),
]),
]),
],
})
export class GameResultComponent {
@Input() gameState: GameState = GameState.IN_PROGRESS;
@Input() amount = 0;
@Input() balance = 0;
@Input() set show(value: boolean) {
console.log('GameResultComponent show input changed:', value, 'gameState:', this.gameState);
this.visible = value;
}
@Output() gameResultClosed = new EventEmitter<void>();
visible = false;
get isWin(): boolean {
return this.gameState === GameState.PLAYER_WON || this.gameState === GameState.PLAYER_BLACKJACK;
}
get isLoss(): boolean {
return this.gameState === GameState.PLAYER_LOST;
}
get isDraw(): boolean {
return this.gameState === GameState.DRAW;
}
getResultTitle(): string {
if (this.gameState === GameState.PLAYER_BLACKJACK) return 'Blackjack!';
if (this.isWin) return 'Gewonnen!';
if (this.isLoss) return 'Verloren!';
if (this.isDraw) return 'Unentschieden!';
return '';
}
getResultMessage(): string {
if (this.gameState === GameState.PLAYER_BLACKJACK)
return 'Glückwunsch! Du hast mit einem Blackjack gewonnen!';
if (this.isWin) return 'Glückwunsch! Du hast diese Runde gewonnen.';
if (this.isLoss) return 'Schade! Du hast diese Runde verloren.';
if (this.isDraw) return 'Diese Runde endet unentschieden. Dein Einsatz wurde zurückgegeben.';
return '';
}
getResultClass(): string {
if (this.gameState === GameState.PLAYER_BLACKJACK) return 'text-emerald font-bold';
if (this.isWin) return 'text-emerald';
if (this.isLoss) return 'text-accent-red';
if (this.isDraw) return 'text-yellow-400';
return '';
}
closeDialog(): void {
this.visible = false;
this.gameResultClosed.emit();
console.log('Dialog closed by user');
}
}

View file

@ -0,0 +1,75 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PlayingCardComponent } from '../playing-card/playing-card.component';
import { Card } from '@blackjack/models/blackjack.model';
import { GameControlsService } from '@blackjack/services/game-controls.service';
@Component({
selector: 'app-player-hand',
standalone: true,
imports: [CommonModule, PlayingCardComponent],
template: `
<div class="space-y-4">
<div class="flex justify-between items-center">
<h3 class="section-heading text-2xl">Deine Karten</h3>
<div class="flex items-center gap-2">
<div class="text-text-secondary">Punkte:</div>
<div class="text-xl font-bold text-emerald">
{{ gameControlsService.calculateHandValue(cards) }}
</div>
</div>
</div>
<div class="card p-6 !bg-emerald">
<div
class="flex justify-center gap-4 min-h-[160px] p-4 border-2 border-emerald-400 rounded-lg"
>
@if (cards.length > 0) {
@for (card of cardsWithState; track card.id) {
<app-playing-card
[rank]="card.rank"
[suit]="card.suit"
[hidden]="card.hidden"
[isNew]="card.isNew"
></app-playing-card>
}
} @else {
<div class="flex items-center justify-center text-white/70 text-lg font-medium">
Platziere eine Wette um zu spielen...
</div>
}
</div>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PlayerHandComponent implements OnChanges {
@Input() cards: Card[] = [];
cardsWithState: (Card & { isNew: boolean; id: string })[] = [];
private lastCardCount = 0;
constructor(protected gameControlsService: GameControlsService) {}
ngOnChanges(changes: SimpleChanges): void {
if (changes['cards']) {
this.updateCardsWithState();
}
}
private updateCardsWithState(): void {
const newCards = this.cards.length > this.lastCardCount;
this.cardsWithState = this.cards.map((card, index) => {
const isNew = newCards && index >= this.lastCardCount;
return {
...card,
isNew,
id: `${card.suit}-${card.rank}-${index}`,
};
});
this.lastCardCount = this.cards.length;
}
}

View file

@ -0,0 +1,133 @@
import {
ChangeDetectionStrategy,
Component,
Input,
AfterViewInit,
ElementRef,
OnChanges,
SimpleChanges,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { gsap } from 'gsap';
import { Suit, suitSymbols } from '@blackjack/models/blackjack.model';
@Component({
selector: 'app-playing-card',
standalone: true,
imports: [CommonModule],
template: `
<div
#cardElement
class="w-24 h-36 rounded-lg p-2 relative flex flex-col justify-between shadow-lg card-element"
[class]="hidden ? 'bg-red-800' : 'bg-white'"
>
@if (!hidden) {
<span class="text-xl font-bold" [class]="isRedSuit ? 'text-accent-red' : 'text-black'">{{
getDisplayRank(rank)
}}</span>
}
@if (!hidden) {
<span
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-3xl"
[class]="isRedSuit ? 'text-accent-red' : 'text-black'"
>{{ getSuitSymbol(suit) }}</span
>
}
@if (!hidden) {
<span
class="text-xl font-bold self-end rotate-180"
[class]="isRedSuit ? 'text-accent-red' : 'text-black'"
>{{ getDisplayRank(rank) }}</span
>
}
</div>
`,
styles: [
`
.card-element {
transform-style: preserve-3d;
backface-visibility: hidden;
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PlayingCardComponent implements AfterViewInit, OnChanges {
@Input({ required: true }) rank!: string;
@Input({ required: true }) suit!: Suit;
@Input({ required: true }) hidden!: boolean;
@Input() isNew = false;
constructor(private elementRef: ElementRef) {}
get isRedSuit(): boolean {
return this.suit === 'HEARTS' || this.suit === 'DIAMONDS';
}
ngAfterViewInit(): void {
if (this.isNew) {
this.animateNewCard();
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['hidden'] && !changes['hidden'].firstChange) {
this.animateCardFlip();
}
}
private animateNewCard(): void {
const cardElement = this.elementRef.nativeElement.querySelector('.card-element');
gsap.fromTo(
cardElement,
{
y: -100,
opacity: 0,
rotation: -10,
scale: 0.7,
},
{
y: 0,
opacity: 1,
rotation: 0,
scale: 1,
duration: 0.5,
ease: 'power2.out',
}
);
}
private animateCardFlip(): void {
const cardElement = this.elementRef.nativeElement.querySelector('.card-element');
gsap.to(cardElement, {
rotationY: 180,
duration: 0.3,
onComplete: () => {
gsap.set(cardElement, { rotationY: 0 });
},
});
}
protected getSuitSymbol(suit: Suit): string {
return suitSymbols[suit];
}
protected getDisplayRank(rank: string): string {
const rankMap: Record<string, string> = {
TWO: '2',
THREE: '3',
FOUR: '4',
FIVE: '5',
SIX: '6',
SEVEN: '7',
EIGHT: '8',
NINE: '9',
TEN: '10',
JACK: 'J',
QUEEN: 'Q',
KING: 'K',
ACE: 'A',
};
return rankMap[rank] || rank;
}
}

View file

@ -0,0 +1,7 @@
export enum GameState {
PLAYER_WON = 'PLAYER_WON',
IN_PROGRESS = 'IN_PROGRESS',
PLAYER_LOST = 'PLAYER_LOST',
DRAW = 'DRAW',
PLAYER_BLACKJACK = 'PLAYER_BLACKJACK',
}

View file

@ -0,0 +1,23 @@
export type Suit = 'HEARTS' | 'DIAMONDS' | 'CLUBS' | 'SPADES';
export interface Card {
suit: Suit;
rank: string;
hidden: boolean;
}
export interface BlackjackGame {
id: number;
state: string;
bet: number;
playerCards: Card[];
dealerCards: Card[];
userId: number;
}
export const suitSymbols: Record<Suit, string> = {
HEARTS: '♥',
DIAMONDS: '♦',
CLUBS: '♣',
SPADES: '♠',
};

View file

@ -0,0 +1,5 @@
export interface Card {
value: string;
suit: string;
hidden: boolean;
}

View file

@ -0,0 +1,31 @@
import { Injectable } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Injectable({
providedIn: 'root',
})
export class BettingService {
constructor(private fb: FormBuilder) {}
createBetForm(): FormGroup {
return this.fb.group({
bet: ['', [Validators.required, Validators.min(1)]],
});
}
updateBetFormValidators(form: FormGroup, balance: number): void {
form.reset();
form
.get('bet')
?.setValidators([Validators.required, Validators.min(1), Validators.max(balance)]);
form.get('bet')?.updateValueAndValidity();
}
calculateBetAmount(balance: number, percentage: number): number {
return Math.floor(balance * percentage * 100) / 100;
}
isValidBet(betAmount: number, balance: number): boolean {
return betAmount >= 1 && betAmount <= balance;
}
}

View file

@ -0,0 +1,66 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, catchError } from 'rxjs';
import { BlackjackGame } from '@blackjack/models/blackjack.model';
@Injectable({
providedIn: 'root',
})
export class BlackjackService {
private http = inject(HttpClient);
startGame(bet: number): Observable<BlackjackGame> {
return this.http
.post<BlackjackGame>('/backend/blackjack/start', { betAmount: bet }, { responseType: 'json' })
.pipe(
catchError((error) => {
console.error('Start game error:', error);
throw error;
})
);
}
hit(gameId: number): Observable<BlackjackGame> {
return this.http
.post<BlackjackGame>(`/backend/blackjack/${gameId}/hit`, {}, { responseType: 'json' })
.pipe(
catchError((error) => {
console.error('Hit error:', error);
throw error;
})
);
}
stand(gameId: number): Observable<BlackjackGame> {
return this.http
.post<BlackjackGame>(`/backend/blackjack/${gameId}/stand`, {}, { responseType: 'json' })
.pipe(
catchError((error) => {
console.error('Stand error:', error);
throw error;
})
);
}
doubleDown(gameId: number): Observable<BlackjackGame> {
return this.http
.post<BlackjackGame>(`/backend/blackjack/${gameId}/doubleDown`, {}, { responseType: 'json' })
.pipe(
catchError((error) => {
console.error('Double Down error:', error);
throw error;
})
);
}
getGame(gameId: number): Observable<BlackjackGame> {
return this.http
.get<BlackjackGame>(`/backend/blackjack/${gameId}`, { responseType: 'json' })
.pipe(
catchError((error) => {
console.error('Get game error:', error);
throw error;
})
);
}
}

View file

@ -0,0 +1,74 @@
import { Injectable } from '@angular/core';
import { Card } from '../models/blackjack.model';
import { GameState } from '../enum/gameState';
@Injectable({
providedIn: 'root',
})
export class GameControlsService {
calculateHandValue(cards: Card[]): number {
let sum = 0;
let aceCount = 0;
const rankValues: Record<string, number> = {
TWO: 2,
THREE: 3,
FOUR: 4,
FIVE: 5,
SIX: 6,
SEVEN: 7,
EIGHT: 8,
NINE: 9,
TEN: 10,
JACK: 10,
QUEEN: 10,
KING: 10,
ACE: 11,
};
for (const card of cards) {
if (!card.hidden) {
const value = rankValues[card.rank] || 0;
sum += value;
if (card.rank === 'ACE') {
aceCount++;
}
}
}
while (sum > 21 && aceCount > 0) {
sum -= 10;
aceCount--;
}
return sum;
}
getStatusText(state: GameState): string {
switch (state) {
case GameState.IN_PROGRESS:
return 'Spiel läuft';
case GameState.PLAYER_WON:
return 'Gewonnen!';
case GameState.PLAYER_LOST:
return 'Verloren!';
case GameState.DRAW:
return 'Unentschieden!';
default:
return state;
}
}
getStatusClass(state: GameState): string {
switch (state) {
case GameState.PLAYER_WON:
return 'text-emerald';
case GameState.PLAYER_LOST:
return 'text-accent-red';
case GameState.DRAW:
return 'text-yellow-400';
default:
return 'text-white';
}
}
}

View file

@ -1,19 +1,117 @@
<app-navbar></app-navbar>
<div class="container mx-auto px-4 py-6 space-y-8">
<div class="flex justify-between items-center">
<div class="flex items-center space-x-4"></div>
</div>
<div class="grid grid-cols-3">
<div class="w-1/3 h-1/4">
<p>Spiel Vorschau</p>
<p>Spiel Name</p>
<button type="button" class="btn-primary">Jetzt spielen</button>
</div>
<div class="w-1/3 h-1/4">
<p>Spiel Vorschau</p>
<p>Spiel Name</p>
<button type="button" class="btn-primary">Jetzt spielen</button>
</div>
<div class="w-1/3 h-1/4">
<p>Spiel Vorschau</p>
<p>Spiel Name</p>
<button type="button" class="btn-primary">Jetzt spielen</button>
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
<div class="lg:col-span-3">
<div class="flex justify-between items-center mb-6">
<h3 class="section-heading text-2xl">Beliebte Spiele</h3>
<div class="flex space-x-2">
<button class="nav-button left-0">
<span class="material-icons">chevron_left</span>
</button>
<button class="nav-button right-0">
<span class="material-icons">chevron_right</span>
</button>
</div>
</div>
<div class="slider-container">
<div class="slider-grid">
<div class="card group" *ngFor="let game of featuredGames">
<div class="relative overflow-hidden rounded-lg">
<img
[src]="game.image"
[alt]="game.name"
class="w-full aspect-[4/3] object-cover transition-transform duration-300 group-hover:scale-105"
/>
<div
class="absolute inset-0 bg-gradient-to-t from-deep-blue/95 via-deep-blue/50 to-transparent opacity-0 group-hover:opacity-100 transition-all duration-300 ease-in-out"
>
<div
class="absolute bottom-4 left-4 right-4 transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300"
>
<h4 class="game-heading">{{ game.name }}</h4>
<button class="button-primary w-full py-2" (click)="navigateToGame(game.route)">
Jetzt Spielen
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="mt-8">
<h3 class="section-heading text-2xl mb-6">Alle Spiele</h3>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4">
<div class="card group" *ngFor="let game of allGames">
<div class="relative overflow-hidden rounded-lg">
<img
[src]="game.image"
[alt]="game.name"
class="w-full aspect-[4/3] object-cover transition-transform duration-300 group-hover:scale-105"
/>
<div
class="absolute inset-0 bg-gradient-to-t from-deep-blue/95 via-deep-blue/50 to-transparent opacity-0 group-hover:opacity-100 transition-all duration-300 ease-in-out"
>
<div
class="absolute bottom-4 left-4 right-4 transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300"
>
<h4 class="game-heading">{{ game.name }}</h4>
<button class="button-primary w-full py-2" (click)="navigateToGame(game.route)">
Jetzt Spielen
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="lg:col-span-1 space-y-6">
<div class="card p-4">
<h3 class="section-heading text-xl mb-4">Konto</h3>
<div class="space-y-4">
<button class="button-primary w-full py-2" (click)="openDepositModal()">Einzahlen</button>
<app-deposit
[isOpen]="isDepositModalOpen"
(closeModalEmitter)="closeDepositModal()"
></app-deposit>
<button class="bg-deep-blue-light hover:bg-deep-blue-contrast w-full py-2 rounded">
Transaktionen
</button>
<button class="bg-deep-blue-light hover:bg-deep-blue-contrast w-full py-2 rounded">
Kontoeinstellungen
</button>
</div>
</div>
<app-confirmation
[successful]="isDepositSuccessful"
(closeConfirmation)="closeDepositConfirmationModal()"
></app-confirmation>
<div class="card p-4">
<h3 class="section-heading text-xl mb-4">Letzte Transaktionen</h3>
<div class="space-y-3">
<div
class="flex justify-between items-center"
*ngFor="let transaction of recentTransactions"
>
<div>
<p class="text-sm font-medium">{{ transaction.type }}</p>
<p class="text-xs text-text-secondary">{{ transaction.date }}</p>
</div>
<span [class]="transaction.amount > 0 ? 'text-emerald' : 'text-accent-red'">
{{ transaction.amount | currency: 'EUR' }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -1,27 +1,96 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
import { MatDialog } from '@angular/material/dialog';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { CurrencyPipe, NgFor } from '@angular/common';
import { DepositComponent } from '../deposit/deposit.component';
import { ActivatedRoute, Router } from '@angular/router';
import { ConfirmationComponent } from '@shared/components/confirmation/confirmation.component';
import { Transaction } from 'app/model/Transaction';
import { NavbarComponent } from '@shared/components/navbar/navbar.component';
import { Game } from 'app/model/Game';
import { NavbarComponent } from '../../shared/components/navbar/navbar.component';
@Component({
selector: 'app-homepage',
standalone: true,
imports: [NavbarComponent],
imports: [NavbarComponent, CurrencyPipe, NgFor, DepositComponent, ConfirmationComponent],
templateUrl: './home.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HomeComponent {
private keycloakService: KeycloakService = inject(KeycloakService);
public dialog: MatDialog = inject(MatDialog);
export default class HomeComponent implements OnInit {
isDepositModalOpen = false;
isDepositSuccessful = false;
public logout() {
const baseUrl = window.location.origin;
constructor(
public route: ActivatedRoute,
public router: Router
) {}
this.keycloakService.logout(`${baseUrl}/`);
ngOnInit() {
this.isDepositSuccessful = this.route.snapshot.queryParams['success'] == 'true';
this.router.navigate([], { queryParams: {} });
if (this.isDepositSuccessful) {
this.openDepositConfirmationModal();
}
}
public openDialog() {
this.dialog.open(DepositComponent);
featuredGames: Game[] = [
{
id: '1',
name: 'Poker',
image: '/poker.webp',
route: '/game/poker',
},
{
id: '2',
name: 'Blackjack',
image: '/blackjack.webp',
route: '/game/blackjack',
},
{
id: '3',
name: 'Slots',
image: '/slots.webp',
route: '/game/slots',
},
{
id: '4',
name: 'Plinko',
image: '/plinko.webp',
route: '/game/plinko',
},
{
id: '5',
name: 'Liars Dice',
image: '/liars-dice.webp',
route: '/game/liars-dice',
},
{
id: '6',
name: 'Lootboxen',
image: '/lootbox.webp',
route: '/game/lootbox',
},
];
allGames: Game[] = [...this.featuredGames];
recentTransactions: Transaction[] = [];
openDepositModal() {
this.isDepositModalOpen = true;
}
closeDepositModal() {
this.isDepositModalOpen = false;
}
openDepositConfirmationModal() {
this.isDepositSuccessful = true;
}
closeDepositConfirmationModal() {
this.isDepositSuccessful = false;
}
navigateToGame(route: string) {
this.router.navigate([route]);
}
}

View file

@ -1,22 +1,22 @@
<app-navbar></app-navbar>
<div class="min-h-screen bg-deep-blue !text-text-primary">
<div class="min-h-screen bg-deep-blue text-text-primary">
<div class="container mx-auto px-4 py-8 sm:py-12">
<div class="max-w-5xl mx-auto">
<div class="!text-center mb-12 sm:mb-16">
<h1 class="!text-3xl sm:!text-4xl lg:!text-5xl section-heading mb-2 sm:mb-3">
<div class="text-center mb-12 sm:mb-16">
<h1 class="text-3xl sm:text-4xl lg:text-5xl section-heading mb-2 sm:mb-3">
Willkommensbonus
</h1>
<div class="welcome-bonus">200% bis zu 500€</div>
<p class="bonus-description">+ 200 Freispiele</p>
<button class="w-full sm:w-auto button-base px-6 sm:px-8 py-3 shadow-lg">
<button class="w-full sm:w-auto button-primary px-6 sm:px-8 py-3 shadow-lg">
Bonus Sichern
</button>
</div>
<div class="relative mb-16">
<h2 class="!text-xl sm:!text-2xl section-heading mb-4 sm:mb-6">Beliebte Spiele</h2>
<h2 class="text-xl sm:text-2xl section-heading mb-4 sm:mb-6">Beliebte Spiele</h2>
<div class="relative group">
<div class="overflow-hidden rounded-lg">
<div
@ -26,23 +26,23 @@
<div class="slider-grid">
<div class="card">
<div class="game-card-content">
<h3 class="game-heading">Slots</h3>
<h3 class="game-heading-sm">Slots</h3>
<p class="game-text">Klassische Spielautomaten</p>
<button class="button-base w-full py-2">Jetzt Spielen</button>
<button class="button-primary w-full py-2">Jetzt Spielen</button>
</div>
</div>
<div class="card">
<div class="game-card-content">
<h3 class="game-heading">Plinko</h3>
<h3 class="game-heading-sm">Plinko</h3>
<p class="game-text">Spannendes Geschicklichkeitsspiel</p>
<button class="button-base w-full py-2">Jetzt Spielen</button>
<button class="button-primary w-full py-2">Jetzt Spielen</button>
</div>
</div>
<div class="hidden lg:block card">
<div class="game-card-content">
<h3 class="game-heading">Blackjack</h3>
<h3 class="game-heading-sm">Blackjack</h3>
<p class="game-text">Klassisches Kartenspiel</p>
<button class="button-base w-full py-2">Jetzt Spielen</button>
<button class="button-primary w-full py-2">Jetzt Spielen</button>
</div>
</div>
</div>
@ -50,23 +50,23 @@
<div class="slider-grid">
<div class="card">
<div class="game-card-content">
<h3 class="game-heading">Poker</h3>
<h3 class="game-heading-sm">Poker</h3>
<p class="game-text">Texas Hold'em & mehr</p>
<button class="button-base w-full py-2">Jetzt Spielen</button>
<button class="button-primary w-full py-2">Jetzt Spielen</button>
</div>
</div>
<div class="card">
<div class="game-card-content">
<h3 class="game-heading">Liars Dice</h3>
<h3 class="game-heading-sm">Liars Dice</h3>
<p class="game-text">Würfelspiel mit Strategie</p>
<button class="button-base w-full py-2">Jetzt Spielen</button>
<button class="button-primary w-full py-2">Jetzt Spielen</button>
</div>
</div>
<div class="hidden lg:block card">
<div class="game-card-content">
<h3 class="game-heading">Lootboxen</h3>
<h3 class="game-heading-sm">Lootboxen</h3>
<p class="game-text">Überraschungskisten</p>
<button class="button-base w-full py-2">Jetzt Spielen</button>
<button class="button-primary w-full py-2">Jetzt Spielen</button>
</div>
</div>
</div>

View file

@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from '@angular/core';
import { NavbarComponent } from '../../shared/components/navbar/navbar.component';
import { NgFor } from '@angular/common';
import { NavbarComponent } from '@shared/components/navbar/navbar.component';
@Component({
selector: 'app-landing-page',

View file

@ -0,0 +1 @@
<p>Logging in...</p>

View file

@ -0,0 +1,43 @@
import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../../service/auth.service';
import { OAuthService } from 'angular-oauth2-oidc';
@Component({
selector: 'app-login-success',
standalone: true,
imports: [],
templateUrl: './login-success.component.html',
styleUrl: './login-success.component.css',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class LoginSuccessComponent implements OnInit {
private authService: AuthService = inject(AuthService);
private oauthService: OAuthService = inject(OAuthService);
private router: Router = inject(Router);
async ngOnInit() {
try {
if (this.oauthService.hasValidAccessToken()) {
this.router.navigate(['/home']);
} else {
setTimeout(() => {
if (this.oauthService.hasValidAccessToken() || this.authService.getUser()) {
this.router.navigate(['/home']);
} else {
this.router.navigate(['/']);
}
}, 3000);
}
} catch (err) {
console.error('Error during login callback:', err);
setTimeout(() => {
if (this.authService.isLoggedIn()) {
this.router.navigate(['/home']);
} else {
this.router.navigate(['/']);
}
}, 3000);
}
}
}

View file

@ -0,0 +1,6 @@
export interface Game {
id: string;
name: string;
image: string;
route: string;
}

View file

@ -0,0 +1,6 @@
export interface Transaction {
id: string;
type: string;
amount: number;
date: string;
}

View file

@ -0,0 +1,5 @@
export interface User {
authentikId: string;
username: string;
balance: number;
}

View file

@ -0,0 +1,208 @@
import { inject, Injectable } from '@angular/core';
import { AuthConfig, OAuthEvent, OAuthService } from 'angular-oauth2-oidc';
import { UserService } from './user.service';
import { User } from '../model/User';
import { Router } from '@angular/router';
import { environment } from '../../environments/environment';
import { catchError, from, of } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class AuthService {
private readonly authConfig: AuthConfig = {
issuer: 'https://oauth.simonis.lol/application/o/casino-dev/',
clientId: environment.OAUTH_CLIENT_ID,
dummyClientSecret: environment.OAUTH_CLIENT_SECRET,
scope: `openid email profile ${environment.OAUTH_CLIENT_ID}`,
responseType: 'code',
redirectUri: window.location.origin + '/auth/callback',
postLogoutRedirectUri: '',
redirectUriAsPostLogoutRedirectUriFallback: false,
oidc: true,
requestAccessToken: true,
tokenEndpoint: 'https://oauth.simonis.lol/application/o/token/',
userinfoEndpoint: 'https://oauth.simonis.lol/application/o/userinfo/',
strictDiscoveryDocumentValidation: false,
skipIssuerCheck: true,
disableAtHashCheck: true,
requireHttps: false,
showDebugInformation: false,
sessionChecksEnabled: false,
};
private userService: UserService = inject(UserService);
private oauthService: OAuthService = inject(OAuthService);
private router: Router = inject(Router);
private user: User | null = null;
constructor() {
this.oauthService.configure(this.authConfig);
this.setupEventHandling();
const hasAuthParams =
window.location.search.includes('code=') ||
window.location.search.includes('token=') ||
window.location.search.includes('id_token=');
if (hasAuthParams) {
this.processCodeFlow();
} else {
this.checkExistingSession();
}
}
private processCodeFlow() {
this.oauthService
.tryLogin({
onTokenReceived: () => {
this.handleSuccessfulLogin();
},
})
.catch((err) => {
console.error('Error processing code flow:', err);
});
}
private checkExistingSession() {
this.oauthService
.loadDiscoveryDocumentAndTryLogin()
.then((isLoggedIn) => {
if (isLoggedIn && !this.user) {
this.handleSuccessfulLogin();
}
})
.catch((err) => {
console.error('Error during initial login attempt:', err);
});
}
private setupEventHandling() {
this.oauthService.events.subscribe((event: OAuthEvent) => {
if (event.type === 'token_received') {
this.handleSuccessfulLogin();
}
});
}
private handleSuccessfulLogin() {
const claims = this.oauthService.getIdentityClaims();
if (claims && (claims['sub'] || claims['email'])) {
this.processUserProfile(claims);
return;
}
try {
from(this.oauthService.loadUserProfile())
.pipe(
catchError((error) => {
console.error('Error loading user profile:', error);
if (this.oauthService.hasValidAccessToken()) {
this.oauthService.getAccessToken();
const minimalProfile = {
sub: 'user-' + Math.random().toString(36).substring(2, 10),
preferred_username: 'user' + Date.now(),
};
return of({ info: minimalProfile });
}
return of(null);
})
)
.subscribe((profile) => {
if (profile) {
this.processUserProfile(profile);
} else {
this.router.navigate(['/']);
}
});
} catch (err) {
console.error('Exception in handleSuccessfulLogin:', err);
if (this.oauthService.hasValidAccessToken()) {
this.router.navigate(['/home']);
} else {
this.router.navigate(['/']);
}
}
}
private processUserProfile(profile: unknown) {
this.fromUserProfile(profile as Record<string, unknown>).subscribe({
next: (user) => {
this.user = user;
this.router.navigate(['home']);
},
error: (err) => {
console.error('Error creating/retrieving user:', err);
if (this.oauthService.hasValidAccessToken()) {
this.router.navigate(['/home']);
} else {
this.router.navigate(['/']);
}
},
});
}
login() {
try {
this.oauthService
.loadDiscoveryDocument()
.then(() => {
this.oauthService.initLoginFlow();
})
.catch((err) => {
console.error('Error loading discovery document:', err);
this.oauthService.initLoginFlow();
});
} catch (err) {
console.error('Exception in login:', err);
const redirectUri = this.authConfig.redirectUri || window.location.origin + '/auth/callback';
const scope = this.authConfig.scope || 'openid email profile';
const authUrl = `${this.authConfig.issuer}authorize?client_id=${this.authConfig.clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}`;
window.location.href = authUrl;
}
}
logout() {
try {
this.user = null;
this.oauthService.logOut(true);
if (window.location.href.includes('id_token') || window.location.href.includes('logout')) {
window.location.href = window.location.origin;
}
localStorage.removeItem('access_token');
localStorage.removeItem('id_token');
localStorage.removeItem('refresh_token');
sessionStorage.removeItem('access_token');
sessionStorage.removeItem('id_token');
sessionStorage.removeItem('refresh_token');
this.router.navigate(['/']);
} catch (err) {
console.error('Exception in logout:', err);
localStorage.clear();
sessionStorage.clear();
this.router.navigate(['/']);
}
}
isLoggedIn() {
return this.oauthService.hasValidAccessToken();
}
private fromUserProfile(profile: Record<string, unknown>) {
return this.userService.getOrCreateUser(profile);
}
getAccessToken() {
return this.oauthService.getAccessToken();
}
getUser() {
return this.user;
}
}

View file

@ -0,0 +1,60 @@
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, catchError, EMPTY, Observable, tap } from 'rxjs';
import { User } from '../model/User';
@Injectable({
providedIn: 'root',
})
export class UserService {
private http: HttpClient = inject(HttpClient);
private currentUserSubject = new BehaviorSubject<User | null>(null);
public currentUser$ = this.currentUserSubject.asObservable();
constructor() {
this.getCurrentUser().subscribe();
}
public getUser(id: string): Observable<User | null> {
return this.http.get<User | null>(`/backend/user/${id}`).pipe(
catchError(() => EMPTY),
tap((user) => this.currentUserSubject.next(user))
);
}
public getCurrentUser(): Observable<User | null> {
return this.http.get<User | null>('/backend/user').pipe(
catchError(() => EMPTY),
tap((user) => this.currentUserSubject.next(user))
);
}
public refreshCurrentUser(): void {
this.getCurrentUser().subscribe();
}
public createUser(id: string, username: string): Observable<User> {
return this.http
.post<User>('/backend/user', {
authentikId: id,
username: username,
})
.pipe(tap((user) => this.currentUserSubject.next(user)));
}
public getOrCreateUser(profile: Record<string, unknown>): Observable<User> {
const info = profile['info'] as Record<string, unknown> | undefined;
const id = (info?.['sub'] as string) || (profile['sub'] as string);
const username =
(info?.['preferred_username'] as string) ||
(profile['preferred_username'] as string) ||
(profile['email'] as string) ||
(profile['name'] as string);
if (!id || !username) {
throw new Error('Invalid user profile data');
}
return this.createUser(id, username);
}
}

View file

@ -0,0 +1,11 @@
@if (successful) {
<div #modalBg class="modal-bg">
<div #modalCard class="modal-card">
<h2 class="modal-heading text-center">Bestätigung</h2>
<p class="py-2">Der Vorgang wurde erfolgreich abgeschlossen.</p>
<button type="button" class="button-primary w-full py-2 my-auto" (click)="closeModal()">
Schließen
</button>
</div>
</div>
}

View file

@ -0,0 +1,49 @@
import {
Component,
ElementRef,
EventEmitter,
Input,
Output,
ViewChild,
AfterViewInit,
OnDestroy,
} from '@angular/core';
import { ModalAnimationService } from '@shared/services/modal-animation.service';
import gsap from 'gsap';
@Component({
selector: 'app-confirmation',
standalone: true,
imports: [],
templateUrl: './confirmation.component.html',
})
export class ConfirmationComponent implements AfterViewInit, OnDestroy {
@Input() successful = true;
@Output() closeConfirmation = new EventEmitter<void>();
@ViewChild('modalBg') modalBg!: ElementRef;
@ViewChild('modalCard') modalCard!: ElementRef;
constructor(private modalAnimationService: ModalAnimationService) {}
ngAfterViewInit() {
if (this.successful) {
this.openModal();
}
}
ngOnDestroy() {
gsap.killTweensOf([this.modalBg?.nativeElement, this.modalCard?.nativeElement]);
}
private openModal() {
this.modalAnimationService.openModal(this.modalCard.nativeElement, this.modalBg.nativeElement);
}
public closeModal() {
this.modalAnimationService.closeModal(
this.modalCard.nativeElement,
this.modalBg.nativeElement,
() => this.closeConfirmation.emit()
);
}
}

View file

@ -0,0 +1,176 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
signal,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { animate, style, transition, trigger } from '@angular/animations';
import { interval, Subscription, takeWhile } from 'rxjs';
import { AnimatedNumberComponent } from '@blackjack/components/animated-number/animated-number.component';
@Component({
selector: 'app-debt-dialog',
standalone: true,
imports: [CommonModule, AnimatedNumberComponent],
template: `
<div *ngIf="visible" [@fadeInOut] class="modal-bg" style="z-index: 1000; position: fixed;">
<div class="modal-card" [@cardAnimation]>
<h2 class="modal-heading text-accent-red">WARNUNG!</h2>
<p class="py-2 text-text-secondary mb-4">
Du hast nicht genug Geld für den Double Down. Du bist jetzt im Minus und schuldest uns
<app-animated-number [value]="amount" [duration]="0.5"></app-animated-number>.
</p>
<p class="py-2 text-accent-red mb-4 font-bold">
Liefer das Geld sofort an den Dead Drop oder es wird unangenehme Konsequenzen geben!
</p>
<div
class="bg-deep-blue-light/50 rounded-lg p-5 mb-6 shadow-inner border border-deep-blue-light/30"
>
<div class="grid grid-cols-2 gap-4">
<div class="text-text-secondary">Schulden:</div>
<div class="font-medium text-right text-accent-red">
<app-animated-number [value]="amount" [duration]="0.5"></app-animated-number>
</div>
</div>
</div>
<div class="text-center mb-6">
<div
class="text-8xl font-bold text-accent-red"
[class.animate-pulse]="timeLeft() <= 10"
[class.animate-bounce]="timeLeft() <= 5"
[@countdown]="timeLeft()"
>
{{ timeLeft() }}
</div>
<div class="text-text-secondary mt-2">Sekunden verbleibend</div>
</div>
@if (timeLeft() === 0) {
<div class="text-center mb-6">
<div class="relative">
<div class="absolute inset-0 bg-accent-red/20 blur-xl rounded-full"></div>
<div
class="relative bg-gradient-to-b from-accent-red to-red-900 p-8 rounded-lg border-2 border-accent-red shadow-lg"
>
<div class="flex items-center justify-center gap-4 mb-4">
<svg
class="w-12 h-12 text-accent-red animate-[spin_2s_linear_infinite]"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"
/>
</svg>
<span
class="text-4xl font-black tracking-wider text-white animate-[pulse_1s_ease-in-out_infinite]"
>
ZEIT ABGELAUFEN
</span>
<svg
class="w-12 h-12 text-accent-red animate-[spin_2s_linear_infinite]"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"
/>
</svg>
</div>
<div
class="text-2xl font-bold text-white/90 tracking-wider animate-[pulse_1s_ease-in-out_infinite]"
>
KONSEQUENZEN FOLGEN
</div>
</div>
</div>
</div>
}
<button type="button" (click)="closeDialog()" class="button-primary w-full py-2">
Verstanden
</button>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
trigger('fadeInOut', [
transition(':enter', [
style({ opacity: 0 }),
animate('150ms ease-out', style({ opacity: 1 })),
]),
transition(':leave', [animate('150ms ease-in', style({ opacity: 0 }))]),
]),
trigger('cardAnimation', [
transition(':enter', [
style({ opacity: 0, transform: 'scale(0.95)' }),
animate('200ms ease-out', style({ opacity: 1, transform: 'scale(1)' })),
]),
]),
trigger('countdown', [
transition('* => *', [
style({ transform: 'scale(1.2)' }),
animate('100ms ease-out', style({ transform: 'scale(1)' })),
]),
]),
],
})
export class DebtDialogComponent implements OnInit, OnDestroy {
@Input() amount = 0;
@Input() set show(value: boolean) {
this.visible = value;
if (value) {
this.startTimer();
}
}
@Output() dialogClosed = new EventEmitter<void>();
visible = false;
timeLeft = signal(30);
private timerSubscription: Subscription | undefined;
private warningSound = new Audio('assets/sounds/warning.mp3');
ngOnInit() {
if (this.visible) {
this.startTimer();
}
}
ngOnDestroy() {
this.stopTimer();
}
private startTimer() {
this.timeLeft.set(30);
this.timerSubscription = interval(1000)
.pipe(takeWhile(() => this.timeLeft() > 0))
.subscribe(() => {
this.timeLeft.update((value) => value - 1);
if (this.timeLeft() <= 5) {
this.warningSound.play();
}
if (this.timeLeft() === 0) {
setTimeout(() => this.closeDialog(), 5000);
}
});
}
private stopTimer() {
if (this.timerSubscription) {
this.timerSubscription.unsubscribe();
}
}
closeDialog(): void {
this.stopTimer();
this.visible = false;
this.dialogClosed.emit();
}
}

View file

@ -47,7 +47,7 @@
</div>
<div class="footer-payment-method">
<fa-icon [icon]="faWallet" class="footer-payment-icon"></fa-icon>
<span class="footer-payment-text">Klara</span>
<span class="footer-payment-text">Klarna</span>
</div>
<div class="footer-payment-method">
<fa-icon [icon]="faMoneyBillTransfer" class="footer-payment-icon"></fa-icon>

View file

@ -6,16 +6,24 @@
<span>Trustworthy Casino</span>
</a>
<div class="hidden md:flex items-center space-x-1">
<a routerLink="/games" class="nav-link">Games</a>
<a routerLink="/home" class="nav-link">Spiele</a>
</div>
</div>
<div class="hidden md:flex items-center space-x-4">
@if (!isLoggedIn) {
<button (click)="login()" class="button-base px-4 py-1.5">Login</button>
<button (click)="login()" class="button-primary px-4 py-1.5">Anmelden</button>
}
@if (isLoggedIn) {
<button (click)="logout()" class="button-base px-4 py-1.5">Logout</button>
<div
class="text-white font-bold bg-deep-blue-contrast rounded-full px-4 py-2 text-sm hover:bg-deep-blue-contrast/80 hover:cursor-pointer hover:scale-105 transition-all active:scale-95 select-none duration-300"
routerLink="/home"
>
<span [class]="balance() < 0 ? 'text-accent-red' : ''">
<app-animated-number [value]="balance()" [duration]="0.5"></app-animated-number>
</span>
</div>
<button (click)="logout()" class="button-primary px-4 py-1.5">Abmelden</button>
}
</div>
@ -55,13 +63,13 @@
<div [class]="isMenuOpen ? 'block' : 'hidden'" class="md:hidden">
<div class="nav-mobile-menu">
<a routerLink="/games" class="nav-mobile-link">Games</a>
<a routerLink="/games" class="nav-mobile-link">Spiele</a>
<div class="pt-2 space-y-2">
@if (!isLoggedIn) {
<button (click)="login()" class="button-base w-full py-1.5">Login</button>
<button (click)="login()" class="button-primary w-full py-1.5">Anmelden</button>
}
@if (isLoggedIn) {
<button (click)="logout()" class="button-base w-full py-1.5">Logout</button>
<button (click)="logout()" class="button-primary w-full py-1.5">Abmelden</button>
}
</div>
</div>

View file

@ -1,31 +1,56 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
inject,
OnInit,
OnDestroy,
signal,
} from '@angular/core';
import { RouterModule } from '@angular/router';
import { KeycloakService } from 'keycloak-angular';
import { AuthService } from '../../../service/auth.service';
import { CurrencyPipe } from '@angular/common';
import { UserService } from '@service/user.service';
import { Subscription } from 'rxjs';
import { AnimatedNumberComponent } from '@blackjack/components/animated-number/animated-number.component';
@Component({
selector: 'app-navbar',
templateUrl: './navbar.component.html',
standalone: true,
imports: [RouterModule],
imports: [RouterModule, CurrencyPipe, AnimatedNumberComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NavbarComponent {
export class NavbarComponent implements OnInit, OnDestroy {
isMenuOpen = false;
private keycloakService: KeycloakService = inject(KeycloakService);
private authService: AuthService = inject(AuthService);
isLoggedIn = this.authService.isLoggedIn();
isLoggedIn = this.keycloakService.isLoggedIn();
private userService = inject(UserService);
private userSubscription: Subscription | undefined;
public balance = signal(0);
ngOnInit() {
this.userSubscription = this.userService.currentUser$.subscribe((user) => {
this.balance.set(user?.balance ?? 0);
});
}
ngOnDestroy() {
if (this.userSubscription) {
this.userSubscription.unsubscribe();
}
}
login() {
try {
const baseUrl = window.location.origin;
this.keycloakService.login({ redirectUri: `${baseUrl}/home` });
this.authService.login();
} catch (error) {
console.error('Login failed:', error);
}
}
logout() {
this.keycloakService.logout();
this.authService.logout();
}
toggleMenu() {

Some files were not shown because too many files have changed in this diff Show more