import io.ktor.server.sessions.*
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
+import java.time.Instant
object SessionStorageMongoDB : SessionStorage {
override suspend fun invalidate(id: String) {
- SessionStorageDoc.Table.del(Id(id))
+ if (!SessionStorageDoc.Table.del(Id(id)))
+ throw NoSuchElementException("Session $id not found")
}
override suspend fun read(id: String): String {
@SerialName(MONGODB_ID_KEY)
override val id: Id<SessionStorageDoc>,
val session: UserSession,
+
+ val expiresAt: @Serializable(with = InstantSerializer::class) Instant = Instant.now().plusSeconds(1_814_400),
) : DataDocument<SessionStorageDoc> {
companion object : TableHolder<SessionStorageDoc> {
override val Table = DocumentTable<SessionStorageDoc>()
- override suspend fun initialize() = Unit
+ override suspend fun initialize() {
+ Table.expire(SessionStorageDoc::expiresAt)
+ }
}
}
import com.github.agadar.nationstates.shard.NationShard
import info.mechyrdia.Configuration
-import info.mechyrdia.data.Id
-import info.mechyrdia.data.NationData
+import info.mechyrdia.data.*
import info.mechyrdia.lore.page
import info.mechyrdia.lore.redirectHref
import info.mechyrdia.lore.standardNavBar
import io.ktor.server.sessions.*
import io.ktor.util.*
import kotlinx.html.*
-import java.util.concurrent.ConcurrentHashMap
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import java.time.Instant
import kotlin.collections.set
val PageDoNotCacheAttributeKey = AttributeKey<Boolean>("Mechyrdia.PageDoNotCache")
-private val nsTokenMap = ConcurrentHashMap<String, String>()
+@Serializable
+data class NsStoredToken(
+ @SerialName(MONGODB_ID_KEY)
+ override val id: Id<NsStoredToken> = Id(),
+ val verifyToken: String = token(),
+
+ val expiresAt: @Serializable(with = InstantSerializer::class) Instant = Instant.now().plusSeconds(1800L),
+) : DataDocument<NsStoredToken> {
+ companion object : TableHolder<NsStoredToken> {
+ override val Table = DocumentTable<NsStoredToken>()
+
+ override suspend fun initialize() {
+ Table.expire(NsStoredToken::expiresAt)
+ }
+
+ suspend fun createToken(): NsLoginToken {
+ return NsStoredToken().also { Table.put(it) }.let { (id, token) ->
+ NsLoginToken(id.id, token)
+ }
+ }
+
+ suspend fun verifyToken(id: String): String? {
+ return Table.get(Id(id))?.verifyToken?.also {
+ Table.del(Id(id))
+ }
+ }
+ }
+}
+
+data class NsLoginToken(val id: String, val token: String)
suspend fun ApplicationCall.loginPage(): HTML.() -> Unit {
attributes.put(PageDoNotCacheAttributeKey, true)
+ val (tokenId, nsToken) = NsStoredToken.createToken()
+
return page("Log In With NationStates", standardNavBar()) {
- val tokenKey = token()
- val nsToken = token()
-
- nsTokenMap[tokenKey] = nsToken
-
section {
h1 { +"Log In With NationStates" }
form(method = FormMethod.post, action = href(Root.Auth.LoginPost())) {
installCsrfToken()
hiddenInput {
- name = "token"
- value = tokenKey
+ name = "tokenId"
+ value = tokenId
}
label {
}
}
-suspend fun ApplicationCall.loginRoute(nation: String, checksum: String, token: String): Nothing {
+suspend fun ApplicationCall.loginRoute(nation: String, checksum: String, tokenId: String): Nothing {
val nationId = nation.toNationId()
- val nsToken = nsTokenMap.remove(token)
- ?: throw MissingRequestParameterException("token")
+ val nsToken = NsStoredToken.verifyToken(tokenId)
+ ?: throw MissingRequestParameterException("tokenId")
val nationData = if (nationId == Configuration.Current.ownerNation && checksum == Configuration.Current.emergencyPassword)
NationData.get(Id(nationId))
@Serializable
data class Comment(
@SerialName(MONGODB_ID_KEY)
- override val id: Id<Comment>,
+ override val id: Id<Comment> = Id(),
val submittedBy: Id<NationData>,
val submittedIn: String,
@Serializable
data class CommentReplyLink(
@SerialName(MONGODB_ID_KEY)
- override val id: Id<CommentReplyLink> = Id(),
+ override val id: Id<CommentReplyLink>,
val originalPost: Id<Comment>,
val replyingPost: Id<Comment>,
import com.mongodb.reactivestreams.client.MongoClients
import com.mongodb.reactivestreams.client.gridfs.GridFSBucket
import com.mongodb.reactivestreams.client.gridfs.GridFSBuckets
+import info.mechyrdia.auth.NsStoredToken
import info.mechyrdia.auth.SessionStorageDoc
import info.mechyrdia.auth.WebDavToken
import info.mechyrdia.robot.RobotGlobals
return collection().find(Filters.eq(MONGODB_ID_KEY, id)).singleOrNull()
}
- suspend fun del(id: Id<T>) {
- collection().deleteOne(Filters.eq(MONGODB_ID_KEY, id))
+ suspend fun del(id: Id<T>): Boolean {
+ return collection().deleteOne(Filters.eq(MONGODB_ID_KEY, id)).deletedCount > 0L
}
suspend fun all(): Flow<T> {
val entries = listOf(
SessionStorageDoc,
NationData,
+ NsStoredToken,
WebDavToken,
Comment,
CommentReplyLink,
val now = Instant.now()
val comment = Comment(
- id = Id(),
submittedBy = loggedInAs.id,
submittedIn = pagePathParts.joinToString("/"),
submittedAt = now,
@Serializable
data class PageVisitData(
@SerialName(MONGODB_ID_KEY)
- override val id: Id<PageVisitData> = Id(),
+ override val id: Id<PageVisitData>,
val path: String,
val visitor: String,
import kotlinx.serialization.Serializable
@Serializable
-class LoginPayload(override val csrfToken: String? = null, val nation: String, val checksum: String, val token: String) : CsrfProtectedResourcePayload
+class LoginPayload(override val csrfToken: String? = null, val nation: String, val checksum: String, val tokenId: String) : CsrfProtectedResourcePayload
@Serializable
class LogoutPayload(override val csrfToken: String? = null) : CsrfProtectedResourcePayload
with(auth) { filterCall() }
with(payload) { call.verifyCsrfToken() }
- call.loginRoute(payload.nation, payload.checksum, payload.token)
+ call.loginRoute(payload.nation, payload.checksum, payload.tokenId)
}
}