From: TheSaminator Date: Tue, 8 Feb 2022 22:17:35 +0000 (-0500) Subject: Add production-env authentication X-Git-Url: https://gitweb.starshipfights.net/?a=commitdiff_plain;h=0019e7e5517465f44a04ea46efc254e44ba772a7;p=starship-fights Add production-env authentication --- diff --git a/build.gradle.kts b/build.gradle.kts index dd4d7b4..74fd25c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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") diff --git a/src/jvmMain/kotlin/starshipfights/auth/providers.kt b/src/jvmMain/kotlin/starshipfights/auth/providers.kt index cfce085..23c2c9a 100644 --- a/src/jvmMain/kotlin/starshipfights/auth/providers.kt +++ b/src/jvmMain/kotlin/starshipfights/auth/providers.kt @@ -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()?.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(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(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(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(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>() + redirect("/") } - call.sessions.clear>() - redirect("/") - } - - get("/logout/{id}") { - val id = Id(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(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("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() + + override suspend fun newNonce(): String { + return createNonce().also { nonces += it } + } + + override suspend fun verifyNonce(nonce: String): Boolean { + return nonces.remove(nonce) + } +} diff --git a/src/jvmMain/kotlin/starshipfights/auth/utils.kt b/src/jvmMain/kotlin/starshipfights/auth/utils.kt index ef350d3..e9d2a52 100644 --- a/src/jvmMain/kotlin/starshipfights/auth/utils.kt +++ b/src/jvmMain/kotlin/starshipfights/auth/utils.kt @@ -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>()?.resolve(it) } +fun ApplicationCall.getUserSession() = principal() suspend fun ApplicationCall.getUser() = getUserSession()?.user?.let { User.get(it) } diff --git a/src/jvmMain/kotlin/starshipfights/data/admiralty/admirals.kt b/src/jvmMain/kotlin/starshipfights/data/admiralty/admirals.kt index 6d97767..f7c605f 100644 --- a/src/jvmMain/kotlin/starshipfights/data/admiralty/admirals.kt +++ b/src/jvmMain/kotlin/starshipfights/data/admiralty/admirals.kt @@ -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) = 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, diff --git a/src/jvmMain/kotlin/starshipfights/data/auth/user_sessions.kt b/src/jvmMain/kotlin/starshipfights/data/auth/user_sessions.kt index d08add5..b5b9b19 100644 --- a/src/jvmMain/kotlin/starshipfights/data/auth/user_sessions.kt +++ b/src/jvmMain/kotlin/starshipfights/data/auth/user_sessions.kt @@ -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 = Id(), - val username: String, + val discordId: String, + val discordName: String, + val discordDiscriminator: String, + val profileName: String, val status: UserStatus = UserStatus.AVAILABLE, ) : DataDocument { companion object Table : DocumentTable by DocumentTable.create({ - unique(User::username) + unique(User::discordId) }) } diff --git a/src/jvmMain/kotlin/starshipfights/data/data_jvm.kt b/src/jvmMain/kotlin/starshipfights/data/data_jvm.kt index 5ea353f..9253fe0 100644 --- a/src/jvmMain/kotlin/starshipfights/data/data_jvm.kt +++ b/src/jvmMain/kotlin/starshipfights/data/data_jvm.kt @@ -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 Id.Companion.invoke() = Id(NanoIdUtils.randomNanoId(NanoIdUtils.DEFAULT_NUMBER_GENERATOR, idAlphabet, idLength)) +private const val idLength = 24 +operator fun Id.Companion.invoke() = Id(NanoIdUtils.randomNanoId(NanoIdUtils.DEFAULT_NUMBER_GENERATOR, alphabet32, idLength)) diff --git a/src/jvmMain/kotlin/starshipfights/game/server_game.kt b/src/jvmMain/kotlin/starshipfights/game/server_game.kt index aa1d030..86a7d61 100644 --- a/src/jvmMain/kotlin/starshipfights/game/server_game.kt +++ b/src/jvmMain/kotlin/starshipfights/game/server_game.kt @@ -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) diff --git a/src/jvmMain/kotlin/starshipfights/info/view_nav.kt b/src/jvmMain/kotlin/starshipfights/info/view_nav.kt index 052cfb4..77b9717 100644 --- a/src/jvmMain/kotlin/starshipfights/info/view_nav.kt +++ b/src/jvmMain/kotlin/starshipfights/info/view_nav.kt @@ -36,10 +36,10 @@ suspend fun ApplicationCall.standardNavBar(): List = 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"), diff --git a/src/jvmMain/kotlin/starshipfights/info/views_user.kt b/src/jvmMain/kotlin/starshipfights/info/views_user.kt index 5ec1975..413975d 100644 --- a/src/jvmMain/kotlin/starshipfights/info/views_user.kt +++ b/src/jvmMain/kotlin/starshipfights/info/views_user.kt @@ -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(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(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(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 diff --git a/src/jvmMain/kotlin/starshipfights/server_conf.kt b/src/jvmMain/kotlin/starshipfights/server_conf.kt index eab4982..48cde73 100644 --- a/src/jvmMain/kotlin/starshipfights/server_conf.kt +++ b/src/jvmMain/kotlin/starshipfights/server_conf.kt @@ -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 diff --git a/src/jvmMain/kotlin/starshipfights/server_utils.kt b/src/jvmMain/kotlin/starshipfights/server_utils.kt index fddfe6c..8fbd578 100644 --- a/src/jvmMain/kotlin/starshipfights/server_utils.kt +++ b/src/jvmMain/kotlin/starshipfights/server_utils.kt @@ -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)