chore: rebase

This commit is contained in:
2025-10-29 15:39:35 +01:00
parent abf689c8e3
commit 465004b9ce
18 changed files with 1795 additions and 37 deletions

View File

@@ -0,0 +1,35 @@
.login-box {
position: fixed; /* changed from absolute to fixed so it centers relative to the viewport */
align-items: center;
justify-content: center;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); /* center exactly */
display: flex;
width: 100%;
max-width: 420px; /* keeps box from stretching too wide */
padding: 1rem;
z-index: 2; /* above particles */
}
.login-card {
max-width: 400px;
width: 100%;
border: none;
border-radius: 1rem;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
position: relative;
z-index: 3; /* ensure card sits above the particles */
}
#particles-js {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0; /* behind content */
pointer-events: none; /* allow clicks through particles */
background-repeat: no-repeat;
background-size: cover;
}

View File

@@ -1,5 +1,6 @@
@import "light-mode.less";
@import "dark-mode.less";
@import "login.less";
@background-image: var(--background-image);
@color: var(--color);
@@ -7,14 +8,16 @@
0% { transform: translateX(-100vw); }
100% { transform: translateX(0); }
}
body {
.game-field-background {
background-image: @background-image;
background-size: 100vw 100vh;
background-repeat: no-repeat;
}
html, body {
height: 100vh;
margin: 0;
.game-field {
position: fixed;
inset: 0;
overflow: auto;
}
#sessions {
display: flex;
@@ -32,7 +35,8 @@ html, body {
animation-fill-mode: backwards;
animation-delay: 1s;
}
#sessions a, h1, p {
#sessions a, #sessions h1, #sessions p {
color: @color;
font-size: 40px;
font-family: Arial, serif;
@@ -44,6 +48,11 @@ html, body {
justify-content: flex-end;
height: 100%;
}
#ingame a, #ingame h1, #ingame p {
color: @color;
font-size: 40px;
font-family: Arial, serif;
}
#playercards {
display: flex;
flex-direction: row;

View File

@@ -62,6 +62,7 @@ class UserController @Inject()(val controllerComponents: ControllerComponents, v
def login_Post(): Action[AnyContent] = {
Action { implicit request =>
val postData = request.body.asFormUrlEncoded
println(request.body.asText)
if (postData.isDefined) {
// Extract username and password from form data
val username = postData.get.get("username").flatMap(_.headOption).getOrElse("")

View File

@@ -10,6 +10,7 @@ trait UserManager {
def addUser(name: String, password: String): Boolean
def authenticate(name: String, password: String): Option[User]
def userExists(name: String): Option[User]
def userExistsById(id: Long): Option[User]
def removeUser(name: String): Boolean
}

View File

@@ -1,37 +1,63 @@
package logic.user.impl
import com.auth0.jwt.JWT
import com.auth0.jwt.{JWT, JWTVerifier}
import com.auth0.jwt.algorithms.Algorithm
import com.github.benmanes.caffeine.cache.{Cache, Caffeine}
import com.typesafe.config.Config
import logic.user.SessionManager
import model.users.User
import scalafx.util.Duration
import services.JwtKeyProvider
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.concurrent.TimeUnit
import javax.inject.{Inject, Singleton}
@Singleton
class BaseSessionManager @Inject()(val keyProvider: JwtKeyProvider, val config: Config) extends SessionManager {
class BaseSessionManager @Inject()(val keyProvider: JwtKeyProvider, val userManager: StubUserManager, val config: Config) extends SessionManager {
private val algorithm = Algorithm.RSA512(keyProvider.publicKey, keyProvider.privateKey)
private val verifier: JWTVerifier = JWT.require(algorithm)
.withIssuer(config.getString("auth.issuer"))
.withAudience(config.getString("auth.audience"))
.build()
//TODO reduce cache to a minimum amount, as JWT should be self-contained
private val cache: Cache[String, User] = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES).build()
override def createSession(user: User): String = {
//Write session identifier to cache and DB
val sessionId = JWT.create()
.withIssuer(config.getString("auth.issuer"))
.withAudience(config.getString("auth.audience"))
.withSubject(user.internalId.toString)
.withSubject(user.id.toString)
.withClaim("id", user.internalId)
.withExpiresAt(Instant.now.plus(7, ChronoUnit.DAYS))
.sign(algorithm)
//TODO write to DB
//TODO write to Redis and DB
cache.put(sessionId, user)
sessionId
}
override def getUserBySession(sessionId: String): Option[User] = {
//TODO verify JWT token instead of looking up in cache
//Read session identifier from cache and DB
None
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
}
}
override def invalidateSession(sessionId: String): Unit = {
//TODO remove from Redis and DB
cache.invalidate(sessionId)
}
}

View File

@@ -40,6 +40,10 @@ class StubUserManager @Inject()(val config: Config) extends UserManager {
user.get(name)
}
override def userExistsById(id: Long): Option[User] = {
user.values.find(_.internalId == id)
}
override def removeUser(name: String): Boolean = {
throw new NotImplementedError("StubUserManager.removeUser is not implemented")
}

View File

@@ -1,8 +1,8 @@
package services
import java.nio.file.{Files, Paths}
import java.security.{KeyFactory, PrivateKey, PublicKey}
import java.security.spec.X509EncodedKeySpec
import java.security.{KeyFactory, KeyPair, PrivateKey, PublicKey}
import java.security.spec.{PKCS8EncodedKeySpec, RSAPublicKeySpec, X509EncodedKeySpec}
import java.util.Base64
import javax.inject.*
import play.api.Configuration
@@ -25,7 +25,7 @@ class JwtKeyProvider @Inject()(config: Configuration) {
private def loadPrivateKeyFromPem(pem: String): RSAPrivateKey = {
val decoded = Base64.getDecoder.decode(cleanPem(pem))
val spec = new X509EncodedKeySpec(decoded)
val spec = new PKCS8EncodedKeySpec(decoded)
KeyFactory.getInstance("RSA").generatePrivate(spec).asInstanceOf[RSAPrivateKey]
}

View File

@@ -1,7 +1,7 @@
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic)
@main("Ingame") {
<div id="ingame">
<div id="ingame" class="game-field game-field-background">
<h1>Knockout Whist</h1>
<div id="nextPlayers">
<p>Next Player:</p>

View File

@@ -1,18 +1,41 @@
@()
@main("Login") {
<div class="container">
<h2>Login</h2>
<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="form-group">
<label for="username">Username:</label>
<input type="text" class="form-control" id="username" name="username" required>
<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="form-group">
<label for="password">Password:</label>
<input type="password" class="form-control" id="password" name="password" required>
<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>
</form>
</div>
</form>
<p class="text-center mt-3">
Dont 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>
}

View File

@@ -13,6 +13,8 @@
<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>
@@ -21,5 +23,6 @@
@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>

View File

@@ -1,7 +1,7 @@
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic)
@main("Selecting Trumpsuit...") {
<div id="selecttrumpsuit">
<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>

View File

@@ -2,7 +2,7 @@
@(sessions: List[PlayerSession])
@main("Sessions") {
<div id="sessions">
<div id="sessions" class="game-field-background">
<h1>Knockout Whist sessions</h1>
<p id="textanimation">Please select your session to jump inside the game!</p>
@for(session <- sessions) {

View File

@@ -1,7 +1,7 @@
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic)
@main("Tie") {
<div id="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) {

View File

@@ -5,8 +5,10 @@ play.filters.disabled += play.filters.csrf.CSRFFilter
auth {
issuer = "knockoutwhistweb"
audience = "ui"
privateKeyFile = D:\Workspaces\Gitops\rsa512-private.pem # ${?PUBLIC_KEY_FILE}
# ${?PUBLIC_KEY_FILE}
privateKeyFile = "D:\\Workspaces\\Gitops\\rsa512-private.pem"
privateKeyPem = ${?PUBLIC_KEY_PEM}
publicKeyFile = D:\Workspaces\Gitops\rsa512-public.pem #${?PUBLIC_KEY_FILE}
#${?PUBLIC_KEY_FILE}
publicKeyFile = "D:\\Workspaces\\Gitops\\rsa512-public.pem"
publicKeyPem = ${?PUBLIC_KEY_PEM}
}

View File

@@ -0,0 +1,110 @@
{
"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
}

View File

@@ -0,0 +1,3 @@
particlesJS.load('particles-js', 'assets/conf/particlesjs-config.json', function() {
console.log('callback - particles.js config loaded');
});

File diff suppressed because it is too large Load Diff