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;
@@ -31,8 +34,9 @@ html, body {
animation: slideIn 0.5s ease-out forwards;
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>
<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="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">
Dont have an account?
<a href="#" class="text-decoration-none">Sign up</a>
</p>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
</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>
@@ -20,6 +22,7 @@
* the page content. *@
@content
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
<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) {