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"> <root level="INFO">
<appender-ref ref="STDOUT"/> <appender-ref ref="STDOUT"/>
</root> </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="com.zaxxer.hikari" level="INFO"/>
<logger name="org.eclipse.jetty" level="INFO"/> <logger name="org.eclipse.jetty" level="INFO"/>
<logger name="io.netty" 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.AuthenticationModule
import be.vandewalleh.auth.SimpleJWT import be.vandewalleh.auth.SimpleJWT
import be.vandewalleh.extensions.ApplicationBuilder import be.vandewalleh.extensions.ApplicationBuilder
import be.vandewalleh.extensions.RoutingBuilder
import be.vandewalleh.factories.configurationFactory import be.vandewalleh.factories.configurationFactory
import be.vandewalleh.factories.dataSourceFactory import be.vandewalleh.factories.dataSourceFactory
import be.vandewalleh.factories.databaseFactory import be.vandewalleh.factories.databaseFactory
import be.vandewalleh.factories.simpleJwtFactory import be.vandewalleh.factories.simpleJwtFactory
import be.vandewalleh.features.* 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.NoteService
import be.vandewalleh.services.UserService import be.vandewalleh.services.UserService
import org.kodein.di.Kodein 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 { MigrationHook(instance()) }
bind<ApplicationBuilder>().inSet() with singleton { ShutdownDatabaseConnection(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 = "auth") with singleton { simpleJwtFactory(instance<Config>().jwt.auth) }
bind<SimpleJWT>(tag = "refresh") with singleton { simpleJwtFactory(instance<Config>().jwt.refresh) } bind<SimpleJWT>(tag = "refresh") with singleton { simpleJwtFactory(instance<Config>().jwt.refresh) }

View File

@ -1,11 +1,8 @@
package be.vandewalleh package be.vandewalleh
import be.vandewalleh.extensions.ApplicationBuilder import be.vandewalleh.extensions.ApplicationBuilder
import be.vandewalleh.routing.noteRoutes import be.vandewalleh.extensions.RoutingBuilder
import be.vandewalleh.routing.tagsRoute
import be.vandewalleh.routing.userRoutes
import io.ktor.application.* import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.routing.* import io.ktor.routing.*
import io.ktor.server.engine.* import io.ktor.server.engine.*
import io.ktor.server.netty.* import io.ktor.server.netty.*
@ -43,7 +40,7 @@ fun serve(kodein: Kodein) {
} }
} }
with(embeddedServer(Netty, env)) { with(embeddedServer(Netty, env)) {
addShutdownHook { stop(3, 5, TimeUnit.SECONDS) } addShutdownHook { stop(1, 5, TimeUnit.SECONDS) }
start(wait = true) start(wait = true)
} }
} }
@ -56,18 +53,8 @@ fun Application.module(kodein: Kodein) {
it.builder(this) it.builder(this)
} }
routing { val routingBuilders: Set<RoutingBuilder> by kodein.instance()
route("/user") { routingBuilders.forEach {
userRoutes(kodein) routing(it.builder)
}
authenticate {
route("/notes") {
noteRoutes(kodein)
}
route("/tags") {
tagsRoute(kodein)
}
}
} }
} }

View File

@ -1,5 +1,6 @@
package be.vandewalleh.routing package be.vandewalleh.routing
import be.vandewalleh.extensions.RoutingBuilder
import be.vandewalleh.extensions.authenticatedUserId import be.vandewalleh.extensions.authenticatedUserId
import be.vandewalleh.extensions.respondStatus import be.vandewalleh.extensions.respondStatus
import be.vandewalleh.features.ValidationException import be.vandewalleh.features.ValidationException
@ -7,69 +8,89 @@ import be.vandewalleh.services.NoteService
import be.vandewalleh.validation.noteValidator import be.vandewalleh.validation.noteValidator
import be.vandewalleh.validation.receiveValidated import be.vandewalleh.validation.receiveValidated
import io.ktor.application.* import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.http.* import io.ktor.http.*
import io.ktor.response.* import io.ktor.response.*
import io.ktor.routing.* import io.ktor.routing.*
import org.kodein.di.Kodein
import org.kodein.di.generic.instance
import java.util.* import java.util.*
fun Route.noteRoutes(kodein: Kodein) { class NoteRoutes(noteService: NoteService) : RoutingBuilder({
val noteService by kodein.instance<NoteService>() authenticate {
route("/notes") {
createNote(noteService)
getAllNotes(noteService)
route("/{uuid}") {
getNote(noteService)
updateNote(noteService)
deleteNote(noteService)
}
}
}
})
private fun Route.createNote(noteService: NoteService) {
post { post {
val userId = call.authenticatedUserId() val userId = call.authenticatedUserId()
val note = call.receiveValidated(noteValidator) val note = call.receiveValidated(noteValidator)
val createdNote = noteService.create(userId, note) val createdNote = noteService.create(userId, note)
call.respond(HttpStatusCode.Created, createdNote) call.respond(HttpStatusCode.Created, createdNote)
} }
}
private fun Route.getAllNotes(noteService: NoteService) {
get { get {
val userId = call.authenticatedUserId() 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) call.respond(notes)
} }
}
route("/{uuid}") { private fun Route.getNote(noteService: NoteService) {
get {
fun ApplicationCall.userIdNoteIdPair(): Pair<Int, UUID> { val userId = call.authenticatedUserId()
val userId = authenticatedUserId() val noteUuid = call.noteUuid()
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)
}
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")
} }
} }

View File

@ -1,17 +1,19 @@
package be.vandewalleh.routing package be.vandewalleh.routing
import be.vandewalleh.extensions.RoutingBuilder
import be.vandewalleh.extensions.authenticatedUserId import be.vandewalleh.extensions.authenticatedUserId
import be.vandewalleh.services.NoteService import be.vandewalleh.services.NoteService
import io.ktor.application.* import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.response.* import io.ktor.response.*
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
fun Route.tagsRoute(kodein: Kodein) { class TagRoutes(noteService: NoteService) : RoutingBuilder({
val noteService by kodein.instance<NoteService>() authenticate {
get("/tags") {
get { call.respond(noteService.getTags(call.authenticatedUserId()))
call.respond(noteService.getTags(call.authenticatedUserId())) }
} }
} })

View File

@ -1,8 +1,8 @@
package be.vandewalleh.routing package be.vandewalleh.routing
import be.vandewalleh.auth.SimpleJWT import be.vandewalleh.auth.SimpleJWT
import be.vandewalleh.auth.UserIdPrincipal
import be.vandewalleh.auth.UsernamePasswordCredential import be.vandewalleh.auth.UsernamePasswordCredential
import be.vandewalleh.extensions.RoutingBuilder
import be.vandewalleh.extensions.authenticatedUserId import be.vandewalleh.extensions.authenticatedUserId
import be.vandewalleh.extensions.respondStatus import be.vandewalleh.extensions.respondStatus
import be.vandewalleh.features.PasswordHash import be.vandewalleh.features.PasswordHash
@ -16,18 +16,51 @@ import io.ktor.http.*
import io.ktor.request.* import io.ktor.request.*
import io.ktor.response.* import io.ktor.response.*
import io.ktor.routing.* import io.ktor.routing.*
import org.kodein.di.Kodein
import org.kodein.di.generic.instance
data class RefreshToken(val refreshToken: String) class UserRoutes(
data class DualToken(val token: String, val refreshToken: String) 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) { private fun Route.userInfo(userService: UserService) {
val authSimpleJwt by kodein.instance<SimpleJWT>("auth") get {
val refreshSimpleJwt by kodein.instance<SimpleJWT>("refresh") val id = call.authenticatedUserId()
val userService by kodein.instance<UserService>() val info = userService.find(id)
val passwordHash by kodein.instance<PasswordHash>() 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 { post {
val user = call.receiveValidated(registerValidator) val user = call.receiveValidated(registerValidator)
@ -39,8 +72,16 @@ fun Route.userRoutes(kodein: Kodein) {
call.respond(HttpStatusCode.Created, newUser) 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 credential = call.receive<UsernamePasswordCredential>()
val user = userService.find(credential.username) val user = userService.find(credential.username)
@ -51,17 +92,20 @@ fun Route.userRoutes(kodein: Kodein) {
} }
val response = DualToken( val response = DualToken(
token = authSimpleJwt.sign(user.id), token = authJWT.sign(user.id),
refreshToken = refreshSimpleJwt.sign(user.id) refreshToken = refreshJWT.sign(user.id)
) )
return@post call.respond(response) 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 token = call.receive<RefreshToken>().refreshToken
val id = try { val id = try {
val decodedJWT = refreshSimpleJwt.verifier.verify(token) val decodedJWT = refreshJWT.verifier.verify(token)
decodedJWT.getClaim("id").asInt() decodedJWT.getClaim("id").asInt()
} catch (e: JWTVerificationException) { } catch (e: JWTVerificationException) {
return@post call.respondStatus(HttpStatusCode.Unauthorized) return@post call.respondStatus(HttpStatusCode.Unauthorized)
@ -71,26 +115,12 @@ fun Route.userRoutes(kodein: Kodein) {
return@post call.respondStatus(HttpStatusCode.Unauthorized) return@post call.respondStatus(HttpStatusCode.Unauthorized)
val response = DualToken( val response = DualToken(
token = authSimpleJwt.sign(id), token = authJWT.sign(id),
refreshToken = refreshSimpleJwt.sign(id) refreshToken = refreshJWT.sign(id)
) )
return@post call.respond(response) 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 * 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) val notes = db.sequenceOf(Notes, withReferences = false)
.filterColumns { it.columns - it.userId } .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 } .sortedByDescending { it.updatedAt }
.take(limit).drop(offset) .take(limit)
.toList() .toList()
if (notes.isEmpty()) return@launchIo emptyList() if (notes.isEmpty()) return@launchIo emptyList()

View File

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