Performance improvements

This commit is contained in:
Hubert Van De Walle 2020-06-14 15:31:36 +02:00
parent 8a9e878d5f
commit f40349ea98
9 changed files with 85 additions and 57 deletions

View File

@ -12,7 +12,7 @@ fun Application.authenticationModule() {
val simpleJwt by kodein.instance<SimpleJWT>(tag = "auth")
verifier(simpleJwt.verifier)
validate {
UserIdPrincipal(it.payload.getClaim("email").asString())
UserDbIdPrincipal(it.payload.getClaim("id").asInt())
}
}
}

View File

@ -11,8 +11,8 @@ class SimpleJWT(secret: String, validity: Long, unit: TimeUnit) {
private val algorithm = Algorithm.HMAC256(secret)
val verifier: JWTVerifier = JWT.require(algorithm).build()
fun sign(email: String): String = JWT.create()
.withClaim("email", email)
fun sign(id: Int): String = JWT.create()
.withClaim("id", id)
.withExpiresAt(getExpiration())
.sign(algorithm)

View File

@ -0,0 +1,5 @@
package be.vandewalleh.auth
import io.ktor.auth.Principal
data class UserDbIdPrincipal(val id: Int) : Principal

View File

@ -1,5 +1,6 @@
package be.vandewalleh.extensions
import be.vandewalleh.auth.UserDbIdPrincipal
import be.vandewalleh.kodein
import be.vandewalleh.services.FullNoteCreateDTO
import be.vandewalleh.services.FullNotePatchDTO
@ -11,21 +12,14 @@ import io.ktor.request.*
import io.ktor.response.*
import org.kodein.di.generic.instance
val userService by kodein.instance<UserService>()
suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) {
respond(status, status.description)
}
/**
* @return the user email for the currently authenticated user
*/
fun ApplicationCall.userEmail() = principal<UserIdPrincipal>()!!.name
/**
* @return the userId for the currently authenticated user
*/
fun ApplicationCall.userId() = userService.getUserId(userEmail())!!
fun ApplicationCall.userId() = principal<UserDbIdPrincipal>()!!.id
class NoteCreate(val title: String, val tags: List<String>)

View File

@ -0,0 +1,8 @@
package be.vandewalleh.extensions
import kotlinx.coroutines.*
fun <T> ioAsync(block: suspend CoroutineScope.() -> T): Deferred<T> {
return CoroutineScope(Dispatchers.IO).async(block = block)
}

View File

@ -1,6 +1,7 @@
package be.vandewalleh.routing
import be.vandewalleh.auth.SimpleJWT
import be.vandewalleh.auth.UserDbIdPrincipal
import be.vandewalleh.auth.UsernamePasswordCredential
import be.vandewalleh.extensions.respondStatus
import be.vandewalleh.services.UserService
@ -26,16 +27,16 @@ fun Routing.auth(kodein: Kodein) {
post("/user/login") {
val credential = call.receive<UsernamePasswordCredential>()
val (email, password) = userService.getEmailAndPasswordFromUsername(credential.username)
val user = userService.getFromUsername(credential.username)
?: return@post call.respondStatus(HttpStatusCode.Unauthorized)
if (!BCrypt.checkpw(credential.password, password)) {
if (!BCrypt.checkpw(credential.password, user.password)) {
return@post call.respondStatus(HttpStatusCode.Unauthorized)
}
val response = DualToken(
token = authSimpleJwt.sign(email),
refreshToken = refreshSimpleJwt.sign(email)
token = authSimpleJwt.sign(user.id),
refreshToken = refreshSimpleJwt.sign(user.id)
)
return@post call.respond(response)
}
@ -43,16 +44,16 @@ fun Routing.auth(kodein: Kodein) {
post("/user/refresh_token") {
val token = call.receive<RefreshToken>().refreshToken
val email = try {
val id = try {
val decodedJWT = refreshSimpleJwt.verifier.verify(token)
decodedJWT.getClaim("email").asString()
decodedJWT.getClaim("id").asInt()
} catch (e: JWTVerificationException) {
return@post call.respondStatus(HttpStatusCode.Unauthorized)
}
val response = DualToken(
token = authSimpleJwt.sign(email),
refreshToken = refreshSimpleJwt.sign(email)
token = authSimpleJwt.sign(id),
refreshToken = refreshSimpleJwt.sign(id)
)
return@post call.respond(response)
}
@ -60,8 +61,8 @@ fun Routing.auth(kodein: Kodein) {
authenticate {
get("/user/me") {
// retrieve email from token
val email = call.principal<UserIdPrincipal>()!!.name
val info = userService.getUserInfo(email)
val id = call.principal<UserDbIdPrincipal>()!!.id
val info = userService.getUserInfo(id)
if (info != null) call.respond(mapOf("user" to info))
else call.respondStatus(HttpStatusCode.Unauthorized)
}

View File

@ -5,10 +5,10 @@ import be.vandewalleh.extensions.receiveNotePatch
import be.vandewalleh.extensions.respondStatus
import be.vandewalleh.extensions.userId
import be.vandewalleh.services.NotesService
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.application.call
import io.ktor.auth.authenticate
import io.ktor.http.HttpStatusCode
import io.ktor.response.respond
import io.ktor.routing.*
import org.kodein.di.Kodein
import org.kodein.di.generic.instance
@ -22,10 +22,8 @@ fun Routing.title(kodein: Kodein) {
val userId = call.userId()
val noteUuid = call.parameters.noteUuid()
val exists = notesService.noteExists(userId, noteUuid)
if (!exists) return@get call.respondStatus(HttpStatusCode.NotFound)
val response = notesService.getNote(noteUuid)
val response =
notesService.getNote(userId, noteUuid) ?: return@get call.respondStatus(HttpStatusCode.NotFound)
call.respond(response)
}

View File

@ -1,9 +1,10 @@
package be.vandewalleh.services
import be.vandewalleh.extensions.ioAsync
import be.vandewalleh.tables.Chapters
import be.vandewalleh.tables.Notes
import be.vandewalleh.tables.Tags
import me.liuwj.ktorm.database.*
import me.liuwj.ktorm.database.Database
import me.liuwj.ktorm.dsl.*
import me.liuwj.ktorm.entity.*
import org.kodein.di.Kodein
@ -23,7 +24,7 @@ class NotesService(override val kodein: Kodein) : KodeinAware {
* returns a list of [BasicNoteDTO] associated with the userId
*/
fun getNotes(userId: Int): List<BasicNoteDTO> {
val notes = db.sequenceOf(Notes)
val notes = db.sequenceOf(Notes, withReferences = false)
.filterColumns { listOf(it.uuid, it.title, it.updatedAt) }
.filter { it.userId eq userId }
.sortedByDescending { it.updatedAt }
@ -31,7 +32,7 @@ class NotesService(override val kodein: Kodein) : KodeinAware {
if (notes.isEmpty()) return emptyList()
val tags = db.sequenceOf(Tags)
val tags = db.sequenceOf(Tags, withReferences = false)
.filterColumns { listOf(it.noteUuid, it.name) }
.filter { it.noteUuid inList notes.map { it.uuid } }
.toList()
@ -89,25 +90,39 @@ class NotesService(override val kodein: Kodein) : KodeinAware {
return uuid
}
fun getNote(noteUuid: UUID): FullNoteDTO {
val note = db.sequenceOf(Notes)
.filterColumns { listOf(it.title, it.updatedAt) }
.find { it.uuid eq noteUuid } ?: error("Note not found")
val tags = db.from(Tags)
suspend fun getNote(userId: Int, noteUuid: UUID): FullNoteDTO? {
val deferredNote = ioAsync {
db.sequenceOf(Notes, withReferences = false)
.filterColumns { listOf(it.title, it.updatedAt) }
.filter { it.uuid eq noteUuid }
.find { it.userId eq userId }
}
val deferredTags = ioAsync {
db.from(Tags)
.select(Tags.name)
.where { Tags.noteUuid eq noteUuid }
.map { it[Tags.name]!! }
.toList()
}
val chapters = db.from(Chapters)
val deferredChapters = ioAsync {
db.from(Chapters)
.select(Chapters.title, Chapters.content)
.where { Chapters.noteUuid eq noteUuid }
.orderBy(Chapters.number.asc())
.map { ChapterDTO(it[Chapters.title]!!, it[Chapters.content]!!) }
.toList()
}
val note = deferredNote.await() ?: return null
val updatedAtFormatted = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(note.updatedAt)
val tags = deferredTags.await()
val chapters = deferredChapters.await()
return FullNoteDTO(
uuid = noteUuid,
title = note.title,

View File

@ -30,11 +30,18 @@ class UserService(override val kodein: Kodein) : KodeinAware {
/**
* returns a user email and password from it's email if found or null
*/
fun getEmailAndPasswordFromUsername(username: String): Pair<String, String>? {
fun getFromUsername(username: String): UserSchema? {
return db.from(Users)
.select(Users.email, Users.password)
.select(Users.email, Users.password, Users.id)
.where { Users.username eq username }
.map { row -> row[Users.email]!! to row[Users.password]!! }
.map { row ->
UserSchema(
row[Users.id]!!,
username,
row[Users.email]!!,
row[Users.password]!!
)
}
.firstOrNull()
}
@ -52,11 +59,10 @@ class UserService(override val kodein: Kodein) : KodeinAware {
.firstOrNull() != null
}
fun getUserInfo(email: String): UserInfoDto? {
fun getUserInfo(id: Int): UserInfoDto? {
return db.from(Users)
.select(Users.email, Users.username)
.where { Users.email eq email }
.limit(0, 1)
.where { Users.id eq id }
.map { UserInfoDto(it[Users.username]!!, it[Users.email]!!) }
.firstOrNull()
}
@ -98,5 +104,6 @@ class UserService(override val kodein: Kodein) : KodeinAware {
}
}
data class UserSchema(val id: Int, val username: String, val email: String, val password: String)
data class UserDto(val username: String, val email: String, val password: String)
data class UserInfoDto(val username: String, val email: String)