Merge branch 'fix/notfound' into refactor/services

This commit is contained in:
Hubert Van De Walle 2020-04-21 16:07:42 +02:00
commit 5850541ad8
20 changed files with 293 additions and 358 deletions

View File

@ -34,7 +34,7 @@
## Authenticate user [/login]
Authenticate one user to access protected routes.
Authenticate one user to access protected routing.
### Authenticate a user [POST]

View File

@ -1,14 +1,20 @@
package be.vandewalleh
import be.vandewalleh.controllers.base.KodeinController
import be.vandewalleh.controllers.controllerModule
import be.vandewalleh.features.configurationFeature
import be.vandewalleh.features.configurationModule
import be.vandewalleh.features.features
import be.vandewalleh.migrations.Migration
import be.vandewalleh.routing.registerRoutes
import be.vandewalleh.services.serviceModule
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.feature
import io.ktor.application.log
import io.ktor.response.respond
import io.ktor.routing.Route
import io.ktor.routing.Routing
import io.ktor.routing.RoutingPath.Companion.root
import io.ktor.routing.get
import io.ktor.routing.routing
import me.liuwj.ktorm.database.Database
import org.kodein.di.Kodein
@ -26,7 +32,6 @@ fun Application.module() {
configurationFeature()
kodein = Kodein {
import(controllerModule)
import(configurationModule)
import(serviceModule)
@ -41,11 +46,18 @@ fun Application.module() {
val migration by kodein.instance<Migration>()
migration.migrate()
val controllers by kodein.instance<Set<KodeinController>>()
routing {
controllers.forEach {
it.apply { registerRoutes() }
}
registerRoutes(kodein)
}
val root = feature(Routing)
val allRoutes = allRoutes(root)
allRoutes.forEach {
println(it.toString())
}
}
fun allRoutes(root: Route): List<Route> {
return listOf(root) + root.children.flatMap { allRoutes(it) }
}

View File

@ -11,7 +11,7 @@ import org.kodein.di.generic.instance
fun Application.authenticationModule() {
install(Authentication) {
jwt {
val simpleJwt: SimpleJWT by kodein.instance()
val simpleJwt by kodein.instance<SimpleJWT>()
verifier(simpleJwt.verifier)
validate {
UserIdPrincipal(it.payload.getClaim("name").asString())

View File

@ -1,24 +0,0 @@
package be.vandewalleh.controllers
import be.vandewalleh.controllers.base.AuthCrudController
import io.ktor.application.ApplicationCall
import io.ktor.routing.Routing
import me.liuwj.ktorm.database.Database
import org.kodein.di.Kodein
import org.kodein.di.generic.instance
class ChaptersController(kodein: Kodein) : AuthCrudController("/notes/{noteTitle}/chapters/{chapterNumber}", kodein) {
private val db by kodein.instance<Database>()
private fun ApplicationCall.noteTitle(): String? {
return this.parameters["noteTitle"]!!
}
private fun ApplicationCall.chapterNumber(): Int? {
return this.parameters["chapterNumber"]?.toIntOrNull()
}
override fun Routing.routes() {
TODO("Not yet implemented")
}
}

View File

@ -1,20 +0,0 @@
package be.vandewalleh.controllers
import be.vandewalleh.controllers.base.KodeinController
import org.kodein.di.Kodein
import org.kodein.di.generic.bind
import org.kodein.di.generic.inSet
import org.kodein.di.generic.setBinding
import org.kodein.di.generic.singleton
/**
* [Kodein] controller module containing the app controllers
*/
val controllerModule = Kodein.Module(name = "Controller") {
bind() from setBinding<KodeinController>()
bind<KodeinController>().inSet() with singleton { RegisterController(this.kodein) }
bind<KodeinController>().inSet() with singleton { LoginController(this.kodein) }
bind<KodeinController>().inSet() with singleton { NotesController(this.kodein) }
bind<KodeinController>().inSet() with singleton { TitleController(this.kodein) }
}

View File

@ -1,35 +0,0 @@
package be.vandewalleh.controllers
import be.vandewalleh.controllers.base.KodeinController
import be.vandewalleh.services.UserRegistrationDto
import be.vandewalleh.services.UserService
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.routing.Routing
import io.ktor.routing.post
import org.kodein.di.Kodein
import org.kodein.di.generic.instance
import org.mindrot.jbcrypt.BCrypt
class RegisterController(kodein: Kodein) : KodeinController("/register", kodein) {
private val userService by instance<UserService>()
override fun Routing.routes() {
post {
val user = call.receive<UserRegistrationDto>()
if (userService.userExists(user.username, user.email))
return@post call.respond(HttpStatusCode.Conflict)
val hashedPassword = BCrypt.hashpw(user.password, BCrypt.gensalt())
userService.createUser(
UserRegistrationDto(user.username, user.email, hashedPassword)
)
return@post call.respondStatus(HttpStatusCode.Created)
}
}
}

View File

@ -1,171 +0,0 @@
package be.vandewalleh.controllers
import be.vandewalleh.controllers.base.AuthCrudController
import be.vandewalleh.entities.Note
import be.vandewalleh.entities.Tag
import be.vandewalleh.entities.User
import be.vandewalleh.tables.Chapters
import be.vandewalleh.tables.Notes
import be.vandewalleh.tables.Tags
import be.vandewalleh.tables.Users
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.routing.*
import me.liuwj.ktorm.database.Database
import me.liuwj.ktorm.dsl.*
import me.liuwj.ktorm.entity.add
import me.liuwj.ktorm.entity.find
import me.liuwj.ktorm.entity.sequenceOf
import org.kodein.di.Kodein
import org.kodein.di.generic.instance
import java.time.LocalDateTime
class TitleController(kodein: Kodein) : AuthCrudController("/notes/{noteTitle}", kodein) {
private val db by kodein.instance<Database>()
private fun ApplicationCall.noteTitle(): String? {
return this.parameters["noteTitle"]!!
}
private fun ApplicationCall.user(): User {
return db.sequenceOf(Users)
.find { it.email eq this.userEmail() }
?: error("")
}
/**
* Method that returns a [Notes] ID from it's title and the currently logged in user.
* returns null if none found
*/
private fun ApplicationCall.requestedNoteId(): Int? {
val user = user()
val title = noteTitle() ?: error("title missing")
return db.from(Notes)
.select(Notes.id)
.where { Notes.userId eq user.id and (Notes.title eq title) }
.limit(0, 1)
.map { it[Notes.id]!! }
.firstOrNull()
}
private class PostRequestBody(val tags: List<String>)
private class ChapterDto(val title: String, val content: String)
private class GetResponseBody(val tags: List<String>, val chapters: List<ChapterDto>)
private class PatchRequestBody(val title: String? = null, val tags: List<String>? = null)
override fun Routing.routes() {
post {
val title = call.noteTitle() ?: error("")
val tags = call.receive<PostRequestBody>().tags
val user = call.user()
val exists = call.requestedNoteId() != null
if (exists) {
return@post call.respondStatus(HttpStatusCode.Conflict)
}
db.useTransaction {
val note = Note {
this.title = title
this.user = user
this.updatedAt = LocalDateTime.now()
}
db.sequenceOf(Notes).add(note)
tags.forEach { tagName ->
val tag = Tag {
this.note = note
this.name = tagName
}
db.sequenceOf(Tags).add(tag)
}
}
call.respondStatus(HttpStatusCode.Created)
}
get {
val noteId = call.requestedNoteId()
?: return@get call.respondStatus(HttpStatusCode.NotFound)
val tags = db.from(Tags)
.select(Tags.name)
.where { Tags.noteId eq noteId }
.map { it[Tags.name]!! }
.toList()
val chapters = db.from(Chapters)
.select(Chapters.title, Chapters.content)
.where { Chapters.noteId eq noteId }
.orderBy(Chapters.number.asc())
.map { ChapterDto(it[Chapters.title]!!, it[Chapters.content]!!) }
.toList()
val response = GetResponseBody(tags, chapters)
call.respond(response)
}
patch {
val requestedChanges = call.receive<PatchRequestBody>()
// This means no changes have been requested..
if (requestedChanges.tags == null && requestedChanges.title == null) {
return@patch call.respondStatus(HttpStatusCode.BadRequest)
}
val noteId = call.requestedNoteId()
?: return@patch call.respondStatus(HttpStatusCode.NotFound)
db.useTransaction {
if (requestedChanges.title != null) {
db.update(Notes) {
it.title to requestedChanges.title
where { it.id eq noteId }
}
}
if (requestedChanges.tags != null) {
// delete all tags
db.delete(Tags) {
it.noteId eq noteId
}
// put new ones
requestedChanges.tags.forEach { tagName ->
db.insert(Tags) {
it.name to tagName
it.noteId to noteId
}
}
}
}
call.respondStatus(HttpStatusCode.OK)
}
delete {
val noteId = call.requestedNoteId()
?: return@delete call.respondStatus(HttpStatusCode.NotFound)
db.useTransaction {
db.delete(Tags) { it.noteId eq noteId }
db.delete(Chapters) { it.noteId eq noteId }
db.delete(Notes) { it.id eq noteId }
}
call.respondStatus(HttpStatusCode.OK)
}
}
}

View File

@ -1,27 +0,0 @@
package be.vandewalleh.controllers.base
import be.vandewalleh.services.UserService
import io.ktor.application.ApplicationCall
import io.ktor.auth.UserIdPrincipal
import io.ktor.auth.principal
import org.kodein.di.Kodein
import org.kodein.di.generic.instance
abstract class AuthCrudController(
path: String,
override val kodein: Kodein
) :
KodeinController(path, kodein, auth = true) {
private val userService by instance<UserService>()
/**
* retrieves the user email from the JWT token
*/
fun ApplicationCall.userEmail(): String =
this.principal<UserIdPrincipal>()!!.name
fun ApplicationCall.userId(): Int =
userService.getUserId(userEmail())!!
}

View File

@ -1,41 +0,0 @@
package be.vandewalleh.controllers.base
import io.ktor.application.ApplicationCall
import io.ktor.auth.authenticate
import io.ktor.http.HttpStatusCode
import io.ktor.response.respond
import io.ktor.routing.Routing
import io.ktor.routing.route
import org.kodein.di.Kodein
import org.kodein.di.KodeinAware
abstract class KodeinController(
private val path: String,
override val kodein: Kodein,
private val auth: Boolean = false
) : KodeinAware {
/**
* Method that subtypes must override to declare their [Routing] routes.
*/
abstract fun Routing.routes()
fun Routing.registerRoutes() {
if (auth) {
authenticate {
route(path) {
this@registerRoutes.routes()
}
}
} else {
route(path) {
this@registerRoutes.routes()
}
}
}
suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) {
this.respond(status, mapOf("message" to status.description))
}
}

View File

@ -0,0 +1,35 @@
package be.vandewalleh.extensions
import be.vandewalleh.kodein
import be.vandewalleh.services.UserService
import io.ktor.application.ApplicationCall
import io.ktor.auth.UserIdPrincipal
import io.ktor.auth.principal
import io.ktor.http.HttpStatusCode
import io.ktor.request.receive
import io.ktor.response.respond
import org.kodein.di.generic.instance
val userService by kodein.instance<UserService>()
suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) {
respond(status, status.description)
}
/**
* @return the userId for the currently authenticated user
*/
fun ApplicationCall.userId(): Int {
val email = principal<UserIdPrincipal>()!!.name
return userService.getUserId(email)!!
}
private class Tags(val tags: List<String>)
suspend fun ApplicationCall.receiveTags(): List<String> {
return receive<Tags>().tags
}
data class NotePatch(val tags: List<String>?, val title: String?)
suspend fun ApplicationCall.receiveNotePatch() = receive<NotePatch>()

View File

@ -0,0 +1,22 @@
package be.vandewalleh.extensions
import be.vandewalleh.kodein
import be.vandewalleh.services.NotesService
import be.vandewalleh.tables.Notes
import io.ktor.http.Parameters
import org.kodein.di.generic.instance
val notesService by kodein.instance<NotesService>()
fun Parameters.noteTitle(): String {
return this["noteTitle"]!!
}
/**
* Method that returns a [Notes] ID from it's title and the currently logged in user.
* returns null if none found
*/
fun Parameters.noteId(userId: Int): Int? {
val title = noteTitle()
return notesService.getNoteIdFromUserIdAndTitle(userId, title)
}

View File

@ -0,0 +1,14 @@
package be.vandewalleh.routing
import io.ktor.auth.authenticate
import io.ktor.routing.Routing
import io.ktor.routing.route
import org.kodein.di.Kodein
fun Routing.chapters(kodein: Kodein) {
authenticate {
route("/notes/{noteTitle}/chapters/{chapterNumber}") {
}
}
}

View File

@ -1,8 +1,7 @@
package be.vandewalleh.controllers
package be.vandewalleh.routing
import be.vandewalleh.auth.SimpleJWT
import be.vandewalleh.auth.UsernamePasswordCredential
import be.vandewalleh.controllers.base.KodeinController
import be.vandewalleh.services.UserService
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
@ -10,30 +9,31 @@ import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.routing.Routing
import io.ktor.routing.post
import io.ktor.routing.route
import org.kodein.di.Kodein
import org.kodein.di.generic.instance
import org.mindrot.jbcrypt.BCrypt
class LoginController(kodein: Kodein) : KodeinController("/login", kodein) {
private val simpleJwt by instance<SimpleJWT>()
private val userService by instance<UserService>()
fun Routing.login(kodein: Kodein) {
val simpleJwt by kodein.instance<SimpleJWT>()
val userService by kodein.instance<UserService>()
data class TokenResponse(val token: String)
override fun Routing.routes() {
post {
route("/login"){
post {
val credential = call.receive<UsernamePasswordCredential>()
val (email, password) = userService.getEmailAndPasswordFromUsername(credential.username)
?: return@post call.respondStatus(HttpStatusCode.Unauthorized)
?: return@post call.respond(HttpStatusCode.Unauthorized)
if (!BCrypt.checkpw(credential.password, password)) {
return@post call.respondStatus(HttpStatusCode.Unauthorized)
return@post call.respond(HttpStatusCode.Unauthorized)
}
return@post call.respond(TokenResponse(simpleJwt.sign(email)))
}
}
}

View File

@ -1,19 +1,20 @@
package be.vandewalleh.controllers
package be.vandewalleh.routing
import be.vandewalleh.controllers.base.AuthCrudController
import be.vandewalleh.extensions.userId
import be.vandewalleh.services.NotesService
import io.ktor.application.call
import io.ktor.auth.authenticate
import io.ktor.response.respond
import io.ktor.routing.Routing
import io.ktor.routing.get
import org.kodein.di.Kodein
import org.kodein.di.generic.instance
class NotesController(kodein: Kodein) : AuthCrudController("/notes", kodein) {
private val notesService by kodein.instance<NotesService>()
fun Routing.notes(kodein: Kodein) {
val notesService by kodein.instance<NotesService>()
override fun Routing.routes() {
get {
authenticate {
get("/notes") {
val userId = call.userId()
val notes = notesService.getNotes(userId)
call.respond(notes)

View File

@ -0,0 +1,33 @@
package be.vandewalleh.routing
import be.vandewalleh.extensions.respondStatus
import be.vandewalleh.services.UserRegistrationDto
import be.vandewalleh.services.UserService
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.routing.Routing
import io.ktor.routing.post
import org.kodein.di.Kodein
import org.kodein.di.generic.instance
import org.mindrot.jbcrypt.BCrypt
fun Routing.register(kodein: Kodein) {
val userService by kodein.instance<UserService>()
post("/register") {
val user = call.receive<UserRegistrationDto>()
if (userService.userExists(user.username, user.email))
return@post call.respond(HttpStatusCode.Conflict)
val hashedPassword = BCrypt.hashpw(user.password, BCrypt.gensalt())
userService.createUser(
UserRegistrationDto(user.username, user.email, hashedPassword)
)
return@post call.respondStatus(HttpStatusCode.Created)
}
}

12
api/src/routing/Routes.kt Normal file
View File

@ -0,0 +1,12 @@
package be.vandewalleh.routing
import io.ktor.routing.Routing
import org.kodein.di.Kodein
fun Routing.registerRoutes(kodein: Kodein) {
login(kodein)
register(kodein)
notes(kodein)
title(kodein)
chapters(kodein)
}

View File

@ -0,0 +1,64 @@
package be.vandewalleh.routing
import be.vandewalleh.extensions.*
import be.vandewalleh.services.NotesService
import io.ktor.application.call
import io.ktor.auth.authenticate
import io.ktor.http.HttpStatusCode
import io.ktor.response.respond
import io.ktor.routing.*
import org.kodein.di.Kodein
import org.kodein.di.generic.instance
fun Routing.title(kodein: Kodein) {
val notesService by kodein.instance<NotesService>()
authenticate {
route("/notes/{noteTitle}") {
post {
val userId = call.userId()
val title = call.parameters.noteTitle()
val tags = call.receiveTags()
val noteId = call.parameters.noteId(userId)
if (noteId != null) {
return@post call.respondStatus(HttpStatusCode.Conflict)
}
notesService.createNote(userId, title, tags)
call.respondStatus(HttpStatusCode.Created)
}
get {
val userId = call.userId()
val noteId = call.parameters.noteId(userId)
?: return@get call.respondStatus(HttpStatusCode.NotFound)
val response = notesService.getTagsAndChapters(noteId)
call.respond(response)
}
patch {
val notePatch = call.receiveNotePatch()
if (notePatch.tags == null && notePatch.title == null)
return@patch call.respondStatus(HttpStatusCode.BadRequest)
val userId = call.userId()
val noteId = call.parameters.noteId(userId)
?: return@patch call.respondStatus(HttpStatusCode.NotFound)
notesService.updateNote(noteId, notePatch.tags, notePatch.title)
call.respondStatus(HttpStatusCode.OK)
}
delete {
val userId = call.userId()
val noteId = call.parameters.noteId(userId)
?: return@delete call.respondStatus(HttpStatusCode.NotFound)
notesService.deleteNote(noteId)
call.respondStatus(HttpStatusCode.OK)
}
}
}
}

View File

@ -1,5 +1,6 @@
package be.vandewalleh.services
import be.vandewalleh.tables.Chapters
import be.vandewalleh.tables.Notes
import be.vandewalleh.tables.Tags
import me.liuwj.ktorm.database.Database
@ -13,22 +14,20 @@ import java.time.format.DateTimeFormatter
* service to handle database queries at the Notes level.
*/
class NotesService(override val kodein: Kodein) : KodeinAware {
val db by instance<Database>()
private val db by instance<Database>()
/**
* returns a list of [NotesDTO] associated with the userId
*/
fun getNotes(userId: Int): List<NotesDTO> {
val notes = db.from(Notes)
.select(Notes.id, Notes.title, Notes.updatedAt)
.where { Notes.userId eq userId }
.orderBy(Notes.updatedAt.desc())
.map { row ->
Notes.createEntity(row)
}
.toList()
return notes.map { note ->
fun getNotes(userId: Int): List<NotesDTO> = db.from(Notes)
.select(Notes.id, Notes.title, Notes.updatedAt)
.where { Notes.userId eq userId }
.orderBy(Notes.updatedAt.desc())
.map { row ->
Notes.createEntity(row)
}
.toList()
.map { note ->
val tags = db.from(Tags)
.select(Tags.name)
.where { Tags.noteId eq note.id }
@ -40,8 +39,67 @@ class NotesService(override val kodein: Kodein) : KodeinAware {
NotesDTO(note.title, tags, updatedAt)
}
fun getNoteIdFromUserIdAndTitle(userId: Int, noteTitle: String): Int? = db.from(Notes)
.select(Notes.id)
.where { Notes.userId eq userId and (Notes.title eq noteTitle) }
.limit(0, 1)
.map { it[Notes.id]!! }
.firstOrNull()
fun createNote(userId: Int, title: String, tags: List<String>) {
TODO()
}
fun getTagsAndChapters(noteId: Int): TagsChaptersDTO {
val tags = db.from(Tags)
.select(Tags.name)
.where { Tags.noteId eq noteId }
.map { it[Tags.name]!! }
.toList()
val chapters = db.from(Chapters)
.select(Chapters.title, Chapters.content)
.where { Chapters.noteId eq noteId }
.orderBy(Chapters.number.asc())
.map { ChaptersDTO(it[Chapters.title]!!, it[Chapters.content]!!) }
.toList()
return TagsChaptersDTO(tags, chapters)
}
fun updateNote(noteId: Int, tags: List<String>?, title: String?): Unit =
db.useTransaction {
if (title != null) {
db.update(Notes) {
it.title to title
where { it.id eq noteId }
}
}
if (tags != null) {
// delete all tags
db.delete(Tags) {
it.noteId eq noteId
}
// put new ones
tags.forEach { tagName ->
db.insert(Tags) {
it.name to tagName
it.noteId to noteId
}
}
}
}
fun deleteNote(noteId: Int): Unit =
db.useTransaction {
db.delete(Tags) { it.noteId eq noteId }
db.delete(Chapters) { it.noteId eq noteId }
db.delete(Notes) { it.id eq noteId }
}
}
data class ChaptersDTO(val title: String, val content: String)
data class TagsChaptersDTO(val tags: List<String>, val chapters: List<ChaptersDTO>)
data class NotesDTO(val title: String, val tags: List<String>, val updatedAt: String)

View File

@ -10,4 +10,5 @@ import org.kodein.di.generic.singleton
*/
val serviceModule = Kodein.Module(name = "Services") {
bind<NotesService>() with singleton { NotesService(this.kodein) }
bind<UserService>() with singleton { UserService(this.kodein) }
}

View File

@ -36,7 +36,8 @@
"<span class="hljs-attribute">type</span>": <span class="hljs-value"><span class="hljs-string">"string"</span>
</span>}
</span>}
</span>}</code></pre><div style="height: 1px;"></div></div></div></div></div></div></div></div><div class="middle"><div id="accounts-create-an-account-post" class="action post"><h4 class="action-heading"><div class="name">Register a new user</div><a href="#accounts-create-an-account-post" class="method post">POST</a><code class="uri">/register</code></h4></div></div><hr class="split"><div class="middle"><div id="accounts-authenticate-user" class="resource"><h3 class="resource-heading">Authenticate user <a href="#accounts-authenticate-user" class="permalink">&para;</a></h3><p>Authenticate one user to access protected routes.</p>
</span>}</code></pre><div style="height: 1px;"></div></div></div></div></div></div></div></div><div class="middle"><div id="accounts-create-an-account-post" class="action post"><h4 class="action-heading"><div class="name">Register a new user</div><a href="#accounts-create-an-account-post" class="method post">POST</a><code class="uri">/register</code></h4></div></div><hr class="split"><div class="middle"><div id="accounts-authenticate-user" class="resource"><h3 class="resource-heading">Authenticate user <a href="#accounts-authenticate-user" class="permalink">&para;</a></h3><p>Authenticate
one user to access protected routing.</p>
</div></div><div class="right"><div class="definition"><span class="method post">POST</span>&nbsp;<span class="uri"><span class="hostname">http://localhost:5000</span>/login</span></div><div class="tabs"><div class="example-names"><span>Requests</span><span class="tab-button">example 1</span></div><div class="tab"><div><div class="inner"><h5>Headers</h5><pre><code><span class="hljs-attribute">Content-Type</span>: <span class="hljs-string">application/json</span></code></pre><div style="height: 1px;"></div><h5>Body</h5><pre><code>{
"<span class="hljs-attribute">username</span>": <span class="hljs-value"><span class="hljs-string">"babar"</span></span>,
"<span class="hljs-attribute">password</span>": <span class="hljs-value"><span class="hljs-string">"tortue"</span>