A lot of various improvements
authorTheSaminator <TheSaminator@users.noreply.github.com>
Mon, 7 Feb 2022 14:57:09 +0000 (09:57 -0500)
committerTheSaminator <TheSaminator@users.noreply.github.com>
Mon, 7 Feb 2022 14:57:09 +0000 (09:57 -0500)
src/jvmMain/kotlin/starshipfights/auth/providers.kt
src/jvmMain/kotlin/starshipfights/auth/utils.kt
src/jvmMain/kotlin/starshipfights/data/auth/user_sessions.kt
src/jvmMain/kotlin/starshipfights/data/data_routines.kt
src/jvmMain/kotlin/starshipfights/game/endpoints_game.kt
src/jvmMain/kotlin/starshipfights/info/views_user.kt
src/jvmMain/kotlin/starshipfights/server.kt

index f76dd3dec8ffb5cfcbb899ef1bad5710292f8b69..3daa6a509690ea9055aa668ac50f44b46fd68ace 100644 (file)
@@ -27,6 +27,7 @@ import starshipfights.info.*
 import starshipfights.redirect
 
 interface AuthProvider {
+       fun installApplication(app: Application) = Unit
        fun installAuth(conf: Authentication.Configuration)
        fun installRouting(conf: Routing)
        
@@ -38,10 +39,15 @@ interface AuthProvider {
                                TODO("Need to implement production AuthProvider")
                
                fun install(into: Application) {
+                       currentProvider.installApplication(into)
+                       
                        into.install(Sessions) {
                                cookie<Id<UserSession>>("sf_user_session") {
+                                       serializer = UserSessionIdSerializer
+                                       
                                        cookie.path = "/"
-                                       cookie.maxAgeInSeconds = 900
+                                       cookie.secure = true
+                                       cookie.extensions["SameSite"] = "Lax"
                                }
                        }
                        
@@ -183,27 +189,42 @@ interface AuthProvider {
 }
 
 object TestAuthProvider : AuthProvider {
-       private const val TEST_PASSWORD = "very secure"
+       private const val USERNAME_KEY = "username"
+       private const val PASSWORD_KEY = "password"
+       private const val REMEMBER_ME_KEY = "remember-me"
+       
+       private const val PASSWORD_VALUE = "very secure"
+       private const val REMEMBER_ME_VALUE = "yes"
+       
+       override fun installApplication(app: Application) {
+               app.install(DoubleReceive)
+       }
        
        override fun installAuth(conf: Authentication.Configuration) {
                with(conf) {
                        form("test-auth") {
-                               userParamName = "username"
-                               passwordParamName = "password"
+                               userParamName = USERNAME_KEY
+                               passwordParamName = PASSWORD_KEY
                                validate { credentials ->
                                        val originAddress = request.origin.remoteHost
                                        val userAgent = request.userAgent()
-                                       if (userAgent != null && credentials.name.isValidUsername() && credentials.password == TEST_PASSWORD) {
+                                       if (userAgent != null && credentials.name.isValidUsername() && credentials.password == PASSWORD_VALUE) {
                                                val user = User.locate(User::username eq credentials.name)
                                                        ?: User(username = credentials.name).also {
                                                                User.put(it)
                                                        }
                                                
+                                               val formParams = receiveOrNull<Parameters>()
+                                               val timeToRemember = if (formParams?.get(REMEMBER_ME_KEY) == REMEMBER_ME_VALUE)
+                                                       31_556_925_216L // 1 solar year
+                                               else
+                                                       3_600_000L // 1 hour
+                                               
                                                UserSession(
                                                        user = user.id,
                                                        clientAddresses = listOf(originAddress),
                                                        userAgent = userAgent,
-                                                       expirationMillis = System.currentTimeMillis() + 900_000
+                                                       expirationMillis = System.currentTimeMillis() + timeToRemember
                                                ).also {
                                                        UserSession.put(it)
                                                }
@@ -217,7 +238,7 @@ object TestAuthProvider : AuthProvider {
                                                "A username must be provided."
                                        else if (!credentials.name.isValidUsername())
                                                invalidUsernameErrorMessage
-                                       else if (credentials.password != TEST_PASSWORD)
+                                       else if (credentials.password != PASSWORD_VALUE)
                                                "Password is incorrect."
                                        else
                                                "An unknown error occurred."
@@ -248,13 +269,13 @@ object TestAuthProvider : AuthProvider {
                                                form(action = "/login", method = FormMethod.post) {
                                                        h3 {
                                                                label {
-                                                                       this.htmlFor = "username"
+                                                                       this.htmlFor = USERNAME_KEY
                                                                        +"Username"
                                                                }
                                                        }
                                                        textInput {
-                                                               id = "username"
-                                                               name = "username"
+                                                               id = USERNAME_KEY
+                                                               name = USERNAME_KEY
                                                                autoComplete = false
                                                                
                                                                required = true
@@ -269,12 +290,23 @@ object TestAuthProvider : AuthProvider {
                                                                        +msg
                                                                }
                                                        }
+                                                       p {
+                                                               label {
+                                                                       htmlFor = REMEMBER_ME_KEY
+                                                                       checkBoxInput {
+                                                                               id = REMEMBER_ME_KEY
+                                                                               name = REMEMBER_ME_KEY
+                                                                               value = REMEMBER_ME_VALUE
+                                                                       }
+                                                                       +"Remember Me"
+                                                               }
+                                                       }
                                                        submitInput {
                                                                value = "Authenticate"
                                                        }
                                                        hiddenInput {
-                                                               name = "password"
-                                                               value = TEST_PASSWORD
+                                                               name = PASSWORD_KEY
+                                                               value = PASSWORD_VALUE
                                                        }
                                                }
                                        }
index 83c8fb2b06b9ac0f2c1be98705a32d173e963381..ef350d3daafb99dfe340668ff13285019ac62917 100644 (file)
@@ -19,3 +19,13 @@ suspend fun UserSession.renewed(clientAddress: String) = copy(
 suspend fun ApplicationCall.getUserSession() = request.userAgent()?.let { sessions.get<Id<UserSession>>()?.resolve(it) }
 
 suspend fun ApplicationCall.getUser() = getUserSession()?.user?.let { User.get(it) }
+
+object UserSessionIdSerializer : SessionSerializer<Id<UserSession>> {
+       override fun serialize(session: Id<UserSession>): String {
+               return session.id
+       }
+       
+       override fun deserialize(text: String): Id<UserSession> {
+               return Id(text)
+       }
+}
index cc4e483f4a2b125cc39ebe5ebf171e399b5bf849..d08add509692d1f8722f3cd766b261a8e016f179 100644 (file)
@@ -3,7 +3,10 @@ package starshipfights.data.auth
 import io.ktor.auth.*
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
-import starshipfights.data.*
+import starshipfights.data.DataDocument
+import starshipfights.data.DocumentTable
+import starshipfights.data.Id
+import starshipfights.data.invoke
 
 const val usernameRegexStr = "[a-zA-Z0-9_\\-]{2,32}"
 val usernameRegex = Regex(usernameRegexStr)
@@ -16,13 +19,17 @@ data class User(
        @SerialName("_id")
        override val id: Id<User> = Id(),
        val username: String,
-       val isInBattle: Boolean = false,
+       val status: UserStatus = UserStatus.AVAILABLE,
 ) : DataDocument<User> {
        companion object Table : DocumentTable<User> by DocumentTable.create({
                unique(User::username)
        })
 }
 
+enum class UserStatus {
+       AVAILABLE, IN_MATCHMAKING, IN_BATTLE
+}
+
 @Serializable
 data class UserSession(
        @SerialName("_id")
index aef066d25186e88b067daf113ea65f1ad204b3ec..053024b2a9dea23fd9898c5add476a494525e365 100644 (file)
@@ -1,13 +1,18 @@
 package starshipfights.data
 
 import kotlinx.coroutines.*
+import org.litote.kmongo.div
+import org.litote.kmongo.lt
 import org.litote.kmongo.lte
+import org.litote.kmongo.setValue
 import starshipfights.data.admiralty.Admiral
 import starshipfights.data.admiralty.BattleRecord
+import starshipfights.data.admiralty.DrydockStatus
 import starshipfights.data.admiralty.ShipInDrydock
 import starshipfights.data.auth.User
 import starshipfights.data.auth.UserSession
 import starshipfights.sfLogger
+import java.time.Instant
 import kotlin.coroutines.CoroutineContext
 
 object DataRoutines : CoroutineScope {
@@ -30,7 +35,17 @@ object DataRoutines : CoroutineScope {
                                        launch {
                                                UserSession.remove(UserSession::expirationMillis lte System.currentTimeMillis())
                                        }
-                                       delay(3600_000)
+                                       delay(900_000)
+                               }
+                       }
+                       
+                       launch {
+                               while (currentCoroutineContext().isActive) {
+                                       val now = Instant.now()
+                                       launch {
+                                               ShipInDrydock.update(ShipInDrydock::status / DrydockStatus.InRepair::until lt now, setValue(ShipInDrydock::status, DrydockStatus.Ready))
+                                       }
+                                       delay(300_000)
                                }
                        }
                }
index 1a1a04e597306a1ab9026592aa6700e30b0bef27..970b898277c940c527a92c8947e013d94b3d0939 100644 (file)
@@ -9,16 +9,17 @@ import kotlinx.coroutines.launch
 import starshipfights.auth.getUser
 import starshipfights.data.admiralty.getAllInGameAdmirals
 import starshipfights.data.auth.User
+import starshipfights.data.auth.UserStatus
 import starshipfights.redirect
 
 fun Routing.installGame() {
        get("/lobby") {
                val user = call.getUser() ?: redirect("/login")
                
-               val clientMode = if (user.isInBattle)
-                       ClientMode.Error("You cannot play in multiple battles at the same time")
-               else
+               val clientMode = if (user.status == UserStatus.AVAILABLE)
                        ClientMode.MatchmakingMenu(getAllInGameAdmirals(user))
+               else
+                       ClientMode.Error("You cannot play in multiple battles at the same time")
                
                call.respondHtml(HttpStatusCode.OK, clientMode.view())
        }
@@ -26,19 +27,25 @@ fun Routing.installGame() {
        post("/play") {
                val user = call.getUser() ?: redirect("/login")
                
-               val clientMode = if (user.isInBattle)
-                       ClientMode.Error("You cannot play in multiple battles at the same time")
-               else
-                       call.getGameClientMode()
+               val clientMode = when (user.status) {
+                       UserStatus.AVAILABLE -> ClientMode.Error("You must use the matchmaking interface to enter a game")
+                       UserStatus.IN_MATCHMAKING -> call.getGameClientMode()
+                       UserStatus.IN_BATTLE -> ClientMode.Error("You cannot play in multiple battles at the same time")
+               }
                
                call.respondHtml(HttpStatusCode.OK, clientMode.view())
        }
        
        webSocket("/matchmaking") {
-               val user = call.getUser() ?: closeAndReturn("You must be logged in to play") { return@webSocket }
-               if (user.isInBattle)
+               val oldUser = call.getUser() ?: closeAndReturn("You must be logged in to play") { return@webSocket }
+               if (oldUser.status != UserStatus.AVAILABLE)
                        closeAndReturn("You cannot play in multiple battles at the same time") { return@webSocket }
                
+               val user = oldUser.copy(status = UserStatus.IN_MATCHMAKING)
+               launch {
+                       User.put(user)
+               }
+               
                matchmakingEndpoint(user)
        }
        
@@ -46,18 +53,19 @@ fun Routing.installGame() {
                val token = call.parameters["token"] ?: closeAndReturn("Invalid or missing battle token") { return@webSocket }
                
                val oldUser = call.getUser() ?: closeAndReturn("You must be logged in to play") { return@webSocket }
-               if (oldUser.isInBattle)
+               
+               if (oldUser.status == UserStatus.IN_BATTLE)
                        closeAndReturn("You cannot play in multiple battles at the same time") { return@webSocket }
+               if (oldUser.status == UserStatus.AVAILABLE)
+                       closeAndReturn("You must use the matchmaking interface to enter a game") { return@webSocket }
                
-               val user = oldUser.copy(isInBattle = true)
-               launch {
-                       User.put(user)
-               }
+               val user = oldUser.copy(status = UserStatus.IN_BATTLE)
+               User.put(user)
                
                gameEndpoint(user, token)
                
-               val postGameUser = user.copy(isInBattle = false)
                launch {
+                       val postGameUser = user.copy(status = UserStatus.AVAILABLE)
                        User.put(postGameUser)
                }
        }
index 8e64fd6f4d89d594411e0196e3e0f5c73367c314..eb16a901269ed7c6d04daec23dff79c6b90330d5 100644 (file)
@@ -156,27 +156,29 @@ suspend fun ApplicationCall.createAdmiralPage(): HTML.() -> Unit {
                                                }
                                                +"Female"
                                        }
-                                       h3 {
-                                               label {
-                                                       htmlFor = "faction"
-                                                       +"Faction"
-                                               }
-                                       }
                                }
-                               Faction.values().forEach { faction ->
-                                       val factionId = "faction-${faction.toUrlSlug()}"
+                               h3 {
                                        label {
-                                               htmlFor = factionId
-                                               radioInput(name = "faction") {
-                                                       id = factionId
-                                                       value = faction.name
-                                                       required = true
-                                               }
-                                               img(src = faction.flagUrl) {
-                                                       style = "height:0.75em;width:1.2em"
+                                               htmlFor = "faction"
+                                               +"Faction"
+                                       }
+                               }
+                               p {
+                                       Faction.values().forEach { faction ->
+                                               val factionId = "faction-${faction.toUrlSlug()}"
+                                               label {
+                                                       htmlFor = factionId
+                                                       radioInput(name = "faction") {
+                                                               id = factionId
+                                                               value = faction.name
+                                                               required = true
+                                                       }
+                                                       img(src = faction.flagUrl) {
+                                                               style = "height:0.75em;width:1.2em"
+                                                       }
+                                                       +Entities.nbsp
+                                                       +faction.shortName
                                                }
-                                               +Entities.nbsp
-                                               +faction.shortName
                                        }
                                }
                                submitInput {
index 663237bdfc834ce3eb39e9c39ff23e15bec4cb60..2285f9b96f2406c236de9636391552532151882a 100644 (file)
@@ -36,7 +36,7 @@ fun main() {
        
        val dataRoutines = DataRoutines.initializeRoutines()
        
-       embeddedServer(Netty, port = 8080, host = "127.0.0.1") {
+       embeddedServer(Netty, port = CurrentConfiguration.port, host = CurrentConfiguration.host) {
                install(XForwardedHeaderSupport)
                
                install(CallId) {