import starshipfights.redirect
interface AuthProvider {
+ fun installApplication(app: Application) = Unit
fun installAuth(conf: Authentication.Configuration)
fun installRouting(conf: Routing)
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"
}
}
}
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)
}
"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."
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
+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
}
}
}
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)
+ }
+}
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)
@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")
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 {
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)
}
}
}
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())
}
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)
}
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)
}
}
}
+"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 {
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) {