Add production-env authentication
authorTheSaminator <TheSaminator@users.noreply.github.com>
Tue, 8 Feb 2022 22:17:35 +0000 (17:17 -0500)
committerTheSaminator <TheSaminator@users.noreply.github.com>
Tue, 8 Feb 2022 22:17:35 +0000 (17:17 -0500)
build.gradle.kts
src/jvmMain/kotlin/starshipfights/auth/providers.kt
src/jvmMain/kotlin/starshipfights/auth/utils.kt
src/jvmMain/kotlin/starshipfights/data/admiralty/admirals.kt
src/jvmMain/kotlin/starshipfights/data/auth/user_sessions.kt
src/jvmMain/kotlin/starshipfights/data/data_jvm.kt
src/jvmMain/kotlin/starshipfights/game/server_game.kt
src/jvmMain/kotlin/starshipfights/info/view_nav.kt
src/jvmMain/kotlin/starshipfights/info/views_user.kt
src/jvmMain/kotlin/starshipfights/server_conf.kt
src/jvmMain/kotlin/starshipfights/server_utils.kt

index dd4d7b4553fc19983e0412b3274a9e1f8a9511a3..74fd25ced5be4838fa561c409dba4a5e123f3d6f 100644 (file)
@@ -54,6 +54,7 @@ kotlin {
                                implementation("io.ktor:ktor-auth:1.6.7")
                                implementation("io.ktor:ktor-serialization:1.6.7")
                                implementation("io.ktor:ktor-websockets:1.6.7")
+                               implementation("io.ktor:ktor-client-apache:1.6.7")
                                
                                implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.7.3")
                                implementation("org.slf4j:slf4j-api:1.7.31")
index cfce085fe66f04451900262e6331e2edded74b6e..23c2c9a7f0b5d62c76c6ef6ea968dfa3d599cb53 100644 (file)
@@ -1,8 +1,10 @@
 package starshipfights.auth
 
-import com.mongodb.MongoException
 import io.ktor.application.*
 import io.ktor.auth.*
+import io.ktor.client.*
+import io.ktor.client.engine.apache.*
+import io.ktor.client.request.*
 import io.ktor.features.*
 import io.ktor.html.*
 import io.ktor.http.*
@@ -14,20 +16,23 @@ import io.ktor.util.*
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
 import kotlinx.html.*
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
 import org.litote.kmongo.and
 import org.litote.kmongo.eq
 import org.litote.kmongo.ne
 import org.litote.kmongo.setValue
-import starshipfights.CurrentConfiguration
+import starshipfights.*
 import starshipfights.data.Id
 import starshipfights.data.admiralty.Admiral
 import starshipfights.data.admiralty.ShipInDrydock
 import starshipfights.data.admiralty.generateFleet
-import starshipfights.data.auth.*
+import starshipfights.data.auth.User
+import starshipfights.data.auth.UserSession
+import starshipfights.data.createNonce
 import starshipfights.game.AdmiralRank
 import starshipfights.game.Faction
 import starshipfights.info.*
-import starshipfights.redirect
 
 interface AuthProvider {
        fun installApplication(app: Application) = Unit
@@ -36,10 +41,7 @@ interface AuthProvider {
        
        companion object Installer {
                private val currentProvider: AuthProvider
-                       get() = if (CurrentConfiguration.isDevEnv)
-                               TestAuthProvider
-                       else
-                               TODO("Need to implement production AuthProvider")
+                       get() = CurrentConfiguration.discordClient?.let { ProductionAuthProvider(it) } ?: TestAuthProvider
                
                fun install(into: Application) {
                        currentProvider.installApplication(into)
@@ -70,145 +72,138 @@ interface AuthProvider {
                                authenticate("session") {
                                        get("/me") {
                                                val redirectTo = call.principal<UserSession>()?.let {
-                                                       User.get(it.user)?.username?.let { name ->
-                                                               "/user/$name"
-                                                       }
+                                                       "/user/${it.user}"
                                                } ?: "/login"
                                                
                                                redirect(redirectTo)
                                        }
-                               }
-                               
-                               get("/me/manage") {
-                                       call.respondHtml(HttpStatusCode.OK, call.manageUserPage())
-                               }
-                               
-                               post("/me/manage") {
-                                       val currentUser = call.getUser() ?: redirect("/login")
-                                       val form = call.receiveParameters()
                                        
-                                       val newName = form.getOrFail("name")
-                                       if (usernameRegex.matchEntire(newName) == null)
-                                               redirect("/me/manage?" + parametersOf("error", invalidUsernameErrorMessage).formUrlEncode())
+                                       get("/me/manage") {
+                                               call.respondHtml(HttpStatusCode.OK, call.manageUserPage())
+                                       }
                                        
-                                       val newUser = currentUser.copy(
-                                               username = form.getOrFail("name")
-                                       )
-                                       try {
+                                       post("/me/manage") {
+                                               val currentUser = call.getUser() ?: redirect("/login")
+                                               val form = call.receiveParameters()
+                                               
+                                               val newUser = currentUser.copy(
+                                                       profileName = form["name"]?.takeIf { it.isNotBlank() } ?: currentUser.profileName
+                                               )
                                                User.put(newUser)
-                                               redirect("/user/${newUser.username}")
-                                       } catch (ex: MongoException) {
-                                               redirect("/me/manage?" + parametersOf("error", "That username is already taken").formUrlEncode())
+                                               redirect("/user/${newUser.id}")
                                        }
                                }
                                
-                               get("/user/{name}") {
+                               get("/user/{id}") {
                                        call.respondHtml(HttpStatusCode.OK, call.userPage())
                                }
                                
-                               get("/admiral/new") {
-                                       call.respondHtml(HttpStatusCode.OK, call.createAdmiralPage())
-                               }
-                               
-                               post("/admiral/new") {
-                                       val currentUser = call.getUserSession()?.user ?: redirect("/login")
-                                       val form = call.receiveParameters()
-                                       
-                                       val newAdmiral = Admiral(
-                                               owningUser = currentUser,
-                                               name = form["name"]?.takeIf { it.isNotBlank() } ?: throw MissingRequestParameterException("name"),
-                                               isFemale = form.getOrFail("sex") == "female",
-                                               faction = Faction.valueOf(form.getOrFail("faction")),
-                                               // TODO change to Rear Admiral
-                                               rank = AdmiralRank.LORD_ADMIRAL
-                                       )
-                                       val newShips = generateFleet(newAdmiral)
+                               authenticate("session") {
+                                       get("/admiral/new") {
+                                               call.respondHtml(HttpStatusCode.OK, call.createAdmiralPage())
+                                       }
                                        
-                                       coroutineScope {
-                                               launch { Admiral.put(newAdmiral) }
-                                               newShips.forEach {
-                                                       launch { ShipInDrydock.put(it) }
+                                       post("/admiral/new") {
+                                               val currentUser = call.getUserSession()?.user ?: redirect("/login")
+                                               val form = call.receiveParameters()
+                                               
+                                               val newAdmiral = Admiral(
+                                                       owningUser = currentUser,
+                                                       name = form["name"]?.takeIf { it.isNotBlank() } ?: throw MissingRequestParameterException("name"),
+                                                       isFemale = form.getOrFail("sex") == "female",
+                                                       faction = Faction.valueOf(form.getOrFail("faction")),
+                                                       // TODO change to Rear Admiral
+                                                       rank = AdmiralRank.LORD_ADMIRAL
+                                               )
+                                               val newShips = generateFleet(newAdmiral)
+                                               
+                                               coroutineScope {
+                                                       launch { Admiral.put(newAdmiral) }
+                                                       newShips.forEach {
+                                                               launch { ShipInDrydock.put(it) }
+                                                       }
                                                }
+                                               
+                                               redirect("/admiral/${newAdmiral.id}")
                                        }
-                                       
-                                       redirect("/admiral/${newAdmiral.id}")
                                }
                                
                                get("/admiral/{id}") {
                                        call.respondHtml(HttpStatusCode.OK, call.admiralPage())
                                }
                                
-                               get("/admiral/{id}/manage") {
-                                       call.respondHtml(HttpStatusCode.OK, call.manageAdmiralPage())
-                               }
-                               
-                               post("/admiral/{id}/manage") {
-                                       val currentUser = call.getUserSession()?.user
-                                       val admiralId = call.parameters["id"]?.let { Id<Admiral>(it) }!!
-                                       val admiral = Admiral.get(admiralId)!!
-                                       
-                                       if (admiral.owningUser != currentUser) throw IllegalArgumentException()
+                               authenticate("session") {
+                                       get("/admiral/{id}/manage") {
+                                               call.respondHtml(HttpStatusCode.OK, call.manageAdmiralPage())
+                                       }
                                        
-                                       val form = call.receiveParameters()
-                                       val newAdmiral = admiral.copy(
-                                               name = form["name"]?.takeIf { it.isNotBlank() } ?: admiral.name,
-                                               isFemale = form["sex"] == "female"
-                                       )
+                                       post("/admiral/{id}/manage") {
+                                               val currentUser = call.getUserSession()?.user
+                                               val admiralId = call.parameters["id"]?.let { Id<Admiral>(it) }!!
+                                               val admiral = Admiral.get(admiralId)!!
+                                               
+                                               if (admiral.owningUser != currentUser) throw ForbiddenException()
+                                               
+                                               val form = call.receiveParameters()
+                                               val newAdmiral = admiral.copy(
+                                                       name = form["name"]?.takeIf { it.isNotBlank() } ?: admiral.name,
+                                                       isFemale = form["sex"] == "female"
+                                               )
+                                               
+                                               Admiral.put(newAdmiral)
+                                               redirect("/admiral/$admiralId")
+                                       }
                                        
-                                       Admiral.put(newAdmiral)
-                                       redirect("/admiral/$admiralId")
-                               }
-                               
-                               get("/admiral/{id}/delete") {
-                                       call.respondHtml(HttpStatusCode.OK, call.deleteAdmiralConfirmPage())
-                               }
-                               
-                               post("/admiral/{id}/delete") {
-                                       val currentUser = call.getUserSession()?.user
-                                       val admiralId = call.parameters["id"]?.let { Id<Admiral>(it) }!!
-                                       val admiral = Admiral.get(admiralId)!!
+                                       get("/admiral/{id}/delete") {
+                                               call.respondHtml(HttpStatusCode.OK, call.deleteAdmiralConfirmPage())
+                                       }
                                        
-                                       if (admiral.owningUser != currentUser) throw IllegalArgumentException()
+                                       post("/admiral/{id}/delete") {
+                                               val currentUser = call.getUserSession()?.user
+                                               val admiralId = call.parameters["id"]?.let { Id<Admiral>(it) }!!
+                                               val admiral = Admiral.get(admiralId)!!
+                                               
+                                               if (admiral.owningUser != currentUser) throw ForbiddenException()
+                                               
+                                               Admiral.del(admiralId)
+                                               redirect("/me")
+                                       }
                                        
-                                       Admiral.del(admiralId)
-                                       redirect("/me")
-                               }
-                               
-                               get("/logout") {
-                                       call.getUserSession()?.let { sess ->
-                                               launch {
-                                                       val newTime = System.currentTimeMillis() - 100
-                                                       UserSession.update(UserSession::id eq sess.id, setValue(UserSession::expirationMillis, newTime))
+                                       get("/logout") {
+                                               call.getUserSession()?.let { sess ->
+                                                       launch {
+                                                               val newTime = System.currentTimeMillis() - 100
+                                                               UserSession.update(UserSession::id eq sess.id, setValue(UserSession::expirationMillis, newTime))
+                                                       }
                                                }
+                                               
+                                               call.sessions.clear<Id<UserSession>>()
+                                               redirect("/")
                                        }
                                        
-                                       call.sessions.clear<Id<UserSession>>()
-                                       redirect("/")
-                               }
-                               
-                               get("/logout/{id}") {
-                                       val id = Id<UserSession>(call.parameters.getOrFail("id"))
-                                       call.getUserSession()?.let { sess ->
-                                               launch {
-                                                       val newTime = System.currentTimeMillis() - 100
-                                                       UserSession.update(and(UserSession::id eq id, UserSession::user eq sess.user), setValue(UserSession::expirationMillis, newTime))
+                                       get("/logout/{id}") {
+                                               val id = Id<UserSession>(call.parameters.getOrFail("id"))
+                                               call.getUserSession()?.let { sess ->
+                                                       launch {
+                                                               val newTime = System.currentTimeMillis() - 100
+                                                               UserSession.update(and(UserSession::id eq id, UserSession::user eq sess.user), setValue(UserSession::expirationMillis, newTime))
+                                                       }
                                                }
+                                               
+                                               redirect("/me/manage")
                                        }
                                        
-                                       redirect("/me/manage")
-                               }
-                               
-                               get("/logout-all") {
-                                       call.getUserSession()?.let { sess ->
-                                               launch {
-                                                       val newTime = System.currentTimeMillis() - 100
-                                                       UserSession.update(and(UserSession::user eq sess.user, UserSession::id ne sess.id), setValue(UserSession::expirationMillis, newTime))
+                                       get("/logout-all") {
+                                               call.getUserSession()?.let { sess ->
+                                                       launch {
+                                                               val newTime = System.currentTimeMillis() - 100
+                                                               UserSession.update(and(UserSession::user eq sess.user, UserSession::id ne sess.id), setValue(UserSession::expirationMillis, newTime))
+                                                       }
                                                }
+                                               
+                                               redirect("/me/manage")
                                        }
-                                       
-                                       redirect("/me/manage")
                                }
-                               
                                currentProvider.installRouting(this)
                        }
                }
@@ -233,9 +228,14 @@ object TestAuthProvider : AuthProvider {
                                validate { credentials ->
                                        val originAddress = request.origin.remoteHost
                                        val userAgent = request.userAgent()
-                                       if (userAgent != null && credentials.name.isValidUsername() && credentials.password == PASSWORD_VALUE) {
-                                               val user = User.locate(User::username eq credentials.name)
-                                                       ?: User(username = credentials.name).also {
+                                       if (userAgent != null && credentials.name.isNotBlank() && credentials.password == PASSWORD_VALUE) {
+                                               val user = User.locate(User::discordId eq credentials.name)
+                                                       ?: User(
+                                                               discordId = credentials.name,
+                                                               discordName = "",
+                                                               discordDiscriminator = "0000",
+                                                               profileName = "Test User"
+                                                       ).also {
                                                                User.put(it)
                                                        }
                                                
@@ -253,10 +253,8 @@ object TestAuthProvider : AuthProvider {
                                challenge { credentials ->
                                        val errorMsg = if (call.request.userAgent() == null)
                                                "User-Agent must be specified when logging in. Are you using some weird API client?"
-                                       else if (credentials == null)
+                                       else if (credentials == null || credentials.name.isBlank())
                                                "A username must be provided."
-                                       else if (!credentials.name.isValidUsername())
-                                               invalidUsernameErrorMessage
                                        else if (credentials.password != PASSWORD_VALUE)
                                                "Password is incorrect."
                                        else
@@ -298,10 +296,6 @@ object TestAuthProvider : AuthProvider {
                                                                autoComplete = false
                                                                
                                                                required = true
-                                                               minLength = "2"
-                                                               maxLength = "32"
-                                                               title = usernameTooltip
-                                                               pattern = usernameRegexStr
                                                        }
                                                        errorMsg?.let { msg ->
                                                                p {
@@ -332,3 +326,107 @@ object TestAuthProvider : AuthProvider {
                }
        }
 }
+
+class ProductionAuthProvider(val discordLogin: DiscordLogin) : AuthProvider {
+       private val httpClient = HttpClient(Apache)
+       
+       override fun installAuth(conf: Authentication.Configuration) {
+               conf.oauth("auth-oauth-discord") {
+                       urlProvider = { discordLogin.redirectUrlOrigin.removeSuffix("/") + "/login/discord/callback" }
+                       providerLookup = {
+                               OAuthServerSettings.OAuth2ServerSettings(
+                                       name = "discord",
+                                       authorizeUrl = "https://discord.com/api/oauth2/authorize",
+                                       accessTokenUrl = "https://discord.com/api/oauth2/token",
+                                       requestMethod = HttpMethod.Post,
+                                       clientId = discordLogin.clientId,
+                                       clientSecret = discordLogin.clientSecret,
+                                       defaultScopes = listOf("identify"),
+                                       nonceManager = StateParameterManager
+                               )
+                       }
+                       client = httpClient
+               }
+       }
+       
+       override fun installRouting(conf: Routing) {
+               with(conf) {
+                       get("/login") {
+                               val errorMsg = call.request.queryParameters["error"]
+                               
+                               call.respondHtml(HttpStatusCode.OK, page("Login with Discord", call.standardNavBar(), null) {
+                                       section {
+                                               if (errorMsg != null)
+                                                       p {
+                                                               style = "color:#d22"
+                                                               +errorMsg
+                                                       }
+                                               p {
+                                                       style = "text-align:center"
+                                                       a(href = "/login/discord") { +"Continue to Discord" }
+                                               }
+                                       }
+                               })
+                       }
+                       
+                       authenticate("auth-oauth-discord") {
+                               get("/login/discord") {
+                                       // Redirects to 'authorizeUrl' automatically
+                               }
+                               
+                               get("/login/discord/callback") {
+                                       val userAgent = call.request.userAgent() ?: throw ForbiddenException()
+                                       val principal: OAuthAccessTokenResponse.OAuth2 = call.principal() ?: redirect("/login")
+                                       //call.sessions.set(UserSession(principal?.accessToken.toString()))
+                                       val userInfoJson = httpClient.get<String>("https://discord.com/api/users/@me") {
+                                               headers {
+                                                       append(HttpHeaders.Authorization, "Bearer ${principal.accessToken}")
+                                               }
+                                       }
+                                       
+                                       val userInfo = JsonConfigCodec.parseToJsonElement(userInfoJson) as? JsonObject ?: redirect("/login")
+                                       val discordId = (userInfo["id"] as? JsonPrimitive)?.content ?: redirect("/login")
+                                       val discordUsername = (userInfo["username"] as? JsonPrimitive)?.content ?: redirect("/login")
+                                       val discordDiscriminator = (userInfo["discriminator"] as? JsonPrimitive)?.content ?: redirect("/login")
+                                       
+                                       val user = User.locate(User::discordId eq discordId)?.copy(
+                                               discordName = discordUsername,
+                                               discordDiscriminator = discordDiscriminator
+                                       ) ?: User(
+                                               discordId = discordId,
+                                               discordName = discordUsername,
+                                               discordDiscriminator = discordDiscriminator,
+                                               profileName = discordUsername
+                                       )
+                                       
+                                       val userSession = UserSession(
+                                               user = user.id,
+                                               clientAddresses = listOf(call.request.origin.host),
+                                               userAgent = userAgent,
+                                               expirationMillis = System.currentTimeMillis() + 86_400_000
+                                       )
+                                       
+                                       launch {
+                                               User.put(user)
+                                               UserSession.put(userSession)
+                                       }
+                                       
+                                       call.sessions.set(userSession.id)
+                                       redirect("/me")
+                               }
+                       }
+               }
+       }
+}
+
+object StateParameterManager : NonceManager {
+       private val nonces = mutableSetOf<String>()
+       
+       override suspend fun newNonce(): String {
+               return createNonce().also { nonces += it }
+       }
+       
+       override suspend fun verifyNonce(nonce: String): Boolean {
+               return nonces.remove(nonce)
+       }
+}
index ef350d3daafb99dfe340668ff13285019ac62917..e9d2a524e2fb4ad652aeccde8eb0c737fe66d9d0 100644 (file)
@@ -1,7 +1,7 @@
 package starshipfights.auth
 
 import io.ktor.application.*
-import io.ktor.request.*
+import io.ktor.auth.*
 import io.ktor.sessions.*
 import starshipfights.data.Id
 import starshipfights.data.auth.User
@@ -16,7 +16,7 @@ suspend fun UserSession.renewed(clientAddress: String) = copy(
        clientAddresses = if (clientAddresses.last() != clientAddress) clientAddresses + clientAddress else clientAddresses
 ).also { UserSession.put(it) }
 
-suspend fun ApplicationCall.getUserSession() = request.userAgent()?.let { sessions.get<Id<UserSession>>()?.resolve(it) }
+fun ApplicationCall.getUserSession() = principal<UserSession>()
 
 suspend fun ApplicationCall.getUser() = getUserSession()?.user?.let { User.get(it) }
 
index 6d97767a273e38a2bad210a0f0e5df460ae82947..f7c605fb13de105837faa52305a4af926271936e 100644 (file)
@@ -64,7 +64,7 @@ data class ShipInDrydock(
 suspend fun getAllInGameAdmirals(user: User) = Admiral.select(Admiral::owningUser eq user.id).map { admiral ->
        InGameAdmiral(
                admiral.id.reinterpret(),
-               InGameUser(user.id.reinterpret(), user.username),
+               InGameUser(user.id.reinterpret(), user.profileName),
                admiral.name,
                admiral.isFemale,
                admiral.faction,
@@ -76,7 +76,7 @@ suspend fun getInGameAdmiral(admiralId: Id<InGameAdmiral>) = Admiral.get(admiral
        User.get(admiral.owningUser)?.let { user ->
                InGameAdmiral(
                        admiralId,
-                       InGameUser(user.id.reinterpret(), user.username),
+                       InGameUser(user.id.reinterpret(), user.profileName),
                        admiral.name,
                        admiral.isFemale,
                        admiral.faction,
index d08add509692d1f8722f3cd766b261a8e016f179..b5b9b19c030289df099341f94ee7f17118b5ec50 100644 (file)
@@ -8,21 +8,18 @@ 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)
-fun String.isValidUsername() = usernameRegex.matchEntire(this) != null
-const val usernameTooltip = "Between 2 and 32 characters that are either letters, numbers, hyphens, or underscores"
-const val invalidUsernameErrorMessage = "Invalid username. Usernames must be between 2 and 32 characters that are either letters, numbers, hyphens, or underscores"
-
 @Serializable
 data class User(
        @SerialName("_id")
        override val id: Id<User> = Id(),
-       val username: String,
+       val discordId: String,
+       val discordName: String,
+       val discordDiscriminator: String,
+       val profileName: String,
        val status: UserStatus = UserStatus.AVAILABLE,
 ) : DataDocument<User> {
        companion object Table : DocumentTable<User> by DocumentTable.create({
-               unique(User::username)
+               unique(User::discordId)
        })
 }
 
index 5ea353f4d8c6ab75ae8bd052b3a74b0e65df974b..9253fe005d982d401da539e8c44d3368aa0549fb 100644 (file)
@@ -2,12 +2,13 @@ package starshipfights.data
 
 import com.aventrix.jnanoid.jnanoid.NanoIdUtils
 
-private val tokenAlphabet = "0123456789ABCDEFGHILMNOPQRSTVXYZ".toCharArray()
-private const val tokenLength = 8
+private val alphabet32 = "BCDFGHLMNPQRSTXZbcdfghlmnpqrstxz".toCharArray()
 
-fun newToken(): String = NanoIdUtils.randomNanoId(NanoIdUtils.DEFAULT_NUMBER_GENERATOR, tokenAlphabet, tokenLength)
+private const val tokenLength = 8
+fun createToken(): String = NanoIdUtils.randomNanoId(NanoIdUtils.DEFAULT_NUMBER_GENERATOR, alphabet32, tokenLength)
 
-private val idAlphabet = "BCDFGHLMNPQRSTXZ".toCharArray()
-private const val idLength = 42
+private const val nonceLength = 16
+fun createNonce(): String = NanoIdUtils.randomNanoId(NanoIdUtils.DEFAULT_NUMBER_GENERATOR, alphabet32, nonceLength)
 
-operator fun <T> Id.Companion.invoke() = Id<T>(NanoIdUtils.randomNanoId(NanoIdUtils.DEFAULT_NUMBER_GENERATOR, idAlphabet, idLength))
+private const val idLength = 24
+operator fun <T> Id.Companion.invoke() = Id<T>(NanoIdUtils.randomNanoId(NanoIdUtils.DEFAULT_NUMBER_GENERATOR, alphabet32, idLength))
index aa1d030216fb671019156dbd640e55dd2eceaa73..86a7d614a3f58f708a5e036510bfeb1753db3397 100644 (file)
@@ -16,7 +16,7 @@ import starshipfights.data.admiralty.BattleRecord
 import starshipfights.data.admiralty.DrydockStatus
 import starshipfights.data.admiralty.ShipInDrydock
 import starshipfights.data.auth.User
-import starshipfights.data.newToken
+import starshipfights.data.createToken
 import java.time.Instant
 import java.time.temporal.ChronoUnit
 
@@ -78,8 +78,8 @@ object GameManager {
                        }
                }
                
-               val hostId = newToken()
-               val joinId = newToken()
+               val hostId = createToken()
+               val joinId = createToken()
                games.use {
                        it[hostId] = GameEntry(hostInfo.user.id.reinterpret(), GlobalSide.HOST, session)
                        it[joinId] = GameEntry(guestInfo.user.id.reinterpret(), GlobalSide.GUEST, session)
index 052cfb47fc126aa78d1e7ccd3d91eb210a691a3a..77b9717f5ba9747dd8c2b397f323ed62b453ae02 100644 (file)
@@ -36,10 +36,10 @@ suspend fun ApplicationCall.standardNavBar(): List<NavItem> = listOf(
        NavHead("Your Account"),
 ) + when (val user = getUser()) {
        null -> listOf(
-               NavLink("/login", "Log In"),
+               NavLink("/login", "Login with Discord"),
        )
        else -> listOf(
-               NavLink("/me", user.username),
+               NavLink("/me", user.profileName),
                NavLink("/me/manage", "User Preferences"),
                NavLink("/lobby", "Enter Game Lobby"),
                NavLink("/logout", "Log Out"),
index 5ec1975fe5fc04f167353020de47830385d54154..413975d3124624b611d7635a826666818f7df79a 100644 (file)
@@ -8,14 +8,13 @@ import kotlinx.html.*
 import org.litote.kmongo.and
 import org.litote.kmongo.eq
 import org.litote.kmongo.or
+import starshipfights.ForbiddenException
 import starshipfights.auth.getUser
 import starshipfights.auth.getUserSession
 import starshipfights.data.Id
 import starshipfights.data.admiralty.*
 import starshipfights.data.auth.User
 import starshipfights.data.auth.UserSession
-import starshipfights.data.auth.usernameRegexStr
-import starshipfights.data.auth.usernameTooltip
 import starshipfights.game.Faction
 import starshipfights.game.GlobalSide
 import starshipfights.game.toUrlSlug
@@ -23,15 +22,15 @@ import starshipfights.redirect
 import java.time.Instant
 
 suspend fun ApplicationCall.userPage(): HTML.() -> Unit {
-       val username = parameters["name"]!!
-       val user = User.locate(User::username eq username)!!
+       val username = Id<User>(parameters["id"]!!)
+       val user = User.get(username)!!
        
        val isCurrentUser = user.id == getUserSession()?.user
        
        val admirals = Admiral.select(Admiral::owningUser eq user.id).toList()
        
        return page(
-               username, standardNavBar(), if (isCurrentUser)
+               user.profileName, standardNavBar(), if (isCurrentUser)
                        PageNavSidebar(
                                listOf(
                                        NavLink("/admiral/new", "New Admiral"),
@@ -43,9 +42,9 @@ suspend fun ApplicationCall.userPage(): HTML.() -> Unit {
                        }
        ) {
                section {
-                       h1 { +username }
+                       h1 { +user.profileName }
                        p {
-                               +"This user's username is $username!"
+                               +"This user's profile name is ${user.profileName}!"
                        }
                        
                        if (isCurrentUser)
@@ -89,24 +88,20 @@ suspend fun ApplicationCall.manageUserPage(): HTML.() -> Unit {
                        form(method = FormMethod.post, action = "/me/manage") {
                                h3 {
                                        label {
-                                               htmlFor = "username"
-                                               +"Username"
+                                               htmlFor = "name"
+                                               +"Profile Name"
                                        }
                                }
                                textInput(name = "name") {
                                        required = true
-                                       value = currentUser.username
+                                       value = currentUser.profileName
                                        autoComplete = false
                                        
                                        required = true
-                                       minLength = "2"
-                                       maxLength = "32"
-                                       title = usernameTooltip
-                                       pattern = usernameRegexStr
                                }
                                request.queryParameters["error"]?.let { errorMsg ->
                                        p {
-                                               style = "color:#d33"
+                                               style = "color:#d22"
                                                +errorMsg
                                        }
                                }
@@ -272,8 +267,6 @@ suspend fun ApplicationCall.admiralPage(): HTML.() -> Unit {
                
                Triple(admiral.await(), ships.await(), records.await())
        }
-       val admiralOwner = User.get(admiral.owningUser)!!.username
-       
        val recordRoles = records.mapNotNull {
                when (admiralId) {
                        it.hostAdmiral -> GlobalSide.HOST
@@ -299,7 +292,7 @@ suspend fun ApplicationCall.admiralPage(): HTML.() -> Unit {
        return page(
                admiral.fullName, standardNavBar(), PageNavSidebar(
                        listOf(
-                               NavLink("/user/${admiralOwner}", "Back to User")
+                               NavLink("/user/${admiral.owningUser}", "Back to User")
                        ) + if (currentUser == admiral.owningUser)
                                listOf(
                                        NavLink("/admiral/${admiral.id}/manage", "Manage Admiral")
@@ -400,7 +393,7 @@ suspend fun ApplicationCall.manageAdmiralPage(): HTML.() -> Unit {
        val admiralId = parameters["id"]?.let { Id<Admiral>(it) }!!
        val admiral = Admiral.get(admiralId)!!
        
-       if (admiral.owningUser != currentUser) throw IllegalArgumentException()
+       if (admiral.owningUser != currentUser) throw ForbiddenException()
        
        return page(
                "Managing ${admiral.name}", standardNavBar(), PageNavSidebar(
@@ -464,7 +457,7 @@ suspend fun ApplicationCall.deleteAdmiralConfirmPage(): HTML.() -> Unit {
        val admiralId = parameters["id"]?.let { Id<Admiral>(it) }!!
        val admiral = Admiral.get(admiralId)!!
        
-       if (admiral.owningUser != currentUser) throw IllegalArgumentException()
+       if (admiral.owningUser != currentUser) throw ForbiddenException()
        
        return page(
                "Are You Sure?", null, null
index eab498262d69b0d19058a077fc550efdb4c5624d..48cde73d5f30952b90c58f55e72c5897a7a8aeba 100644 (file)
@@ -14,7 +14,19 @@ data class Configuration(
        val port: Int,
        
        val dbConn: ConnectionType,
-       val dbName: String
+       val dbName: String,
+       
+       val discordClient: DiscordLogin? = null
+)
+
+@Serializable
+data class DiscordLogin(
+       val redirectUrlOrigin: String,
+       
+       val clientId: String,
+       val clientSecret: String,
+       
+       val ownerId: String,
 )
 
 private val DEFAULT_CONFIG = Configuration(
@@ -22,7 +34,8 @@ private val DEFAULT_CONFIG = Configuration(
        host = "127.0.0.1",
        port = 8080,
        dbConn = ConnectionType.Embedded(),
-       dbName = "sf"
+       dbName = "sf",
+       discordClient = null
 )
 
 private var currentConfig: Configuration? = null
index fddfe6c4711fc3fcf6bc9b08692c4ff397b96eb5..8fbd578e6642ac7a6f0dae5b8218aac3b73e044a 100644 (file)
@@ -3,6 +3,8 @@ package starshipfights
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
 
+class ForbiddenException : IllegalArgumentException()
+
 data class HttpRedirectException(val url: String, val permanent: Boolean) : RuntimeException()
 fun redirect(url: String, permanent: Boolean = false): Nothing = throw HttpRedirectException(url, permanent)