Compare commits
177 Commits
44c88c8f60
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad5add0ffc | ||
| 30c05c3014 | |||
|
|
d75d64113b | ||
| 66b55945eb | |||
|
|
50145c914b | ||
| 0430c7f4de | |||
|
|
1317fd40f5 | ||
| daa072f2bf | |||
|
|
c36720e548 | ||
| 4b74de1261 | |||
|
|
60ec7de366 | ||
| 4a6598f102 | |||
|
|
b278f755b4 | ||
| 23cdbe9bd1 | |||
|
|
416baebab5 | ||
| 2f38855449 | |||
|
|
1a3cb5044f | ||
| 861f2f1184 | |||
|
|
773b760bc1 | ||
| b729344d1e | |||
|
|
9a194a9fd7 | ||
| fccd94cc11 | |||
|
|
27da1327c1 | ||
| 82a9706deb | |||
|
|
6479f68b6c | ||
| 4f52c1a0f3 | |||
|
|
3787e0f3ed | ||
| 45dec00b86 | |||
|
|
fbb3f48b6d | ||
| e32f4eb8ff | |||
|
|
66edab8ffe | ||
| 9ca1813f06 | |||
|
|
0572af6ea2 | ||
| 9fa1e5e071 | |||
|
|
d34f0d16cc | ||
| 476db28821 | |||
|
|
18c347b6ad | ||
| 4aa8709eb5 | |||
|
|
3766241dad | ||
| 009b2b1ad9 | |||
|
|
ab29008a06 | ||
| 71a549b7f0 | |||
|
|
3ef79e5838 | ||
| f4290b4497 | |||
|
|
5a1296ff3a | ||
| a7292e3b5d | |||
|
|
d2538a7a34 | ||
| f6d3a18452 | |||
|
|
9d72cda5ff | ||
| 3ce2b133bc | |||
| 802b6bf764 | |||
|
|
410b4829f4 | ||
| 4b17af2c2f | |||
|
|
a163d9f8fe | ||
| dc3da9a75c | |||
|
|
4a4e9c48fc | ||
| 859dfce521 | |||
| 61ae9b5a5e | |||
|
|
98fa5f63d6 | ||
| 0e555cdfeb | |||
| b4bf2ceb4d | |||
|
|
1542906edf | ||
| cf1854976a | |||
|
|
723a4be33f | ||
| 2f89951c25 | |||
|
|
f330c5f3d8 | ||
| 4a5af36ae0 | |||
|
|
edcab594a7 | ||
| 26157076d6 | |||
|
|
103b341488 | ||
| 6ef7401443 | |||
|
|
bbbbf33c41 | ||
| dbad818fda | |||
|
|
5f8afbf236 | ||
| 3b7a1e3c64 | |||
|
|
5af5785fa0 | ||
|
|
9510147806 | ||
| e17f59e614 | |||
| 8126b46a0a | |||
| 25dd9264b5 | |||
| 5f9ef2beb0 | |||
| ccf993bff2 | |||
| 1d05bd43b4 | |||
| 3e067346ff | |||
| b2527ed041 | |||
|
|
06d150b8c5 | ||
| 0cbac61b4a | |||
| 088dda8d52 | |||
|
|
c3d10f31ac | ||
| e4384ee894 | |||
|
|
d667a2f68c | ||
| 35f608513d | |||
|
|
13038b0cb9 | ||
| b17aae5795 | |||
|
|
421f769cb6 | ||
| bd7a055a09 | |||
|
|
e2a2b56174 | ||
| 2a29ca8cdd | |||
|
|
266cbe7509 | ||
| e8b31b1748 | |||
|
|
8812b0fad4 | ||
| dd5e8e65e5 | |||
|
|
bf6ffeadb0 | ||
| fa3d21e303 | |||
|
|
33efc4e107 | ||
| 8d697fd311 | |||
|
|
b9e60b5d4a | ||
| 270f44cc1f | |||
|
|
73dbe5826a | ||
| 194df5691c | |||
|
|
49a1bd40ff | ||
| f847424b9c | |||
|
|
d50a576e31 | ||
|
|
eba2ad6232 | ||
| 14961cce01 | |||
|
|
dcb5d7373f | ||
| d57e6efa98 | |||
|
|
4156e1c9ce | ||
| 358556612e | |||
|
|
7f82d2eeae | ||
| a55f0b4b61 | |||
|
|
f115c03ecb | ||
| fd2467a9ea | |||
|
|
9d3f3940a9 | ||
| 0541bb58d1 | |||
|
|
89a6aa22f7 | ||
| 6e17328846 | |||
| 0037820905 | |||
| cfcd967ce0 | |||
| 1f96290371 | |||
| 2aee79bb68 | |||
| 46c96d4ceb | |||
|
|
14b4473f72 | ||
| 1ef5e8a72f | |||
|
|
576e5af87e | ||
| 3c0828fdbe | |||
|
|
ae7f04abc3 | ||
| b81bb3d0ae | |||
| 52e5033afc | |||
|
|
10a26404b3 | ||
|
|
11478a096d | ||
| 8ca909db52 | |||
|
|
1edb3bfd89 | ||
| 9738a04b7a | |||
|
|
2bc50664e0 | ||
| 3e3a062a06 | |||
| 641c892981 | |||
| a58b2e03b1 | |||
| e60fe7c98d | |||
| 370de175db | |||
| 5d245d0011 | |||
| c220e54bb8 | |||
|
|
b847d3c054 | ||
| c7dd72ecc2 | |||
| 42a5adbae0 | |||
|
|
ae9a8f2af9 | ||
| 7adc8b8645 | |||
|
|
146348470f | ||
| 5e503cbc36 | |||
|
|
126e2030ae | ||
| 54e3215127 | |||
|
|
72d2845772 | ||
| 64a7a63ab3 | |||
|
|
51c36348b9 | ||
| 2e54880302 | |||
|
|
266406fe7c | ||
| 5c6d3ac436 | |||
|
|
674619dadc | ||
|
|
ef539d3eea | ||
|
|
e09463b138 | ||
| 4d6ea54771 | |||
| aa83082d09 | |||
| 42e7597e26 | |||
|
|
6befd9c722 | ||
| 51d9c0b5f6 | |||
| de565b52dc | |||
| 051e7406e3 |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
.env
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -134,7 +134,9 @@ target
|
||||
/.project
|
||||
/.settings
|
||||
/RUNNING_PID
|
||||
/knockoutwhistwebfrontend/
|
||||
/knockoutwhist/
|
||||
/knockoutwhistweb/.g8/
|
||||
/knockoutwhistweb/.bsp/
|
||||
/currentSnapshot.json
|
||||
.env
|
||||
5
.gitmodules
vendored
5
.gitmodules
vendored
@@ -2,3 +2,8 @@
|
||||
path = knockoutwhist
|
||||
branch = main
|
||||
url = https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist.git
|
||||
|
||||
[submodule "knockoutwhistfrontend"]
|
||||
path = knockoutwhistfrontend
|
||||
branch = main
|
||||
url = https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Frontend.git
|
||||
|
||||
2
.sbtopts
Normal file
2
.sbtopts
Normal file
@@ -0,0 +1,2 @@
|
||||
-J--add-opens=java.base/java.util=ALL-UNNAMED
|
||||
-J--add-opens=java.base/java.lang=ALL-UNNAMED
|
||||
470
CHANGELOG.md
470
CHANGELOG.md
@@ -1,4 +1,4 @@
|
||||
## (2025-11-03)
|
||||
## (2025-11-07)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
### Features
|
||||
|
||||
* **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2))
|
||||
* **docker:** added docker container support ([#47](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/47)) ([4d6ea54](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/4d6ea54771c284d5ea0bb798fedaf0423a6cdd58))
|
||||
* implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb))
|
||||
* **ui:** add Lobby and Main Menu Body ([#38](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/38)) ([051e740](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/051e7406e30385d3260631a783f507b90d9f94d1))
|
||||
* **ui:** add main menu navbar and join game functionality ([#35](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/35)) ([32d4f9c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/32d4f9c6cefa20f10816dad9f33548a146d4c2c3))
|
||||
* **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f))
|
||||
* **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186))
|
||||
* **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17)
|
||||
@@ -18,17 +21,22 @@
|
||||
|
||||
* **base:** fixed persistence logic ([#21](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/21)) ([7f765b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7f765b4514459202df3768c64a1df383fecb66e2))
|
||||
* **config:** modified git module to use the main branch ([#10](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/10)) ([ccf44ed](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/ccf44ede41e10df4b973c9cecc09d1e7105143f5))
|
||||
* disabled external node ([51d9c0b](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/51d9c0b5f687d951f42480e333f37b3155d58dbd))
|
||||
* ensure proper unlocking of user session locks in game actions ([#37](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/37)) ([44c88c8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/44c88c8f60abf4a54e721f8e7f6de44a6d28b258))
|
||||
* **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa))
|
||||
* improve lock handling in user session interactions ([#36](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/36)) ([96c3846](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/96c38466d20203a561a5bc3bba560c9d9fd6d754))
|
||||
* **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f))
|
||||
* **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782))
|
||||
* **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c))
|
||||
* update allowed hosts filter and adjust background color in login page ([#45](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/45)) ([aa83082](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/aa83082d095b30fd3eae336ad5f1aa5d409fbdab))
|
||||
* update file paths and improve session handling in user interactions ([#39](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/39)) ([de565b5](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/de565b52dca6b49b903eceaecd47cc3a5429608b))
|
||||
|
||||
### Reverts
|
||||
|
||||
* ci: bump version to v1.0.0 [skip ci] ([91d7f6c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/91d7f6ca003edb368aba76b7a82c8b42d22bdbfe))
|
||||
* version bumb ([2e10059](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2e10059a6756befe6f8e70c94fd34b865693efb8))
|
||||
* version bump ([7879d1a](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7879d1ab6ee8f227c19b69b467a28dd7b479ff73))
|
||||
## (2025-11-03)
|
||||
## (2025-11-07)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
@@ -37,7 +45,10 @@
|
||||
### Features
|
||||
|
||||
* **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2))
|
||||
* **docker:** added docker container support ([#47](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/47)) ([4d6ea54](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/4d6ea54771c284d5ea0bb798fedaf0423a6cdd58))
|
||||
* implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb))
|
||||
* **ui:** add Lobby and Main Menu Body ([#38](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/38)) ([051e740](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/051e7406e30385d3260631a783f507b90d9f94d1))
|
||||
* **ui:** add main menu navbar and join game functionality ([#35](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/35)) ([32d4f9c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/32d4f9c6cefa20f10816dad9f33548a146d4c2c3))
|
||||
* **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f))
|
||||
* **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186))
|
||||
* **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17)
|
||||
@@ -48,94 +59,407 @@
|
||||
|
||||
* **base:** fixed persistence logic ([#21](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/21)) ([7f765b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7f765b4514459202df3768c64a1df383fecb66e2))
|
||||
* **config:** modified git module to use the main branch ([#10](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/10)) ([ccf44ed](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/ccf44ede41e10df4b973c9cecc09d1e7105143f5))
|
||||
* disabled external node ([51d9c0b](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/51d9c0b5f687d951f42480e333f37b3155d58dbd))
|
||||
* ensure proper unlocking of user session locks in game actions ([#37](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/37)) ([44c88c8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/44c88c8f60abf4a54e721f8e7f6de44a6d28b258))
|
||||
* **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa))
|
||||
* improve lock handling in user session interactions ([#36](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/36)) ([96c3846](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/96c38466d20203a561a5bc3bba560c9d9fd6d754))
|
||||
* **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f))
|
||||
* **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782))
|
||||
* **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c))
|
||||
* update allowed hosts filter and adjust background color in login page ([#45](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/45)) ([aa83082](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/aa83082d095b30fd3eae336ad5f1aa5d409fbdab))
|
||||
* update file paths and improve session handling in user interactions ([#39](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/39)) ([de565b5](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/de565b52dca6b49b903eceaecd47cc3a5429608b))
|
||||
|
||||
### Reverts
|
||||
|
||||
* ci: bump version to v1.0.0 [skip ci] ([91d7f6c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/91d7f6ca003edb368aba76b7a82c8b42d22bdbfe))
|
||||
* version bumb ([2e10059](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2e10059a6756befe6f8e70c94fd34b865693efb8))
|
||||
* version bump ([7879d1a](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7879d1ab6ee8f227c19b69b467a28dd7b479ff73))
|
||||
## (2025-11-03)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* implemented multigame support (#34)
|
||||
|
||||
### Features
|
||||
|
||||
* **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2))
|
||||
* implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb))
|
||||
* **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f))
|
||||
* **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186))
|
||||
* **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17)
|
||||
* **ui:** LESS Integration ([72fcf78](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/72fcf783b8ca5ce260e4b152016353e7dc1edb69)), closes [#15-Create-a-default-theme-with-Less-](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/15-Create-a-default-theme-with-Less-) [#23](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/23)
|
||||
* **ui:** UI now shows player names instead of their id ([#11](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/11)) ([c168ae7](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c168ae7dc05e99c1890661aa3b63f4d4d12779fb))
|
||||
## (2025-11-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **base:** fixed persistence logic ([#21](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/21)) ([7f765b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7f765b4514459202df3768c64a1df383fecb66e2))
|
||||
* **config:** modified git module to use the main branch ([#10](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/10)) ([ccf44ed](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/ccf44ede41e10df4b973c9cecc09d1e7105143f5))
|
||||
* **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa))
|
||||
* **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f))
|
||||
* **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782))
|
||||
* **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c))
|
||||
|
||||
### Reverts
|
||||
|
||||
* ci: bump version to v1.0.0 [skip ci] ([91d7f6c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/91d7f6ca003edb368aba76b7a82c8b42d22bdbfe))
|
||||
* version bumb ([2e10059](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2e10059a6756befe6f8e70c94fd34b865693efb8))
|
||||
## (2025-11-03)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* implemented multigame support (#34)
|
||||
|
||||
### Features
|
||||
|
||||
* **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2))
|
||||
* implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb))
|
||||
* **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f))
|
||||
* **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186))
|
||||
* **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17)
|
||||
* **ui:** LESS Integration ([72fcf78](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/72fcf783b8ca5ce260e4b152016353e7dc1edb69)), closes [#15-Create-a-default-theme-with-Less-](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/15-Create-a-default-theme-with-Less-) [#23](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/23)
|
||||
* **ui:** UI now shows player names instead of their id ([#11](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/11)) ([c168ae7](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c168ae7dc05e99c1890661aa3b63f4d4d12779fb))
|
||||
* ensure proper CMD syntax in Dockerfile ([#48](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/48)) ([5c6d3ac](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/5c6d3ac436f6d23a36f58b6835c9bd50feddc789))
|
||||
## (2025-11-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **base:** fixed persistence logic ([#21](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/21)) ([7f765b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7f765b4514459202df3768c64a1df383fecb66e2))
|
||||
* **config:** modified git module to use the main branch ([#10](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/10)) ([ccf44ed](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/ccf44ede41e10df4b973c9cecc09d1e7105143f5))
|
||||
* **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa))
|
||||
* **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f))
|
||||
* **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782))
|
||||
* **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c))
|
||||
|
||||
### Reverts
|
||||
|
||||
* ci: bump version to v1.0.0 [skip ci] ([91d7f6c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/91d7f6ca003edb368aba76b7a82c8b42d22bdbfe))
|
||||
## (2025-11-03)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* implemented multigame support (#34)
|
||||
|
||||
### Features
|
||||
|
||||
* **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2))
|
||||
* implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb))
|
||||
* **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f))
|
||||
* **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186))
|
||||
* **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17)
|
||||
* **ui:** LESS Integration ([72fcf78](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/72fcf783b8ca5ce260e4b152016353e7dc1edb69)), closes [#15-Create-a-default-theme-with-Less-](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/15-Create-a-default-theme-with-Less-) [#23](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/23)
|
||||
* **ui:** UI now shows player names instead of their id ([#11](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/11)) ([c168ae7](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c168ae7dc05e99c1890661aa3b63f4d4d12779fb))
|
||||
* changelog syntax ([2e54880](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2e548803020c99f62644283fcf3570048261173a))
|
||||
## (2025-11-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **base:** fixed persistence logic ([#21](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/21)) ([7f765b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7f765b4514459202df3768c64a1df383fecb66e2))
|
||||
* **config:** modified git module to use the main branch ([#10](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/10)) ([ccf44ed](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/ccf44ede41e10df4b973c9cecc09d1e7105143f5))
|
||||
* **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa))
|
||||
* **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f))
|
||||
* **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782))
|
||||
* **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c))
|
||||
* removed trailing ([64a7a63](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/64a7a63ab3dff59e66f62328e3b5865bb177fcde))
|
||||
## (2025-11-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* traling ([54e3215](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/54e321512777f6722864694eb677eab0e8418a9f))
|
||||
## (2025-11-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* removed trailing ([5e503cb](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/5e503cbc364f7cb23926976acc6cee575eadd9d6))
|
||||
## (2025-11-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* trailing ([7adc8b8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7adc8b8645390cd18d63b4eee6db8ef448b7a46a))
|
||||
## (2025-11-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* removed trailing ([c7dd72e](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c7dd72ecc2786ef63daf2b4288093025a8e22bfd))
|
||||
* removed trailing ([42a5adb](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/42a5adbae01802587a48a9de15bad44b5ef014cf))
|
||||
## (2025-11-20)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **game:** Fixed polling, SPA, Gameplayloop etc. (#59)
|
||||
|
||||
### Features
|
||||
|
||||
* **ci:** Polling ([370de17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/370de175db65edf87e7ab211190977b540a39d85))
|
||||
* **ci:** Polling Added polling for when the game starts and a card gets played ([#58](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/58)) ([e60fe7c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/e60fe7c98dcab05949140a8a54ed6e4e2fbbc022))
|
||||
* **game:** Fixed polling, SPA, Gameplayloop etc. ([#59](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/59)) ([a58b2e0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/a58b2e03b11a54667d63ba6604f579a8e328c9d1))
|
||||
* **ui:** added js routing, updated ingame ui, added tricktable ([#50](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/50)) ([c220e54](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c220e54bb8d87f4f0f37a089bcd993e8df806123)), closes [#43](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/43) [#43](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/43)
|
||||
* **ui:** implement tie & trump menu, fixed some critical bugs ([#52](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/52)) ([5d245d0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/5d245d0011a5fb03193514303b45702cd8329224))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **polling:** Improve polling mechanism and delay handling ([#60](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/60)) ([641c892](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/641c892981649eb85640527cc0fe325ff683fa77))
|
||||
## (2025-11-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** Fixed a bug where the game would reload on game start ([#81](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/81)) ([9738a04](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/9738a04b7a3c63c8cd1450e563ec04823fb3c35a))
|
||||
## (2025-11-23)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **websocket:** Implement WebSocket connection and event handling (#82)
|
||||
|
||||
### Features
|
||||
|
||||
* **websocket:** Implement WebSocket connection and event handling ([#82](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/82)) ([8ca909d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/8ca909db522dd7108a3e40ce84811eaf8695eaa5))
|
||||
## (2025-11-24)
|
||||
## (2025-11-26)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** Implement received hand event handling and UI updates ([#83](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/83)) ([52e5033](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/52e5033afca344ae40a644196555a9655913710a)), closes [#76](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/76)
|
||||
* **base:** Fixed logic for websockets and added GameStateEvent. Might've caused instability on other feature branches! ([#84](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/84)) ([b81bb3d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/b81bb3d0aeb8500a9d7417a10e24e7f8a17d71d2))
|
||||
## (2025-11-26)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** Implemented card played event via websocket ([#85](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/85)) ([3c0828f](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/3c0828fdbeb507706b86f1662476c46e760533e4))
|
||||
## (2025-11-26)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** Implemented session closed and kick event via websocket ([#87](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/87)) ([1ef5e8a](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1ef5e8a72fdf8a3d1ae624c8c3d7c6595017bc6f))
|
||||
## (2025-12-01)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** Implemented turn event via websocket ([#86](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/86)) ([2aee79b](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2aee79bb6887008397aa0780d1d74ce96af1c202))
|
||||
* GameState to Title Mapping BAC-1 ([#92](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/92)) ([6e17328](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6e17328846745375482c97383b143d86a86e7f32))
|
||||
* **ui:** Implement countless feature using the SJWP ([#89](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/89)) ([1f96290](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f962903712163543fd4f98e696be5e7e29d88a6))
|
||||
* **ui:** Popups ([#91](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/91)) ([0037820](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/003782090509bca1c5022c308231b7560dd9b23d))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** Fixed websocket routing ([#88](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/88)) ([46c96d4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/46c96d4ceb935ac91fc515a1fdaef195e5ebc0a7))
|
||||
* **api:** fixes - reimplemented animations ([#90](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/90)) ([cfcd967](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/cfcd967ce08ecf07f3f06826c337f684eb3b0c5f))
|
||||
## (2025-12-01)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** BAC-10 Websockets - Kick Users ([#93](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/93)) ([0541bb5](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/0541bb58d19efd98d134b3d0412f39b4b1001783))
|
||||
## (2025-12-01)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** BAC-11 Websocket - Return to Lobby ([#94](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/94)) ([fd2467a](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/fd2467a9ea22dca64d5152a5a3e6db86d9a6f345))
|
||||
## (2025-12-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** BAC-23 Remove old polling code ([#95](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/95)) ([a55f0b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/a55f0b4b6164a47e3524422650ed99d10f9c8b0d))
|
||||
## (2025-12-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* FRO-6 Websocket Close Handle ([#96](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/96)) ([3585566](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/358556612ec74601c8b31125e4e65f750abf8c4c))
|
||||
## (2025-12-03)
|
||||
|
||||
### Features
|
||||
|
||||
* **ui:** FRO-7 Endscreen ([#97](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/97)) ([d57e6ef](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d57e6efa985ca07c32f9f54595fe7393dbdf4d8a))
|
||||
## (2025-12-03)
|
||||
## (2025-12-03)
|
||||
## (2025-12-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* BAC-25 Race Condition: Websocket Promises ([#99](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/99)) ([f847424](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/f847424b9cea423ace5661d1efb6e4f01483c655))
|
||||
## (2025-12-04)
|
||||
|
||||
### Features
|
||||
|
||||
* FRO-3 FRO-4 Added vue compontents to ingame and lobby ([#100](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/100)) ([194df56](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/194df5691ccda1c21ebe9157c4396a4a21aa921d))
|
||||
## (2025-12-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* BAC-29 Implement Mappers for Common Classes ([#101](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/101)) ([270f44c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/270f44cc1f3447ffcc33fb19a47c52391c69972b))
|
||||
## (2025-12-06)
|
||||
|
||||
### Features
|
||||
|
||||
* BAC-30 Implement Jackson Mapping via DTOs ([#102](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/102)) ([8d697fd](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/8d697fd311478cf792b4631377de4522ecbda9f7))
|
||||
## (2025-12-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* FRO-29 Websocket Communication ([#104](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/104)) ([fa3d21e](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/fa3d21e3038eb07369764850a9ad9badd269ac57))
|
||||
## (2025-12-10)
|
||||
|
||||
### Features
|
||||
|
||||
* BAC-27 Implemented endpoint which returns information about the current state ([#103](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/103)) ([dd5e8e6](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/dd5e8e65e55f02a7618b3c60e8fc7087774e5106))
|
||||
## (2025-12-10)
|
||||
|
||||
### Features
|
||||
|
||||
* FRO-2 Implement Login Component ([#105](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/105)) ([e8b31b1](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/e8b31b174819b5f033034501856c4b1189c4c4ee))
|
||||
## (2025-12-10)
|
||||
|
||||
### Features
|
||||
|
||||
* FRO-20 Create scoreboard component ([#106](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/106)) ([2a29ca8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2a29ca8cdd3ef55f6f66f00b5e7727e1b1af1458))
|
||||
## (2025-12-10)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** FRO-14 Create Game ([#107](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/107)) ([bd7a055](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bd7a055a0944a1c5219f21bb080bf658229f49e9))
|
||||
## (2025-12-11)
|
||||
|
||||
### Features
|
||||
|
||||
* FRO-31 Small backend changes ([#108](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/108)) ([b17aae5](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/b17aae5795b35ce3805db87c9bf741a5a96cd5ac))
|
||||
## (2025-12-14)
|
||||
|
||||
### Features
|
||||
|
||||
* Update routing and websocket configuration for game state management ([#109](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/109)) ([35f6085](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/35f608513dd80eece46d49b40ecf31c8e915d307))
|
||||
## (2025-12-18)
|
||||
|
||||
### Features
|
||||
|
||||
* **ui:** FRO-36 PWA ([#110](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/110)) ([e4384ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/e4384ee8945ac462fe1f3580215117e0a438f71a))
|
||||
## (2026-01-06)
|
||||
|
||||
### Features
|
||||
|
||||
* Enhance win effects and animations in OfflineView component ([088dda8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/088dda8d528edb9f3fd420e7e69eb44144d39eff))
|
||||
## (2026-01-07)
|
||||
|
||||
### Features
|
||||
|
||||
* Add GatewayModule for enhanced module configuration ([1d05bd4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1d05bd43b482564636345322a2cecf58f7d229d0))
|
||||
* Enhance Dockerfile with secret management for GitHub credentials ([5f9ef2b](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/5f9ef2beb0a6e7d5e8caf436f545bbd78a6b242e))
|
||||
* Simplify Dockerfile by removing multi-stage build and adjusting file copy paths ([e17f59e](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/e17f59e614a0060b70514c2337ac6fd84688546e))
|
||||
* Update build.sbt and Dockerfile for improved GitHub credentials handling ([8126b46](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/8126b46a0a1649d2b20b31975603f7610fafd18b))
|
||||
* Update build.sbt and Dockerfile for improved GitHub credentials handling ([25dd926](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/25dd9264b57ffafc5b01587ff5dda2e2188b5fbd))
|
||||
* Update Dockerfile for multi-platform support and add nginx configuration ([ccf993b](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/ccf993bff25b4551a70c1f0263695f828df15a02))
|
||||
* Update Dockerfile for multi-platform support and add nginx configuration ([3e06734](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/3e067346ffe3bdc62dc936ea8e79ae9293d86351))
|
||||
* Update Dockerfile for multi-platform support and add nginx configuration ([b2527ed](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/b2527ed041568d20f880515b406fe0b0e10c12c1))
|
||||
## (2026-01-07)
|
||||
## (2026-01-07)
|
||||
|
||||
### Features
|
||||
|
||||
* Update configuration files for CORS settings and add production environment ([3b7a1e3](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/3b7a1e3c646d870134d8d06b4962498b0e282cbd))
|
||||
## (2026-01-07)
|
||||
|
||||
### Features
|
||||
|
||||
* Add caching headers for env.js in Nginx configuration ([dbad818](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/dbad818fdaeb237a05f583e5402773a4339e7aa1))
|
||||
## (2026-01-07)
|
||||
|
||||
### Features
|
||||
|
||||
* Add logging to Gateway for pod synchronization and startup events ([6ef7401](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6ef74014430673e725245bf37e44c5b90b81abb3))
|
||||
## (2026-01-07)
|
||||
|
||||
### Features
|
||||
|
||||
* Add logging to Gateway for pod synchronization and startup events ([2615707](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/26157076d686a5dd3f8157ec2b2d1ae9d9e9eedf))
|
||||
## (2026-01-07)
|
||||
|
||||
### Features
|
||||
|
||||
* Add Health and Login endpoints with updated Redis configuration ([4a5af36](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/4a5af36ae0dcb540e02b7a1cd042e54cc6342c78))
|
||||
## (2026-01-07)
|
||||
|
||||
### Features
|
||||
|
||||
* Update Gateway to use ArrayList for game IDs and bound users ([2f89951](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2f89951c25484d6bc412536a83019ee6d0b7f780))
|
||||
## (2026-01-07)
|
||||
|
||||
### Features
|
||||
|
||||
* Update joinGame endpoint to accept gameId as a path parameter ([cf18549](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/cf1854976a51eb4931d50cf93640498ed18686fc))
|
||||
## (2026-01-07)
|
||||
|
||||
### Features
|
||||
|
||||
* Enhance user state management with polling and WebSocket connection handling ([b4bf2ce](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/b4bf2ceb4dc76ac388124b9705a1aa9e577582af))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Update knockoutwhistfrontend hash for consistency ([0e555cd](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/0e555cdfeb114464c9438bfd5dc397201a073867))
|
||||
## (2026-01-07)
|
||||
|
||||
### Features
|
||||
|
||||
* Implement PlayDogCard functionality in user session and update Vue component ([859dfce](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/859dfce521b193b9208d0c70fca88016f8fe08f4))
|
||||
* Implement PlayDogCard functionality in user session and update Vue component ([61ae9b5](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/61ae9b5a5e7cd9fd82b77e9159814b0066874c2d))
|
||||
## (2026-01-13)
|
||||
|
||||
### Features
|
||||
|
||||
* **ui:** Tie selection ([#111](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/111)) ([dc3da9a](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/dc3da9a75c75597ce81ce4d023af5390197012c9))
|
||||
## (2026-01-14)
|
||||
|
||||
### Features
|
||||
|
||||
* CORE-4 Rework the delay handler ([#113](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/113)) ([4b17af2](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/4b17af2c2f50a9d67cf1cf49cafdaac8f807d4b6))
|
||||
## (2026-01-14)
|
||||
|
||||
### Features
|
||||
|
||||
* CORE-4 Rework the delay handler ([3ce2b13](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/3ce2b133bccf4dd591b6d038d6fa0d409a907775))
|
||||
* Update LobbyComponent to use icons for player removal buttons ([802b6bf](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/802b6bf764eb41b806888e1b46a3e6d379d31f1b))
|
||||
## (2026-01-20)
|
||||
|
||||
### Features
|
||||
|
||||
* BAC-39 Authentication ([#114](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/114)) ([f6d3a18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/f6d3a1845205318f43eb443601fd257613b7defb))
|
||||
## (2026-01-20)
|
||||
|
||||
### Features
|
||||
|
||||
* Update persistence.xml and build.sbt for resource management ([a7292e3](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/a7292e3b5df4788f2f8bea5a2ec7b209b7357608))
|
||||
## (2026-01-20)
|
||||
|
||||
### Features
|
||||
|
||||
* Update connection provider in persistence.xml for HikariCP ([f4290b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/f4290b44976fb6dcd4fc4b896614ba6062da73b1))
|
||||
## (2026-01-20)
|
||||
|
||||
### Features
|
||||
|
||||
* Update Hibernate connection provider and database configuration ([71a549b](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/71a549b7f059e748f7691bb9a27e2861b61c6f6f))
|
||||
## (2026-01-20)
|
||||
|
||||
### Features
|
||||
|
||||
* Add HikariCP specific configuration to db.conf ([009b2b1](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/009b2b1ad9180f58a0b1434354f8a467b4e452ca))
|
||||
## (2026-01-20)
|
||||
|
||||
### Features
|
||||
|
||||
* Add HikariCP specific configuration to db.conf ([4aa8709](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/4aa8709eb593b03254efc616b6b04c23b23ab6ab))
|
||||
## (2026-01-20)
|
||||
|
||||
### Features
|
||||
|
||||
* Enhance EntityManagerProvider to use Play configuration for database settings ([476db28](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/476db288216ed2c1013fe3ddb9b82472254e352b))
|
||||
## (2026-01-20)
|
||||
|
||||
### Features
|
||||
|
||||
* Disable default JPA and Hibernate modules and enhance EntityManagerProvider for HikariCP integration ([9fa1e5e](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/9fa1e5e07122aebd0391d47c3513013243a72a0f))
|
||||
## (2026-01-20)
|
||||
|
||||
### Features
|
||||
|
||||
* Add logging for user management operations in HibernateUserManager ([9ca1813](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/9ca1813f06539cffeb573d0e00571e4f2d5144f1))
|
||||
## (2026-01-20)
|
||||
|
||||
### Features
|
||||
|
||||
* Integrate UserManager and HibernateUserManager in session management ([e32f4eb](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/e32f4eb8fff9daec46f20284e28e94a59231d033))
|
||||
## (2026-01-20)
|
||||
|
||||
### Features
|
||||
|
||||
* Implement transaction management for user addition and removal in HibernateUserManager ([45dec00](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/45dec00b86a1395457226ed62ac319c61e38739a))
|
||||
## (2026-01-20)
|
||||
|
||||
### Features
|
||||
|
||||
* Add mainRoute configuration for OpenID in application and environment files ([4f52c1a](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/4f52c1a0f30cf0b917452149a52b53b94d82a7c9))
|
||||
## (2026-01-20)
|
||||
|
||||
### Features
|
||||
|
||||
* Simplify authorization request creation in OpenIDConnectService and use environment variables for Keycloak configuration ([82a9706](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/82a9706deb97db193015e55a048830d496e76d83))
|
||||
## (2026-01-20)
|
||||
|
||||
### Features
|
||||
|
||||
* Use environment variables for Keycloak client configuration in staging ([fccd94c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/fccd94cc11db43f99eded13207cc93fb59d8703e))
|
||||
## (2026-01-20)
|
||||
|
||||
### Features
|
||||
|
||||
* Add logging for OpenID authentication process in OpenIDController ([b729344](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/b729344d1e79c631146dd988248d033d97e12d93))
|
||||
## (2026-01-20)
|
||||
|
||||
### Features
|
||||
|
||||
* Add error logging for user info retrieval in OpenIDConnectService ([861f2f1](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/861f2f1184e860fdd164481167f941c11accfedd))
|
||||
## (2026-01-20)
|
||||
|
||||
### Features
|
||||
|
||||
* Add idClaimName parameter to OpenIDConnectService for flexible ID claim mapping ([2f38855](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2f388554495d8bd44b4c533baa0f2fc27eea22c2))
|
||||
## (2026-01-21)
|
||||
|
||||
### Features
|
||||
|
||||
* Change log level to warn for OpenID callback in OpenIDController ([23cdbe9](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/23cdbe9bd127bc26405fd216372bbb2d7e718a77))
|
||||
## (2026-01-21)
|
||||
|
||||
### Features
|
||||
|
||||
* Change log level to warn for OpenID callback in OpenIDController ([4a6598f](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/4a6598f102d8934c7502944a0addd400dd0cbcac))
|
||||
## (2026-01-21)
|
||||
|
||||
### Features
|
||||
|
||||
* Update ID mapping in OpenIDUserInfo to use hashed value and remove name field ([4b74de1](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/4b74de12610120831fc1529f0409db66aafd4d03))
|
||||
## (2026-01-21)
|
||||
|
||||
### Features
|
||||
|
||||
* Update ID mapping in OpenIDUserInfo to use hashed value and remove name field ([daa072f](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/daa072f2bf260ed62402096b41ce85aa071efaf5))
|
||||
## (2026-01-21)
|
||||
|
||||
### Features
|
||||
|
||||
* Implement OAuth session management with Redis caching for OpenID user data ([0430c7f](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/0430c7f4deb37661c697406a714e2693bf0c7307))
|
||||
## (2026-01-21)
|
||||
|
||||
### Features
|
||||
|
||||
* Update CORS allowed origins to use production URL ([66b5594](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/66b55945eb8f095c08a8f859955d31f7fc32ce0e))
|
||||
## (2026-01-22)
|
||||
|
||||
### Features
|
||||
|
||||
* **ui:** FRO-26 Nice Tie UI ([#115](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/115)) ([30c05c3](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/30c05c3014e9198e87706518bfa8d070f33242dc))
|
||||
|
||||
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM --platform=$TARGETPLATFORM eclipse-temurin:22-jre-alpine
|
||||
|
||||
# Install Argon2 CLI and libraries
|
||||
RUN apk add --no-cache bash argon2 argon2-libs
|
||||
|
||||
WORKDIR /opt/playapp
|
||||
|
||||
# Copy staged Play build
|
||||
COPY ./knockoutwhistweb/target/universal/stage /opt/playapp
|
||||
|
||||
# Expose the default Play port
|
||||
EXPOSE 9000
|
||||
|
||||
# Set environment variables
|
||||
ENV PLAY_HTTP_PORT=9000
|
||||
|
||||
# Run the Play app
|
||||
ENTRYPOINT ["./bin/knockoutwhistweb"]
|
||||
CMD ["-Dplay.server.pidfile.path=/dev/null"]
|
||||
@@ -1,6 +1,6 @@
|
||||
meta {
|
||||
name: Game
|
||||
seq: 3
|
||||
seq: 2
|
||||
}
|
||||
|
||||
auth {
|
||||
|
||||
16
bruno/KnockOutWhist/Health.bru
Normal file
16
bruno/KnockOutWhist/Health.bru
Normal file
@@ -0,0 +1,16 @@
|
||||
meta {
|
||||
name: Health
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/health/simple
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
15
bruno/KnockOutWhist/Login/Request Status.bru
Normal file
15
bruno/KnockOutWhist/Login/Request Status.bru
Normal file
@@ -0,0 +1,15 @@
|
||||
meta {
|
||||
name: Request Status
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/status
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
8
bruno/KnockOutWhist/Login/folder.bru
Normal file
8
bruno/KnockOutWhist/Login/folder.bru
Normal file
@@ -0,0 +1,8 @@
|
||||
meta {
|
||||
name: Login
|
||||
seq: 3
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
3
bruno/KnockOutWhist/environments/Prod.bru
Normal file
3
bruno/KnockOutWhist/environments/Prod.bru
Normal file
@@ -0,0 +1,3 @@
|
||||
vars {
|
||||
host: https://knockout.janis-eccarius.de/api
|
||||
}
|
||||
3
bruno/KnockOutWhist/environments/Staging.bru
Normal file
3
bruno/KnockOutWhist/environments/Staging.bru
Normal file
@@ -0,0 +1,3 @@
|
||||
vars {
|
||||
host: https://st.knockout.janis-eccarius.de/api
|
||||
}
|
||||
41
build.sbt
41
build.sbt
@@ -1,12 +1,19 @@
|
||||
ThisBuild / scalaVersion := "3.5.1"
|
||||
|
||||
credentials += Credentials(
|
||||
"GitHub Package Registry",
|
||||
"maven.pkg.github.com",
|
||||
sys.env.getOrElse("GITHUB_USER", sys.error("GITHUB_USER not set")),
|
||||
sys.env.getOrElse("GITHUB_TOKEN", sys.error("GITHUB_TOKEN not set"))
|
||||
)
|
||||
|
||||
lazy val commonSettings = Seq(
|
||||
libraryDependencies += "org.scalactic" %% "scalactic" % "3.2.18",
|
||||
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.18" % "test",
|
||||
libraryDependencies += "io.github.mkpaz" % "atlantafx-base" % "2.0.1",
|
||||
libraryDependencies += "org.scalafx" %% "scalafx" % "22.0.0-R33",
|
||||
libraryDependencies += "org.scala-lang.modules" %% "scala-xml" % "2.3.0",
|
||||
libraryDependencies += "org.playframework" %% "play-json" % "3.1.0-M1",
|
||||
libraryDependencies += "org.scalactic" %% "scalactic" % "3.2.19",
|
||||
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.19" % "test",
|
||||
libraryDependencies += "io.github.mkpaz" % "atlantafx-base" % "2.1.0",
|
||||
libraryDependencies += "org.scalafx" %% "scalafx" % "24.0.2-R36",
|
||||
libraryDependencies += "org.scala-lang.modules" %% "scala-xml" % "2.4.0",
|
||||
libraryDependencies += "org.playframework" %% "play-json" % "3.1.0-M9",
|
||||
libraryDependencies ++= {
|
||||
// Determine OS version of JavaFX binaries
|
||||
lazy val osName = System.getProperty("os.name") match {
|
||||
@@ -19,7 +26,6 @@ lazy val commonSettings = Seq(
|
||||
.map(m => "org.openjfx" % s"javafx-$m" % "21" classifier osName)
|
||||
},
|
||||
libraryDependencies += guice,
|
||||
coverageEnabled := true,
|
||||
coverageFailOnMinimum := true,
|
||||
coverageMinimumStmtTotal := 85,
|
||||
coverageMinimumBranchTotal := 100
|
||||
@@ -36,11 +42,28 @@ lazy val knockoutwhistweb = project.in(file("knockoutwhistweb"))
|
||||
.enablePlugins(PlayScala)
|
||||
.dependsOn(knockoutwhist % "compile->compile;test->test")
|
||||
.settings(
|
||||
|
||||
resolvers += "GitHub Packages" at "https://maven.pkg.github.com/16Janis12/KnockOutWhist-Web",
|
||||
|
||||
commonSettings,
|
||||
libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "7.0.2" % Test,
|
||||
libraryDependencies += "de.mkammerer" % "argon2-jvm" % "2.12",
|
||||
libraryDependencies += "com.auth0" % "java-jwt" % "4.3.0",
|
||||
libraryDependencies += "com.github.ben-manes.caffeine" % "caffeine" % "3.2.2"
|
||||
libraryDependencies += "com.auth0" % "java-jwt" % "4.5.0",
|
||||
libraryDependencies += "com.github.ben-manes.caffeine" % "caffeine" % "3.2.3",
|
||||
libraryDependencies += "tools.jackson.module" %% "jackson-module-scala" % "3.0.2",
|
||||
libraryDependencies += "de.janis" % "knockoutwhist-data" % "1.0-SNAPSHOT",
|
||||
libraryDependencies += "org.hibernate.orm" % "hibernate-core" % "6.4.4.Final",
|
||||
libraryDependencies += "jakarta.persistence" % "jakarta.persistence-api" % "3.1.0",
|
||||
libraryDependencies += "org.postgresql" % "postgresql" % "42.7.4",
|
||||
libraryDependencies += "org.playframework" %% "play-jdbc" % "3.0.6",
|
||||
libraryDependencies += "org.playframework" %% "play-java-jpa" % "3.0.6",
|
||||
libraryDependencies += "com.nimbusds" % "oauth2-oidc-sdk" % "11.31.1",
|
||||
libraryDependencies += "org.playframework" %% "play-ws" % "3.0.6",
|
||||
libraryDependencies += "org.hibernate.orm" % "hibernate-hikaricp" % "7.2.1.Final",
|
||||
libraryDependencies += ws,
|
||||
JsEngineKeys.engineType := JsEngineKeys.EngineType.Node,
|
||||
|
||||
PlayKeys.externalizeResourcesExcludes += baseDirectory.value / "conf" / "META-INF" / "persistence.xml"
|
||||
)
|
||||
|
||||
lazy val root = (project in file("."))
|
||||
|
||||
@@ -113,4 +113,4 @@
|
||||
"description": "The commit references another commit by its hash ID.<br>For multiple hash IDs, use a comma as separator"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Submodule knockoutwhist updated: b9a7b0a2af...0b35e1a649
1
knockoutwhistfrontend
Submodule
1
knockoutwhistfrontend
Submodule
Submodule knockoutwhistfrontend added at 65591ad392
@@ -1,6 +1,14 @@
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background-image: url('/assets/images/background.png');
|
||||
--color: white;
|
||||
--background-image: url('/assets/images/background.png') !important;
|
||||
--color: #f8f9fa !important; /* Light text on dark bg */
|
||||
--highlightscolor: rgba(131, 131, 131, 0.75) !important;
|
||||
--background-color: #192734;
|
||||
/* Bootstrap variable overrides for dark mode */
|
||||
--bs-body-color: var(--color);
|
||||
--bs-link-color: #66b2ff;
|
||||
--bs-link-hover-color: #99ccff;
|
||||
--bs-border-color: rgba(255, 255, 255, 0.2);
|
||||
--bs-heading-color: var(--color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
:root {
|
||||
--background-image: url('/assets/images/img.png');
|
||||
--color: black;
|
||||
--highlightscolor: rgba(0, 0, 0, 0.75);
|
||||
--background-color: rgba(228, 232, 237, 1);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
z-index: 3; /* ensure card sits above the particles */
|
||||
}
|
||||
|
||||
@@ -2,141 +2,295 @@
|
||||
@import "dark-mode.less";
|
||||
@import "login.less";
|
||||
|
||||
/* Provide default (light) variables so the site works even if light-mode.less fails */
|
||||
:root {
|
||||
--background-image: url('/assets/images/img.png');
|
||||
--color: #212529; /* Bootstrap body text default */
|
||||
|
||||
/* Bootstrap variable overrides for light mode */
|
||||
--bs-body-color: var(--color) !important;
|
||||
--bs-link-color: #0d6efd !important;
|
||||
--bs-link-hover-color: #0a58ca !important;
|
||||
--bs-border-color: rgba(0, 0, 0, 0.125) !important;
|
||||
--bs-heading-color: var(--color) !important;
|
||||
}
|
||||
|
||||
@background-color: var(--background-color);
|
||||
@highlightcolor: var(--highlightscolor);
|
||||
@background-image: var(--background-image);
|
||||
@color: var(--color);
|
||||
@keyframes slideIn {
|
||||
0% { transform: translateX(-100vw); }
|
||||
100% { transform: translateX(0); }
|
||||
0% {
|
||||
transform: translateX(-100vw);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.game-field-background {
|
||||
background-image: @background-image;
|
||||
background-size: 100vw 100vh;
|
||||
background-repeat: no-repeat;
|
||||
background-image: @background-image;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.lobby-background {
|
||||
background-color: @background-color;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.navbar-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.navbar-toggle {
|
||||
float: none;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.handcard :hover {
|
||||
box-shadow: 3px 3px 3px @highlightcolor;
|
||||
}
|
||||
|
||||
.inactive::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0; /* cover the whole container */
|
||||
background: rgba(0, 0, 0, 0.50);
|
||||
z-index: 10;
|
||||
border-radius: 6px;
|
||||
pointer-events: none; /* user can't click through overlay */
|
||||
}
|
||||
|
||||
.bottom-div {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
max-width: 1400px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Ensure body text color follows theme variable and works with Bootstrap */
|
||||
body {
|
||||
color: @color;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: @color;
|
||||
padding: 0.5rem 0;
|
||||
flex-grow: 1; /* fill remaining vertical space as visual footer background */
|
||||
}
|
||||
|
||||
.game-field {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
overflow: auto;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.navbar-drop-shadow {
|
||||
box-shadow: 0 1px 15px 0 #000000
|
||||
}
|
||||
|
||||
.ingame-side-shadow {
|
||||
box-shadow: 0 1px 15px 0 #000000
|
||||
}
|
||||
|
||||
#sessions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
h1 {
|
||||
animation: slideIn 0.5s ease-out forwards;
|
||||
animation-fill-mode: backwards;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
h1 {
|
||||
animation: slideIn 0.5s ease-out forwards;
|
||||
animation-fill-mode: backwards;
|
||||
}
|
||||
}
|
||||
|
||||
#textanimation {
|
||||
animation: slideIn 0.5s ease-out forwards;
|
||||
animation-fill-mode: backwards;
|
||||
animation-delay: 1s;
|
||||
animation: slideIn 0.5s ease-out forwards;
|
||||
animation-fill-mode: backwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
#sessions a, #sessions h1, #sessions p {
|
||||
color: @color;
|
||||
font-size: 40px;
|
||||
font-family: Arial, serif;
|
||||
color: @color;
|
||||
font-size: 40px;
|
||||
font-family: Arial, serif;
|
||||
}
|
||||
|
||||
#ingame {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#ingame a, #ingame h1, #ingame p {
|
||||
color: @color;
|
||||
font-size: 40px;
|
||||
font-family: Arial, serif;
|
||||
color: @color;
|
||||
font-size: 40px;
|
||||
font-family: Arial, serif;
|
||||
}
|
||||
#playercards {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
height: 20%;
|
||||
img {
|
||||
animation: slideIn 0.5s ease-out forwards;
|
||||
animation-fill-mode: backwards;
|
||||
&:nth-child(1) { animation-delay: 0.5s; }
|
||||
&:nth-child(2) { animation-delay: 1s; }
|
||||
&:nth-child(3) { animation-delay: 1.5s; }
|
||||
&:nth-child(4) { animation-delay: 2s; }
|
||||
&:nth-child(5) { animation-delay: 2.5s; }
|
||||
&:nth-child(6) { animation-delay: 3s; }
|
||||
&:nth-child(7) { animation-delay: 3.5s; }
|
||||
|
||||
.ingame-cards-slide {
|
||||
div {
|
||||
animation: slideIn 0.5s ease-out forwards;
|
||||
animation-fill-mode: backwards;
|
||||
|
||||
&:nth-child(1) {
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
animation-delay: 1.5s;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
&:nth-child(5) {
|
||||
animation-delay: 2.5s;
|
||||
}
|
||||
|
||||
&:nth-child(6) {
|
||||
animation-delay: 3s;
|
||||
}
|
||||
|
||||
&:nth-child(7) {
|
||||
animation-delay: 3.5s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#cardsplayed {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 10%;
|
||||
min-height: 10%
|
||||
}
|
||||
#playedcardplayer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
#playedcardplayer p {
|
||||
font-size: 12px;
|
||||
height: 4%;
|
||||
font-size: 12px;
|
||||
height: 4%;
|
||||
}
|
||||
|
||||
#playedcardplayer img {
|
||||
height: 90%;
|
||||
height: 90%;
|
||||
}
|
||||
|
||||
#firstCard {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 20%;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 20%;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#firstCardObject {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 4%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 4%;
|
||||
}
|
||||
#firstCardObject img{
|
||||
height: 90%;
|
||||
|
||||
#firstCardObject img {
|
||||
height: 90%;
|
||||
}
|
||||
#firstCardObject p{
|
||||
height: 10%;
|
||||
font-size: 20px;
|
||||
|
||||
#firstCardObject p {
|
||||
height: 10%;
|
||||
font-size: 20px;
|
||||
|
||||
}
|
||||
#trumpsuit {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: 4%;
|
||||
}
|
||||
#nextPlayers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 0;
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
#invisible {
|
||||
visibility: hidden;
|
||||
|
||||
#next-players-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
height: 0;
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#selecttrumpsuit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#rules {
|
||||
color: @color;
|
||||
font-size: 1.5em;
|
||||
font-family: Arial, serif;
|
||||
}
|
||||
color: @color;
|
||||
font-size: 1.5em;
|
||||
font-family: Arial, serif;
|
||||
}
|
||||
|
||||
.score-table {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
backdrop-filter: blur(8px);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.score-header {
|
||||
font-weight: bold;
|
||||
color: #000000;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.score-row {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
/* In-game centered stage and blurred sides overlay */
|
||||
.ingame-stage {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
/* Wrapper that adds a backdrop blur to the background outside the centered card */
|
||||
.blur-sides {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Create an overlay that blurs everything behind it, except the central content area */
|
||||
.blur-sides::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
/* fallback: subtle vignette if backdrop-filter unsupported */
|
||||
background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0) 30%, rgba(0, 0, 0, 0.35) 100%);
|
||||
}
|
||||
|
||||
@supports ((-webkit-backdrop-filter: blur(8px)) or (backdrop-filter: blur(8px))) {
|
||||
.blur-sides::before {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
-webkit-backdrop-filter: blur(10px) saturate(110%);
|
||||
backdrop-filter: blur(10px) saturate(110%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,14 +14,7 @@ class AuthAction @Inject()(val sessionManager: SessionManager, val parser: BodyP
|
||||
extends ActionBuilder[AuthenticatedRequest, AnyContent] {
|
||||
|
||||
override def executionContext: ExecutionContext = ec
|
||||
|
||||
private def getUserFromSession(request: RequestHeader): Option[User] = {
|
||||
val session = request.cookies.get("sessionId")
|
||||
if (session.isDefined)
|
||||
return sessionManager.getUserBySession(session.get.value)
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
override def invokeBlock[A](
|
||||
request: Request[A],
|
||||
block: AuthenticatedRequest[A] => Future[Result]
|
||||
@@ -30,8 +23,15 @@ class AuthAction @Inject()(val sessionManager: SessionManager, val parser: BodyP
|
||||
case Some(user) =>
|
||||
block(new AuthenticatedRequest(user, request))
|
||||
case None =>
|
||||
Future.successful(Results.Redirect(routes.UserController.login()))
|
||||
Future.successful(Results.Unauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
protected def getUserFromSession(request: RequestHeader): Option[User] = {
|
||||
val session = request.cookies.get("accessToken")
|
||||
if (session.isDefined)
|
||||
return sessionManager.getUserBySession(session.get.value)
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ package components
|
||||
|
||||
import de.knockoutwhist.components.DefaultConfiguration
|
||||
import de.knockoutwhist.ui.UI
|
||||
import de.knockoutwhist.utils.DelayHandler
|
||||
import de.knockoutwhist.utils.events.EventListener
|
||||
|
||||
class WebApplicationConfiguration extends DefaultConfiguration {
|
||||
|
||||
override def uis: Set[UI] = Set()
|
||||
override def listener: Set[EventListener] = Set()
|
||||
|
||||
override def listener: Set[EventListener] = Set(DelayHandler)
|
||||
|
||||
}
|
||||
|
||||
30
knockoutwhistweb/app/controllers/HealthController.scala
Normal file
30
knockoutwhistweb/app/controllers/HealthController.scala
Normal file
@@ -0,0 +1,30 @@
|
||||
package controllers
|
||||
|
||||
import auth.{AuthAction, AuthenticatedRequest}
|
||||
import dto.subDTO.UserDTO
|
||||
import logic.user.{SessionManager, UserManager}
|
||||
import model.users.User
|
||||
import play.api.*
|
||||
import play.api.libs.json.Json
|
||||
import play.api.mvc.*
|
||||
import play.api.mvc.Cookie.SameSite.{Lax, None, Strict}
|
||||
|
||||
import javax.inject.*
|
||||
|
||||
|
||||
/**
|
||||
* This controller creates an `Action` to handle HTTP requests to the
|
||||
* application's home page.
|
||||
*/
|
||||
@Singleton
|
||||
class HealthController @Inject()(
|
||||
val controllerComponents: ControllerComponents,
|
||||
) extends BaseController {
|
||||
|
||||
def simple(): Action[AnyContent] = {
|
||||
Action { implicit request =>
|
||||
Ok("OK")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import auth.{AuthAction, AuthenticatedRequest}
|
||||
import de.knockoutwhist.control.GameState.{InGame, Lobby, SelectTrump, TieBreak}
|
||||
import exceptions.{CantPlayCardException, GameFullException, NotEnoughPlayersException, NotHostException, NotInThisGameException}
|
||||
import logic.PodManager
|
||||
import model.sessions.{PlayerSession, UserSession}
|
||||
import play.api.*
|
||||
import play.api.mvc.*
|
||||
|
||||
import javax.inject.*
|
||||
import scala.util.Try
|
||||
|
||||
|
||||
/**
|
||||
* This controller creates an `Action` to handle HTTP requests to the
|
||||
* application's home page.
|
||||
*/
|
||||
@Singleton
|
||||
class IngameController @Inject()(
|
||||
val controllerComponents: ControllerComponents,
|
||||
val authAction: AuthAction,
|
||||
val podManager: PodManager
|
||||
) extends BaseController {
|
||||
|
||||
def game(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = podManager.getGame(gameId)
|
||||
game match {
|
||||
case Some(g) =>
|
||||
g.logic.getCurrentState match {
|
||||
case Lobby => Ok("Lobby: " + gameId)
|
||||
case InGame =>
|
||||
Ok(views.html.ingame.ingame(
|
||||
g.getPlayerByUser(request.user),
|
||||
g.logic
|
||||
))
|
||||
case SelectTrump =>
|
||||
Ok(views.html.ingame.selecttrump(
|
||||
g.getPlayerByUser(request.user),
|
||||
g.logic
|
||||
))
|
||||
case TieBreak =>
|
||||
Ok(views.html.ingame.tie(
|
||||
g.getPlayerByUser(request.user),
|
||||
g.logic
|
||||
))
|
||||
case _ =>
|
||||
InternalServerError(s"Invalid game state for in-game view. GameId: $gameId" + s" State: ${g.logic.getCurrentState}")
|
||||
}
|
||||
case None =>
|
||||
NotFound("Game not found")
|
||||
}
|
||||
//NotFound(s"Reached end of game method unexpectedly. GameId: $gameId")
|
||||
}
|
||||
def startGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = podManager.getGame(gameId)
|
||||
val result = Try {
|
||||
game match {
|
||||
case Some(g) =>
|
||||
g.startGame(request.user)
|
||||
case None =>
|
||||
NotFound("Game not found")
|
||||
}
|
||||
}
|
||||
if (result.isSuccess) {
|
||||
NoContent
|
||||
} else {
|
||||
val throwable = result.failed.get
|
||||
throwable match {
|
||||
case _: NotInThisGameException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
case _: NotHostException =>
|
||||
Forbidden(throwable.getMessage)
|
||||
case _: NotEnoughPlayersException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
case _ =>
|
||||
InternalServerError(throwable.getMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
def joinGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = podManager.getGame(gameId)
|
||||
val result = Try {
|
||||
game match {
|
||||
case Some(g) =>
|
||||
g.addUser(request.user)
|
||||
case None =>
|
||||
NotFound("Game not found")
|
||||
}
|
||||
}
|
||||
if (result.isSuccess) {
|
||||
Redirect(routes.IngameController.game(gameId))
|
||||
} else {
|
||||
val throwable = result.failed.get
|
||||
throwable match {
|
||||
case _: GameFullException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
case _: IllegalArgumentException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
case _: IllegalStateException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
case _ =>
|
||||
InternalServerError(throwable.getMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
def playCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => {
|
||||
val game = podManager.getGame(gameId)
|
||||
game match {
|
||||
case Some(g) =>
|
||||
val cardIdOpt = request.body.asFormUrlEncoded.flatMap(_.get("cardId").flatMap(_.headOption))
|
||||
cardIdOpt match {
|
||||
case Some(cardId) =>
|
||||
var optSession: Option[UserSession] = None
|
||||
val result = Try {
|
||||
val session = g.getUserSession(request.user.id)
|
||||
optSession = Some(session)
|
||||
session.lock.lock()
|
||||
g.playCard(session, cardId.toInt)
|
||||
}
|
||||
optSession.foreach(_.lock.unlock())
|
||||
if (result.isSuccess) {
|
||||
NoContent
|
||||
} else {
|
||||
val throwable = result.failed.get
|
||||
throwable match {
|
||||
case _: CantPlayCardException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
case _: NotInThisGameException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
case _: IllegalArgumentException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
case _: IllegalStateException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
case _ =>
|
||||
InternalServerError(throwable.getMessage)
|
||||
}
|
||||
}
|
||||
case None =>
|
||||
BadRequest("cardId parameter is missing")
|
||||
}
|
||||
case None =>
|
||||
NotFound("Game not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
def playDogCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => {
|
||||
val game = podManager.getGame(gameId)
|
||||
game match {
|
||||
case Some(g) => {
|
||||
val cardIdOpt = request.body.asFormUrlEncoded.flatMap(_.get("cardId").flatMap(_.headOption))
|
||||
var optSession: Option[UserSession] = None
|
||||
val result = Try {
|
||||
cardIdOpt match {
|
||||
case Some(cardId) if cardId == "skip" =>
|
||||
val session = g.getUserSession(request.user.id)
|
||||
optSession = Some(session)
|
||||
session.lock.lock()
|
||||
g.playDogCard(session, -1)
|
||||
case Some(cardId) =>
|
||||
val session = g.getUserSession(request.user.id)
|
||||
optSession = Some(session)
|
||||
session.lock.lock()
|
||||
g.playDogCard(session, cardId.toInt)
|
||||
case None =>
|
||||
throw new IllegalArgumentException("cardId parameter is missing")
|
||||
}
|
||||
}
|
||||
optSession.foreach(_.lock.unlock())
|
||||
if (result.isSuccess) {
|
||||
NoContent
|
||||
} else {
|
||||
val throwable = result.failed.get
|
||||
throwable match {
|
||||
case _: CantPlayCardException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
case _: NotInThisGameException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
case _: IllegalArgumentException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
case _: IllegalStateException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
case _ =>
|
||||
InternalServerError(throwable.getMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
case None =>
|
||||
NotFound("Game not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
def playTrump(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = podManager.getGame(gameId)
|
||||
game match {
|
||||
case Some(g) =>
|
||||
val trumpOpt = request.body.asFormUrlEncoded.flatMap(_.get("trump").flatMap(_.headOption))
|
||||
trumpOpt match {
|
||||
case Some(trump) =>
|
||||
var optSession: Option[UserSession] = None
|
||||
val result = Try {
|
||||
val session = g.getUserSession(request.user.id)
|
||||
optSession = Some(session)
|
||||
session.lock.lock()
|
||||
g.selectTrump(session, trump.toInt)
|
||||
}
|
||||
optSession.foreach(_.lock.unlock())
|
||||
if (result.isSuccess) {
|
||||
NoContent
|
||||
} else {
|
||||
val throwable = result.failed.get
|
||||
throwable match {
|
||||
case _: IllegalArgumentException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
case _: NotInThisGameException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
case _: IllegalStateException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
case _ =>
|
||||
InternalServerError(throwable.getMessage)
|
||||
}
|
||||
}
|
||||
case None =>
|
||||
BadRequest("trump parameter is missing")
|
||||
}
|
||||
case None =>
|
||||
NotFound("Game not found")
|
||||
}
|
||||
}
|
||||
def playTie(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = podManager.getGame(gameId)
|
||||
game match {
|
||||
case Some(g) =>
|
||||
val tieOpt = request.body.asFormUrlEncoded.flatMap(_.get("tie").flatMap(_.headOption))
|
||||
tieOpt match {
|
||||
case Some(tie) =>
|
||||
var optSession: Option[UserSession] = None
|
||||
val result = Try {
|
||||
val session = g.getUserSession(request.user.id)
|
||||
optSession = Some(session)
|
||||
session.lock.lock()
|
||||
g.selectTie(g.getUserSession(request.user.id), tie.toInt)
|
||||
}
|
||||
optSession.foreach(_.lock.unlock())
|
||||
if (result.isSuccess) {
|
||||
NoContent
|
||||
} else {
|
||||
val throwable = result.failed.get
|
||||
throwable match {
|
||||
case _: IllegalArgumentException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
case _: NotInThisGameException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
case _: IllegalStateException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
case _ =>
|
||||
InternalServerError(throwable.getMessage)
|
||||
}
|
||||
}
|
||||
case None =>
|
||||
BadRequest("tie parameter is missing")
|
||||
}
|
||||
case None =>
|
||||
NotFound("Game not found")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package controllers
|
||||
import auth.{AuthAction, AuthenticatedRequest}
|
||||
import logic.PodManager
|
||||
import play.api.*
|
||||
import play.api.libs.json.Json
|
||||
import play.api.mvc.*
|
||||
|
||||
import javax.inject.*
|
||||
@@ -15,47 +16,49 @@ import javax.inject.*
|
||||
@Singleton
|
||||
class MainMenuController @Inject()(
|
||||
val controllerComponents: ControllerComponents,
|
||||
val authAction: AuthAction,
|
||||
val podManager: PodManager
|
||||
val authAction: AuthAction
|
||||
) extends BaseController {
|
||||
|
||||
// Pass the request-handling function directly to authAction (no nested Action)
|
||||
def mainMenu(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
Ok(views.html.mainmenu.navbar(Some(request.user)))
|
||||
}
|
||||
|
||||
def index(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
Redirect(routes.MainMenuController.mainMenu())
|
||||
}
|
||||
|
||||
def createGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val gameLobby = podManager.createGame(
|
||||
host = request.user,
|
||||
name = s"${request.user.name}'s Game",
|
||||
maxPlayers = 4
|
||||
)
|
||||
Redirect(routes.IngameController.game(gameLobby.id))
|
||||
}
|
||||
|
||||
def joinGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val postData = request.body.asFormUrlEncoded
|
||||
if (postData.isDefined) {
|
||||
val gameId = postData.get.get("gameId").flatMap(_.headOption).getOrElse("")
|
||||
val game = podManager.getGame(gameId)
|
||||
game match {
|
||||
case Some(g) =>
|
||||
Redirect(routes.IngameController.joinGame(gameId))
|
||||
case None =>
|
||||
NotFound("Game not found")
|
||||
}
|
||||
val jsonBody = request.body.asJson
|
||||
if (jsonBody.isDefined) {
|
||||
val gamename: String = (jsonBody.get \ "lobbyname").asOpt[String]
|
||||
.getOrElse(s"${request.user.name}'s Game")
|
||||
|
||||
val playeramount: String = (jsonBody.get \ "playeramount").asOpt[String]
|
||||
.getOrElse(throw new IllegalArgumentException("Player amount is required."))
|
||||
|
||||
val gameLobby = PodManager.createGame(
|
||||
host = request.user,
|
||||
name = gamename,
|
||||
maxPlayers = playeramount.toInt
|
||||
)
|
||||
Ok(Json.obj(
|
||||
"status" -> "success",
|
||||
"gameId" -> gameLobby.id,
|
||||
))
|
||||
} else {
|
||||
BadRequest("Invalid form submission")
|
||||
BadRequest(Json.obj(
|
||||
"status" -> "failure",
|
||||
"errorMessage" -> "Invalid form submission"
|
||||
))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def rules(): Action[AnyContent] = {
|
||||
Action { implicit request =>
|
||||
Ok(views.html.mainmenu.rules())
|
||||
def joinGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = PodManager.getGame(gameId)
|
||||
game match {
|
||||
case Some(g) =>
|
||||
g.addUser(request.user)
|
||||
Ok(Json.obj(
|
||||
"status" -> "success"
|
||||
))
|
||||
case None =>
|
||||
NotFound(Json.obj(
|
||||
"status" -> "failure",
|
||||
"errorMessage" -> "No Game found"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
185
knockoutwhistweb/app/controllers/OpenIDController.scala
Normal file
185
knockoutwhistweb/app/controllers/OpenIDController.scala
Normal file
@@ -0,0 +1,185 @@
|
||||
package controllers
|
||||
|
||||
import com.github.benmanes.caffeine.cache.{Cache, Caffeine}
|
||||
import logic.user.{SessionManager, UserManager}
|
||||
import model.users.User
|
||||
import play.api.{Configuration, Logger}
|
||||
import play.api.libs.json.Json
|
||||
import play.api.mvc.*
|
||||
import play.api.mvc.Cookie.SameSite.Lax
|
||||
import services.{OpenIDConnectService, OpenIDUserInfo, OAuthCacheService}
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.*
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
@Singleton
|
||||
class OpenIDController @Inject()(
|
||||
val controllerComponents: ControllerComponents,
|
||||
val openIDService: OpenIDConnectService,
|
||||
val sessionManager: SessionManager,
|
||||
val userManager: UserManager,
|
||||
val config: Configuration,
|
||||
val oauthCache: OAuthCacheService
|
||||
)(implicit ec: ExecutionContext) extends BaseController {
|
||||
|
||||
private val logger = Logger(this.getClass)
|
||||
|
||||
def loginWithProvider(provider: String): Action[AnyContent] = Action.async { implicit request =>
|
||||
val state = openIDService.generateState()
|
||||
val nonce = openIDService.generateNonce()
|
||||
|
||||
// Store state and nonce in session
|
||||
openIDService.getAuthorizationUrl(provider, state, nonce) match {
|
||||
case Some(authUrl) =>
|
||||
Future.successful(Redirect(authUrl)
|
||||
.withSession(
|
||||
"oauth_state" -> state,
|
||||
"oauth_nonce" -> nonce,
|
||||
"oauth_provider" -> provider
|
||||
))
|
||||
case None =>
|
||||
Future.successful(BadRequest(Json.obj("error" -> "Unsupported provider")))
|
||||
}
|
||||
}
|
||||
|
||||
def callback(provider: String): Action[AnyContent] = Action.async { implicit request =>
|
||||
val sessionState = request.session.get("oauth_state")
|
||||
val sessionNonce = request.session.get("oauth_nonce")
|
||||
val sessionProvider = request.session.get("oauth_provider")
|
||||
|
||||
val returnedState = request.getQueryString("state")
|
||||
val code = request.getQueryString("code")
|
||||
val error = request.getQueryString("error")
|
||||
|
||||
logger.warn(s"Received callback from $provider with state $sessionState, nonce $sessionNonce, provider $sessionProvider, returned state $returnedState, code $code, error $error")
|
||||
|
||||
error match {
|
||||
case Some(err) =>
|
||||
logger.error(s"Authentication failed: $err")
|
||||
Future.successful(Redirect("/login").flashing("error" -> s"Authentication failed: $err"))
|
||||
case None =>
|
||||
(for {
|
||||
_ <- Option(sessionState.contains(returnedState.getOrElse("")))
|
||||
_ <- Option(sessionProvider.contains(provider))
|
||||
authCode <- code
|
||||
} yield {
|
||||
logger.warn(s"Authentication successful for $provider")
|
||||
openIDService.exchangeCodeForTokens(provider, authCode, sessionState.get).flatMap {
|
||||
case Some(tokenResponse) =>
|
||||
openIDService.getUserInfo(provider, tokenResponse.accessToken).flatMap {
|
||||
case Some(userInfo) =>
|
||||
// Check if user already exists
|
||||
userManager.authenticateOpenID(provider, userInfo.id) match {
|
||||
case Some(user) =>
|
||||
logger.warn(s"User ${userInfo.name} (${userInfo.id}) already exists, logging them in")
|
||||
// User already exists, log them in
|
||||
val sessionToken = sessionManager.createSession(user)
|
||||
Future.successful(Redirect(config.getOptional[String]("openid.mainRoute").getOrElse("/"))
|
||||
.withCookies(Cookie(
|
||||
name = "accessToken",
|
||||
value = sessionToken,
|
||||
httpOnly = true,
|
||||
secure = false,
|
||||
sameSite = Some(Lax)
|
||||
))
|
||||
.removingFromSession("oauth_state", "oauth_nonce", "oauth_provider", "oauth_access_token"))
|
||||
case None =>
|
||||
logger.warn(s"User ${userInfo.name} (${userInfo.id}) not found, creating new user")
|
||||
// Store OAuth data in cache and get session ID
|
||||
val oauthSessionId = oauthCache.storeOAuthData(userInfo, tokenResponse.accessToken, provider)
|
||||
// New user, redirect to username selection with only session ID
|
||||
Future.successful(Redirect(config.get[String]("openid.selectUserRoute"))
|
||||
.withSession("oauth_session_id" -> oauthSessionId))
|
||||
}
|
||||
case None =>
|
||||
logger.error("Failed to retrieve user information")
|
||||
Future.successful(Redirect("/login").flashing("error" -> "Failed to retrieve user information"))
|
||||
}
|
||||
case None =>
|
||||
logger.error("Failed to exchange authorization code")
|
||||
Future.successful(Redirect("/login").flashing("error" -> "Failed to exchange authorization code"))
|
||||
}
|
||||
}).getOrElse {
|
||||
logger.error("Invalid state parameter")
|
||||
Future.successful(Redirect("/login").flashing("error" -> "Invalid state parameter"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def selectUsername(): Action[AnyContent] = Action.async { implicit request =>
|
||||
request.session.get("oauth_session_id") match {
|
||||
case Some(sessionId) =>
|
||||
oauthCache.getOAuthData(sessionId) match {
|
||||
case Some((userInfo, _, _)) =>
|
||||
Future.successful(Ok(Json.obj(
|
||||
"id" -> userInfo.id,
|
||||
"email" -> userInfo.email,
|
||||
"name" -> userInfo.name,
|
||||
"picture" -> userInfo.picture,
|
||||
"provider" -> userInfo.provider,
|
||||
"providerName" -> userInfo.providerName
|
||||
)))
|
||||
case None =>
|
||||
logger.error(s"OAuth session data not found for session ID: $sessionId")
|
||||
Future.successful(Redirect("/login").flashing("error" -> "Session expired"))
|
||||
}
|
||||
case None =>
|
||||
logger.error("No OAuth session ID found")
|
||||
Future.successful(Redirect("/login").flashing("error" -> "No authentication information found"))
|
||||
}
|
||||
}
|
||||
|
||||
def submitUsername(): Action[AnyContent] = Action.async { implicit request =>
|
||||
val username = request.body.asJson.flatMap(json => (json \ "username").asOpt[String])
|
||||
.orElse(request.body.asFormUrlEncoded.flatMap(_.get("username").flatMap(_.headOption)))
|
||||
val sessionId = request.session.get("oauth_session_id")
|
||||
|
||||
(username, sessionId) match {
|
||||
case (Some(uname), Some(sid)) =>
|
||||
oauthCache.getOAuthData(sid) match {
|
||||
case Some((userInfo, accessToken, provider)) =>
|
||||
// Check if username already exists
|
||||
val trimmedUsername = uname.trim
|
||||
userManager.userExists(trimmedUsername) match {
|
||||
case Some(_) =>
|
||||
Future.successful(Conflict(Json.obj("error" -> "Username already taken")))
|
||||
case None =>
|
||||
// Create new user with OpenID info (no password needed)
|
||||
val success = userManager.addOpenIDUser(trimmedUsername, userInfo)
|
||||
if (success) {
|
||||
// Get created user and create session
|
||||
userManager.userExists(trimmedUsername) match {
|
||||
case Some(user) =>
|
||||
val sessionToken = sessionManager.createSession(user)
|
||||
// Clean up cache after successful user creation
|
||||
oauthCache.removeOAuthData(sid)
|
||||
Future.successful(Ok(Json.obj(
|
||||
"message" -> "User created successfully",
|
||||
"user" -> Json.obj(
|
||||
"id" -> user.id,
|
||||
"username" -> user.name
|
||||
)
|
||||
)).withCookies(Cookie(
|
||||
name = "accessToken",
|
||||
value = sessionToken,
|
||||
httpOnly = true,
|
||||
secure = false,
|
||||
sameSite = Some(Lax)
|
||||
)).removingFromSession("oauth_session_id"))
|
||||
case None =>
|
||||
Future.successful(InternalServerError(Json.obj("error" -> "Failed to create user session")))
|
||||
}
|
||||
} else {
|
||||
Future.successful(InternalServerError(Json.obj("error" -> "Failed to create user")))
|
||||
}
|
||||
}
|
||||
case None =>
|
||||
logger.error(s"OAuth session data not found for session ID: $sid")
|
||||
Future.successful(Redirect("/login").flashing("error" -> "Session expired"))
|
||||
}
|
||||
case _ =>
|
||||
Future.successful(BadRequest(Json.obj("error" -> "Username and valid session required")))
|
||||
}
|
||||
}
|
||||
}
|
||||
96
knockoutwhistweb/app/controllers/StatusController.scala
Normal file
96
knockoutwhistweb/app/controllers/StatusController.scala
Normal file
@@ -0,0 +1,96 @@
|
||||
package controllers
|
||||
|
||||
import auth.AuthAction
|
||||
import logic.PodManager
|
||||
import logic.game.GameLobby
|
||||
import logic.user.SessionManager
|
||||
import model.users.User
|
||||
import play.api.libs.json.{JsValue, Json}
|
||||
import play.api.mvc.*
|
||||
import util.WebsocketEventMapper
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
class StatusController @Inject()(
|
||||
val controllerComponents: ControllerComponents,
|
||||
val sessionManager: SessionManager,
|
||||
val authAction: AuthAction
|
||||
) extends BaseController {
|
||||
|
||||
def requestStatus(): Action[AnyContent] = {
|
||||
Action { implicit request =>
|
||||
val userOpt = getUserFromSession(request)
|
||||
if (userOpt.isEmpty) {
|
||||
Ok(
|
||||
Json.obj(
|
||||
"status" -> "unauthenticated"
|
||||
)
|
||||
)
|
||||
} else {
|
||||
val user = userOpt.get
|
||||
val gameOpt = PodManager.identifyGameOfUser(user)
|
||||
if (gameOpt.isEmpty) {
|
||||
Ok(
|
||||
Json.obj(
|
||||
"status" -> "authenticated",
|
||||
"username" -> user.name,
|
||||
"userId" -> user.id,
|
||||
"inGame" -> false
|
||||
)
|
||||
)
|
||||
} else {
|
||||
val game = gameOpt.get
|
||||
Ok(
|
||||
Json.obj(
|
||||
"status" -> "authenticated",
|
||||
"username" -> user.name,
|
||||
"userId" -> user.id,
|
||||
"inGame" -> true,
|
||||
"gameId" -> game.id
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def game(gameId: String): Action[AnyContent] = {
|
||||
Action { implicit request =>
|
||||
val userOpt = getUserFromSession(request)
|
||||
if (userOpt.isEmpty) {
|
||||
Unauthorized("User not authenticated")
|
||||
} else {
|
||||
val user = userOpt.get
|
||||
val gameOpt = PodManager.getGame(gameId)
|
||||
if (gameOpt.isEmpty) {
|
||||
NotFound("Game not found")
|
||||
} else {
|
||||
val game = gameOpt.get
|
||||
if (!game.getPlayers.contains(user.id)) {
|
||||
Forbidden("User not part of this game")
|
||||
} else {
|
||||
Ok(
|
||||
Json.obj(
|
||||
"gameId" -> game.id,
|
||||
"state" -> game.logic.getCurrentState.toString,
|
||||
"data" -> mapGameState(game, user)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
private def getUserFromSession(request: RequestHeader): Option[User] = {
|
||||
val session = request.cookies.get("accessToken")
|
||||
if (session.isDefined)
|
||||
return sessionManager.getUserBySession(session.get.value)
|
||||
None
|
||||
}
|
||||
|
||||
private def mapGameState(gameLobby: GameLobby, user: User): JsValue = {
|
||||
val userSession = gameLobby.getUserSession(user.id)
|
||||
WebsocketEventMapper.stateToJson(userSession)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
package controllers
|
||||
|
||||
import auth.{AuthAction, AuthenticatedRequest}
|
||||
import dto.subDTO.UserDTO
|
||||
import logic.user.{SessionManager, UserManager}
|
||||
import model.users.User
|
||||
import play.api.*
|
||||
import play.api.libs.json.Json
|
||||
import play.api.mvc.*
|
||||
import play.api.mvc.Cookie.SameSite.{Lax, None, Strict}
|
||||
|
||||
import javax.inject.*
|
||||
|
||||
@@ -20,36 +24,32 @@ class UserController @Inject()(
|
||||
val authAction: AuthAction
|
||||
) extends BaseController {
|
||||
|
||||
def login(): Action[AnyContent] = {
|
||||
Action { implicit request =>
|
||||
val session = request.cookies.get("sessionId")
|
||||
if (session.isDefined) {
|
||||
val possibleUser = sessionManager.getUserBySession(session.get.value)
|
||||
if (possibleUser.isDefined) {
|
||||
Redirect(routes.MainMenuController.mainMenu())
|
||||
} else {
|
||||
Ok(views.html.login.login())
|
||||
}
|
||||
} else {
|
||||
Ok(views.html.login.login())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def login_Post(): Action[AnyContent] = {
|
||||
Action { implicit request =>
|
||||
val postData = request.body.asFormUrlEncoded
|
||||
if (postData.isDefined) {
|
||||
val jsonBody = request.body.asJson
|
||||
val username: Option[String] = jsonBody.flatMap { jsValue =>
|
||||
(jsValue \ "username").asOpt[String]
|
||||
}
|
||||
val password: Option[String] = jsonBody.flatMap { jsValue =>
|
||||
(jsValue \ "password").asOpt[String]
|
||||
}
|
||||
if (username.isDefined && password.isDefined) {
|
||||
// Extract username and password from form data
|
||||
val username = postData.get.get("username").flatMap(_.headOption).getOrElse("")
|
||||
val password = postData.get.get("password").flatMap(_.headOption).getOrElse("")
|
||||
val possibleUser = userManager.authenticate(username, password)
|
||||
val possibleUser = userManager.authenticate(username.get, password.get)
|
||||
if (possibleUser.isDefined) {
|
||||
Redirect(routes.MainMenuController.mainMenu()).withCookies(
|
||||
Cookie("sessionId", sessionManager.createSession(possibleUser.get))
|
||||
)
|
||||
Ok(Json.obj(
|
||||
"user" -> Json.obj(
|
||||
"id" -> possibleUser.get.id,
|
||||
"username" -> possibleUser.get.name
|
||||
)
|
||||
)).withCookies(Cookie(
|
||||
name = "accessToken",
|
||||
value = sessionManager.createSession(possibleUser.get),
|
||||
httpOnly = true,
|
||||
secure = false,
|
||||
sameSite = Some(Lax)
|
||||
))
|
||||
} else {
|
||||
println("Failed login attempt for user: " + username)
|
||||
Unauthorized("Invalid username or password")
|
||||
}
|
||||
} else {
|
||||
@@ -57,14 +57,62 @@ class UserController @Inject()(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getUserInfo(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val user: User = request.user
|
||||
Ok(Json.obj(
|
||||
"id" -> user.id,
|
||||
"username" -> user.name
|
||||
))
|
||||
}
|
||||
|
||||
// Pass the request-handling function directly to authAction (no nested Action)
|
||||
def logout(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val sessionCookie = request.cookies.get("sessionId")
|
||||
def register(): Action[AnyContent] = {
|
||||
Action { implicit request =>
|
||||
val jsonBody = request.body.asJson
|
||||
val username: Option[String] = jsonBody.flatMap { jsValue =>
|
||||
(jsValue \ "username").asOpt[String]
|
||||
}
|
||||
val password: Option[String] = jsonBody.flatMap { jsValue =>
|
||||
(jsValue \ "password").asOpt[String]
|
||||
}
|
||||
|
||||
if (username.isDefined && password.isDefined) {
|
||||
// Validate input
|
||||
if (username.get.trim.isEmpty || password.get.length < 6) {
|
||||
BadRequest(Json.obj(
|
||||
"error" -> "Invalid input",
|
||||
"message" -> "Username must not be empty and password must be at least 6 characters"
|
||||
))
|
||||
} else {
|
||||
// Try to register user
|
||||
val registrationSuccess = userManager.addUser(username.get.trim, password.get)
|
||||
if (registrationSuccess) {
|
||||
Created(Json.obj(
|
||||
"message" -> "User registered successfully",
|
||||
"username" -> username.get.trim
|
||||
))
|
||||
} else {
|
||||
Conflict(Json.obj(
|
||||
"error" -> "User already exists",
|
||||
"message" -> "Username is already taken"
|
||||
))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
BadRequest(Json.obj(
|
||||
"error" -> "Invalid request",
|
||||
"message" -> "Username and password are required"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def logoutPost(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val sessionCookie = request.cookies.get("accessToken")
|
||||
if (sessionCookie.isDefined) {
|
||||
sessionManager.invalidateSession(sessionCookie.get.value)
|
||||
}
|
||||
Redirect(routes.UserController.login()).discardingCookies(DiscardingCookie("sessionId"))
|
||||
NoContent.discardingCookies(DiscardingCookie("accessToken"))
|
||||
}
|
||||
|
||||
}
|
||||
45
knockoutwhistweb/app/controllers/WebsocketController.scala
Normal file
45
knockoutwhistweb/app/controllers/WebsocketController.scala
Normal file
@@ -0,0 +1,45 @@
|
||||
package controllers
|
||||
|
||||
|
||||
import auth.AuthAction
|
||||
import logic.PodManager
|
||||
import logic.user.SessionManager
|
||||
import model.sessions.{UserSession, UserWebsocketActor}
|
||||
import org.apache.pekko.actor.{ActorRef, ActorSystem, Props}
|
||||
import org.apache.pekko.stream.Materializer
|
||||
import play.api.*
|
||||
import play.api.libs.streams.ActorFlow
|
||||
import play.api.mvc.*
|
||||
|
||||
import javax.inject.*
|
||||
|
||||
|
||||
@Singleton
|
||||
class WebsocketController @Inject()(
|
||||
cc: ControllerComponents,
|
||||
val sessionManger: SessionManager,
|
||||
)(implicit system: ActorSystem, mat: Materializer) extends AbstractController(cc) {
|
||||
|
||||
def socket(): WebSocket = WebSocket.accept[String, String] { request =>
|
||||
val session = request.cookies.get("accessToken")
|
||||
if (session.isEmpty) throw new Exception("No session cookie found")
|
||||
val userOpt = sessionManger.getUserBySession(session.get.value)
|
||||
if (userOpt.isEmpty) throw new Exception("Invalid session")
|
||||
val user = userOpt.get
|
||||
val game = PodManager.identifyGameOfUser(user)
|
||||
if (game.isEmpty) throw new Exception("User is not in a game")
|
||||
val userSession = game.get.getUserSession(user.id)
|
||||
ActorFlow.actorRef { out =>
|
||||
println("Connect received")
|
||||
KnockOutWebSocketActorFactory.create(out, userSession)
|
||||
}
|
||||
}
|
||||
|
||||
object KnockOutWebSocketActorFactory {
|
||||
def create(out: ActorRef, userSession: UserSession): Props = {
|
||||
Props(new UserWebsocketActor(out, userSession))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
41
knockoutwhistweb/app/di/EntityManagerProvider.scala
Normal file
41
knockoutwhistweb/app/di/EntityManagerProvider.scala
Normal file
@@ -0,0 +1,41 @@
|
||||
package di
|
||||
|
||||
import com.google.inject.{Inject, Provider}
|
||||
import jakarta.inject.Singleton
|
||||
import jakarta.persistence.{EntityManager, EntityManagerFactory, Persistence}
|
||||
import play.api.Configuration
|
||||
|
||||
@Singleton
|
||||
class EntityManagerProvider @Inject()(config: Configuration) extends Provider[EntityManager] {
|
||||
|
||||
private val emf: EntityManagerFactory = {
|
||||
val dbConfig = config.get[Configuration]("db.default")
|
||||
val props = new java.util.HashMap[String, Object]()
|
||||
|
||||
// Map Play configuration to Jakarta Persistence properties
|
||||
props.put("jakarta.persistence.jdbc.driver", dbConfig.get[String]("driver"))
|
||||
props.put("jakarta.persistence.jdbc.url", dbConfig.get[String]("url"))
|
||||
props.put("jakarta.persistence.jdbc.user", dbConfig.get[String]("username"))
|
||||
props.put("jakarta.persistence.jdbc.password", dbConfig.get[String]("password"))
|
||||
|
||||
// Also pass HikariCP settings if present
|
||||
dbConfig.getOptional[Configuration]("hikaricp").foreach { hikariConfig =>
|
||||
hikariConfig.keys.foreach { key =>
|
||||
val value = hikariConfig.underlying.getValue(key).unwrapped()
|
||||
props.put(s"hibernate.hikari.$key", value)
|
||||
}
|
||||
}
|
||||
|
||||
Persistence.createEntityManagerFactory("defaultPersistenceUnit", props)
|
||||
}
|
||||
|
||||
override def get(): EntityManager = {
|
||||
emf.createEntityManager()
|
||||
}
|
||||
|
||||
def close(): Unit = {
|
||||
if (emf.isOpen) {
|
||||
emf.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
35
knockoutwhistweb/app/dto/GameInfoDTO.scala
Normal file
35
knockoutwhistweb/app/dto/GameInfoDTO.scala
Normal file
@@ -0,0 +1,35 @@
|
||||
package dto
|
||||
|
||||
import dto.subDTO.*
|
||||
import logic.game.GameLobby
|
||||
import model.users.User
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
case class GameInfoDTO(
|
||||
gameId: String,
|
||||
self: Option[PlayerDTO],
|
||||
hand: Option[HandDTO],
|
||||
playerQueue: PlayerQueueDTO,
|
||||
currentTrick: Option[TrickDTO],
|
||||
currentRound: Option[RoundDTO]
|
||||
)
|
||||
|
||||
object GameInfoDTO {
|
||||
|
||||
def apply(lobby: GameLobby, user: User): GameInfoDTO = {
|
||||
val selfPlayer = Try {
|
||||
Some(lobby.getPlayerByUser(user))
|
||||
}.getOrElse(None)
|
||||
|
||||
GameInfoDTO(
|
||||
gameId = lobby.id,
|
||||
self = selfPlayer.map(PlayerDTO(_)),
|
||||
hand = selfPlayer.flatMap(_.currentHand()).map(HandDTO(_)),
|
||||
playerQueue = PlayerQueueDTO(lobby.logic),
|
||||
currentTrick = lobby.logic.getCurrentTrick.map(TrickDTO(_)),
|
||||
currentRound = lobby.logic.getCurrentRound.map(r => RoundDTO(r, lobby.logic.getCurrentMatch))
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
22
knockoutwhistweb/app/dto/LobbyInfoDTO.scala
Normal file
22
knockoutwhistweb/app/dto/LobbyInfoDTO.scala
Normal file
@@ -0,0 +1,22 @@
|
||||
package dto
|
||||
|
||||
import dto.subDTO.UserDTO
|
||||
import logic.game.GameLobby
|
||||
import model.users.User
|
||||
|
||||
case class LobbyInfoDTO(gameId: String, users: List[UserDTO], self: UserDTO, maxPlayers: Int)
|
||||
|
||||
object LobbyInfoDTO {
|
||||
|
||||
def apply(lobby: GameLobby, user: User): LobbyInfoDTO = {
|
||||
val session = lobby.getUserSession(user.id)
|
||||
|
||||
LobbyInfoDTO(
|
||||
gameId = lobby.id,
|
||||
users = lobby.getPlayers.values.map(user => UserDTO(user)).toList,
|
||||
self = UserDTO(session),
|
||||
maxPlayers = lobby.maxPlayers,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
32
knockoutwhistweb/app/dto/TieInfoDTO.scala
Normal file
32
knockoutwhistweb/app/dto/TieInfoDTO.scala
Normal file
@@ -0,0 +1,32 @@
|
||||
package dto
|
||||
|
||||
import de.knockoutwhist.cards.Card
|
||||
import dto.subDTO.{CardDTO, PlayerDTO}
|
||||
import logic.game.GameLobby
|
||||
import model.users.User
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
case class TieInfoDTO(gameId: String, currentPlayer: Option[PlayerDTO], self: Option[PlayerDTO], tiedPlayers: Seq[PlayerDTO], highestAmount: Int, selectedCards: Map[String, CardDTO], winners: Option[Seq[PlayerDTO]])
|
||||
|
||||
object TieInfoDTO {
|
||||
|
||||
def apply(lobby: GameLobby, user: User): TieInfoDTO = {
|
||||
val selfPlayer = Try {
|
||||
Some(lobby.getPlayerByUser(user))
|
||||
}.getOrElse(None)
|
||||
|
||||
TieInfoDTO(
|
||||
gameId = lobby.id,
|
||||
currentPlayer = lobby.logic.playerTieLogic.currentTiePlayer().map(PlayerDTO.apply),
|
||||
self = selfPlayer.map(PlayerDTO.apply),
|
||||
tiedPlayers = lobby.logic.playerTieLogic.getTiedPlayers.map(PlayerDTO.apply),
|
||||
highestAmount = lobby.logic.playerTieLogic.highestAllowedNumber(),
|
||||
selectedCards = lobby.logic.playerTieLogic.getSelectedCard.map {
|
||||
case (player, card) => player.id.toString -> CardDTO(card)
|
||||
},
|
||||
winners = Some(lobby.logic.playerTieLogic.getSelectedCard.filter((_,card) => card == lobby.logic.playerTieLogic.getSelectedCard.values.maxBy(_.cardValue.ordinal)).keySet.toList.map(PlayerDTO.apply))
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
31
knockoutwhistweb/app/dto/TrumpInfoDTO.scala
Normal file
31
knockoutwhistweb/app/dto/TrumpInfoDTO.scala
Normal file
@@ -0,0 +1,31 @@
|
||||
package dto
|
||||
|
||||
import dto.subDTO.{HandDTO, PlayerDTO}
|
||||
import logic.game.GameLobby
|
||||
import model.users.User
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
case class TrumpInfoDTO(
|
||||
gameId: String,
|
||||
chooser: Option[PlayerDTO],
|
||||
self: Option[PlayerDTO],
|
||||
selfHand: Option[HandDTO],
|
||||
)
|
||||
|
||||
object TrumpInfoDTO {
|
||||
|
||||
def apply(lobby: GameLobby, user: User): TrumpInfoDTO = {
|
||||
val selfPlayer = Try {
|
||||
Some(lobby.getPlayerByUser(user))
|
||||
}.getOrElse(None)
|
||||
|
||||
TrumpInfoDTO(
|
||||
gameId = lobby.id,
|
||||
chooser = lobby.logic.getTrumpPlayer.map(PlayerDTO(_)),
|
||||
self = selfPlayer.map(PlayerDTO(_)),
|
||||
selfHand = selfPlayer.flatMap(_.currentHand()).map(HandDTO(_))
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
34
knockoutwhistweb/app/dto/WonInfoDTO.scala
Normal file
34
knockoutwhistweb/app/dto/WonInfoDTO.scala
Normal file
@@ -0,0 +1,34 @@
|
||||
package dto
|
||||
|
||||
import dto.subDTO.PodiumPlayerDTO
|
||||
import logic.game.GameLobby
|
||||
import model.users.User
|
||||
|
||||
case class WonInfoDTO(
|
||||
gameId: String,
|
||||
winner: Option[PodiumPlayerDTO],
|
||||
allPlayers: Seq[PodiumPlayerDTO]
|
||||
)
|
||||
|
||||
object WonInfoDTO {
|
||||
|
||||
def apply(lobby: GameLobby, user: User): WonInfoDTO = {
|
||||
val matchImpl = lobby.logic.getCurrentMatch
|
||||
if (matchImpl.isEmpty) {
|
||||
throw new IllegalStateException("No current match available in game logic")
|
||||
}
|
||||
val allPlayersDTO: Seq[PodiumPlayerDTO] = matchImpl.get.totalplayers.map { player =>
|
||||
PodiumPlayerDTO(lobby.logic, player)
|
||||
}
|
||||
|
||||
val selfPlayerDTO = lobby.getPlayerByUser(user)
|
||||
val winnerDTO = lobby.logic.getWinner
|
||||
|
||||
WonInfoDTO(
|
||||
gameId = lobby.id,
|
||||
winner = winnerDTO.map(player => PodiumPlayerDTO(lobby.logic, player)),
|
||||
allPlayers = allPlayersDTO
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
31
knockoutwhistweb/app/dto/subDTO/CardDTO.scala
Normal file
31
knockoutwhistweb/app/dto/subDTO/CardDTO.scala
Normal file
@@ -0,0 +1,31 @@
|
||||
package dto.subDTO
|
||||
|
||||
import de.knockoutwhist.cards.Card
|
||||
import util.WebUIUtils
|
||||
|
||||
case class CardDTO(identifier: String, path: String, idx: Option[Int]) {
|
||||
|
||||
def toCard: Card = {
|
||||
WebUIUtils.stringToCard(identifier)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object CardDTO {
|
||||
|
||||
def apply(card: Card, index: Int): CardDTO = {
|
||||
CardDTO(
|
||||
identifier = WebUIUtils.cardtoString(card),
|
||||
path = WebUIUtils.cardToPath(card),
|
||||
idx = Some(index)
|
||||
)
|
||||
}
|
||||
|
||||
def apply(card: Card): CardDTO = {
|
||||
CardDTO(
|
||||
identifier = WebUIUtils.cardtoString(card),
|
||||
path = WebUIUtils.cardToPath(card),
|
||||
idx = None
|
||||
)
|
||||
}
|
||||
}
|
||||
15
knockoutwhistweb/app/dto/subDTO/HandDTO.scala
Normal file
15
knockoutwhistweb/app/dto/subDTO/HandDTO.scala
Normal file
@@ -0,0 +1,15 @@
|
||||
package dto.subDTO
|
||||
|
||||
import de.knockoutwhist.cards.Hand
|
||||
|
||||
case class HandDTO(cards: List[CardDTO])
|
||||
|
||||
object HandDTO {
|
||||
|
||||
def apply(hand: Hand): HandDTO = {
|
||||
HandDTO(
|
||||
cards = hand.cards.zipWithIndex.map { case (card, idx) => CardDTO(card, idx) }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
17
knockoutwhistweb/app/dto/subDTO/PlayerDTO.scala
Normal file
17
knockoutwhistweb/app/dto/subDTO/PlayerDTO.scala
Normal file
@@ -0,0 +1,17 @@
|
||||
package dto.subDTO
|
||||
|
||||
import de.knockoutwhist.player.AbstractPlayer
|
||||
|
||||
case class PlayerDTO(id: String, name: String, dogLife: Boolean)
|
||||
|
||||
object PlayerDTO {
|
||||
|
||||
def apply(player: AbstractPlayer): PlayerDTO = {
|
||||
PlayerDTO(
|
||||
id = player.id.toString,
|
||||
name = player.name,
|
||||
dogLife = player.isInDogLife
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
19
knockoutwhistweb/app/dto/subDTO/PlayerQueueDTO.scala
Normal file
19
knockoutwhistweb/app/dto/subDTO/PlayerQueueDTO.scala
Normal file
@@ -0,0 +1,19 @@
|
||||
package dto.subDTO
|
||||
|
||||
import de.knockoutwhist.control.GameLogic
|
||||
|
||||
case class PlayerQueueDTO(currentPlayer: Option[PlayerDTO], queue: Seq[PlayerDTO])
|
||||
|
||||
object PlayerQueueDTO {
|
||||
|
||||
def apply(logic: GameLogic): PlayerQueueDTO = {
|
||||
val currentPlayerDTO = logic.getCurrentPlayer.map(PlayerDTO(_))
|
||||
val queueDTO = logic.getPlayerQueue.map(_.duplicate().flatMap(player => Some(PlayerDTO(player))).toSeq)
|
||||
if (queueDTO.isEmpty) {
|
||||
PlayerQueueDTO(currentPlayerDTO, Seq.empty)
|
||||
} else {
|
||||
PlayerQueueDTO(currentPlayerDTO, queueDTO.get)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
47
knockoutwhistweb/app/dto/subDTO/PodiumPlayerDTO.scala
Normal file
47
knockoutwhistweb/app/dto/subDTO/PodiumPlayerDTO.scala
Normal file
@@ -0,0 +1,47 @@
|
||||
package dto.subDTO
|
||||
|
||||
import de.knockoutwhist.control.GameLogic
|
||||
import de.knockoutwhist.player.AbstractPlayer
|
||||
import de.knockoutwhist.rounds.Match
|
||||
|
||||
case class PodiumPlayerDTO(
|
||||
player: PlayerDTO,
|
||||
position: Int,
|
||||
roundsWon: Int,
|
||||
tricksWon: Int
|
||||
)
|
||||
|
||||
object PodiumPlayerDTO {
|
||||
|
||||
def apply(gameLogic: GameLogic, player: AbstractPlayer): PodiumPlayerDTO = {
|
||||
val matchImplOpt = gameLogic.getCurrentMatch
|
||||
if (matchImplOpt.isEmpty) {
|
||||
throw new IllegalStateException("No current match available in game logic")
|
||||
}
|
||||
val matchImpl: Match = matchImplOpt.get
|
||||
var roundsWon = 0
|
||||
var tricksWon = 0
|
||||
for (round <- matchImpl.roundlist) {
|
||||
if (round.winner.contains(player)) {
|
||||
roundsWon += 1
|
||||
}
|
||||
for (trick <- round.tricklist) {
|
||||
if (trick.winner.contains(player)) {
|
||||
tricksWon += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PodiumPlayerDTO(
|
||||
player = PlayerDTO(player),
|
||||
position = if (gameLogic.getWinner.contains(player)) {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
},
|
||||
roundsWon = roundsWon,
|
||||
tricksWon = tricksWon
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
20
knockoutwhistweb/app/dto/subDTO/RoundDTO.scala
Normal file
20
knockoutwhistweb/app/dto/subDTO/RoundDTO.scala
Normal file
@@ -0,0 +1,20 @@
|
||||
package dto.subDTO
|
||||
|
||||
import de.knockoutwhist.cards.Card
|
||||
import de.knockoutwhist.cards.CardValue.Ace
|
||||
import de.knockoutwhist.rounds.{Match, Round}
|
||||
|
||||
case class RoundDTO(trumpSuit: CardDTO, playersIn: Seq[PlayerDTO], firstRound: Boolean, trickList: List[TrickDTO])
|
||||
|
||||
object RoundDTO {
|
||||
|
||||
def apply(round: Round, matchImpl: Option[Match]): RoundDTO = {
|
||||
RoundDTO(
|
||||
trumpSuit = CardDTO(Card(Ace, round.trumpSuit)),
|
||||
playersIn = matchImpl.map(_.playersIn.map(PlayerDTO(_))).getOrElse(Seq.empty),
|
||||
firstRound = round.firstRound,
|
||||
trickList = round.tricklist.map(trick => TrickDTO(trick))
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
17
knockoutwhistweb/app/dto/subDTO/TrickDTO.scala
Normal file
17
knockoutwhistweb/app/dto/subDTO/TrickDTO.scala
Normal file
@@ -0,0 +1,17 @@
|
||||
package dto.subDTO
|
||||
|
||||
import de.knockoutwhist.rounds.Trick
|
||||
|
||||
case class TrickDTO(cards: Map[String, CardDTO], firstCard: Option[CardDTO], winner: Option[PlayerDTO])
|
||||
|
||||
object TrickDTO {
|
||||
|
||||
def apply(trick: Trick): TrickDTO = {
|
||||
TrickDTO(
|
||||
cards = trick.cards.map { case (card, player) => player.name -> CardDTO(card) },
|
||||
firstCard = trick.firstCard.map(card => CardDTO(card)),
|
||||
winner = trick.winner.map(player => PlayerDTO(player))
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
17
knockoutwhistweb/app/dto/subDTO/UserDTO.scala
Normal file
17
knockoutwhistweb/app/dto/subDTO/UserDTO.scala
Normal file
@@ -0,0 +1,17 @@
|
||||
package dto.subDTO
|
||||
|
||||
import model.sessions.UserSession
|
||||
|
||||
case class UserDTO(id: String, username: String, host: Boolean = false)
|
||||
|
||||
object UserDTO {
|
||||
|
||||
def apply(user: UserSession): UserDTO = {
|
||||
UserDTO(
|
||||
id = user.id.toString,
|
||||
username = user.name,
|
||||
host = user.host
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
9
knockoutwhistweb/app/events/KickEvent.scala
Normal file
9
knockoutwhistweb/app/events/KickEvent.scala
Normal file
@@ -0,0 +1,9 @@
|
||||
package events
|
||||
|
||||
import model.users.User
|
||||
|
||||
case class KickEvent(user: User) extends UserEvent(user) {
|
||||
|
||||
override def id: String = "KickEvent"
|
||||
|
||||
}
|
||||
9
knockoutwhistweb/app/events/LeftEvent.scala
Normal file
9
knockoutwhistweb/app/events/LeftEvent.scala
Normal file
@@ -0,0 +1,9 @@
|
||||
package events
|
||||
|
||||
import model.users.User
|
||||
|
||||
case class LeftEvent(user: User) extends UserEvent(user) {
|
||||
|
||||
override def id: String = "LeftEvent"
|
||||
|
||||
}
|
||||
9
knockoutwhistweb/app/events/LobbyUpdateEvent.scala
Normal file
9
knockoutwhistweb/app/events/LobbyUpdateEvent.scala
Normal file
@@ -0,0 +1,9 @@
|
||||
package events
|
||||
|
||||
import de.knockoutwhist.utils.events.SimpleEvent
|
||||
|
||||
case class LobbyUpdateEvent() extends SimpleEvent {
|
||||
|
||||
override def id: String = "LobbyUpdateEvent"
|
||||
|
||||
}
|
||||
12
knockoutwhistweb/app/events/UserEvent.scala
Normal file
12
knockoutwhistweb/app/events/UserEvent.scala
Normal file
@@ -0,0 +1,12 @@
|
||||
package events
|
||||
|
||||
import de.knockoutwhist.utils.events.SimpleEvent
|
||||
import model.users.User
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
abstract class UserEvent(user: User) extends SimpleEvent {
|
||||
|
||||
def userId: UUID = user.id
|
||||
|
||||
}
|
||||
54
knockoutwhistweb/app/logic/Gateway.scala
Normal file
54
knockoutwhistweb/app/logic/Gateway.scala
Normal file
@@ -0,0 +1,54 @@
|
||||
package logic
|
||||
|
||||
import de.knockoutwhist.data.Pod
|
||||
import de.knockoutwhist.data.redis.RedisManager
|
||||
import org.apache.pekko.actor.ActorSystem
|
||||
import org.redisson.config.Config
|
||||
import play.api.Logger
|
||||
import play.api.inject.ApplicationLifecycle
|
||||
|
||||
import java.util
|
||||
import java.util.UUID
|
||||
import javax.inject.*
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.jdk.CollectionConverters.*
|
||||
|
||||
@Singleton
|
||||
class Gateway @Inject()(
|
||||
lifecycle: ApplicationLifecycle,
|
||||
actorSystem: ActorSystem
|
||||
)(implicit ec: ExecutionContext) {
|
||||
|
||||
private val logger = Logger(getClass.getName)
|
||||
|
||||
val redis: RedisManager = {
|
||||
val config: Config = Config()
|
||||
val url = "redis://" + sys.env.getOrElse("REDIS_HOST", "localhost") + ":" + sys.env.getOrElse("REDIS_PORT", "6379")
|
||||
logger.info(s"Connecting to Redis at $url")
|
||||
config.useSingleServer.setAddress(url)
|
||||
RedisManager(config)
|
||||
}
|
||||
|
||||
redis.continuousSyncPod(() => {
|
||||
logger.info("Syncing pod with Redis")
|
||||
createPod()
|
||||
})
|
||||
|
||||
logger.info("Gateway started")
|
||||
|
||||
def syncPod(): Unit = {
|
||||
redis.syncPod(createPod())
|
||||
}
|
||||
|
||||
private def createPod(): Pod = {
|
||||
Pod(
|
||||
UUID.randomUUID().toString,
|
||||
PodManager.podName,
|
||||
PodManager.podIp,
|
||||
9000,
|
||||
new util.ArrayList[String](PodManager.getAllGameIds().asJava),
|
||||
new util.ArrayList[String](PodManager.allBoundUsers().asJava)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,20 +11,22 @@ import util.GameUtil
|
||||
import javax.inject.Singleton
|
||||
import scala.collection.mutable
|
||||
|
||||
@Singleton
|
||||
class PodManager {
|
||||
object PodManager {
|
||||
|
||||
val TTL: Long = System.currentTimeMillis() + 86400000L // 24 hours in milliseconds
|
||||
val podIp: String = System.getenv("POD_IP")
|
||||
val podName: String = System.getenv("POD_NAME")
|
||||
|
||||
|
||||
private val sessions: mutable.Map[String, GameLobby] = mutable.Map()
|
||||
private val userSession: mutable.Map[User, String] = mutable.Map()
|
||||
private val injector: Injector = Guice.createInjector(KnockOutWebConfigurationModule())
|
||||
|
||||
|
||||
private[logic] var redis: Option[Gateway] = None
|
||||
|
||||
def createGame(
|
||||
host: User,
|
||||
name: String,
|
||||
maxPlayers: Int
|
||||
host: User,
|
||||
name: String,
|
||||
maxPlayers: Int
|
||||
): GameLobby = {
|
||||
val gameLobby = GameLobby(
|
||||
logic = BaseGameLogic(injector.getInstance(classOf[Configuration])),
|
||||
@@ -35,15 +37,45 @@ class PodManager {
|
||||
host = host
|
||||
)
|
||||
sessions += (gameLobby.id -> gameLobby)
|
||||
registerUserToGame(host, gameLobby.id)
|
||||
redis.foreach(gateway => gateway.syncPod())
|
||||
gameLobby
|
||||
}
|
||||
|
||||
def getGame(gameId: String): Option[GameLobby] = {
|
||||
sessions.get(gameId)
|
||||
}
|
||||
|
||||
private[logic] def removeGame(gameId: String): Unit = {
|
||||
sessions.remove(gameId)
|
||||
|
||||
def registerUserToGame(user: User, gameId: String): Boolean = {
|
||||
if (sessions.contains(gameId)) {
|
||||
userSession += (user -> gameId)
|
||||
redis.foreach(gateway => gateway.syncPod())
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
def unregisterUserFromGame(user: User): Unit = {
|
||||
userSession.remove(user)
|
||||
redis.foreach(gateway => gateway.redis.invalidateUser(user.id.toString))
|
||||
}
|
||||
|
||||
def identifyGameOfUser(user: User): Option[GameLobby] = {
|
||||
userSession.get(user) match {
|
||||
case Some(gameId) => sessions.get(gameId)
|
||||
case None => None
|
||||
}
|
||||
}
|
||||
|
||||
private[logic] def removeGame(gameId: String): Unit = {
|
||||
sessions.remove(gameId)
|
||||
redis.foreach(gateway => gateway.redis.invalidateGame(gameId))
|
||||
// Also remove all user sessions associated with this game
|
||||
userSession.filterInPlace((_, v) => v != gameId)
|
||||
}
|
||||
|
||||
private[logic] def getAllGameIds(): List[String] = sessions.keys.toList
|
||||
private[logic] def allBoundUsers(): List[String] = userSession.keys.map(_.id.toString).toList
|
||||
|
||||
}
|
||||
|
||||
@@ -9,36 +9,46 @@ import de.knockoutwhist.events.player.PlayerEvent
|
||||
import de.knockoutwhist.player.Playertype.HUMAN
|
||||
import de.knockoutwhist.player.{AbstractPlayer, PlayerFactory}
|
||||
import de.knockoutwhist.rounds.{Match, Round, Trick}
|
||||
import de.knockoutwhist.utils.DelayHandler
|
||||
import de.knockoutwhist.utils.events.{EventListener, SimpleEvent}
|
||||
import events.{KickEvent, LeftEvent, LobbyUpdateEvent, UserEvent}
|
||||
import exceptions.*
|
||||
import logic.PodManager
|
||||
import model.sessions.{InteractionType, UserSession}
|
||||
import model.users.User
|
||||
import play.api.libs.json.{JsObject, Json}
|
||||
|
||||
import java.util.UUID
|
||||
import java.util.{Timer, TimerTask, UUID}
|
||||
import scala.collection.mutable
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import scala.util.Try
|
||||
|
||||
class GameLobby private(
|
||||
val logic: GameLogic,
|
||||
val id: String,
|
||||
val internalId: UUID,
|
||||
val name: String,
|
||||
val maxPlayers: Int
|
||||
) extends EventListener {
|
||||
logic.addListener(this)
|
||||
logic.createSession()
|
||||
val logic: GameLogic,
|
||||
val id: String,
|
||||
val internalId: UUID,
|
||||
val name: String,
|
||||
val maxPlayers: Int
|
||||
) extends EventListener {
|
||||
|
||||
|
||||
private val users: mutable.Map[UUID, UserSession] = mutable.Map()
|
||||
|
||||
logic.addListener(this)
|
||||
logic.addListener(DelayHandler)
|
||||
logic.createSession()
|
||||
|
||||
def addUser(user: User): UserSession = {
|
||||
if (users.size >= maxPlayers) throw new GameFullException("The game is full!")
|
||||
if (users.contains(user.id)) throw new IllegalArgumentException("User is already in the game!")
|
||||
if (logic.getCurrentState != Lobby) throw new IllegalStateException("The game has already started!")
|
||||
val userSession = new UserSession(
|
||||
user = user,
|
||||
host = false
|
||||
host = false,
|
||||
gameLobby = this
|
||||
)
|
||||
users += (user.id -> userSession)
|
||||
PodManager.registerUserToGame(user, id)
|
||||
logic.invoke(LobbyUpdateEvent())
|
||||
userSession
|
||||
}
|
||||
|
||||
@@ -46,13 +56,13 @@ class GameLobby private(
|
||||
event match {
|
||||
case event: PlayerEvent =>
|
||||
users.get(event.playerId).foreach(session => session.updatePlayer(event))
|
||||
case event: UserEvent =>
|
||||
users.get(event.userId).foreach(session => session.updatePlayer(event))
|
||||
case event: GameStateChangeEvent =>
|
||||
if (event.oldState == MainMenu && event.newState == Lobby) {
|
||||
return
|
||||
}
|
||||
users.values.foreach(session => session.updatePlayer(event))
|
||||
case event: SessionClosed =>
|
||||
users.values.foreach(session => session.updatePlayer(event))
|
||||
case event: SimpleEvent =>
|
||||
users.values.foreach(session => session.updatePlayer(event))
|
||||
}
|
||||
@@ -60,6 +70,7 @@ class GameLobby private(
|
||||
|
||||
/**
|
||||
* Start the game if the user is the host.
|
||||
*
|
||||
* @param user the user who wants to start the game.
|
||||
*/
|
||||
def startGame(user: User): Unit = {
|
||||
@@ -86,20 +97,39 @@ class GameLobby private(
|
||||
|
||||
/**
|
||||
* Remove the user from the game lobby.
|
||||
*
|
||||
* @param user the user who wants to leave the game.
|
||||
* @param kicked whether the user was kicked or left voluntarily.
|
||||
*/
|
||||
def leaveGame(user: User): Unit = {
|
||||
val sessionOpt = users.get(user.id)
|
||||
def leaveGame(userId: UUID, kicked: Boolean): Unit = {
|
||||
val sessionOpt = users.get(userId)
|
||||
if (sessionOpt.isEmpty) {
|
||||
throw new NotInThisGameException("You are not in this game!")
|
||||
}
|
||||
users.remove(user.id)
|
||||
if (sessionOpt.get.host) {
|
||||
logic.invoke(SessionClosed())
|
||||
for (session <- users.values) {
|
||||
PodManager.unregisterUserFromGame(session.user)
|
||||
}
|
||||
users.clear()
|
||||
PodManager.removeGame(id)
|
||||
return
|
||||
}
|
||||
if (kicked) {
|
||||
logic.invoke(KickEvent(sessionOpt.get.user))
|
||||
} else {
|
||||
logic.invoke(LeftEvent(sessionOpt.get.user))
|
||||
}
|
||||
users.remove(userId)
|
||||
PodManager.unregisterUserFromGame(sessionOpt.get.user)
|
||||
logic.invoke(LobbyUpdateEvent())
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a card from the player's hand.
|
||||
*
|
||||
* @param userSession the user session of the player.
|
||||
* @param cardIndex the index of the card in the player's hand.
|
||||
* @param cardIndex the index of the card in the player's hand.
|
||||
*/
|
||||
def playCard(userSession: UserSession, cardIndex: Int): Unit = {
|
||||
val player = getPlayerInteractable(userSession, InteractionType.Card)
|
||||
@@ -115,85 +145,6 @@ class GameLobby private(
|
||||
logic.playerInputLogic.receivedCard(card)
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a card from the player's hand while in dog life or skip the round.
|
||||
* @param userSession the user session of the player.
|
||||
* @param cardIndex the index of the card in the player's hand or -1 if the player wants to skip the round.
|
||||
*/
|
||||
def playDogCard(userSession: UserSession, cardIndex: Int): Unit = {
|
||||
val player = getPlayerInteractable(userSession, InteractionType.DogCard)
|
||||
if (!player.isInDogLife) {
|
||||
throw new CantPlayCardException("You are not in dog life!")
|
||||
}
|
||||
if (cardIndex == -1) {
|
||||
if (!MatchUtil.dogNeedsToPlay(getMatch, getRound)) {
|
||||
throw new CantPlayCardException("You can't skip this round!")
|
||||
}
|
||||
logic.playerInputLogic.receivedDog(None)
|
||||
}
|
||||
val hand = getHand(player)
|
||||
val card = hand.cards(cardIndex)
|
||||
userSession.resetCanInteract()
|
||||
logic.playerInputLogic.receivedDog(Some(card))
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the trump suit for the round.
|
||||
* @param userSession the user session of the player.
|
||||
* @param trumpIndex the index of the trump suit.
|
||||
*/
|
||||
def selectTrump(userSession: UserSession, trumpIndex: Int): Unit = {
|
||||
val player = getPlayerInteractable(userSession, InteractionType.TrumpSuit)
|
||||
val trumpSuits = Suit.values.toList
|
||||
val selectedTrump = trumpSuits(trumpIndex)
|
||||
userSession.resetCanInteract()
|
||||
logic.playerInputLogic.receivedTrumpSuit(selectedTrump)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param userSession
|
||||
* @param tieNumber
|
||||
*/
|
||||
def selectTie(userSession: UserSession, tieNumber: Int): Unit = {
|
||||
val player = getPlayerInteractable(userSession, InteractionType.TieChoice)
|
||||
userSession.resetCanInteract()
|
||||
logic.playerTieLogic.receivedTieBreakerCard(tieNumber)
|
||||
}
|
||||
|
||||
|
||||
//-------------------
|
||||
|
||||
def getUserSession(userId: UUID): UserSession = {
|
||||
val sessionOpt = users.get(userId)
|
||||
if (sessionOpt.isEmpty) {
|
||||
throw new NotInThisGameException("You are not in this game!")
|
||||
}
|
||||
sessionOpt.get
|
||||
}
|
||||
|
||||
def getPlayerByUser(user: User): AbstractPlayer = {
|
||||
getPlayerBySession(getUserSession(user.id))
|
||||
}
|
||||
|
||||
private def getPlayerBySession(userSession: UserSession): AbstractPlayer = {
|
||||
val playerOption = getMatch.totalplayers.find(_.id == userSession.id)
|
||||
if (playerOption.isEmpty) {
|
||||
throw new NotInThisGameException("You are not in this game!")
|
||||
}
|
||||
playerOption.get
|
||||
}
|
||||
|
||||
private def getPlayerInteractable(userSession: UserSession, iType: InteractionType): AbstractPlayer = {
|
||||
if (!userSession.lock.isHeldByCurrentThread) {
|
||||
throw new IllegalStateException("The user session is not locked!")
|
||||
}
|
||||
if (userSession.canInteract.isEmpty || userSession.canInteract.get != iType) {
|
||||
throw new NotInteractableException("You can't play a card!")
|
||||
}
|
||||
getPlayerBySession(userSession)
|
||||
}
|
||||
|
||||
private def getHand(player: AbstractPlayer): Hand = {
|
||||
val handOption = player.currentHand()
|
||||
if (handOption.isEmpty) {
|
||||
@@ -201,14 +152,6 @@ class GameLobby private(
|
||||
}
|
||||
handOption.get
|
||||
}
|
||||
|
||||
private def getMatch: Match = {
|
||||
val matchOpt = logic.getCurrentMatch
|
||||
if (matchOpt.isEmpty) {
|
||||
throw new IllegalStateException("No match is currently running!")
|
||||
}
|
||||
matchOpt.get
|
||||
}
|
||||
|
||||
private def getRound: Round = {
|
||||
val roundOpt = logic.getCurrentRound
|
||||
@@ -225,7 +168,159 @@ class GameLobby private(
|
||||
}
|
||||
trickOpt.get
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Play a card from the player's hand while in dog life or skip the round.
|
||||
*
|
||||
* @param userSession the user session of the player.
|
||||
* @param cardIndex the index of the card in the player's hand or -1 if the player wants to skip the round.
|
||||
*/
|
||||
def playDogCard(userSession: UserSession, cardIndex: Int): Unit = {
|
||||
val player = getPlayerInteractable(userSession, InteractionType.DogCard)
|
||||
if (!player.isInDogLife) {
|
||||
throw new CantPlayCardException("You are not in dog life!")
|
||||
}
|
||||
if (cardIndex == -1) {
|
||||
if (MatchUtil.dogNeedsToPlay(getMatch, getRound)) {
|
||||
throw new CantPlayCardException("You can't skip this round!")
|
||||
}
|
||||
logic.playerInputLogic.receivedDog(None)
|
||||
return
|
||||
}
|
||||
val hand = getHand(player)
|
||||
val card = hand.cards(cardIndex)
|
||||
userSession.resetCanInteract()
|
||||
logic.playerInputLogic.receivedDog(Some(card))
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the trump suit for the round.
|
||||
*
|
||||
* @param userSession the user session of the player.
|
||||
* @param trumpIndex the index of the trump suit.
|
||||
*/
|
||||
def selectTrump(userSession: UserSession, trumpIndex: Int): Unit = {
|
||||
val player = getPlayerInteractable(userSession, InteractionType.TrumpSuit)
|
||||
val trumpSuits = Suit.values.toList
|
||||
val selectedTrump = trumpSuits(trumpIndex)
|
||||
userSession.resetCanInteract()
|
||||
logic.playerInputLogic.receivedTrumpSuit(selectedTrump)
|
||||
}
|
||||
|
||||
|
||||
//-------------------
|
||||
|
||||
private def getPlayerInteractable(userSession: UserSession, iType: InteractionType): AbstractPlayer = {
|
||||
if (!userSession.lock.isHeldByCurrentThread) {
|
||||
throw new IllegalStateException("The user session is not locked!")
|
||||
}
|
||||
if (userSession.canInteract.isEmpty || userSession.canInteract.get != iType) {
|
||||
throw new NotInteractableException("You can't play a card!")
|
||||
}
|
||||
getPlayerBySession(userSession)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param userSession
|
||||
* @param tieNumber
|
||||
*/
|
||||
def selectTie(userSession: UserSession, tieNumber: Int): Unit = {
|
||||
val player = getPlayerInteractable(userSession, InteractionType.TieChoice)
|
||||
userSession.resetCanInteract()
|
||||
logic.playerTieLogic.receivedTieBreakerCard(tieNumber)
|
||||
}
|
||||
|
||||
def returnToLobby(userSession: UserSession): Unit = {
|
||||
if (!users.contains(userSession.id)) {
|
||||
throw new NotInThisGameException("You are not in this game!")
|
||||
}
|
||||
val session = users(userSession.id)
|
||||
if (session != userSession) {
|
||||
throw new IllegalArgumentException("User session does not match!")
|
||||
}
|
||||
if (!session.host)
|
||||
throw new NotHostException("Only the host can return to the lobby!")
|
||||
logic.createSession()
|
||||
}
|
||||
|
||||
def getPlayerByUser(user: User): AbstractPlayer = {
|
||||
getPlayerBySession(getUserSession(user.id))
|
||||
}
|
||||
|
||||
def getUserSession(userId: UUID): UserSession = {
|
||||
val sessionOpt = users.get(userId)
|
||||
if (sessionOpt.isEmpty) {
|
||||
throw new NotInThisGameException("You are not in this game!")
|
||||
}
|
||||
sessionOpt.get
|
||||
}
|
||||
|
||||
private def getPlayerBySession(userSession: UserSession): AbstractPlayer = {
|
||||
val playerOption = getMatch.totalplayers.find(_.id == userSession.id)
|
||||
if (playerOption.isEmpty) {
|
||||
throw new NotInThisGameException("You are not in this game!")
|
||||
}
|
||||
playerOption.get
|
||||
}
|
||||
|
||||
private def getMatch: Match = {
|
||||
val matchOpt = logic.getCurrentMatch
|
||||
if (matchOpt.isEmpty) {
|
||||
throw new IllegalStateException("No match is currently running!")
|
||||
}
|
||||
matchOpt.get
|
||||
}
|
||||
|
||||
def getPlayers: mutable.Map[UUID, UserSession] = {
|
||||
users.clone()
|
||||
}
|
||||
|
||||
def getLogic: GameLogic = {
|
||||
logic
|
||||
}
|
||||
|
||||
def getUsers: Set[User] = {
|
||||
users.values.map(d => d.user).toSet
|
||||
}
|
||||
def getFinalRanking: List[(String, (Int, Int))] = {
|
||||
Try {
|
||||
val match1 = getMatch
|
||||
if (!match1.isOver) {
|
||||
List.empty
|
||||
} else {
|
||||
val winnerName = logic.getWinner.get.name
|
||||
|
||||
val allPlayerNames = match1.totalplayers.map(_.name)
|
||||
val roundlist = match1.roundlist
|
||||
|
||||
val playerMetrics: Map[String, (Int, Int)] = allPlayerNames.map { name =>
|
||||
val roundsWon = roundlist.count { round =>
|
||||
round.winner.exists(_.name == name)
|
||||
}
|
||||
val totalTricksWon = roundlist.flatMap(_.tricklist).count { trick =>
|
||||
trick.winner.exists(_.name == name)
|
||||
}
|
||||
name -> (roundsWon, totalTricksWon)
|
||||
}.toMap
|
||||
|
||||
val winnerMetrics = playerMetrics(winnerName)
|
||||
val remainingPlayersMetrics = playerMetrics.view.filterKeys(_ != winnerName).toList
|
||||
|
||||
val sortedRemainingPlayers = remainingPlayersMetrics.sortBy { case (_, (rounds, tricks)) =>
|
||||
(-rounds, -tricks)
|
||||
}
|
||||
(winnerName, winnerMetrics) :: sortedRemainingPlayers
|
||||
}
|
||||
}.getOrElse(List())
|
||||
}
|
||||
|
||||
private def transmitToAll(event: JsObject): Unit = {
|
||||
users.values.foreach(session => {
|
||||
session.websocketActor.foreach(act => act.transmitJsonToClient(event))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object GameLobby {
|
||||
@@ -246,7 +341,8 @@ object GameLobby {
|
||||
)
|
||||
lobby.users += (host.id -> new UserSession(
|
||||
user = host,
|
||||
host = true
|
||||
host = true,
|
||||
gameLobby = lobby
|
||||
))
|
||||
lobby
|
||||
}
|
||||
|
||||
@@ -6,9 +6,12 @@ import model.users.User
|
||||
|
||||
@ImplementedBy(classOf[BaseSessionManager])
|
||||
trait SessionManager {
|
||||
|
||||
|
||||
def createSession(user: User): String
|
||||
|
||||
|
||||
def getUserBySession(sessionId: String): Option[User]
|
||||
|
||||
def invalidateSession(sessionId: String): Unit
|
||||
|
||||
}
|
||||
|
||||
@@ -3,14 +3,23 @@ package logic.user
|
||||
import com.google.inject.ImplementedBy
|
||||
import logic.user.impl.StubUserManager
|
||||
import model.users.User
|
||||
import services.OpenIDUserInfo
|
||||
|
||||
@ImplementedBy(classOf[StubUserManager])
|
||||
trait UserManager {
|
||||
|
||||
def addUser(name: String, password: String): Boolean
|
||||
|
||||
def addOpenIDUser(name: String, userInfo: OpenIDUserInfo): Boolean
|
||||
|
||||
def authenticate(name: String, password: String): Option[User]
|
||||
|
||||
def authenticateOpenID(provider: String, providerId: String): Option[User]
|
||||
|
||||
def userExists(name: String): Option[User]
|
||||
|
||||
def userExistsById(id: Long): Option[User]
|
||||
|
||||
def removeUser(name: String): Boolean
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import com.auth0.jwt.algorithms.Algorithm
|
||||
import com.auth0.jwt.{JWT, JWTVerifier}
|
||||
import com.github.benmanes.caffeine.cache.{Cache, Caffeine}
|
||||
import com.typesafe.config.Config
|
||||
import logic.user.SessionManager
|
||||
import logic.user.{SessionManager, UserManager}
|
||||
import model.users.User
|
||||
import scalafx.util.Duration
|
||||
import services.JwtKeyProvider
|
||||
@@ -13,9 +13,10 @@ import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.{Inject, Singleton}
|
||||
import scala.util.Try
|
||||
|
||||
@Singleton
|
||||
class BaseSessionManager @Inject()(val keyProvider: JwtKeyProvider, val userManager: StubUserManager, val config: Config) extends SessionManager {
|
||||
class BaseSessionManager @Inject()(val keyProvider: JwtKeyProvider, val userManager: UserManager, val config: Config) extends SessionManager {
|
||||
|
||||
private val algorithm = Algorithm.RSA512(keyProvider.publicKey, keyProvider.privateKey)
|
||||
private val verifier: JWTVerifier = JWT.require(algorithm)
|
||||
@@ -44,15 +45,21 @@ class BaseSessionManager @Inject()(val keyProvider: JwtKeyProvider, val userMana
|
||||
}
|
||||
|
||||
override def getUserBySession(sessionId: String): Option[User] = {
|
||||
//TODO verify JWT token instead of looking up in cache
|
||||
val cachedUser = cache.getIfPresent(sessionId)
|
||||
if (cachedUser != null) {
|
||||
Some(cachedUser)
|
||||
} else {
|
||||
val decoded = verifier.verify(sessionId)
|
||||
val user = userManager.userExistsById(decoded.getClaim("id").asLong())
|
||||
user.foreach(u => cache.put(sessionId, u))
|
||||
user
|
||||
val result = Try {
|
||||
val decoded = verifier.verify(sessionId)
|
||||
val user = userManager.userExistsById(decoded.getClaim("id").asLong())
|
||||
user.foreach(u => cache.put(sessionId, u))
|
||||
user
|
||||
}
|
||||
if (result.isSuccess) {
|
||||
result.get
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
204
knockoutwhistweb/app/logic/user/impl/HibernateUserManager.scala
Normal file
204
knockoutwhistweb/app/logic/user/impl/HibernateUserManager.scala
Normal file
@@ -0,0 +1,204 @@
|
||||
package logic.user.impl
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import jakarta.inject.Inject
|
||||
import jakarta.persistence.EntityManager
|
||||
import logic.user.UserManager
|
||||
import model.users.{User, UserEntity}
|
||||
import play.api.Logger
|
||||
import services.OpenIDUserInfo
|
||||
import util.UserHash
|
||||
|
||||
import javax.inject.Singleton
|
||||
import scala.jdk.CollectionConverters.*
|
||||
|
||||
@Singleton
|
||||
class HibernateUserManager @Inject()(em: EntityManager, config: Config) extends UserManager {
|
||||
|
||||
private val logger = Logger(getClass.getName)
|
||||
|
||||
override def addUser(name: String, password: String): Boolean = {
|
||||
val tx = em.getTransaction
|
||||
try {
|
||||
tx.begin()
|
||||
// Check if user already exists
|
||||
val existing = em.createQuery("SELECT u FROM UserEntity u WHERE u.username = :username", classOf[UserEntity])
|
||||
.setParameter("username", name)
|
||||
.getResultList
|
||||
|
||||
if (!existing.isEmpty) {
|
||||
logger.warn(s"User $name already exists")
|
||||
tx.rollback()
|
||||
return false
|
||||
}
|
||||
|
||||
// Create new user
|
||||
val userEntity = UserEntity.fromUser(User(
|
||||
internalId = 0L, // Will be set by database
|
||||
id = java.util.UUID.randomUUID(),
|
||||
name = name,
|
||||
passwordHash = UserHash.hashPW(password)
|
||||
))
|
||||
|
||||
em.persist(userEntity)
|
||||
em.flush()
|
||||
tx.commit()
|
||||
|
||||
true
|
||||
} catch {
|
||||
case e: Exception => {
|
||||
if (tx.isActive) tx.rollback()
|
||||
logger.error(s"Error adding user $name", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def addOpenIDUser(name: String, userInfo: OpenIDUserInfo): Boolean = {
|
||||
val tx = em.getTransaction
|
||||
try {
|
||||
tx.begin()
|
||||
// Check if user already exists
|
||||
val existing = em.createQuery("SELECT u FROM UserEntity u WHERE u.username = :username", classOf[UserEntity])
|
||||
.setParameter("username", name)
|
||||
.getResultList
|
||||
|
||||
if (!existing.isEmpty) {
|
||||
logger.warn(s"User $name already exists")
|
||||
tx.rollback()
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if OpenID user already exists
|
||||
val existingOpenID = em.createQuery(
|
||||
"SELECT u FROM UserEntity u WHERE u.openidProvider = :provider AND u.openidProviderId = :providerId",
|
||||
classOf[UserEntity]
|
||||
)
|
||||
.setParameter("provider", userInfo.provider)
|
||||
.setParameter("providerId", userInfo.id)
|
||||
.getResultList
|
||||
|
||||
if (!existingOpenID.isEmpty) {
|
||||
logger.warn(s"OpenID user ${userInfo.provider}_${userInfo.id} already exists")
|
||||
tx.rollback()
|
||||
return false
|
||||
}
|
||||
|
||||
// Create new OpenID user
|
||||
val userEntity = UserEntity.fromOpenIDUser(name, userInfo)
|
||||
|
||||
em.persist(userEntity)
|
||||
em.flush()
|
||||
tx.commit()
|
||||
true
|
||||
} catch {
|
||||
case e: Exception => {
|
||||
if (tx.isActive) tx.rollback()
|
||||
logger.error(s"Error adding OpenID user ${userInfo.provider}_${userInfo.id}", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def authenticate(name: String, password: String): Option[User] = {
|
||||
try {
|
||||
val users = em.createQuery("SELECT u FROM UserEntity u WHERE u.username = :username", classOf[UserEntity])
|
||||
.setParameter("username", name)
|
||||
.getResultList
|
||||
|
||||
if (users.isEmpty) {
|
||||
return None
|
||||
}
|
||||
|
||||
val userEntity = users.get(0)
|
||||
if (UserHash.verifyUser(password, userEntity.toUser)) {
|
||||
Some(userEntity.toUser)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} catch {
|
||||
case e: Exception => {
|
||||
logger.error(s"Error authenticating user $name", e)
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def authenticateOpenID(provider: String, providerId: String): Option[User] = {
|
||||
try {
|
||||
val users = em.createQuery(
|
||||
"SELECT u FROM UserEntity u WHERE u.openidProvider = :provider AND u.openidProviderId = :providerId",
|
||||
classOf[UserEntity]
|
||||
)
|
||||
.setParameter("provider", provider)
|
||||
.setParameter("providerId", providerId)
|
||||
.getResultList
|
||||
|
||||
if (users.isEmpty) {
|
||||
None
|
||||
} else {
|
||||
Some(users.get(0).toUser)
|
||||
}
|
||||
} catch {
|
||||
case e: Exception => {
|
||||
logger.error(s"Error authenticating OpenID user ${provider}_$providerId", e)
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def userExists(name: String): Option[User] = {
|
||||
try {
|
||||
val users = em.createQuery("SELECT u FROM UserEntity u WHERE u.username = :username", classOf[UserEntity])
|
||||
.setParameter("username", name)
|
||||
.getResultList
|
||||
|
||||
if (users.isEmpty) {
|
||||
None
|
||||
} else {
|
||||
Some(users.get(0).toUser)
|
||||
}
|
||||
} catch {
|
||||
case e: Exception => {
|
||||
logger.error(s"Error checking if user $name exists", e)
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def userExistsById(id: Long): Option[User] = {
|
||||
try {
|
||||
Option(em.find(classOf[UserEntity], id)).map(_.toUser)
|
||||
} catch {
|
||||
case e: Exception => {
|
||||
logger.error(s"Error checking if user with ID $id exists", e)
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def removeUser(name: String): Boolean = {
|
||||
val tx = em.getTransaction
|
||||
try {
|
||||
tx.begin()
|
||||
val users = em.createQuery("SELECT u FROM UserEntity u WHERE u.username = :username", classOf[UserEntity])
|
||||
.setParameter("username", name)
|
||||
.getResultList
|
||||
|
||||
if (users.isEmpty) {
|
||||
tx.rollback()
|
||||
false
|
||||
} else {
|
||||
em.remove(users.get(0))
|
||||
em.flush()
|
||||
tx.commit()
|
||||
true
|
||||
}
|
||||
} catch {
|
||||
case _: Exception => {
|
||||
if (tx.isActive) tx.rollback()
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,16 @@ package logic.user.impl
|
||||
import com.typesafe.config.Config
|
||||
import logic.user.UserManager
|
||||
import model.users.User
|
||||
import services.OpenIDUserInfo
|
||||
import util.UserHash
|
||||
|
||||
import javax.inject.{Inject, Singleton}
|
||||
import scala.collection.mutable
|
||||
|
||||
@Singleton
|
||||
class StubUserManager @Inject()(val config: Config) extends UserManager {
|
||||
|
||||
private val user: Map[String, User] = Map(
|
||||
class StubUserManager @Inject()(config: Config) extends UserManager {
|
||||
|
||||
private val user: mutable.Map[String, User] = mutable.Map(
|
||||
"Janis" -> User(
|
||||
internalId = 1L,
|
||||
id = java.util.UUID.fromString("123e4567-e89b-12d3-a456-426614174000"),
|
||||
@@ -19,14 +21,39 @@ class StubUserManager @Inject()(val config: Config) extends UserManager {
|
||||
),
|
||||
"Leon" -> User(
|
||||
internalId = 2L,
|
||||
id = java.util.UUID.fromString("223e4567-e89b-12d3-a456-426614174000"),
|
||||
name = "Leon",
|
||||
id = java.util.UUID.randomUUID(),
|
||||
name = "Jakob",
|
||||
passwordHash = UserHash.hashPW("password123")
|
||||
),
|
||||
"Jakob" -> User(
|
||||
internalId = 2L,
|
||||
id = java.util.UUID.fromString("323e4567-e89b-12d3-a456-426614174000"),
|
||||
name = "Jakob",
|
||||
passwordHash = UserHash.hashPW("password123")
|
||||
)
|
||||
)
|
||||
|
||||
override def addUser(name: String, password: String): Boolean = {
|
||||
throw new NotImplementedError("StubUserManager.addUser is not implemented")
|
||||
val newUser = User(
|
||||
internalId = user.size.toLong + 1,
|
||||
id = java.util.UUID.randomUUID(),
|
||||
name = name,
|
||||
passwordHash = UserHash.hashPW(password)
|
||||
)
|
||||
user(name) = newUser
|
||||
true
|
||||
}
|
||||
|
||||
override def addOpenIDUser(name: String, userInfo: OpenIDUserInfo): Boolean = {
|
||||
// For stub implementation, just add a user without password
|
||||
val newUser = User(
|
||||
internalId = user.size.toLong + 1,
|
||||
id = java.util.UUID.randomUUID(),
|
||||
name = name,
|
||||
passwordHash = "" // No password for OpenID users
|
||||
)
|
||||
user(name) = newUser
|
||||
true
|
||||
}
|
||||
|
||||
override def authenticate(name: String, password: String): Option[User] = {
|
||||
@@ -36,6 +63,13 @@ class StubUserManager @Inject()(val config: Config) extends UserManager {
|
||||
}
|
||||
}
|
||||
|
||||
override def authenticateOpenID(provider: String, providerId: String): Option[User] = {
|
||||
user.values.find { u =>
|
||||
// In a real implementation, this would check stored OpenID provider info
|
||||
u.name.startsWith(s"${provider}_") && u.name.contains(providerId)
|
||||
}
|
||||
}
|
||||
|
||||
override def userExists(name: String): Option[User] = {
|
||||
user.get(name)
|
||||
}
|
||||
@@ -45,7 +79,6 @@ class StubUserManager @Inject()(val config: Config) extends UserManager {
|
||||
}
|
||||
|
||||
override def removeUser(name: String): Boolean = {
|
||||
throw new NotImplementedError("StubUserManager.removeUser is not implemented")
|
||||
user.remove(name).isDefined
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package model.sessions
|
||||
|
||||
enum InteractionType {
|
||||
|
||||
|
||||
case TrumpSuit
|
||||
case Card
|
||||
case DogCard
|
||||
|
||||
@@ -5,9 +5,11 @@ import de.knockoutwhist.utils.events.SimpleEvent
|
||||
import java.util.UUID
|
||||
|
||||
trait PlayerSession {
|
||||
|
||||
|
||||
def id: UUID
|
||||
|
||||
def name: String
|
||||
|
||||
def updatePlayer(event: SimpleEvent): Unit
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import de.knockoutwhist.utils.events.SimpleEvent
|
||||
import java.util.UUID
|
||||
|
||||
case class SimpleSession(id: UUID, player: AbstractPlayer) extends PlayerSession {
|
||||
|
||||
|
||||
def name: String = player.name
|
||||
|
||||
|
||||
override def updatePlayer(event: SimpleEvent): Unit = {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,18 @@ package model.sessions
|
||||
|
||||
import de.knockoutwhist.events.player.{RequestCardEvent, RequestTieChoiceEvent, RequestTrumpSuitEvent}
|
||||
import de.knockoutwhist.utils.events.SimpleEvent
|
||||
import logic.game.GameLobby
|
||||
import model.users.User
|
||||
import play.api.libs.json.{JsObject, JsValue}
|
||||
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.locks.{Lock, ReentrantLock}
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import scala.util.Try
|
||||
|
||||
class UserSession(user: User, val host: Boolean) extends PlayerSession {
|
||||
var canInteract: Option[InteractionType] = None
|
||||
class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) extends PlayerSession {
|
||||
val lock: ReentrantLock = ReentrantLock()
|
||||
var canInteract: Option[InteractionType] = None
|
||||
var websocketActor: Option[UserWebsocketActor] = None
|
||||
|
||||
override def updatePlayer(event: SimpleEvent): Unit = {
|
||||
event match {
|
||||
@@ -22,14 +26,81 @@ class UserSession(user: User, val host: Boolean) extends PlayerSession {
|
||||
else canInteract = Some(InteractionType.Card)
|
||||
case _ =>
|
||||
}
|
||||
|
||||
lock.lock()
|
||||
websocketActor.foreach(_.solveRequests())
|
||||
websocketActor.foreach(_.transmitEventToClient(event))
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
override def id: UUID = user.id
|
||||
|
||||
override def name: String = user.name
|
||||
|
||||
|
||||
def resetCanInteract(): Unit = {
|
||||
canInteract = None
|
||||
}
|
||||
|
||||
|
||||
def handleWebResponse(eventType: String, data: JsObject): Unit = {
|
||||
eventType match {
|
||||
case "ping" =>
|
||||
// No action needed for Ping
|
||||
()
|
||||
case "StartGame" =>
|
||||
gameLobby.startGame(user)
|
||||
case "PlayCard" =>
|
||||
val maybeCardIndex: Option[Int] = (data \ "cardindex").asOpt[Int]
|
||||
maybeCardIndex match {
|
||||
case Some(index) =>
|
||||
val session = gameLobby.getUserSession(user.id)
|
||||
gameLobby.playCard(session, index)
|
||||
case None =>
|
||||
println("Card Index not found or is not a number." + data)
|
||||
}
|
||||
case "PlayDogCard" =>
|
||||
val maybeCardIndex: Option[Int] = (data \ "cardindex").asOpt[Int]
|
||||
maybeCardIndex match {
|
||||
case Some(index) =>
|
||||
val session = gameLobby.getUserSession(user.id)
|
||||
gameLobby.playDogCard(session, index)
|
||||
case None =>
|
||||
val session = gameLobby.getUserSession(user.id)
|
||||
gameLobby.playDogCard(session, -1)
|
||||
}
|
||||
case "PickTrumpsuit" =>
|
||||
val maybeSuitIndex: Option[Int] = (data \ "suitIndex").asOpt[Int]
|
||||
maybeSuitIndex match {
|
||||
case Some(index) =>
|
||||
val session = gameLobby.getUserSession(user.id)
|
||||
gameLobby.selectTrump(session, index)
|
||||
case None =>
|
||||
println("Card Index not found or is not a number.")
|
||||
}
|
||||
case "PickTie" =>
|
||||
val maybeCardIndex: Option[Int] = (data \ "cardIndex").asOpt[Int]
|
||||
maybeCardIndex match {
|
||||
case Some(index) =>
|
||||
val session = gameLobby.getUserSession(user.id)
|
||||
gameLobby.selectTie(session, index)
|
||||
case None =>
|
||||
println("Card Index not found or is not a number.")
|
||||
}
|
||||
case "KickPlayer" =>
|
||||
val maybePlayerId: Option[String] = (data \ "playerId").asOpt[String]
|
||||
maybePlayerId match {
|
||||
case Some(id) =>
|
||||
val playerUUID = UUID.fromString(id)
|
||||
gameLobby.leaveGame(playerUUID, true)
|
||||
case None =>
|
||||
println("Player ID not found or is not a valid UUID.")
|
||||
}
|
||||
case "ReturnToLobby" =>
|
||||
gameLobby.returnToLobby(this)
|
||||
case "LeaveGame" =>
|
||||
gameLobby.leaveGame(user.id, false)
|
||||
case _ =>
|
||||
println("Unknown event type: " + eventType + " with data: " + data)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
133
knockoutwhistweb/app/model/sessions/UserWebsocketActor.scala
Normal file
133
knockoutwhistweb/app/model/sessions/UserWebsocketActor.scala
Normal file
@@ -0,0 +1,133 @@
|
||||
package model.sessions
|
||||
|
||||
import de.knockoutwhist.utils.events.SimpleEvent
|
||||
import org.apache.pekko.actor.{Actor, ActorRef}
|
||||
import play.api.libs.json.{JsObject, JsValue, Json}
|
||||
import util.WebsocketEventMapper
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
class UserWebsocketActor(
|
||||
out: ActorRef,
|
||||
session: UserSession
|
||||
) extends Actor {
|
||||
|
||||
private val requests: mutable.Map[String, String] = mutable.Map()
|
||||
|
||||
{
|
||||
session.lock.lock()
|
||||
if (session.websocketActor.isDefined) {
|
||||
val otherWebsocket = session.websocketActor.get
|
||||
otherWebsocket.transmitTextToClient("Error: Multiple websocket connections detected. Closing your connection.")
|
||||
context.stop(otherWebsocket.self)
|
||||
transmitTextToClient("Previous websocket connection closed. You are now connected.")
|
||||
}
|
||||
session.websocketActor = Some(this)
|
||||
session.lock.unlock()
|
||||
}
|
||||
|
||||
|
||||
override def receive: Receive = {
|
||||
case msg: String =>
|
||||
val jsonObject = Try {
|
||||
Json.parse(msg)
|
||||
}
|
||||
Try {
|
||||
jsonObject match {
|
||||
case Success(value) =>
|
||||
handle(value)
|
||||
case Failure(exception) =>
|
||||
transmitTextToClient(s"Error parsing JSON: ${exception.getMessage}")
|
||||
}
|
||||
}.failed.foreach(
|
||||
ex => transmitTextToClient(s"Error handling message: ${ex.getMessage}")
|
||||
)
|
||||
case other =>
|
||||
}
|
||||
|
||||
private def transmitTextToClient(text: String): Unit = {
|
||||
out ! text
|
||||
}
|
||||
|
||||
private def handle(json: JsValue): Unit = {
|
||||
session.lock.lock()
|
||||
val idOpt = (json \ "id").asOpt[String]
|
||||
if (idOpt.isEmpty) {
|
||||
transmitJsonToClient(Json.obj(
|
||||
"status" -> "error",
|
||||
"error" -> "Missing 'id' field"
|
||||
))
|
||||
session.lock.unlock()
|
||||
return
|
||||
}
|
||||
val id = idOpt.get
|
||||
val eventOpt = (json \ "event").asOpt[String]
|
||||
if (eventOpt.isEmpty) {
|
||||
transmitJsonToClient(Json.obj(
|
||||
"id" -> id,
|
||||
"event" -> null,
|
||||
"status" -> "error",
|
||||
"error" -> "Missing 'event' field"
|
||||
))
|
||||
session.lock.unlock()
|
||||
return
|
||||
}
|
||||
val statusOpt = (json \ "status").asOpt[String]
|
||||
if (statusOpt.isDefined) {
|
||||
session.lock.unlock()
|
||||
return
|
||||
}
|
||||
val event = eventOpt.get
|
||||
val data = (json \ "data").asOpt[JsObject].getOrElse(Json.obj())
|
||||
requests += (id -> event)
|
||||
val result = Try {
|
||||
session.handleWebResponse(event, data)
|
||||
}
|
||||
if (!requests.contains(id)) {
|
||||
session.lock.unlock()
|
||||
return
|
||||
}
|
||||
requests -= id
|
||||
if (result.isSuccess) {
|
||||
transmitJsonToClient(Json.obj(
|
||||
"id" -> id,
|
||||
"event" -> event,
|
||||
"status" -> "success"
|
||||
))
|
||||
} else {
|
||||
transmitJsonToClient(Json.obj(
|
||||
"id" -> id,
|
||||
"event" -> event,
|
||||
"status" -> "error",
|
||||
"error" -> result.failed.get.getMessage
|
||||
))
|
||||
}
|
||||
session.lock.unlock()
|
||||
}
|
||||
|
||||
def transmitJsonToClient(jsonObj: JsValue): Unit = {
|
||||
transmitTextToClient(jsonObj.toString())
|
||||
}
|
||||
|
||||
def transmitEventToClient(event: SimpleEvent): Unit = {
|
||||
transmitJsonToClient(WebsocketEventMapper.toJson(event, session))
|
||||
}
|
||||
|
||||
def solveRequests(): Unit = {
|
||||
if (!session.lock.isHeldByCurrentThread)
|
||||
return;
|
||||
if (requests.isEmpty)
|
||||
return;
|
||||
val pendingRequests = requests.toMap
|
||||
requests.clear()
|
||||
pendingRequests.foreach { case (id, event) =>
|
||||
transmitJsonToClient(Json.obj(
|
||||
"id" -> id,
|
||||
"event" -> event,
|
||||
"status" -> "success"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
package model.users
|
||||
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
case class User(
|
||||
internalId: Long,
|
||||
id: UUID,
|
||||
name: String,
|
||||
passwordHash: String
|
||||
internalId: Long,
|
||||
id: UUID,
|
||||
name: String,
|
||||
passwordHash: String
|
||||
) {
|
||||
|
||||
def withName(newName: String): User = {
|
||||
@@ -16,5 +17,4 @@ case class User(
|
||||
private def withPasswordHash(newPasswordHash: String): User = {
|
||||
this.copy(passwordHash = newPasswordHash)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
80
knockoutwhistweb/app/model/users/UserEntity.scala
Normal file
80
knockoutwhistweb/app/model/users/UserEntity.scala
Normal file
@@ -0,0 +1,80 @@
|
||||
package model.users
|
||||
|
||||
import jakarta.persistence.*
|
||||
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import scala.compiletime.uninitialized
|
||||
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
class UserEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long = uninitialized
|
||||
|
||||
@Column(name = "uuid", nullable = false, unique = true)
|
||||
var uuid: UUID = uninitialized
|
||||
|
||||
@Column(name = "username", nullable = false, unique = true)
|
||||
var username: String = uninitialized
|
||||
|
||||
@Column(name = "password_hash", nullable = false)
|
||||
var passwordHash: String = uninitialized
|
||||
|
||||
@Column(name = "openid_provider")
|
||||
var openidProvider: String = uninitialized
|
||||
|
||||
@Column(name = "openid_provider_id")
|
||||
var openidProviderId: String = uninitialized
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
var createdAt: LocalDateTime = uninitialized
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
var updatedAt: LocalDateTime = uninitialized
|
||||
|
||||
@PrePersist
|
||||
def onCreate(): Unit = {
|
||||
val now = LocalDateTime.now()
|
||||
createdAt = now
|
||||
updatedAt = now
|
||||
if (uuid == null) {
|
||||
uuid = UUID.randomUUID()
|
||||
}
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
def onUpdate(): Unit = {
|
||||
updatedAt = LocalDateTime.now()
|
||||
}
|
||||
|
||||
def toUser: User = {
|
||||
User(
|
||||
internalId = id,
|
||||
id = uuid,
|
||||
name = username,
|
||||
passwordHash = passwordHash
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object UserEntity {
|
||||
def fromUser(user: User): UserEntity = {
|
||||
val entity = new UserEntity()
|
||||
entity.uuid = user.id
|
||||
entity.username = user.name
|
||||
entity.passwordHash = user.passwordHash
|
||||
entity
|
||||
}
|
||||
|
||||
def fromOpenIDUser(username: String, userInfo: services.OpenIDUserInfo): UserEntity = {
|
||||
val entity = new UserEntity()
|
||||
entity.username = username
|
||||
entity.passwordHash = "" // No password for OpenID users
|
||||
entity.openidProvider = userInfo.provider
|
||||
entity.openidProviderId = userInfo.id
|
||||
entity
|
||||
}
|
||||
}
|
||||
24
knockoutwhistweb/app/modules/GatewayModule.scala
Normal file
24
knockoutwhistweb/app/modules/GatewayModule.scala
Normal file
@@ -0,0 +1,24 @@
|
||||
package modules
|
||||
|
||||
import com.google.inject.AbstractModule
|
||||
import di.EntityManagerProvider
|
||||
import jakarta.persistence.EntityManager
|
||||
import logic.Gateway
|
||||
import logic.user.UserManager
|
||||
import logic.user.impl.HibernateUserManager
|
||||
|
||||
class GatewayModule extends AbstractModule {
|
||||
override def configure(): Unit = {
|
||||
bind(classOf[Gateway]).asEagerSingleton()
|
||||
|
||||
// Bind HibernateUserManager for production (when GatewayModule is used)
|
||||
bind(classOf[UserManager])
|
||||
.to(classOf[HibernateUserManager])
|
||||
.asEagerSingleton()
|
||||
|
||||
// Bind EntityManager for JPA
|
||||
bind(classOf[EntityManager])
|
||||
.toProvider(classOf[EntityManagerProvider])
|
||||
.asEagerSingleton()
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,28 @@ import javax.inject.*
|
||||
@Singleton
|
||||
class JwtKeyProvider @Inject()(config: Configuration) {
|
||||
|
||||
private def cleanPem(pem: String): String =
|
||||
pem.replaceAll("-----BEGIN (.*)-----", "")
|
||||
.replaceAll("-----END (.*)-----", "")
|
||||
.replaceAll("\\s", "")
|
||||
val publicKey: RSAPublicKey = {
|
||||
val pemOpt = config.getOptional[String]("auth.publicKeyPem")
|
||||
val fileOpt = config.getOptional[String]("auth.publicKeyFile")
|
||||
|
||||
pemOpt.orElse(fileOpt.map { path =>
|
||||
new String(Files.readAllBytes(Paths.get(path)))
|
||||
}) match {
|
||||
case Some(pem) => loadPublicKeyFromPem(pem)
|
||||
case None => throw new RuntimeException("No RSA public key configured.")
|
||||
}
|
||||
}
|
||||
val privateKey: RSAPrivateKey = {
|
||||
val pemOpt = config.getOptional[String]("auth.privateKeyPem")
|
||||
val fileOpt = config.getOptional[String]("auth.privateKeyFile")
|
||||
|
||||
pemOpt.orElse(fileOpt.map { path =>
|
||||
new String(Files.readAllBytes(Paths.get(path)))
|
||||
}) match {
|
||||
case Some(pem) => loadPrivateKeyFromPem(pem)
|
||||
case None => throw new RuntimeException("No RSA private key configured.")
|
||||
}
|
||||
}
|
||||
|
||||
private def loadPublicKeyFromPem(pem: String): RSAPublicKey = {
|
||||
val decoded = Base64.getDecoder.decode(cleanPem(pem))
|
||||
@@ -29,28 +47,9 @@ class JwtKeyProvider @Inject()(config: Configuration) {
|
||||
KeyFactory.getInstance("RSA").generatePrivate(spec).asInstanceOf[RSAPrivateKey]
|
||||
}
|
||||
|
||||
val publicKey: RSAPublicKey = {
|
||||
val pemOpt = config.getOptional[String]("auth.publicKeyPem")
|
||||
val fileOpt = config.getOptional[String]("auth.publicKeyFile")
|
||||
private def cleanPem(pem: String): String =
|
||||
pem.replaceAll("-----BEGIN (.*)-----", "")
|
||||
.replaceAll("-----END (.*)-----", "")
|
||||
.replaceAll("\\s", "")
|
||||
|
||||
pemOpt.orElse(fileOpt.map { path =>
|
||||
new String(Files.readAllBytes(Paths.get(path)))
|
||||
}) match {
|
||||
case Some(pem) => loadPublicKeyFromPem(pem)
|
||||
case None => throw new RuntimeException("No RSA public key configured.")
|
||||
}
|
||||
}
|
||||
|
||||
val privateKey: RSAPrivateKey = {
|
||||
val pemOpt = config.getOptional[String]("auth.privateKeyPem")
|
||||
val fileOpt = config.getOptional[String]("auth.privateKeyFile")
|
||||
|
||||
pemOpt.orElse(fileOpt.map { path =>
|
||||
new String(Files.readAllBytes(Paths.get(path)))
|
||||
}) match {
|
||||
case Some(pem) => loadPrivateKeyFromPem(pem)
|
||||
case None => throw new RuntimeException("No RSA private key configured.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
89
knockoutwhistweb/app/services/OAuthCacheService.scala
Normal file
89
knockoutwhistweb/app/services/OAuthCacheService.scala
Normal file
@@ -0,0 +1,89 @@
|
||||
package services
|
||||
|
||||
import org.redisson.Redisson
|
||||
import org.redisson.api.RMapCache
|
||||
import org.redisson.config.Config
|
||||
import play.api.Logger
|
||||
import play.api.libs.json.Json
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.*
|
||||
|
||||
@Singleton
|
||||
class OAuthCacheService @Inject()() {
|
||||
|
||||
private val logger = Logger(this.getClass)
|
||||
|
||||
// Initialize Redis connection similar to Gateway
|
||||
private val redis = {
|
||||
val config: Config = Config()
|
||||
val url = "redis://" + sys.env.getOrElse("REDIS_HOST", "localhost") + ":" + sys.env.getOrElse("REDIS_PORT", "6379")
|
||||
logger.info(s"OAuthCacheService connecting to Redis at $url")
|
||||
config.useSingleServer.setAddress(url)
|
||||
Redisson.create(config)
|
||||
}
|
||||
|
||||
// Cache for OAuth data with 30 minute TTL
|
||||
private val oauthCache: RMapCache[String, String] = redis.getMapCache("oauth_cache")
|
||||
|
||||
/**
|
||||
* Store OAuth data with random ID and return the ID
|
||||
*/
|
||||
def storeOAuthData(userInfo: OpenIDUserInfo, accessToken: String, provider: String): String = {
|
||||
val sessionId = java.util.UUID.randomUUID().toString
|
||||
|
||||
val oauthData = Json.obj(
|
||||
"userInfo" -> Json.toJson(userInfo),
|
||||
"accessToken" -> accessToken,
|
||||
"provider" -> provider,
|
||||
"timestamp" -> System.currentTimeMillis()
|
||||
).toString()
|
||||
|
||||
// Store with 30 minute TTL
|
||||
oauthCache.put(sessionId, oauthData, 30, TimeUnit.MINUTES)
|
||||
logger.info(s"Stored OAuth data for session $sessionId")
|
||||
|
||||
sessionId
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve OAuth data by session ID
|
||||
*/
|
||||
def getOAuthData(sessionId: String): Option[(OpenIDUserInfo, String, String)] = {
|
||||
Option(oauthCache.get(sessionId)) match {
|
||||
case Some(dataJson) =>
|
||||
try {
|
||||
val json = Json.parse(dataJson)
|
||||
val userInfo = (json \ "userInfo").as[OpenIDUserInfo]
|
||||
val accessToken = (json \ "accessToken").as[String]
|
||||
val provider = (json \ "provider").as[String]
|
||||
|
||||
logger.info(s"Retrieved OAuth data for session $sessionId")
|
||||
Some((userInfo, accessToken, provider))
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
logger.error(s"Failed to parse OAuth data for session $sessionId: ${e.getMessage}")
|
||||
None
|
||||
}
|
||||
case None =>
|
||||
logger.warn(s"No OAuth data found for session $sessionId")
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove OAuth data after use
|
||||
*/
|
||||
def removeOAuthData(sessionId: String): Unit = {
|
||||
oauthCache.remove(sessionId)
|
||||
logger.info(s"Removed OAuth data for session $sessionId")
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up expired sessions (optional maintenance)
|
||||
*/
|
||||
def cleanupExpiredSessions(): Unit = {
|
||||
oauthCache.clear()
|
||||
logger.info("Cleaned up expired OAuth sessions")
|
||||
}
|
||||
}
|
||||
173
knockoutwhistweb/app/services/OpenIDConnectService.scala
Normal file
173
knockoutwhistweb/app/services/OpenIDConnectService.scala
Normal file
@@ -0,0 +1,173 @@
|
||||
package services
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import play.api.libs.ws.WSClient
|
||||
import play.api.{Configuration, Logger}
|
||||
import play.api.libs.json.*
|
||||
|
||||
import java.net.URI
|
||||
import javax.inject.*
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import com.nimbusds.oauth2.sdk.*
|
||||
import com.nimbusds.oauth2.sdk.id.*
|
||||
import com.nimbusds.openid.connect.sdk.*
|
||||
import play.api.libs.ws.DefaultBodyWritables.*
|
||||
|
||||
case class OpenIDUserInfo(
|
||||
id: String,
|
||||
email: Option[String],
|
||||
name: Option[String],
|
||||
picture: Option[String],
|
||||
provider: String,
|
||||
providerName: String
|
||||
)
|
||||
|
||||
object OpenIDUserInfo {
|
||||
implicit val writes: Writes[OpenIDUserInfo] = Json.writes[OpenIDUserInfo]
|
||||
implicit val reads: Reads[OpenIDUserInfo] = Json.reads[OpenIDUserInfo]
|
||||
}
|
||||
|
||||
case class OpenIDProvider(
|
||||
name: String,
|
||||
clientId: String,
|
||||
clientSecret: String,
|
||||
redirectUri: String,
|
||||
authorizationEndpoint: String,
|
||||
tokenEndpoint: String,
|
||||
userInfoEndpoint: String,
|
||||
scopes: Set[String] = Set("openid", "profile", "email"),
|
||||
idClaimName: String = "id"
|
||||
)
|
||||
|
||||
case class TokenResponse(
|
||||
accessToken: String,
|
||||
tokenType: String,
|
||||
expiresIn: Option[Int],
|
||||
refreshToken: Option[String],
|
||||
idToken: Option[String]
|
||||
)
|
||||
|
||||
@Singleton
|
||||
class OpenIDConnectService@Inject(ws: WSClient, config: Configuration)(implicit ec: ExecutionContext) {
|
||||
|
||||
private val logger = Logger(this.getClass)
|
||||
|
||||
private val providers = Map(
|
||||
"discord" -> OpenIDProvider(
|
||||
name = "Discord",
|
||||
clientId = config.get[String]("openid.discord.clientId"),
|
||||
clientSecret = config.get[String]("openid.discord.clientSecret"),
|
||||
redirectUri = config.get[String]("openid.discord.redirectUri"),
|
||||
authorizationEndpoint = "https://discord.com/oauth2/authorize",
|
||||
tokenEndpoint = "https://discord.com/api/oauth2/token",
|
||||
userInfoEndpoint = "https://discord.com/api/users/@me",
|
||||
scopes = Set("identify", "email")
|
||||
),
|
||||
"keycloak" -> OpenIDProvider(
|
||||
name = "Identity",
|
||||
clientId = config.get[String]("openid.keycloak.clientId"),
|
||||
clientSecret = config.get[String]("openid.keycloak.clientSecret"),
|
||||
redirectUri = config.get[String]("openid.keycloak.redirectUri"),
|
||||
authorizationEndpoint = config.get[String]("openid.keycloak.authUrl") + "/protocol/openid-connect/auth",
|
||||
tokenEndpoint = config.get[String]("openid.keycloak.authUrl") + "/protocol/openid-connect/token",
|
||||
userInfoEndpoint = config.get[String]("openid.keycloak.authUrl") + "/protocol/openid-connect/userinfo",
|
||||
scopes = Set("openid", "profile", "email"),
|
||||
idClaimName = "sub"
|
||||
)
|
||||
)
|
||||
|
||||
def getAuthorizationUrl(providerName: String, state: String, nonce: String): Option[String] = {
|
||||
providers.get(providerName).map { provider =>
|
||||
val authRequest = new AuthorizationRequest.Builder(
|
||||
new ResponseType(ResponseType.Value.CODE),
|
||||
new com.nimbusds.oauth2.sdk.id.ClientID(provider.clientId)
|
||||
)
|
||||
.scope(new com.nimbusds.oauth2.sdk.Scope(provider.scopes.mkString(" ")))
|
||||
.state(new com.nimbusds.oauth2.sdk.id.State(state))
|
||||
.redirectionURI(URI.create(provider.redirectUri))
|
||||
.endpointURI(URI.create(provider.authorizationEndpoint))
|
||||
.build()
|
||||
|
||||
authRequest.toURI.toString
|
||||
}
|
||||
}
|
||||
|
||||
def exchangeCodeForTokens(providerName: String, code: String, state: String): Future[Option[TokenResponse]] = {
|
||||
providers.get(providerName) match {
|
||||
case Some(provider) =>
|
||||
ws.url(provider.tokenEndpoint)
|
||||
.withHttpHeaders(
|
||||
"Accept" -> "application/json",
|
||||
"Content-Type" -> "application/x-www-form-urlencoded"
|
||||
)
|
||||
.post(
|
||||
Map(
|
||||
"client_id" -> Seq(provider.clientId),
|
||||
"client_secret" -> Seq(provider.clientSecret),
|
||||
"code" -> Seq(code),
|
||||
"grant_type" -> Seq("authorization_code"),
|
||||
"redirect_uri" -> Seq(provider.redirectUri)
|
||||
)
|
||||
)
|
||||
.map { response =>
|
||||
if (response.status == 200) {
|
||||
val json = response.json
|
||||
Some(TokenResponse(
|
||||
accessToken = (json \ "access_token").as[String],
|
||||
tokenType = (json \ "token_type").as[String],
|
||||
expiresIn = (json \ "expires_in").asOpt[Int],
|
||||
refreshToken = (json \ "refresh_token").asOpt[String],
|
||||
idToken = (json \ "id_token").asOpt[String]
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
.recover { case _ => None }
|
||||
case None => Future.successful(None)
|
||||
}
|
||||
}
|
||||
|
||||
def getUserInfo(providerName: String, accessToken: String): Future[Option[OpenIDUserInfo]] = {
|
||||
providers.get(providerName) match {
|
||||
case Some(provider) =>
|
||||
ws.url(provider.userInfoEndpoint)
|
||||
.withHttpHeaders("Authorization" -> s"Bearer $accessToken")
|
||||
.get()
|
||||
.map { response =>
|
||||
if (response.status == 200) {
|
||||
val json = response.json
|
||||
Some(OpenIDUserInfo(
|
||||
id = (json \ provider.idClaimName).as[String],
|
||||
email = (json \ "email").asOpt[String],
|
||||
name = (json \ "name").asOpt[String].orElse((json \ "login").asOpt[String]),
|
||||
picture = (json \ "picture").asOpt[String].orElse((json \ "avatar_url").asOpt[String]),
|
||||
provider = providerName,
|
||||
providerName = provider.name
|
||||
))
|
||||
} else {
|
||||
logger.error(s"Failed to retrieve user info from ${provider.userInfoEndpoint}, status code ${response.status}")
|
||||
None
|
||||
}
|
||||
}
|
||||
.recover { case e => {
|
||||
logger.error(s"Failed to retrieve user info from ${provider.userInfoEndpoint}", e)
|
||||
None
|
||||
}
|
||||
}
|
||||
case None => Future.successful(None)
|
||||
}
|
||||
}
|
||||
|
||||
def validateState(sessionState: String, returnedState: String): Boolean = {
|
||||
sessionState == returnedState
|
||||
}
|
||||
|
||||
def generateState(): String = {
|
||||
java.util.UUID.randomUUID().toString.replace("-", "")
|
||||
}
|
||||
|
||||
def generateNonce(): String = {
|
||||
java.util.UUID.randomUUID().toString.replace("-", "")
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package util
|
||||
|
||||
import de.knockoutwhist.control.GameState
|
||||
import de.knockoutwhist.control.GameState.{FinishedMatch, InGame, Lobby, MainMenu, SelectTrump, TieBreak}
|
||||
|
||||
import scala.util.Random
|
||||
|
||||
object GameUtil {
|
||||
@@ -26,4 +29,15 @@ object GameUtil {
|
||||
code.toString()
|
||||
}
|
||||
|
||||
def stateToTitle(gameState: GameState): String = {
|
||||
gameState match {
|
||||
case Lobby => "Lobby"
|
||||
case MainMenu => "Main Menu"
|
||||
case InGame => "In Game"
|
||||
case SelectTrump => "Select Trump"
|
||||
case TieBreak => "Tie Break"
|
||||
case FinishedMatch => "Finished Match"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
package util
|
||||
|
||||
import de.knockoutwhist.cards.Card
|
||||
import de.knockoutwhist.cards.CardValue.*
|
||||
import de.knockoutwhist.cards.Suit.{Clubs, Diamonds, Hearts, Spades}
|
||||
import de.knockoutwhist.cards.{Card, Hand}
|
||||
import play.api.libs.json.{JsArray, Json}
|
||||
import play.twirl.api.Html
|
||||
import scalafx.scene.image.Image
|
||||
|
||||
object WebUIUtils {
|
||||
def cardtoImage(card: Card): Html = {
|
||||
|
||||
def cardToPath(card: Card): String = {
|
||||
f"images/cards/${cardtoString(card)}.png"
|
||||
}
|
||||
|
||||
def cardtoString(card: Card): String = {
|
||||
val s = card.suit match {
|
||||
case Spades => "S"
|
||||
case Hearts => "H"
|
||||
@@ -29,6 +35,50 @@ object WebUIUtils {
|
||||
case Three => "3"
|
||||
case Two => "2"
|
||||
}
|
||||
views.html.render.card.apply(f"images/cards/$cv$s.png")(card.toString)
|
||||
f"$cv$s"
|
||||
}
|
||||
|
||||
def stringToCard(cardStr: String): Card = {
|
||||
val cv = cardStr.charAt(0) match {
|
||||
case 'A' => Ace
|
||||
case 'K' => King
|
||||
case 'Q' => Queen
|
||||
case 'J' => Jack
|
||||
case 'T' => Ten
|
||||
case '9' => Nine
|
||||
case '8' => Eight
|
||||
case '7' => Seven
|
||||
case '6' => Six
|
||||
case '5' => Five
|
||||
case '4' => Four
|
||||
case '3' => Three
|
||||
case '2' => Two
|
||||
}
|
||||
val s = cardStr.charAt(1) match {
|
||||
case 'S' => Spades
|
||||
case 'H' => Hearts
|
||||
case 'C' => Clubs
|
||||
case 'D' => Diamonds
|
||||
}
|
||||
Card(cv, s)
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a Hand to a JsArray of cards
|
||||
* Per card it has the string and the index in the hand
|
||||
* @param hand
|
||||
* @return
|
||||
*/
|
||||
def handToJson(hand: Hand): JsArray = {
|
||||
val cards = hand.cards
|
||||
JsArray(
|
||||
cards.zipWithIndex.map { case (card, index) =>
|
||||
Json.obj(
|
||||
"idx" -> index,
|
||||
"card" -> cardtoString(card)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
73
knockoutwhistweb/app/util/WebsocketEventMapper.scala
Normal file
73
knockoutwhistweb/app/util/WebsocketEventMapper.scala
Normal file
@@ -0,0 +1,73 @@
|
||||
package util
|
||||
|
||||
import de.knockoutwhist.control.GameState
|
||||
import de.knockoutwhist.control.GameState.{FinishedMatch, InGame, Lobby, SelectTrump, TieBreak}
|
||||
import de.knockoutwhist.utils.events.SimpleEvent
|
||||
import dto.subDTO.{CardDTO, HandDTO, PlayerDTO, PlayerQueueDTO, PodiumPlayerDTO, RoundDTO, TrickDTO, UserDTO}
|
||||
import dto.{GameInfoDTO, LobbyInfoDTO, TieInfoDTO, TrumpInfoDTO, WonInfoDTO}
|
||||
import model.sessions.UserSession
|
||||
import play.api.libs.json.{JsValue, Json, OFormat}
|
||||
import tools.jackson.databind.json.JsonMapper
|
||||
import tools.jackson.module.scala.ScalaModule
|
||||
import util.mapper.*
|
||||
|
||||
object WebsocketEventMapper {
|
||||
|
||||
implicit val cardFormat: OFormat[CardDTO] = Json.format[CardDTO]
|
||||
implicit val handFormat: OFormat[HandDTO] = Json.format[HandDTO]
|
||||
implicit val playerFormat: OFormat[PlayerDTO] = Json.format[PlayerDTO]
|
||||
implicit val queueFormat: OFormat[PlayerQueueDTO] = Json.format[PlayerQueueDTO]
|
||||
implicit val podiumPlayerFormat: OFormat[PodiumPlayerDTO] = Json.format[PodiumPlayerDTO]
|
||||
implicit val roundFormat: OFormat[RoundDTO] = Json.format[RoundDTO]
|
||||
implicit val trickFormat: OFormat[TrickDTO] = Json.format[TrickDTO]
|
||||
implicit val userFormat: OFormat[UserDTO] = Json.format[UserDTO]
|
||||
|
||||
implicit val gameInfoDTOFormat: OFormat[GameInfoDTO] = Json.format[GameInfoDTO]
|
||||
implicit val lobbyFormat: OFormat[LobbyInfoDTO] = Json.format[LobbyInfoDTO]
|
||||
implicit val tieInfoFormat: OFormat[TieInfoDTO] = Json.format[TieInfoDTO]
|
||||
implicit val trumpInfoFormat: OFormat[TrumpInfoDTO] = Json.format[TrumpInfoDTO]
|
||||
implicit val wonInfoDTOFormat: OFormat[WonInfoDTO] = Json.format[WonInfoDTO]
|
||||
|
||||
private var specialMappers: Map[String,SimpleEventMapper[SimpleEvent]] = Map()
|
||||
|
||||
private def registerCustomMapper[T <: SimpleEvent](mapper: SimpleEventMapper[T]): Unit = {
|
||||
specialMappers = specialMappers + (mapper.id -> mapper.asInstanceOf[SimpleEventMapper[SimpleEvent]])
|
||||
}
|
||||
|
||||
// Register all custom mappers here
|
||||
registerCustomMapper(ReceivedHandEventMapper)
|
||||
registerCustomMapper(CardPlayedEventMapper)
|
||||
registerCustomMapper(NewRoundEventMapper)
|
||||
registerCustomMapper(NewTrickEventMapper)
|
||||
registerCustomMapper(TrickEndEventMapper)
|
||||
registerCustomMapper(RequestCardEventMapper)
|
||||
registerCustomMapper(LobbyUpdateEventMapper)
|
||||
registerCustomMapper(TurnEventMapper)
|
||||
|
||||
def toJson(obj: SimpleEvent, session: UserSession): JsValue = {
|
||||
val data: Option[JsValue] = if (specialMappers.contains(obj.id)) {
|
||||
Some(specialMappers(obj.id).toJson(obj, session))
|
||||
}else {
|
||||
None
|
||||
}
|
||||
Json.obj(
|
||||
"id" -> ("request-" + java.util.UUID.randomUUID().toString),
|
||||
"event" -> obj.id,
|
||||
"state" -> session.gameLobby.getLogic.getCurrentState.toString,
|
||||
"stateData" -> stateToJson(session),
|
||||
"data" -> data
|
||||
)
|
||||
}
|
||||
|
||||
def stateToJson(session: UserSession): JsValue = {
|
||||
session.gameLobby.getLogic.getCurrentState match {
|
||||
case Lobby => Json.toJson(LobbyInfoDTO(session.gameLobby, session.user))
|
||||
case InGame => Json.toJson(GameInfoDTO(session.gameLobby, session.user))
|
||||
case SelectTrump => Json.toJson(TrumpInfoDTO(session.gameLobby, session.user))
|
||||
case TieBreak => Json.toJson(TieInfoDTO(session.gameLobby, session.user))
|
||||
case FinishedMatch => Json.toJson(WonInfoDTO(session.gameLobby, session.user))
|
||||
case _ => Json.obj()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
20
knockoutwhistweb/app/util/mapper/CardPlayedEventMapper.scala
Normal file
20
knockoutwhistweb/app/util/mapper/CardPlayedEventMapper.scala
Normal file
@@ -0,0 +1,20 @@
|
||||
package util.mapper
|
||||
|
||||
import de.knockoutwhist.events.global.CardPlayedEvent
|
||||
import model.sessions.UserSession
|
||||
import play.api.libs.json.{JsArray, JsObject, Json}
|
||||
import util.WebUIUtils
|
||||
|
||||
object CardPlayedEventMapper extends SimpleEventMapper[CardPlayedEvent]{
|
||||
|
||||
override def id: String = "CardPlayedEvent"
|
||||
|
||||
override def toJson(event: CardPlayedEvent, session: UserSession): JsObject = {
|
||||
Json.obj(
|
||||
"firstCard" -> (if (event.trick.firstCard.isDefined) WebUIUtils.cardtoString(event.trick.firstCard.get) else "BLANK"),
|
||||
"playedCards" -> JsArray(event.trick.cards.map { case (card, player) =>
|
||||
Json.obj("cardId" -> WebUIUtils.cardtoString(card), "player" -> player.name)
|
||||
}.toList)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package util.mapper
|
||||
|
||||
import events.LobbyUpdateEvent
|
||||
import model.sessions.UserSession
|
||||
import play.api.libs.json.{JsArray, JsObject, Json}
|
||||
|
||||
object LobbyUpdateEventMapper extends SimpleEventMapper[LobbyUpdateEvent] {
|
||||
|
||||
override def id: String = "LobbyUpdateEvent"
|
||||
|
||||
override def toJson(event: LobbyUpdateEvent, session: UserSession): JsObject = {
|
||||
Json.obj(
|
||||
"host" -> session.host,
|
||||
"maxPlayers" -> session.gameLobby.maxPlayers,
|
||||
"players" -> JsArray(session.gameLobby.getPlayers.values.map(player => {
|
||||
Json.obj(
|
||||
"id" -> player.id,
|
||||
"name" -> player.name,
|
||||
"self" -> (player.id == session.user.id)
|
||||
)
|
||||
}).toList)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
18
knockoutwhistweb/app/util/mapper/NewRoundEventMapper.scala
Normal file
18
knockoutwhistweb/app/util/mapper/NewRoundEventMapper.scala
Normal file
@@ -0,0 +1,18 @@
|
||||
package util.mapper
|
||||
|
||||
import de.knockoutwhist.events.global.NewRoundEvent
|
||||
import logic.game.GameLobby
|
||||
import model.sessions.UserSession
|
||||
import play.api.libs.json.{JsObject, Json}
|
||||
|
||||
object NewRoundEventMapper extends SimpleEventMapper[NewRoundEvent]{
|
||||
override def id: String = "NewRoundEvent"
|
||||
|
||||
override def toJson(event: NewRoundEvent, session: UserSession): JsObject = {
|
||||
val gameLobby = session.gameLobby
|
||||
Json.obj(
|
||||
"trumpsuit" -> gameLobby.getLogic.getCurrentRound.get.trumpSuit.toString,
|
||||
"players" -> gameLobby.getLogic.getCurrentMatch.get.playersIn.map(player => player.toString)
|
||||
)
|
||||
}
|
||||
}
|
||||
14
knockoutwhistweb/app/util/mapper/NewTrickEventMapper.scala
Normal file
14
knockoutwhistweb/app/util/mapper/NewTrickEventMapper.scala
Normal file
@@ -0,0 +1,14 @@
|
||||
package util.mapper
|
||||
|
||||
import de.knockoutwhist.events.global.NewTrickEvent
|
||||
import logic.game.GameLobby
|
||||
import model.sessions.UserSession
|
||||
import play.api.libs.json.{JsObject, Json}
|
||||
|
||||
object NewTrickEventMapper extends SimpleEventMapper[NewTrickEvent]{
|
||||
override def id: String = "NewTrickEvent"
|
||||
|
||||
override def toJson(event: NewTrickEvent, session: UserSession): JsObject = {
|
||||
Json.obj()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package util.mapper
|
||||
|
||||
import de.knockoutwhist.events.player.ReceivedHandEvent
|
||||
import model.sessions.UserSession
|
||||
import play.api.libs.json.{JsObject, Json}
|
||||
import util.WebUIUtils
|
||||
|
||||
object ReceivedHandEventMapper extends SimpleEventMapper[ReceivedHandEvent] {
|
||||
|
||||
override def id: String = "ReceivedHandEvent"
|
||||
override def toJson(event: ReceivedHandEvent, session: UserSession): JsObject = {
|
||||
Json.obj(
|
||||
"dog" -> event.player.isInDogLife,
|
||||
"hand" -> event.player.currentHand().map(hand => WebUIUtils.handToJson(hand))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package util.mapper
|
||||
|
||||
import de.knockoutwhist.events.player.RequestCardEvent
|
||||
import logic.game.GameLobby
|
||||
import model.sessions.UserSession
|
||||
import play.api.libs.json.{JsObject, Json}
|
||||
|
||||
object RequestCardEventMapper extends SimpleEventMapper[RequestCardEvent]{
|
||||
override def id: String = "RequestCardEvent"
|
||||
|
||||
override def toJson(event: RequestCardEvent, session: UserSession): JsObject = {
|
||||
Json.obj(
|
||||
"player" -> event.player.name
|
||||
)
|
||||
}
|
||||
}
|
||||
20
knockoutwhistweb/app/util/mapper/RoundEndEventMapper.scala
Normal file
20
knockoutwhistweb/app/util/mapper/RoundEndEventMapper.scala
Normal file
@@ -0,0 +1,20 @@
|
||||
package util.mapper
|
||||
|
||||
import controllers.routes
|
||||
import de.knockoutwhist.events.global.RoundEndEvent
|
||||
import model.sessions.UserSession
|
||||
import play.api.libs.json.{JsObject, Json}
|
||||
|
||||
object RoundEndEventMapper extends SimpleEventMapper[RoundEndEvent] {
|
||||
|
||||
override def id: String = "RoundEndEvent"
|
||||
|
||||
override def toJson(event: RoundEndEvent, session: UserSession): JsObject = {
|
||||
|
||||
Json.obj(
|
||||
"player" -> event.winner.name,
|
||||
"tricks" -> event.amountOfTricks
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
13
knockoutwhistweb/app/util/mapper/SimpleEventMapper.scala
Normal file
13
knockoutwhistweb/app/util/mapper/SimpleEventMapper.scala
Normal file
@@ -0,0 +1,13 @@
|
||||
package util.mapper
|
||||
|
||||
import de.knockoutwhist.utils.events.SimpleEvent
|
||||
import logic.game.GameLobby
|
||||
import model.sessions.UserSession
|
||||
import play.api.libs.json.JsObject
|
||||
|
||||
trait SimpleEventMapper[T <: SimpleEvent] {
|
||||
|
||||
def id: String
|
||||
def toJson(event: T, session: UserSession): JsObject
|
||||
|
||||
}
|
||||
20
knockoutwhistweb/app/util/mapper/TrickEndEventMapper.scala
Normal file
20
knockoutwhistweb/app/util/mapper/TrickEndEventMapper.scala
Normal file
@@ -0,0 +1,20 @@
|
||||
package util.mapper
|
||||
|
||||
import de.knockoutwhist.events.global.TrickEndEvent
|
||||
import de.knockoutwhist.rounds.Trick
|
||||
import logic.game.GameLobby
|
||||
import model.sessions.UserSession
|
||||
import play.api.libs.json.{JsObject, Json}
|
||||
|
||||
object TrickEndEventMapper extends SimpleEventMapper[TrickEndEvent]{
|
||||
override def id: String = "TrickEndEvent"
|
||||
|
||||
override def toJson(event: TrickEndEvent, session: UserSession): JsObject = {
|
||||
val gameLobby = session.gameLobby
|
||||
Json.obj(
|
||||
"playerwon" -> event.winner.name,
|
||||
"playersin" -> gameLobby.getLogic.getCurrentMatch.get.playersIn.map(player => player.name),
|
||||
"tricklist" -> gameLobby.getLogic.getCurrentRound.get.tricklist.map(trick => trick.winner.map(player => player.name).getOrElse("Trick in Progress"))
|
||||
)
|
||||
}
|
||||
}
|
||||
35
knockoutwhistweb/app/util/mapper/TurnEventMapper.scala
Normal file
35
knockoutwhistweb/app/util/mapper/TurnEventMapper.scala
Normal file
@@ -0,0 +1,35 @@
|
||||
package util.mapper
|
||||
|
||||
import de.knockoutwhist.events.global.TurnEvent
|
||||
import de.knockoutwhist.player.AbstractPlayer
|
||||
import model.sessions.UserSession
|
||||
import play.api.libs.json.{JsArray, JsObject, Json}
|
||||
|
||||
object TurnEventMapper extends SimpleEventMapper[TurnEvent] {
|
||||
|
||||
override def id: String = "TurnEvent"
|
||||
|
||||
override def toJson(event: TurnEvent, session: UserSession): JsObject = {
|
||||
val nextPlayers = if (session.gameLobby.logic.getPlayerQueue.isEmpty) {
|
||||
Json.arr()
|
||||
} else {
|
||||
val queue = session.gameLobby.logic.getPlayerQueue.get
|
||||
JsArray(
|
||||
queue.duplicate().map(player => mapPlayer(player)).toList
|
||||
)
|
||||
}
|
||||
|
||||
Json.obj(
|
||||
"currentPlayer" -> mapPlayer(event.player),
|
||||
"nextPlayers" -> nextPlayers
|
||||
)
|
||||
}
|
||||
|
||||
private def mapPlayer(player: AbstractPlayer): JsObject = {
|
||||
Json.obj(
|
||||
"name" -> player.name,
|
||||
"dog" -> player.isInDogLife
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic)
|
||||
|
||||
@main("Ingame") {
|
||||
<div id="ingame" class="game-field game-field-background">
|
||||
<h1>Knockout Whist</h1>
|
||||
<div id="nextPlayers">
|
||||
<p>Next Player:</p>
|
||||
<p>@logic.getPlayerQueue.get.duplicate().nextPlayer()</p>
|
||||
</div>
|
||||
<div id="firstCard">
|
||||
<div id="trumpsuit">
|
||||
<p>Trumpsuit: </p>
|
||||
<p>@logic.getCurrentRound.get.trumpSuit</p>
|
||||
</div>
|
||||
<div id="firstCardObject">
|
||||
<p>First Card</p>
|
||||
@if(logic.getCurrentTrick.get.firstCard.isDefined) {
|
||||
@util.WebUIUtils.cardtoImage(logic.getCurrentTrick.get.firstCard.get)
|
||||
} else {
|
||||
@views.html.render.card.apply("images/cards/1B.png")("Blank Card")
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>@logic.getCurrentPlayer.get has to play a card!</p>
|
||||
@if(logic.getCurrentTrick.get.cards.nonEmpty) {
|
||||
<p>Cards played</p>
|
||||
} else {
|
||||
<p id="invisible">Cards played</p>
|
||||
}
|
||||
|
||||
<div id="cardsplayed">
|
||||
@for((cardplayed, player) <- logic.getCurrentTrick.get.cards) {
|
||||
<div id="playedcardplayer">
|
||||
<p>@player</p>
|
||||
@util.WebUIUtils.cardtoImage(cardplayed)
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<p>Your cards</p>
|
||||
<div id="playercards">
|
||||
@for(card <- player.currentHand().get.cards) {
|
||||
@util.WebUIUtils.cardtoImage(card)
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic)
|
||||
|
||||
@main("Selecting Trumpsuit...") {
|
||||
<div id="selecttrumpsuit" class="game-field game-field-background">
|
||||
@if(player.equals(logic.getCurrentMatch.get.roundlist.last.winner.get)) {
|
||||
<h1>Knockout Whist</h1>
|
||||
<p>You (@player.toString) have won the last round! Select a trumpsuit for the next round!</p>
|
||||
<p>Available trumpsuits are displayed below:</p>
|
||||
<div id="playercards">
|
||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Spades))
|
||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Clubs))
|
||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Hearts))
|
||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Diamonds))
|
||||
</div>
|
||||
<p>Your cards</p>
|
||||
|
||||
<div id="playercards">
|
||||
@for(card <- player.currentHand().get.cards) {
|
||||
@util.WebUIUtils.cardtoImage(card)
|
||||
}
|
||||
</div>
|
||||
} else {
|
||||
<h1>Knockout Whist</h1>
|
||||
<p>@player.toString is choosing a trumpsuit. Starting new round when @player.toString picked a trumpsuit...</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic)
|
||||
|
||||
@main("Tie") {
|
||||
<div id="tie" class="game-field game-field-background">
|
||||
<h1>Knockout Whist</h1>
|
||||
<p>The last Round was tied between
|
||||
@for(players <- logic.playerTieLogic.getTiedPlayers) {
|
||||
@players
|
||||
}
|
||||
</p>
|
||||
@if(player.equals(logic.playerTieLogic.currentTiePlayer())) {
|
||||
<p>Pick a card between 1 and @logic.playerTieLogic.highestAllowedNumber()! The resulting card will be your card for the cut.</p>
|
||||
} else {
|
||||
<p>@logic.playerTieLogic.currentTiePlayer() is currently picking his number for the cut.</p>
|
||||
<p>Currently picked Cards:</p>
|
||||
<div id="cardsplayed">
|
||||
@for((player, card) <- logic.playerTieLogic.getSelectedCard) {
|
||||
<div id="playedcardplayer">
|
||||
<p>@player</p>
|
||||
@util.WebUIUtils.cardtoImage(card)
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
@()
|
||||
|
||||
@main("Login") {
|
||||
<div class="login-box">
|
||||
<div class="card login-card p-4">
|
||||
<div class="card-body">
|
||||
<h3 class="text-center mb-4">Login</h3>
|
||||
|
||||
<form action="@routes.UserController.login_Post()" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="username" name="username" placeholder="Enter Username" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" placeholder="Enter password" required>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<a href="#" class="text-decoration-none">Forgot password?</a>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="text-center mt-3">
|
||||
Don’t have an account?
|
||||
<a href="#" class="text-decoration-none">Sign up</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="@routes.Assets.versioned("/javascripts/particles.js")"></script>
|
||||
<div id="particles-js" style="background-color: rgb(182, 25, 36);
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;"></div>
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
@*
|
||||
* This template is called from the `index` template. This template
|
||||
* handles the rendering of the page header and body tags. It takes
|
||||
* two arguments, a `String` for the title of the page and an `Html`
|
||||
* object to insert into the body of the page.
|
||||
*@
|
||||
@(title: String)(content: Html)
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@* Here's where we render the page title `String`. *@
|
||||
<title>@title</title>
|
||||
<link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
|
||||
<link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@* And here's where we render the `Html` object containing
|
||||
* the page content. *@
|
||||
@content
|
||||
|
||||
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,56 +0,0 @@
|
||||
@(user: Option[model.users.User])
|
||||
@main("Knockout Whist - Main Menu") {
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||
<div class="container-fluid">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navBar" aria-controls="navBar" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navBar">
|
||||
<a class="navbar-brand" href="@routes.MainMenuController.mainMenu()">KnockOutWhist</a>
|
||||
<div class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<ul class="navbar-nav mb-2 mb-lg-0">
|
||||
@if(user.isDefined) {
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="#">Create Game</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" aria-disabled="true">Lobbies</a>
|
||||
</li>
|
||||
}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="@routes.MainMenuController.rules()">Rules</a>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="navbar-nav me-auto mb-2 mb-lg-0" method="post" action="@routes.MainMenuController.joinGame()">
|
||||
<input class="form-control me-2" type="text" placeholder="Enter GameCode" name="gameId" aria-label="Join Game"/>
|
||||
<button class="btn btn-outline-success" type="submit">Join</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@* Right side: profile dropdown if logged in, otherwise Login / Sign Up buttons *@
|
||||
@if(user.isDefined) {
|
||||
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle d-flex align-items-center" href="#" id="profileDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<img src="@routes.Assets.versioned("images/profile.png")" alt="Profile" class="rounded-circle" width="30" height="30" />
|
||||
<span class="ms-2">@user.get.name</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="profileDropdown">
|
||||
<li><a class="dropdown-item disabled" href="#" tabindex="-1" aria-disabled="true">Stats</a></li>
|
||||
<li><a class="dropdown-item disabled" href="#" tabindex="-1" aria-disabled="true">Settings</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="@routes.UserController.logout()">Logout</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
} else {
|
||||
<div class="d-flex ms-auto">
|
||||
<a class="btn btn-outline-primary me-2" href="@routes.UserController.login()">Login</a>
|
||||
<a class="btn btn-primary" href="@routes.UserController.login()">Sign Up</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
@()
|
||||
|
||||
@main("Rules") {
|
||||
<div id="rules" class="game-field game-field-background">
|
||||
<table>
|
||||
<caption>Rules Overview and Equipment</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Section</th>
|
||||
<th>Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Players</td>
|
||||
<td>Two to seven players. The aim is to be the last player left in the game.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Aim</td>
|
||||
<td>To be the last player left in at the end of the game, with the object in each hand being to win a majority of tricks.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Equipment</td>
|
||||
<td>A standard 52-card pack is used.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Card Ranks</td>
|
||||
<td>In each suit, cards rank from highest to lowest: A K Q J 10 9 8 7 6 5 4 3 2.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Deal (First Hand)</td>
|
||||
<td>The dealer deals seven cards to each player. One card is turned up to determine the trump suit for the round.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Deal (Subsequent Hands)</td>
|
||||
<td>The deal rotates clockwise. The player who took the most tricks in the previous hand selects the trump suit. If there's a tie for the highest number of tricks, players cut cards to decide who calls trumps. One fewer card is dealt in each successive hand until the final hand consists of one card each.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Play</td>
|
||||
<td>The player to the dealer's left (eldest hand) leads the first trick. Any card can be led. Other players must follow suit if possible. A player with no cards of the suit led may play any card.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Winning a Trick</td>
|
||||
<td>The highest card of the suit led wins, unless a trump is played, in which case the highest trump wins. The winner of the trick leads the next.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Leading Trumps</td>
|
||||
<td>Some rules disallow leading trumps before the suit has been 'broken' (a trump has been played to the lead of another suit). Leading trumps is always permissible if a player holds only trumps.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Knockout</td>
|
||||
<td>At the end of each hand, any player who took no tricks is knocked out and takes no further part in the game.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Winning the Game</td>
|
||||
<td>The game is won when a player takes all the tricks in a round, as all other players are knocked out, leaving only one player remaining.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<td>Dog Life</td>
|
||||
<td>The first player who takes no tricks is awarded a "dog's life". In the next hand, that player is dealt one card and can decide which trick to play it to. Each time a trick is played the "dog" may either play the card or knock on the table and wait to play it later. If the dog wins a trick, the player to the left leads to the next and the dog re-enters the game properly in the next hand. If the dog fails, they are knocked out.</td>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
@(src: String)(alt: String)
|
||||
<img src="@routes.Assets.versioned(src)" alt="@alt"/>
|
||||
@@ -1,3 +0,0 @@
|
||||
@(text: String)
|
||||
<p>@text</p>
|
||||
|
||||
32
knockoutwhistweb/conf/META-INF/persistence.xml
Normal file
32
knockoutwhistweb/conf/META-INF/persistence.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence
|
||||
https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
|
||||
version="3.0">
|
||||
|
||||
<persistence-unit name="defaultPersistenceUnit">
|
||||
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
|
||||
|
||||
<class>model.users.UserEntity</class>
|
||||
|
||||
<properties>
|
||||
<!-- Hibernate specific settings -->
|
||||
<property name="hibernate.hbm2ddl.auto" value="update"/>
|
||||
<property name="hibernate.archive.autodetection" value="class"/>
|
||||
<property name="hibernate.show_sql" value="false"/>
|
||||
<property name="hibernate.format_sql" value="true"/>
|
||||
<property name="hibernate.use_sql_comments" value="true"/>
|
||||
|
||||
<!-- Connection pool settings -->
|
||||
<property name="hibernate.connection.provider_class" value="org.hibernate.hikaricp.internal.HikariCPConnectionProvider"/>
|
||||
<property name="hibernate.hikari.maximumPoolSize" value="20"/>
|
||||
<property name="hibernate.hikari.minimumIdle" value="5"/>
|
||||
<property name="hibernate.hikari.connectionTimeout" value="30000"/>
|
||||
<property name="hibernate.hikari.idleTimeout" value="600000"/>
|
||||
<property name="hibernate.hikari.maxLifetime" value="1800000"/>
|
||||
<property name="hibernate.hikari.poolName" value="KnockOutWhistPool"/>
|
||||
</properties>
|
||||
</persistence-unit>
|
||||
|
||||
</persistence>
|
||||
@@ -1,14 +1,53 @@
|
||||
# https://www.playframework.com/documentation/latest/Configuration
|
||||
play.filters.disabled += play.filters.csrf.CSRFFilter
|
||||
play.filters.disabled += play.filters.hosts.AllowedHostsFilter
|
||||
|
||||
# Disable default JPA and Hibernate modules to use custom EntityManagerProvider
|
||||
play.modules.disabled += "play.db.jpa.JPAModule"
|
||||
|
||||
play.http.secret.key="QCY?tAnfk?aZ?iwrNwnxIlR6CTf:G3gf:90Latabg@5241AB`R5W:1uDFN];Ik@n"
|
||||
play.http.secret.key=${?APPLICATION_SECRET}
|
||||
|
||||
auth {
|
||||
issuer = "knockoutwhistweb"
|
||||
audience = "ui"
|
||||
# ${?PUBLIC_KEY_FILE}
|
||||
privateKeyFile = "D:\\Workspaces\\Gitops\\rsa512-private.pem"
|
||||
privateKeyPem = ${?PUBLIC_KEY_PEM}
|
||||
#${?PUBLIC_KEY_FILE}
|
||||
publicKeyFile = "D:\\Workspaces\\Gitops\\rsa512-public.pem"
|
||||
privateKeyFile = ${?PRIVATE_KEY_FILE}
|
||||
privateKeyPem = ${?PRIVATE_KEY_PEM}
|
||||
publicKeyFile = ${?PUBLIC_KEY_FILE}
|
||||
publicKeyPem = ${?PUBLIC_KEY_PEM}
|
||||
}
|
||||
|
||||
play.filters.enabled += "play.filters.cors.CORSFilter"
|
||||
|
||||
play.filters.cors {
|
||||
allowedOrigins = ["http://localhost:5173"]
|
||||
allowedCredentials = true
|
||||
allowedHttpMethods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
|
||||
allowedHttpHeaders = ["Accept", "Content-Type", "Origin", "X-Requested-With"]
|
||||
}
|
||||
|
||||
# Local Development OpenID Connect Configuration
|
||||
openid {
|
||||
selectUserRoute="http://localhost:5173/select-username"
|
||||
mainRoute="http://localhost:5173/"
|
||||
|
||||
discord {
|
||||
clientId = ${?DISCORD_CLIENT_ID}
|
||||
clientId = "1462555597118509126"
|
||||
clientSecret = ${?DISCORD_CLIENT_SECRET}
|
||||
clientSecret = "xZZrdd7_tNpfJgnk-6phSG53DSTy-eMK"
|
||||
redirectUri = ${?DISCORD_REDIRECT_URI}
|
||||
redirectUri = "http://localhost:9000/auth/discord/callback"
|
||||
}
|
||||
|
||||
keycloak {
|
||||
clientId = ${?KEYCLOAK_CLIENT_ID}
|
||||
clientId = "your-keycloak-client-id"
|
||||
clientSecret = ${?KEYCLOAK_CLIENT_SECRET}
|
||||
clientSecret = "your-keycloak-client-secret"
|
||||
redirectUri = ${?KEYCLOAK_REDIRECT_URI}
|
||||
redirectUri = "http://localhost:9000/auth/keycloak/callback"
|
||||
authUrl = ${?KEYCLOAK_AUTH_URL}
|
||||
authUrl = "http://localhost:8080/realms/master"
|
||||
}
|
||||
}
|
||||
|
||||
39
knockoutwhistweb/conf/db.conf
Normal file
39
knockoutwhistweb/conf/db.conf
Normal file
@@ -0,0 +1,39 @@
|
||||
|
||||
# Database configuration - PostgreSQL with environment variables
|
||||
db.default.driver="org.postgresql.Driver"
|
||||
db.default.url="jdbc:postgresql://localhost:5432/knockoutwhist"
|
||||
db.default.url=${?DATABASE_URL}
|
||||
db.default.username="kw_user"
|
||||
db.default.username=${?DB_USER}
|
||||
db.default.password="postgres"
|
||||
db.default.password=${?DB_PASSWORD}
|
||||
|
||||
# HikariCP specific configuration
|
||||
db.default.hikaricp.driverClassName="org.postgresql.Driver"
|
||||
db.default.hikaricp.jdbcUrl="jdbc:postgresql://localhost:5432/knockoutwhist"
|
||||
db.default.hikaricp.jdbcUrl=${?DATABASE_URL}
|
||||
db.default.hikaricp.username="kw_user"
|
||||
db.default.hikaricp.username=${?DB_USER}
|
||||
db.default.hikaricp.password="postgres"
|
||||
db.default.hikaricp.password=${?DB_PASSWORD}
|
||||
|
||||
# JPA/Hibernate configuration
|
||||
jpa.default=defaultPersistenceUnit
|
||||
|
||||
# Hibernate specific settings
|
||||
hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
|
||||
hibernate.hbm2ddl.auto=update
|
||||
hibernate.show_sql=false
|
||||
hibernate.format_sql=true
|
||||
hibernate.use_sql_comments=true
|
||||
|
||||
# Connection pool settings
|
||||
db.default.hikaricp.maximumPoolSize=20
|
||||
db.default.hikaricp.minimumIdle=5
|
||||
db.default.hikaricp.connectionTimeout=30000
|
||||
db.default.hikaricp.idleTimeout=600000
|
||||
db.default.hikaricp.maxLifetime=1800000
|
||||
|
||||
# PostgreSQL specific settings
|
||||
db.default.hikaricp.connectionTestQuery="SELECT 1"
|
||||
db.default.hikaricp.poolName="KnockOutWhistPool"
|
||||
@@ -5,46 +5,48 @@
|
||||
<!DOCTYPE configuration>
|
||||
|
||||
<configuration>
|
||||
<import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
|
||||
<import class="ch.qos.logback.classic.AsyncAppender"/>
|
||||
<import class="ch.qos.logback.core.FileAppender"/>
|
||||
<import class="ch.qos.logback.core.ConsoleAppender"/>
|
||||
<import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder" />
|
||||
<import class="ch.qos.logback.classic.AsyncAppender" />
|
||||
<import class="ch.qos.logback.core.FileAppender" />
|
||||
<import class="ch.qos.logback.core.ConsoleAppender" />
|
||||
|
||||
<appender name="FILE" class="FileAppender">
|
||||
<file>${application.home:-.}/logs/application.log</file>
|
||||
<encoder class="PatternLayoutEncoder">
|
||||
<charset>UTF-8</charset>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{36}) %magenta(%X{pekkoSource}) %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="FILE" class="FileAppender">
|
||||
<file>${application.home:-.}/logs/application.log</file>
|
||||
<encoder class="PatternLayoutEncoder">
|
||||
<charset>UTF-8</charset>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{36}) %magenta(%X{pekkoSource}) %msg%n
|
||||
</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="STDOUT" class="ConsoleAppender">
|
||||
<!--
|
||||
On Windows, enabling Jansi is recommended to benefit from color code interpretation on DOS command prompts,
|
||||
which otherwise risk being sent ANSI escape sequences that they cannot interpret.
|
||||
See https://logback.qos.ch/manual/layouts.html#coloring
|
||||
-->
|
||||
<!-- <withJansi>true</withJansi> -->
|
||||
<encoder class="PatternLayoutEncoder">
|
||||
<charset>UTF-8</charset>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{36}) %magenta(%X{pekkoSource}) %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="STDOUT" class="ConsoleAppender">
|
||||
<!--
|
||||
On Windows, enabling Jansi is recommended to benefit from color code interpretation on DOS command prompts,
|
||||
which otherwise risk being sent ANSI escape sequences that they cannot interpret.
|
||||
See https://logback.qos.ch/manual/layouts.html#coloring
|
||||
-->
|
||||
<!-- <withJansi>true</withJansi> -->
|
||||
<encoder class="PatternLayoutEncoder">
|
||||
<charset>UTF-8</charset>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{36}) %magenta(%X{pekkoSource}) %msg%n
|
||||
</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="ASYNCFILE" class="AsyncAppender">
|
||||
<appender-ref ref="FILE"/>
|
||||
</appender>
|
||||
<appender name="ASYNCFILE" class="AsyncAppender">
|
||||
<appender-ref ref="FILE" />
|
||||
</appender>
|
||||
|
||||
<appender name="ASYNCSTDOUT" class="AsyncAppender">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</appender>
|
||||
<appender name="ASYNCSTDOUT" class="AsyncAppender">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</appender>
|
||||
|
||||
<logger name="play" level="INFO"/>
|
||||
<logger name="application" level="DEBUG"/>
|
||||
<logger name="play" level="INFO" />
|
||||
<logger name="application" level="DEBUG" />
|
||||
|
||||
<root level="WARN">
|
||||
<appender-ref ref="ASYNCFILE"/>
|
||||
<appender-ref ref="ASYNCSTDOUT"/>
|
||||
</root>
|
||||
<root level="WARN">
|
||||
<appender-ref ref="ASYNCFILE" />
|
||||
<appender-ref ref="ASYNCSTDOUT" />
|
||||
</root>
|
||||
|
||||
</configuration>
|
||||
|
||||
37
knockoutwhistweb/conf/prod.conf
Normal file
37
knockoutwhistweb/conf/prod.conf
Normal file
@@ -0,0 +1,37 @@
|
||||
include "application.conf"
|
||||
include "db.conf"
|
||||
|
||||
play.http.secret.key="zg8^v0R*:7-m.>^8T2B1q)sE3MV_9=M{K9zx8,<3}"
|
||||
|
||||
play.http.context="/api"
|
||||
|
||||
play.modules.enabled += "modules.GatewayModule"
|
||||
|
||||
play.filters.cors {
|
||||
allowedOrigins = ["https://knockout.janis-eccarius.de"]
|
||||
allowedCredentials = true
|
||||
allowedHttpMethods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
|
||||
allowedHttpHeaders = ["Accept", "Content-Type", "Origin", "X-Requested-With"]
|
||||
}
|
||||
|
||||
# OpenID Connect Configuration
|
||||
openid {
|
||||
|
||||
selectUserRoute="https://knockout.janis-eccarius.de/select-username"
|
||||
mainRoute="https://knockout.janis-eccarius.de/"
|
||||
|
||||
discord {
|
||||
clientId = ${?DISCORD_CLIENT_ID}
|
||||
clientSecret = ${?DISCORD_CLIENT_SECRET}
|
||||
redirectUri = ${?DISCORD_REDIRECT_URI}
|
||||
redirectUri = "https://knockout.janis-eccarius.de/api/auth/discord/callback"
|
||||
}
|
||||
|
||||
keycloak {
|
||||
clientId = ${?KEYCLOAK_CLIENT_ID}
|
||||
clientSecret = ${?KEYCLOAK_CLIENT_SECRET}
|
||||
redirectUri = "https://knockout.janis-eccarius.de/api/auth/keycloak/callback"
|
||||
authUrl = ${?KEYCLOAK_AUTH_URL}
|
||||
authUrl = "https://identity.janis-eccarius.de/realms/master"
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,25 @@
|
||||
# Routes
|
||||
# This file defines all application routes (Higher priority routes first)
|
||||
# https://www.playframework.com/documentation/latest/ScalaRouting
|
||||
# ~~~~
|
||||
|
||||
|
||||
# Primary routes
|
||||
GET / controllers.MainMenuController.index()
|
||||
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
|
||||
|
||||
# Main menu routes
|
||||
GET /mainmenu controllers.MainMenuController.mainMenu()
|
||||
GET /rules controllers.MainMenuController.rules()
|
||||
|
||||
POST /createGame controllers.MainMenuController.createGame()
|
||||
POST /joinGame controllers.MainMenuController.joinGame()
|
||||
# Create game rounds
|
||||
POST /createGame controllers.MainMenuController.createGame()
|
||||
POST /joinGame/:gameId controllers.MainMenuController.joinGame(gameId: String)
|
||||
|
||||
# User authentication routes
|
||||
GET /login controllers.UserController.login()
|
||||
POST /login controllers.UserController.login_Post()
|
||||
POST /login controllers.UserController.login_Post()
|
||||
POST /register controllers.UserController.register()
|
||||
POST /logout controllers.UserController.logoutPost()
|
||||
GET /userInfo controllers.UserController.getUserInfo()
|
||||
|
||||
GET /logout controllers.UserController.logout()
|
||||
# OpenID Connect routes
|
||||
GET /auth/:provider controllers.OpenIDController.loginWithProvider(provider: String)
|
||||
GET /auth/:provider/callback controllers.OpenIDController.callback(provider: String)
|
||||
GET /select-username controllers.OpenIDController.selectUsername()
|
||||
POST /submit-username controllers.OpenIDController.submitUsername()
|
||||
|
||||
# In-game routes
|
||||
GET /game/:id controllers.IngameController.game(id: String)
|
||||
GET /game/:id/join controllers.IngameController.joinGame(id: String)
|
||||
POST /game/:id/start controllers.IngameController.startGame(id: String)
|
||||
# Websocket
|
||||
GET /websocket controllers.WebsocketController.socket()
|
||||
|
||||
POST /game/:id/playCard controllers.IngameController.playCard(id: String)
|
||||
# Status
|
||||
GET /status controllers.StatusController.requestStatus()
|
||||
GET /status/:gameId controllers.StatusController.game(gameId: String)
|
||||
|
||||
# Health
|
||||
GET /health/simple controllers.HealthController.simple()
|
||||
34
knockoutwhistweb/conf/staging.conf
Normal file
34
knockoutwhistweb/conf/staging.conf
Normal file
@@ -0,0 +1,34 @@
|
||||
include "application.conf"
|
||||
include "db.conf"
|
||||
|
||||
play.http.context="/api"
|
||||
|
||||
play.modules.enabled += "modules.GatewayModule"
|
||||
|
||||
play.filters.cors {
|
||||
allowedOrigins = ["https://st.knockout.janis-eccarius.de"]
|
||||
allowedCredentials = true
|
||||
allowedHttpMethods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
|
||||
allowedHttpHeaders = ["Accept", "Content-Type", "Origin", "X-Requested-With"]
|
||||
}
|
||||
|
||||
openid {
|
||||
|
||||
selectUserRoute="https://st.knockout.janis-eccarius.de/select-username"
|
||||
mainRoute="https://st.knockout.janis-eccarius.de/"
|
||||
|
||||
discord {
|
||||
clientId = ${?DISCORD_CLIENT_ID}
|
||||
clientSecret = ${?DISCORD_CLIENT_SECRET}
|
||||
redirectUri = ${?DISCORD_REDIRECT_URI}
|
||||
redirectUri = "https://st.knockout.janis-eccarius.de/api/auth/discord/callback"
|
||||
}
|
||||
|
||||
keycloak {
|
||||
clientId = ${?KEYCLOAK_CLIENT_ID}
|
||||
clientSecret = ${?KEYCLOAK_CLIENT_SECRET}
|
||||
redirectUri = "https://st.knockout.janis-eccarius.de/api/auth/keycloak/callback"
|
||||
authUrl = ${?KEYCLOAK_AUTH_URL}
|
||||
authUrl = "https://identity.janis-eccarius.de/realms/master"
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
{
|
||||
"particles": {
|
||||
"number": {
|
||||
"value": 80,
|
||||
"density": {
|
||||
"enable": true,
|
||||
"value_area": 800
|
||||
}
|
||||
},
|
||||
"color": {
|
||||
"value": "#ffffff"
|
||||
},
|
||||
"shape": {
|
||||
"type": "circle",
|
||||
"stroke": {
|
||||
"width": 0,
|
||||
"color": "#000000"
|
||||
},
|
||||
"polygon": {
|
||||
"nb_sides": 5
|
||||
},
|
||||
"image": {
|
||||
"src": "img/github.svg",
|
||||
"width": 100,
|
||||
"height": 100
|
||||
}
|
||||
},
|
||||
"opacity": {
|
||||
"value": 0.5,
|
||||
"random": false,
|
||||
"anim": {
|
||||
"enable": false,
|
||||
"speed": 1,
|
||||
"opacity_min": 0.1,
|
||||
"sync": false
|
||||
}
|
||||
},
|
||||
"size": {
|
||||
"value": 3,
|
||||
"random": true,
|
||||
"anim": {
|
||||
"enable": false,
|
||||
"speed": 40,
|
||||
"size_min": 0.1,
|
||||
"sync": false
|
||||
}
|
||||
},
|
||||
"line_linked": {
|
||||
"enable": true,
|
||||
"distance": 150,
|
||||
"color": "#ffffff",
|
||||
"opacity": 0.4,
|
||||
"width": 1
|
||||
},
|
||||
"move": {
|
||||
"enable": true,
|
||||
"speed": 1,
|
||||
"direction": "none",
|
||||
"random": false,
|
||||
"straight": false,
|
||||
"out_mode": "out",
|
||||
"bounce": false,
|
||||
"attract": {
|
||||
"enable": false,
|
||||
"rotateX": 600,
|
||||
"rotateY": 1200
|
||||
}
|
||||
}
|
||||
},
|
||||
"interactivity": {
|
||||
"detect_on": "canvas",
|
||||
"events": {
|
||||
"onhover": {
|
||||
"enable": false,
|
||||
"mode": "repulse"
|
||||
},
|
||||
"onclick": {
|
||||
"enable": false,
|
||||
"mode": "push"
|
||||
},
|
||||
"resize": true
|
||||
},
|
||||
"modes": {
|
||||
"grab": {
|
||||
"distance": 400,
|
||||
"line_linked": {
|
||||
"opacity": 1
|
||||
}
|
||||
},
|
||||
"bubble": {
|
||||
"distance": 400,
|
||||
"size": 40,
|
||||
"duration": 2,
|
||||
"opacity": 8,
|
||||
"speed": 3
|
||||
},
|
||||
"repulse": {
|
||||
"distance": 200,
|
||||
"duration": 0.4
|
||||
},
|
||||
"push": {
|
||||
"particles_nb": 4
|
||||
},
|
||||
"remove": {
|
||||
"particles_nb": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"retina_detect": true
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user