Performance improvements
This commit is contained in:
parent
8a9e878d5f
commit
f40349ea98
@ -12,8 +12,8 @@ fun Application.authenticationModule() {
|
|||||||
val simpleJwt by kodein.instance<SimpleJWT>(tag = "auth")
|
val simpleJwt by kodein.instance<SimpleJWT>(tag = "auth")
|
||||||
verifier(simpleJwt.verifier)
|
verifier(simpleJwt.verifier)
|
||||||
validate {
|
validate {
|
||||||
UserIdPrincipal(it.payload.getClaim("email").asString())
|
UserDbIdPrincipal(it.payload.getClaim("id").asInt())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,10 +11,10 @@ class SimpleJWT(secret: String, validity: Long, unit: TimeUnit) {
|
|||||||
private val algorithm = Algorithm.HMAC256(secret)
|
private val algorithm = Algorithm.HMAC256(secret)
|
||||||
|
|
||||||
val verifier: JWTVerifier = JWT.require(algorithm).build()
|
val verifier: JWTVerifier = JWT.require(algorithm).build()
|
||||||
fun sign(email: String): String = JWT.create()
|
fun sign(id: Int): String = JWT.create()
|
||||||
.withClaim("email", email)
|
.withClaim("id", id)
|
||||||
.withExpiresAt(getExpiration())
|
.withExpiresAt(getExpiration())
|
||||||
.sign(algorithm)
|
.sign(algorithm)
|
||||||
|
|
||||||
private fun getExpiration() = Date(System.currentTimeMillis() + validityInMs)
|
private fun getExpiration() = Date(System.currentTimeMillis() + validityInMs)
|
||||||
}
|
}
|
||||||
|
|||||||
5
api/src/auth/UserDbIdPrincipal.kt
Normal file
5
api/src/auth/UserDbIdPrincipal.kt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package be.vandewalleh.auth
|
||||||
|
|
||||||
|
import io.ktor.auth.Principal
|
||||||
|
|
||||||
|
data class UserDbIdPrincipal(val id: Int) : Principal
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package be.vandewalleh.extensions
|
package be.vandewalleh.extensions
|
||||||
|
|
||||||
|
import be.vandewalleh.auth.UserDbIdPrincipal
|
||||||
import be.vandewalleh.kodein
|
import be.vandewalleh.kodein
|
||||||
import be.vandewalleh.services.FullNoteCreateDTO
|
import be.vandewalleh.services.FullNoteCreateDTO
|
||||||
import be.vandewalleh.services.FullNotePatchDTO
|
import be.vandewalleh.services.FullNotePatchDTO
|
||||||
@ -11,24 +12,17 @@ import io.ktor.request.*
|
|||||||
import io.ktor.response.*
|
import io.ktor.response.*
|
||||||
import org.kodein.di.generic.instance
|
import org.kodein.di.generic.instance
|
||||||
|
|
||||||
val userService by kodein.instance<UserService>()
|
|
||||||
|
|
||||||
suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) {
|
suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) {
|
||||||
respond(status, status.description)
|
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
|
* @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>)
|
class NoteCreate(val title: String, val tags: List<String>)
|
||||||
|
|
||||||
suspend fun ApplicationCall.receiveNoteCreate(): FullNoteCreateDTO = receive()
|
suspend fun ApplicationCall.receiveNoteCreate(): FullNoteCreateDTO = receive()
|
||||||
|
|
||||||
suspend fun ApplicationCall.receiveNotePatch(): FullNotePatchDTO = receive()
|
suspend fun ApplicationCall.receiveNotePatch(): FullNotePatchDTO = receive()
|
||||||
|
|||||||
8
api/src/extensions/Coroutines.kt
Normal file
8
api/src/extensions/Coroutines.kt
Normal 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)
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package be.vandewalleh.routing
|
package be.vandewalleh.routing
|
||||||
|
|
||||||
import be.vandewalleh.auth.SimpleJWT
|
import be.vandewalleh.auth.SimpleJWT
|
||||||
|
import be.vandewalleh.auth.UserDbIdPrincipal
|
||||||
import be.vandewalleh.auth.UsernamePasswordCredential
|
import be.vandewalleh.auth.UsernamePasswordCredential
|
||||||
import be.vandewalleh.extensions.respondStatus
|
import be.vandewalleh.extensions.respondStatus
|
||||||
import be.vandewalleh.services.UserService
|
import be.vandewalleh.services.UserService
|
||||||
@ -26,16 +27,16 @@ fun Routing.auth(kodein: Kodein) {
|
|||||||
post("/user/login") {
|
post("/user/login") {
|
||||||
val credential = call.receive<UsernamePasswordCredential>()
|
val credential = call.receive<UsernamePasswordCredential>()
|
||||||
|
|
||||||
val (email, password) = userService.getEmailAndPasswordFromUsername(credential.username)
|
val user = userService.getFromUsername(credential.username)
|
||||||
?: return@post call.respondStatus(HttpStatusCode.Unauthorized)
|
?: 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)
|
return@post call.respondStatus(HttpStatusCode.Unauthorized)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = DualToken(
|
val response = DualToken(
|
||||||
token = authSimpleJwt.sign(email),
|
token = authSimpleJwt.sign(user.id),
|
||||||
refreshToken = refreshSimpleJwt.sign(email)
|
refreshToken = refreshSimpleJwt.sign(user.id)
|
||||||
)
|
)
|
||||||
return@post call.respond(response)
|
return@post call.respond(response)
|
||||||
}
|
}
|
||||||
@ -43,16 +44,16 @@ fun Routing.auth(kodein: Kodein) {
|
|||||||
post("/user/refresh_token") {
|
post("/user/refresh_token") {
|
||||||
val token = call.receive<RefreshToken>().refreshToken
|
val token = call.receive<RefreshToken>().refreshToken
|
||||||
|
|
||||||
val email = try {
|
val id = try {
|
||||||
val decodedJWT = refreshSimpleJwt.verifier.verify(token)
|
val decodedJWT = refreshSimpleJwt.verifier.verify(token)
|
||||||
decodedJWT.getClaim("email").asString()
|
decodedJWT.getClaim("id").asInt()
|
||||||
} catch (e: JWTVerificationException) {
|
} catch (e: JWTVerificationException) {
|
||||||
return@post call.respondStatus(HttpStatusCode.Unauthorized)
|
return@post call.respondStatus(HttpStatusCode.Unauthorized)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = DualToken(
|
val response = DualToken(
|
||||||
token = authSimpleJwt.sign(email),
|
token = authSimpleJwt.sign(id),
|
||||||
refreshToken = refreshSimpleJwt.sign(email)
|
refreshToken = refreshSimpleJwt.sign(id)
|
||||||
)
|
)
|
||||||
return@post call.respond(response)
|
return@post call.respond(response)
|
||||||
}
|
}
|
||||||
@ -60,8 +61,8 @@ fun Routing.auth(kodein: Kodein) {
|
|||||||
authenticate {
|
authenticate {
|
||||||
get("/user/me") {
|
get("/user/me") {
|
||||||
// retrieve email from token
|
// retrieve email from token
|
||||||
val email = call.principal<UserIdPrincipal>()!!.name
|
val id = call.principal<UserDbIdPrincipal>()!!.id
|
||||||
val info = userService.getUserInfo(email)
|
val info = userService.getUserInfo(id)
|
||||||
if (info != null) call.respond(mapOf("user" to info))
|
if (info != null) call.respond(mapOf("user" to info))
|
||||||
else call.respondStatus(HttpStatusCode.Unauthorized)
|
else call.respondStatus(HttpStatusCode.Unauthorized)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,10 +5,10 @@ import be.vandewalleh.extensions.receiveNotePatch
|
|||||||
import be.vandewalleh.extensions.respondStatus
|
import be.vandewalleh.extensions.respondStatus
|
||||||
import be.vandewalleh.extensions.userId
|
import be.vandewalleh.extensions.userId
|
||||||
import be.vandewalleh.services.NotesService
|
import be.vandewalleh.services.NotesService
|
||||||
import io.ktor.application.*
|
import io.ktor.application.call
|
||||||
import io.ktor.auth.*
|
import io.ktor.auth.authenticate
|
||||||
import io.ktor.http.*
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.response.*
|
import io.ktor.response.respond
|
||||||
import io.ktor.routing.*
|
import io.ktor.routing.*
|
||||||
import org.kodein.di.Kodein
|
import org.kodein.di.Kodein
|
||||||
import org.kodein.di.generic.instance
|
import org.kodein.di.generic.instance
|
||||||
@ -22,10 +22,8 @@ fun Routing.title(kodein: Kodein) {
|
|||||||
val userId = call.userId()
|
val userId = call.userId()
|
||||||
val noteUuid = call.parameters.noteUuid()
|
val noteUuid = call.parameters.noteUuid()
|
||||||
|
|
||||||
val exists = notesService.noteExists(userId, noteUuid)
|
val response =
|
||||||
if (!exists) return@get call.respondStatus(HttpStatusCode.NotFound)
|
notesService.getNote(userId, noteUuid) ?: return@get call.respondStatus(HttpStatusCode.NotFound)
|
||||||
|
|
||||||
val response = notesService.getNote(noteUuid)
|
|
||||||
call.respond(response)
|
call.respond(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
package be.vandewalleh.services
|
package be.vandewalleh.services
|
||||||
|
|
||||||
|
import be.vandewalleh.extensions.ioAsync
|
||||||
import be.vandewalleh.tables.Chapters
|
import be.vandewalleh.tables.Chapters
|
||||||
import be.vandewalleh.tables.Notes
|
import be.vandewalleh.tables.Notes
|
||||||
import be.vandewalleh.tables.Tags
|
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.dsl.*
|
||||||
import me.liuwj.ktorm.entity.*
|
import me.liuwj.ktorm.entity.*
|
||||||
import org.kodein.di.Kodein
|
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
|
* returns a list of [BasicNoteDTO] associated with the userId
|
||||||
*/
|
*/
|
||||||
fun getNotes(userId: Int): List<BasicNoteDTO> {
|
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) }
|
.filterColumns { listOf(it.uuid, it.title, it.updatedAt) }
|
||||||
.filter { it.userId eq userId }
|
.filter { it.userId eq userId }
|
||||||
.sortedByDescending { it.updatedAt }
|
.sortedByDescending { it.updatedAt }
|
||||||
@ -31,7 +32,7 @@ class NotesService(override val kodein: Kodein) : KodeinAware {
|
|||||||
|
|
||||||
if (notes.isEmpty()) return emptyList()
|
if (notes.isEmpty()) return emptyList()
|
||||||
|
|
||||||
val tags = db.sequenceOf(Tags)
|
val tags = db.sequenceOf(Tags, withReferences = false)
|
||||||
.filterColumns { listOf(it.noteUuid, it.name) }
|
.filterColumns { listOf(it.noteUuid, it.name) }
|
||||||
.filter { it.noteUuid inList notes.map { it.uuid } }
|
.filter { it.noteUuid inList notes.map { it.uuid } }
|
||||||
.toList()
|
.toList()
|
||||||
@ -89,25 +90,39 @@ class NotesService(override val kodein: Kodein) : KodeinAware {
|
|||||||
return uuid
|
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)
|
suspend fun getNote(userId: Int, noteUuid: UUID): FullNoteDTO? {
|
||||||
.where { Chapters.noteUuid eq noteUuid }
|
val deferredNote = ioAsync {
|
||||||
.orderBy(Chapters.number.asc())
|
db.sequenceOf(Notes, withReferences = false)
|
||||||
.map { ChapterDTO(it[Chapters.title]!!, it[Chapters.content]!!) }
|
.filterColumns { listOf(it.title, it.updatedAt) }
|
||||||
.toList()
|
.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 updatedAtFormatted = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(note.updatedAt)
|
||||||
|
val tags = deferredTags.await()
|
||||||
|
val chapters = deferredChapters.await()
|
||||||
return FullNoteDTO(
|
return FullNoteDTO(
|
||||||
uuid = noteUuid,
|
uuid = noteUuid,
|
||||||
title = note.title,
|
title = note.title,
|
||||||
@ -191,4 +206,4 @@ data class BasicNoteDTO(
|
|||||||
val title: String,
|
val title: String,
|
||||||
val tags: List<String>,
|
val tags: List<String>,
|
||||||
val updatedAt: String
|
val updatedAt: String
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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
|
* 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)
|
return db.from(Users)
|
||||||
.select(Users.email, Users.password)
|
.select(Users.email, Users.password, Users.id)
|
||||||
.where { Users.username eq username }
|
.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()
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,11 +59,10 @@ class UserService(override val kodein: Kodein) : KodeinAware {
|
|||||||
.firstOrNull() != null
|
.firstOrNull() != null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUserInfo(email: String): UserInfoDto? {
|
fun getUserInfo(id: Int): UserInfoDto? {
|
||||||
return db.from(Users)
|
return db.from(Users)
|
||||||
.select(Users.email, Users.username)
|
.select(Users.email, Users.username)
|
||||||
.where { Users.email eq email }
|
.where { Users.id eq id }
|
||||||
.limit(0, 1)
|
|
||||||
.map { UserInfoDto(it[Users.username]!!, it[Users.email]!!) }
|
.map { UserInfoDto(it[Users.username]!!, it[Users.email]!!) }
|
||||||
.firstOrNull()
|
.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 UserDto(val username: String, val email: String, val password: String)
|
||||||
data class UserInfoDto(val username: String, val email: String)
|
data class UserInfoDto(val username: String, val email: String)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user