From f40349ea98cb301ecc32c8e6729f3daeed61a525 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Sun, 14 Jun 2020 15:31:36 +0200 Subject: [PATCH] Performance improvements --- api/src/auth/AuthenticationModule.kt | 4 +- api/src/auth/SimpleJWT.kt | 6 +-- api/src/auth/UserDbIdPrincipal.kt | 5 ++ .../extensions/ApplicationCallExtensions.kt | 12 ++--- api/src/extensions/Coroutines.kt | 8 +++ api/src/routing/AuthController.kt | 21 ++++---- api/src/routing/TitleController.kt | 14 +++-- api/src/services/NotesService.kt | 53 ++++++++++++------- api/src/services/UserService.kt | 19 ++++--- 9 files changed, 85 insertions(+), 57 deletions(-) create mode 100644 api/src/auth/UserDbIdPrincipal.kt create mode 100644 api/src/extensions/Coroutines.kt diff --git a/api/src/auth/AuthenticationModule.kt b/api/src/auth/AuthenticationModule.kt index 30b7c7f..cea8b90 100644 --- a/api/src/auth/AuthenticationModule.kt +++ b/api/src/auth/AuthenticationModule.kt @@ -12,8 +12,8 @@ fun Application.authenticationModule() { val simpleJwt by kodein.instance(tag = "auth") verifier(simpleJwt.verifier) validate { - UserIdPrincipal(it.payload.getClaim("email").asString()) + UserDbIdPrincipal(it.payload.getClaim("id").asInt()) } } } -} \ No newline at end of file +} diff --git a/api/src/auth/SimpleJWT.kt b/api/src/auth/SimpleJWT.kt index 0d7b0ad..9830e61 100644 --- a/api/src/auth/SimpleJWT.kt +++ b/api/src/auth/SimpleJWT.kt @@ -11,10 +11,10 @@ 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) private fun getExpiration() = Date(System.currentTimeMillis() + validityInMs) -} \ No newline at end of file +} diff --git a/api/src/auth/UserDbIdPrincipal.kt b/api/src/auth/UserDbIdPrincipal.kt new file mode 100644 index 0000000..375c88b --- /dev/null +++ b/api/src/auth/UserDbIdPrincipal.kt @@ -0,0 +1,5 @@ +package be.vandewalleh.auth + +import io.ktor.auth.Principal + +data class UserDbIdPrincipal(val id: Int) : Principal diff --git a/api/src/extensions/ApplicationCallExtensions.kt b/api/src/extensions/ApplicationCallExtensions.kt index 88a93dc..97abb62 100644 --- a/api/src/extensions/ApplicationCallExtensions.kt +++ b/api/src/extensions/ApplicationCallExtensions.kt @@ -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,24 +12,17 @@ import io.ktor.request.* import io.ktor.response.* import org.kodein.di.generic.instance -val userService by kodein.instance() - suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) { respond(status, status.description) } -/** - * @return the user email for the currently authenticated user - */ -fun ApplicationCall.userEmail() = principal()!!.name - /** * @return the userId for the currently authenticated user */ -fun ApplicationCall.userId() = userService.getUserId(userEmail())!! +fun ApplicationCall.userId() = principal()!!.id class NoteCreate(val title: String, val tags: List) suspend fun ApplicationCall.receiveNoteCreate(): FullNoteCreateDTO = receive() -suspend fun ApplicationCall.receiveNotePatch(): FullNotePatchDTO = receive() \ No newline at end of file +suspend fun ApplicationCall.receiveNotePatch(): FullNotePatchDTO = receive() diff --git a/api/src/extensions/Coroutines.kt b/api/src/extensions/Coroutines.kt new file mode 100644 index 0000000..3985325 --- /dev/null +++ b/api/src/extensions/Coroutines.kt @@ -0,0 +1,8 @@ +package be.vandewalleh.extensions + +import kotlinx.coroutines.* + + +fun ioAsync(block: suspend CoroutineScope.() -> T): Deferred { + return CoroutineScope(Dispatchers.IO).async(block = block) +} diff --git a/api/src/routing/AuthController.kt b/api/src/routing/AuthController.kt index 3cea139..93d4f26 100644 --- a/api/src/routing/AuthController.kt +++ b/api/src/routing/AuthController.kt @@ -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() - 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 - 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()!!.name - val info = userService.getUserInfo(email) + val id = call.principal()!!.id + val info = userService.getUserInfo(id) if (info != null) call.respond(mapOf("user" to info)) else call.respondStatus(HttpStatusCode.Unauthorized) } diff --git a/api/src/routing/TitleController.kt b/api/src/routing/TitleController.kt index 8c71777..7bfae54 100644 --- a/api/src/routing/TitleController.kt +++ b/api/src/routing/TitleController.kt @@ -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) } diff --git a/api/src/services/NotesService.kt b/api/src/services/NotesService.kt index 38f6d6e..113c3c8 100644 --- a/api/src/services/NotesService.kt +++ b/api/src/services/NotesService.kt @@ -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 { - 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) - .select(Tags.name) - .where { Tags.noteUuid eq noteUuid } - .map { it[Tags.name]!! } - .toList() - val chapters = 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() + + 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 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, @@ -191,4 +206,4 @@ data class BasicNoteDTO( val title: String, val tags: List, val updatedAt: String -) \ No newline at end of file +) diff --git a/api/src/services/UserService.kt b/api/src/services/UserService.kt index bd58ebd..0ed10f9 100644 --- a/api/src/services/UserService.kt +++ b/api/src/services/UserService.kt @@ -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? { + 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)