From 02c6b2a0c5c9942d1d2065bb3c9921ec695aacd8 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Wed, 1 Jul 2020 16:35:13 +0200 Subject: [PATCH] Clean routes --- api/resources/logback.xml | 2 +- api/src/Dependencies.kt | 17 ++++ api/src/NotesApplication.kt | 23 ++---- api/src/routing/NoteRoutes.kt | 111 ++++++++++++++++----------- api/src/routing/TagRoutes.kt | 14 ++-- api/src/routing/UserRoutes.kt | 98 +++++++++++++++-------- api/src/services/NoteService.kt | 19 ++++- api/src/validation/NoteValidation.kt | 5 +- 8 files changed, 181 insertions(+), 108 deletions(-) diff --git a/api/resources/logback.xml b/api/resources/logback.xml index ad8c3e6..3a06365 100644 --- a/api/resources/logback.xml +++ b/api/resources/logback.xml @@ -9,7 +9,7 @@ - + diff --git a/api/src/Dependencies.kt b/api/src/Dependencies.kt index 210d4b6..ab863f9 100644 --- a/api/src/Dependencies.kt +++ b/api/src/Dependencies.kt @@ -3,11 +3,15 @@ package be.vandewalleh import be.vandewalleh.auth.AuthenticationModule import be.vandewalleh.auth.SimpleJWT import be.vandewalleh.extensions.ApplicationBuilder +import be.vandewalleh.extensions.RoutingBuilder import be.vandewalleh.factories.configurationFactory import be.vandewalleh.factories.dataSourceFactory import be.vandewalleh.factories.databaseFactory import be.vandewalleh.factories.simpleJwtFactory import be.vandewalleh.features.* +import be.vandewalleh.routing.NoteRoutes +import be.vandewalleh.routing.TagRoutes +import be.vandewalleh.routing.UserRoutes import be.vandewalleh.services.NoteService import be.vandewalleh.services.UserService import org.kodein.di.Kodein @@ -28,6 +32,19 @@ val mainModule = Kodein.Module("main") { bind().inSet() with singleton { MigrationHook(instance()) } bind().inSet() with singleton { ShutdownDatabaseConnection(instance()) } + bind() from setBinding() + bind().inSet() with singleton { NoteRoutes(instance()) } + bind().inSet() with singleton { TagRoutes(instance()) } + bind().inSet() with singleton { + UserRoutes( + instance(tag = "auth"), + instance(tag = "refresh"), + instance(), + instance() + ) + } + + bind(tag = "auth") with singleton { simpleJwtFactory(instance().jwt.auth) } bind(tag = "refresh") with singleton { simpleJwtFactory(instance().jwt.refresh) } diff --git a/api/src/NotesApplication.kt b/api/src/NotesApplication.kt index a6d66e6..0b27dbb 100644 --- a/api/src/NotesApplication.kt +++ b/api/src/NotesApplication.kt @@ -1,11 +1,8 @@ package be.vandewalleh import be.vandewalleh.extensions.ApplicationBuilder -import be.vandewalleh.routing.noteRoutes -import be.vandewalleh.routing.tagsRoute -import be.vandewalleh.routing.userRoutes +import be.vandewalleh.extensions.RoutingBuilder import io.ktor.application.* -import io.ktor.auth.* import io.ktor.routing.* import io.ktor.server.engine.* import io.ktor.server.netty.* @@ -43,7 +40,7 @@ fun serve(kodein: Kodein) { } } with(embeddedServer(Netty, env)) { - addShutdownHook { stop(3, 5, TimeUnit.SECONDS) } + addShutdownHook { stop(1, 5, TimeUnit.SECONDS) } start(wait = true) } } @@ -56,18 +53,8 @@ fun Application.module(kodein: Kodein) { it.builder(this) } - routing { - route("/user") { - userRoutes(kodein) - } - authenticate { - route("/notes") { - noteRoutes(kodein) - } - route("/tags") { - tagsRoute(kodein) - } - } + val routingBuilders: Set by kodein.instance() + routingBuilders.forEach { + routing(it.builder) } - } diff --git a/api/src/routing/NoteRoutes.kt b/api/src/routing/NoteRoutes.kt index c3d3e1f..2cba7ba 100644 --- a/api/src/routing/NoteRoutes.kt +++ b/api/src/routing/NoteRoutes.kt @@ -1,5 +1,6 @@ package be.vandewalleh.routing +import be.vandewalleh.extensions.RoutingBuilder import be.vandewalleh.extensions.authenticatedUserId import be.vandewalleh.extensions.respondStatus import be.vandewalleh.features.ValidationException @@ -7,69 +8,89 @@ import be.vandewalleh.services.NoteService import be.vandewalleh.validation.noteValidator import be.vandewalleh.validation.receiveValidated import io.ktor.application.* +import io.ktor.auth.* import io.ktor.http.* import io.ktor.response.* import io.ktor.routing.* -import org.kodein.di.Kodein -import org.kodein.di.generic.instance import java.util.* -fun Route.noteRoutes(kodein: Kodein) { - val noteService by kodein.instance() +class NoteRoutes(noteService: NoteService) : RoutingBuilder({ + authenticate { + route("/notes") { + createNote(noteService) + getAllNotes(noteService) + route("/{uuid}") { + getNote(noteService) + updateNote(noteService) + deleteNote(noteService) + } + } + } +}) + + +private fun Route.createNote(noteService: NoteService) { post { val userId = call.authenticatedUserId() val note = call.receiveValidated(noteValidator) val createdNote = noteService.create(userId, note) call.respond(HttpStatusCode.Created, createdNote) } +} +private fun Route.getAllNotes(noteService: NoteService) { get { val userId = call.authenticatedUserId() - val notes = noteService.findAll(userId) + val limit = call.parameters["limit"]?.toInt() ?: 20// FIXME validate + val after = call.parameters["after"]?.let { UUID.fromString(it) } // FIXME validate + val notes = noteService.findAll(userId, limit, after) call.respond(notes) } +} - route("/{uuid}") { - - fun ApplicationCall.userIdNoteIdPair(): Pair { - val userId = authenticatedUserId() - val uuid = parameters["uuid"] - val noteUuid = try { - UUID.fromString(uuid) - } catch (e: IllegalArgumentException) { - throw ValidationException("`$uuid` is not a valid UUID") - } - return userId to noteUuid - } - - get { - val (userId, noteUuid) = call.userIdNoteIdPair() - - val response = noteService.find(userId, noteUuid) - ?: return@get call.respondStatus(HttpStatusCode.NotFound) - call.respond(response) - } - - put { - val (userId, noteUuid) = call.userIdNoteIdPair() - - val note = call.receiveValidated(noteValidator) - note.uuid = noteUuid - - if (noteService.updateNote(userId, note)) - call.respondStatus(HttpStatusCode.OK) - else call.respondStatus(HttpStatusCode.NotFound) - } - - delete { - val (userId, noteUuid) = call.userIdNoteIdPair() - - if (noteService.delete(userId, noteUuid)) - call.respondStatus(HttpStatusCode.OK) - else - call.respondStatus(HttpStatusCode.NotFound) - } +private fun Route.getNote(noteService: NoteService) { + get { + val userId = call.authenticatedUserId() + val noteUuid = call.noteUuid() + val response = noteService.find(userId, noteUuid) + ?: return@get call.respondStatus(HttpStatusCode.NotFound) + call.respond(response) + } +} + +private fun Route.updateNote(noteService: NoteService) { + put { + val userId = call.authenticatedUserId() + val noteUuid = call.noteUuid() + + val note = call.receiveValidated(noteValidator) + note.uuid = noteUuid + + if (noteService.updateNote(userId, note)) + call.respondStatus(HttpStatusCode.OK) + else call.respondStatus(HttpStatusCode.NotFound) + } +} + +private fun Route.deleteNote(noteService: NoteService) { + delete { + val userId = call.authenticatedUserId() + val noteUuid = call.noteUuid() + + if (noteService.delete(userId, noteUuid)) + call.respondStatus(HttpStatusCode.OK) + else + call.respondStatus(HttpStatusCode.NotFound) + } +} + +private fun ApplicationCall.noteUuid(): UUID { + val uuid = parameters["uuid"] + return try { + UUID.fromString(uuid) + } catch (e: IllegalArgumentException) { + throw ValidationException("`$uuid` is not a valid UUID") } } diff --git a/api/src/routing/TagRoutes.kt b/api/src/routing/TagRoutes.kt index e721c51..91a65d8 100644 --- a/api/src/routing/TagRoutes.kt +++ b/api/src/routing/TagRoutes.kt @@ -1,17 +1,19 @@ package be.vandewalleh.routing +import be.vandewalleh.extensions.RoutingBuilder import be.vandewalleh.extensions.authenticatedUserId import be.vandewalleh.services.NoteService import io.ktor.application.* +import io.ktor.auth.* import io.ktor.response.* import io.ktor.routing.* import org.kodein.di.Kodein import org.kodein.di.generic.instance -fun Route.tagsRoute(kodein: Kodein) { - val noteService by kodein.instance() - - get { - call.respond(noteService.getTags(call.authenticatedUserId())) +class TagRoutes(noteService: NoteService) : RoutingBuilder({ + authenticate { + get("/tags") { + call.respond(noteService.getTags(call.authenticatedUserId())) + } } -} +}) diff --git a/api/src/routing/UserRoutes.kt b/api/src/routing/UserRoutes.kt index 6db3b69..15e217a 100644 --- a/api/src/routing/UserRoutes.kt +++ b/api/src/routing/UserRoutes.kt @@ -1,8 +1,8 @@ package be.vandewalleh.routing import be.vandewalleh.auth.SimpleJWT -import be.vandewalleh.auth.UserIdPrincipal import be.vandewalleh.auth.UsernamePasswordCredential +import be.vandewalleh.extensions.RoutingBuilder import be.vandewalleh.extensions.authenticatedUserId import be.vandewalleh.extensions.respondStatus import be.vandewalleh.features.PasswordHash @@ -16,18 +16,51 @@ import io.ktor.http.* import io.ktor.request.* import io.ktor.response.* import io.ktor.routing.* -import org.kodein.di.Kodein -import org.kodein.di.generic.instance -data class RefreshToken(val refreshToken: String) -data class DualToken(val token: String, val refreshToken: String) +class UserRoutes( + authJWT: SimpleJWT, + refreshJWT: SimpleJWT, + userService: UserService, + passwordHash: PasswordHash +) : RoutingBuilder({ + route("/user") { + createUser(userService) + route("/login") { + login(userService, passwordHash, authJWT, refreshJWT) + } + route("/refresh_token") { + refreshToken(userService, authJWT, refreshJWT) + } + authenticate { + deleteUser(userService) + route("/me") { + userInfo(userService) + } + } + } +}) -fun Route.userRoutes(kodein: Kodein) { - val authSimpleJwt by kodein.instance("auth") - val refreshSimpleJwt by kodein.instance("refresh") - val userService by kodein.instance() - val passwordHash by kodein.instance() +private fun Route.userInfo(userService: UserService) { + get { + val id = call.authenticatedUserId() + val info = userService.find(id) + if (info != null) call.respond(mapOf("user" to info)) + else call.respondStatus(HttpStatusCode.Unauthorized) + } +} +private fun Route.deleteUser(userService: UserService) { + delete { + val userId = call.authenticatedUserId() + call.respondStatus( + if (userService.delete(userId)) HttpStatusCode.OK + else HttpStatusCode.NotFound + ) + } +} + + +private fun Route.createUser(userService: UserService) { post { val user = call.receiveValidated(registerValidator) @@ -39,8 +72,16 @@ fun Route.userRoutes(kodein: Kodein) { call.respond(HttpStatusCode.Created, newUser) } +} - post("/login") { + +private fun Route.login( + userService: UserService, + passwordHash: PasswordHash, + authJWT: SimpleJWT, + refreshJWT: SimpleJWT +) { + post { val credential = call.receive() val user = userService.find(credential.username) @@ -51,17 +92,20 @@ fun Route.userRoutes(kodein: Kodein) { } val response = DualToken( - token = authSimpleJwt.sign(user.id), - refreshToken = refreshSimpleJwt.sign(user.id) + token = authJWT.sign(user.id), + refreshToken = refreshJWT.sign(user.id) ) return@post call.respond(response) } +} - post("/refresh_token") { + +private fun Route.refreshToken(userService: UserService, authJWT: SimpleJWT, refreshJWT: SimpleJWT) { + post { val token = call.receive().refreshToken val id = try { - val decodedJWT = refreshSimpleJwt.verifier.verify(token) + val decodedJWT = refreshJWT.verifier.verify(token) decodedJWT.getClaim("id").asInt() } catch (e: JWTVerificationException) { return@post call.respondStatus(HttpStatusCode.Unauthorized) @@ -71,26 +115,12 @@ fun Route.userRoutes(kodein: Kodein) { return@post call.respondStatus(HttpStatusCode.Unauthorized) val response = DualToken( - token = authSimpleJwt.sign(id), - refreshToken = refreshSimpleJwt.sign(id) + token = authJWT.sign(id), + refreshToken = refreshJWT.sign(id) ) return@post call.respond(response) } - - authenticate { - delete { - val userId = call.authenticatedUserId() - call.respondStatus( - if (userService.delete(userId)) HttpStatusCode.OK - else HttpStatusCode.NotFound - ) - } - - get("/me") { - val id = call.principal()!!.id - val info = userService.find(id) - if (info != null) call.respond(mapOf("user" to info)) - else call.respondStatus(HttpStatusCode.Unauthorized) - } - } } + +private data class RefreshToken(val refreshToken: String) +private data class DualToken(val token: String, val refreshToken: String) diff --git a/api/src/services/NoteService.kt b/api/src/services/NoteService.kt index cd07b2c..894e816 100644 --- a/api/src/services/NoteService.kt +++ b/api/src/services/NoteService.kt @@ -18,12 +18,25 @@ class NoteService(private val db: Database) { /** * returns a list of [Note] associated with the userId */ - suspend fun findAll(userId: Int, limit: Int = 20, offset: Int = 0): List = launchIo { + suspend fun findAll(userId: Int, limit: Int = 20, after: UUID? = null): List = launchIo { + + var previous: LocalDateTime? = null + + if (after != null) { + previous = db.sequenceOf(Notes, withReferences = false) + .filter { it.userId eq userId and (it.uuid eq after) } + .mapColumns { it.updatedAt } + .firstOrNull() ?: return@launchIo emptyList() + } + val notes = db.sequenceOf(Notes, withReferences = false) .filterColumns { it.columns - it.userId } - .filter { it.userId eq userId } + .filter { + if (previous == null) it.userId eq userId + else (it.userId eq userId) and (it.updatedAt less previous) + } .sortedByDescending { it.updatedAt } - .take(limit).drop(offset) + .take(limit) .toList() if (notes.isEmpty()) return@launchIo emptyList() diff --git a/api/src/validation/NoteValidation.kt b/api/src/validation/NoteValidation.kt index f9262dc..221e4fb 100644 --- a/api/src/validation/NoteValidation.kt +++ b/api/src/validation/NoteValidation.kt @@ -10,6 +10,9 @@ val noteValidator: Validator = ValidatorBuilder.of() notNull().notBlank().lessThanOrEqual(50) } .konstraint(Note::tags) { - this.lessThanOrEqual(10) + lessThanOrEqual(10) + } + .konstraint(Note::content) { + notNull().notBlank() } .build()