chore: rebase
This commit is contained in:
Submodule knockoutwhist updated: f998d5f6f0...fbc0ea2277
35
knockoutwhistweb/app/assets/stylesheets/login.less
Normal file
35
knockoutwhistweb/app/assets/stylesheets/login.less
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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("")
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
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>
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
110
knockoutwhistweb/public/conf/particlesjs-config.json
Normal file
110
knockoutwhistweb/public/conf/particlesjs-config.json
Normal 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
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
particlesJS.load('particles-js', 'assets/conf/particlesjs-config.json', function() {
|
||||
console.log('callback - particles.js config loaded');
|
||||
});
|
||||
1541
knockoutwhistweb/public/javascripts/particles.js
Normal file
1541
knockoutwhistweb/public/javascripts/particles.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user