diff --git a/knockoutwhist b/knockoutwhist index f998d5f..fbc0ea2 160000 --- a/knockoutwhist +++ b/knockoutwhist @@ -1 +1 @@ -Subproject commit f998d5f6f01ca6d86d23efade8615b52ab23c484 +Subproject commit fbc0ea2277596e2a2d29125b5f9a84213336dc18 diff --git a/knockoutwhistweb/app/assets/stylesheets/login.less b/knockoutwhistweb/app/assets/stylesheets/login.less new file mode 100644 index 0000000..2e3bb1e --- /dev/null +++ b/knockoutwhistweb/app/assets/stylesheets/login.less @@ -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; +} \ No newline at end of file diff --git a/knockoutwhistweb/app/assets/stylesheets/main.less b/knockoutwhistweb/app/assets/stylesheets/main.less index 6c77638..8bc3b69 100644 --- a/knockoutwhistweb/app/assets/stylesheets/main.less +++ b/knockoutwhistweb/app/assets/stylesheets/main.less @@ -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; diff --git a/knockoutwhistweb/app/controllers/UserController.scala b/knockoutwhistweb/app/controllers/UserController.scala index db36946..5e566ea 100644 --- a/knockoutwhistweb/app/controllers/UserController.scala +++ b/knockoutwhistweb/app/controllers/UserController.scala @@ -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("") diff --git a/knockoutwhistweb/app/logic/user/UserManager.scala b/knockoutwhistweb/app/logic/user/UserManager.scala index b6a4f47..ecf3a8d 100644 --- a/knockoutwhistweb/app/logic/user/UserManager.scala +++ b/knockoutwhistweb/app/logic/user/UserManager.scala @@ -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 } diff --git a/knockoutwhistweb/app/logic/user/impl/BaseSessionManager.scala b/knockoutwhistweb/app/logic/user/impl/BaseSessionManager.scala index b7ba80e..88e046c 100644 --- a/knockoutwhistweb/app/logic/user/impl/BaseSessionManager.scala +++ b/knockoutwhistweb/app/logic/user/impl/BaseSessionManager.scala @@ -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) } } diff --git a/knockoutwhistweb/app/logic/user/impl/StubUserManager.scala b/knockoutwhistweb/app/logic/user/impl/StubUserManager.scala index a367358..44f71ee 100644 --- a/knockoutwhistweb/app/logic/user/impl/StubUserManager.scala +++ b/knockoutwhistweb/app/logic/user/impl/StubUserManager.scala @@ -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") } diff --git a/knockoutwhistweb/app/services/JwtKeyProvider.scala b/knockoutwhistweb/app/services/JwtKeyProvider.scala index 9a4d624..7e87c0d 100644 --- a/knockoutwhistweb/app/services/JwtKeyProvider.scala +++ b/knockoutwhistweb/app/services/JwtKeyProvider.scala @@ -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] } diff --git a/knockoutwhistweb/app/views/ingame.scala.html b/knockoutwhistweb/app/views/ingame.scala.html index 379f219..23eaf99 100644 --- a/knockoutwhistweb/app/views/ingame.scala.html +++ b/knockoutwhistweb/app/views/ingame.scala.html @@ -1,7 +1,7 @@ @(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic) @main("Ingame") { -
Next Player:
diff --git a/knockoutwhistweb/app/views/login.scala.html b/knockoutwhistweb/app/views/login.scala.html index e254fa4..f2a5365 100644 --- a/knockoutwhistweb/app/views/login.scala.html +++ b/knockoutwhistweb/app/views/login.scala.html @@ -1,18 +1,41 @@ @() @main("Login") { -