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()