From a9edec4c0fb126c96a76c5588040715bec5edcc1 Mon Sep 17 00:00:00 2001 From: TheSaminator Date: Mon, 7 Feb 2022 09:57:09 -0500 Subject: [PATCH] A lot of various improvements --- .../kotlin/starshipfights/auth/providers.kt | 56 +++++++++++++++---- .../kotlin/starshipfights/auth/utils.kt | 10 ++++ .../starshipfights/data/auth/user_sessions.kt | 11 +++- .../starshipfights/data/data_routines.kt | 17 +++++- .../starshipfights/game/endpoints_game.kt | 38 ++++++++----- .../kotlin/starshipfights/info/views_user.kt | 38 +++++++------ src/jvmMain/kotlin/starshipfights/server.kt | 2 +- 7 files changed, 123 insertions(+), 49 deletions(-) diff --git a/src/jvmMain/kotlin/starshipfights/auth/providers.kt b/src/jvmMain/kotlin/starshipfights/auth/providers.kt index f76dd3d..3daa6a5 100644 --- a/src/jvmMain/kotlin/starshipfights/auth/providers.kt +++ b/src/jvmMain/kotlin/starshipfights/auth/providers.kt @@ -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>("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() + 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 } } } diff --git a/src/jvmMain/kotlin/starshipfights/auth/utils.kt b/src/jvmMain/kotlin/starshipfights/auth/utils.kt index 83c8fb2..ef350d3 100644 --- a/src/jvmMain/kotlin/starshipfights/auth/utils.kt +++ b/src/jvmMain/kotlin/starshipfights/auth/utils.kt @@ -19,3 +19,13 @@ suspend fun UserSession.renewed(clientAddress: String) = copy( suspend fun ApplicationCall.getUserSession() = request.userAgent()?.let { sessions.get>()?.resolve(it) } suspend fun ApplicationCall.getUser() = getUserSession()?.user?.let { User.get(it) } + +object UserSessionIdSerializer : SessionSerializer> { + override fun serialize(session: Id): String { + return session.id + } + + override fun deserialize(text: String): Id { + return Id(text) + } +} diff --git a/src/jvmMain/kotlin/starshipfights/data/auth/user_sessions.kt b/src/jvmMain/kotlin/starshipfights/data/auth/user_sessions.kt index cc4e483..d08add5 100644 --- a/src/jvmMain/kotlin/starshipfights/data/auth/user_sessions.kt +++ b/src/jvmMain/kotlin/starshipfights/data/auth/user_sessions.kt @@ -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 = Id(), val username: String, - val isInBattle: Boolean = false, + val status: UserStatus = UserStatus.AVAILABLE, ) : DataDocument { companion object Table : DocumentTable by DocumentTable.create({ unique(User::username) }) } +enum class UserStatus { + AVAILABLE, IN_MATCHMAKING, IN_BATTLE +} + @Serializable data class UserSession( @SerialName("_id") diff --git a/src/jvmMain/kotlin/starshipfights/data/data_routines.kt b/src/jvmMain/kotlin/starshipfights/data/data_routines.kt index aef066d..053024b 100644 --- a/src/jvmMain/kotlin/starshipfights/data/data_routines.kt +++ b/src/jvmMain/kotlin/starshipfights/data/data_routines.kt @@ -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) } } } diff --git a/src/jvmMain/kotlin/starshipfights/game/endpoints_game.kt b/src/jvmMain/kotlin/starshipfights/game/endpoints_game.kt index 1a1a04e..970b898 100644 --- a/src/jvmMain/kotlin/starshipfights/game/endpoints_game.kt +++ b/src/jvmMain/kotlin/starshipfights/game/endpoints_game.kt @@ -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) } } diff --git a/src/jvmMain/kotlin/starshipfights/info/views_user.kt b/src/jvmMain/kotlin/starshipfights/info/views_user.kt index 8e64fd6..eb16a90 100644 --- a/src/jvmMain/kotlin/starshipfights/info/views_user.kt +++ b/src/jvmMain/kotlin/starshipfights/info/views_user.kt @@ -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 { diff --git a/src/jvmMain/kotlin/starshipfights/server.kt b/src/jvmMain/kotlin/starshipfights/server.kt index 663237b..2285f9b 100644 --- a/src/jvmMain/kotlin/starshipfights/server.kt +++ b/src/jvmMain/kotlin/starshipfights/server.kt @@ -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) { -- 2.25.1