Clean routes

This commit is contained in:
Hubert Van De Walle 2020-07-01 16:35:13 +02:00
parent a7c832236c
commit 02c6b2a0c5
8 changed files with 181 additions and 108 deletions

View File

@ -9,7 +9,7 @@
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
<logger name="me.liuwj.ktorm.database" level="INFO"/>
<logger name="me.liuwj.ktorm.database" level="DEBUG"/>
<logger name="com.zaxxer.hikari" level="INFO"/>
<logger name="org.eclipse.jetty" level="INFO"/>
<logger name="io.netty" level="INFO"/>

View File

@ -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<ApplicationBuilder>().inSet() with singleton { MigrationHook(instance()) }
bind<ApplicationBuilder>().inSet() with singleton { ShutdownDatabaseConnection(instance()) }
bind() from setBinding<RoutingBuilder>()
bind<RoutingBuilder>().inSet() with singleton { NoteRoutes(instance()) }
bind<RoutingBuilder>().inSet() with singleton { TagRoutes(instance()) }
bind<RoutingBuilder>().inSet() with singleton {
UserRoutes(
instance(tag = "auth"),
instance(tag = "refresh"),
instance(),
instance()
)
}
bind<SimpleJWT>(tag = "auth") with singleton { simpleJwtFactory(instance<Config>().jwt.auth) }
bind<SimpleJWT>(tag = "refresh") with singleton { simpleJwtFactory(instance<Config>().jwt.refresh) }

View File

@ -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<RoutingBuilder> by kodein.instance()
routingBuilders.forEach {
routing(it.builder)
}
}
}
}

View File

@ -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,52 +8,62 @@ 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<NoteService>()
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<Int, UUID> {
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
}
private fun Route.getNote(noteService: NoteService) {
get {
val (userId, noteUuid) = call.userIdNoteIdPair()
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, noteUuid) = call.userIdNoteIdPair()
val userId = call.authenticatedUserId()
val noteUuid = call.noteUuid()
val note = call.receiveValidated(noteValidator)
note.uuid = noteUuid
@ -61,15 +72,25 @@ fun Route.noteRoutes(kodein: Kodein) {
call.respondStatus(HttpStatusCode.OK)
else call.respondStatus(HttpStatusCode.NotFound)
}
}
private fun Route.deleteNote(noteService: NoteService) {
delete {
val (userId, noteUuid) = call.userIdNoteIdPair()
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")
}
}

View File

@ -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<NoteService>()
get {
class TagRoutes(noteService: NoteService) : RoutingBuilder({
authenticate {
get("/tags") {
call.respond(noteService.getTags(call.authenticatedUserId()))
}
}
})

View File

@ -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<SimpleJWT>("auth")
val refreshSimpleJwt by kodein.instance<SimpleJWT>("refresh")
val userService by kodein.instance<UserService>()
val passwordHash by kodein.instance<PasswordHash>()
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<UsernamePasswordCredential>()
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>().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<UserIdPrincipal>()!!.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)

View File

@ -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<Note> = launchIo {
suspend fun findAll(userId: Int, limit: Int = 20, after: UUID? = null): List<Note> = 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()

View File

@ -10,6 +10,9 @@ val noteValidator: Validator<Note> = ValidatorBuilder.of<Note>()
notNull().notBlank().lessThanOrEqual(50)
}
.konstraint(Note::tags) {
this.lessThanOrEqual(10)
lessThanOrEqual(10)
}
.konstraint(Note::content) {
notNull().notBlank()
}
.build()