WIP
This commit is contained in:
parent
cbee40f5e4
commit
1f9923dc82
@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
|
|
||||||
## Authenticate user [/login]
|
## Authenticate user [/login]
|
||||||
Authenticate one user to access protected routes.
|
Authenticate one user to access protected routing.
|
||||||
|
|
||||||
### Authenticate a user [POST]
|
### Authenticate a user [POST]
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
package be.vandewalleh
|
package be.vandewalleh
|
||||||
|
|
||||||
import be.vandewalleh.controllers.base.KodeinController
|
|
||||||
import be.vandewalleh.controllers.controllerModule
|
|
||||||
import be.vandewalleh.features.configurationFeature
|
import be.vandewalleh.features.configurationFeature
|
||||||
import be.vandewalleh.features.configurationModule
|
import be.vandewalleh.features.configurationModule
|
||||||
import be.vandewalleh.features.features
|
import be.vandewalleh.features.features
|
||||||
import be.vandewalleh.migrations.Migration
|
import be.vandewalleh.migrations.Migration
|
||||||
|
import be.vandewalleh.routing.registerRoutes
|
||||||
import be.vandewalleh.services.serviceModule
|
import be.vandewalleh.services.serviceModule
|
||||||
import io.ktor.application.Application
|
import io.ktor.application.Application
|
||||||
|
import io.ktor.application.feature
|
||||||
import io.ktor.application.log
|
import io.ktor.application.log
|
||||||
|
import io.ktor.routing.Route
|
||||||
|
import io.ktor.routing.Routing
|
||||||
|
import io.ktor.routing.RoutingPath.Companion.root
|
||||||
import io.ktor.routing.routing
|
import io.ktor.routing.routing
|
||||||
import me.liuwj.ktorm.database.Database
|
import me.liuwj.ktorm.database.Database
|
||||||
import org.kodein.di.Kodein
|
import org.kodein.di.Kodein
|
||||||
@ -26,7 +29,6 @@ fun Application.module() {
|
|||||||
configurationFeature()
|
configurationFeature()
|
||||||
|
|
||||||
kodein = Kodein {
|
kodein = Kodein {
|
||||||
import(controllerModule)
|
|
||||||
import(configurationModule)
|
import(configurationModule)
|
||||||
import(serviceModule)
|
import(serviceModule)
|
||||||
|
|
||||||
@ -41,11 +43,18 @@ fun Application.module() {
|
|||||||
val migration by kodein.instance<Migration>()
|
val migration by kodein.instance<Migration>()
|
||||||
migration.migrate()
|
migration.migrate()
|
||||||
|
|
||||||
val controllers by kodein.instance<Set<KodeinController>>()
|
|
||||||
|
|
||||||
routing {
|
routing {
|
||||||
controllers.forEach {
|
registerRoutes(kodein)
|
||||||
it.apply { registerRoutes() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) }
|
||||||
}
|
}
|
||||||
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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) }
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
package be.vandewalleh.controllers
|
|
||||||
|
|
||||||
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
|
|
||||||
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 LoginController(kodein: Kodein) : KodeinController("/login", kodein) {
|
|
||||||
private val simpleJwt by instance<SimpleJWT>()
|
|
||||||
private val userService by instance<UserService>()
|
|
||||||
|
|
||||||
data class TokenResponse(val token: String)
|
|
||||||
|
|
||||||
override fun Routing.routes() {
|
|
||||||
post {
|
|
||||||
|
|
||||||
val credential = call.receive<UsernamePasswordCredential>()
|
|
||||||
|
|
||||||
val (email, password) = userService.getEmailAndPasswordFromUsername(credential.username)
|
|
||||||
?: return@post call.respondStatus(HttpStatusCode.Unauthorized)
|
|
||||||
|
|
||||||
|
|
||||||
if (!BCrypt.checkpw(credential.password, password)) {
|
|
||||||
return@post call.respondStatus(HttpStatusCode.Unauthorized)
|
|
||||||
}
|
|
||||||
|
|
||||||
return@post call.respond(TokenResponse(simpleJwt.sign(email)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
package be.vandewalleh.controllers
|
|
||||||
|
|
||||||
import be.vandewalleh.controllers.base.AuthCrudController
|
|
||||||
import be.vandewalleh.services.NotesService
|
|
||||||
import io.ktor.application.call
|
|
||||||
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>()
|
|
||||||
|
|
||||||
override fun Routing.routes() {
|
|
||||||
get {
|
|
||||||
val userId = call.userId()
|
|
||||||
val notes = notesService.getNotes(userId)
|
|
||||||
call.respond(notes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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())!!
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
35
api/src/extensions/ApplicationCallExtensions.kt
Normal file
35
api/src/extensions/ApplicationCallExtensions.kt
Normal 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>()
|
||||||
22
api/src/extensions/ParametersExtensions.kt
Normal file
22
api/src/extensions/ParametersExtensions.kt
Normal 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)
|
||||||
|
}
|
||||||
8
api/src/routing/ChaptersController.kt
Normal file
8
api/src/routing/ChaptersController.kt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package be.vandewalleh.routing
|
||||||
|
|
||||||
|
import io.ktor.routing.Routing
|
||||||
|
import org.kodein.di.Kodein
|
||||||
|
|
||||||
|
fun Routing.chapters(kodein: Kodein) {
|
||||||
|
|
||||||
|
}
|
||||||
35
api/src/routing/LoginController.kt
Normal file
35
api/src/routing/LoginController.kt
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package be.vandewalleh.routing
|
||||||
|
|
||||||
|
import be.vandewalleh.auth.SimpleJWT
|
||||||
|
import be.vandewalleh.auth.UsernamePasswordCredential
|
||||||
|
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.login(kodein: Kodein) {
|
||||||
|
val simpleJwt by kodein.instance<SimpleJWT>()
|
||||||
|
val userService by kodein.instance<UserService>()
|
||||||
|
|
||||||
|
data class TokenResponse(val token: String)
|
||||||
|
|
||||||
|
post {
|
||||||
|
val credential = call.receive<UsernamePasswordCredential>()
|
||||||
|
|
||||||
|
val (email, password) = userService.getEmailAndPasswordFromUsername(credential.username)
|
||||||
|
?: return@post call.respond(HttpStatusCode.Unauthorized)
|
||||||
|
|
||||||
|
if (!BCrypt.checkpw(credential.password, password)) {
|
||||||
|
return@post call.respond(HttpStatusCode.Unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
return@post call.respond(TokenResponse(simpleJwt.sign(email)))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
20
api/src/routing/NotesController.kt
Normal file
20
api/src/routing/NotesController.kt
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package be.vandewalleh.routing
|
||||||
|
|
||||||
|
import be.vandewalleh.extensions.userId
|
||||||
|
import be.vandewalleh.services.NotesService
|
||||||
|
import io.ktor.application.call
|
||||||
|
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
|
||||||
|
|
||||||
|
fun Routing.notes(kodein: Kodein) {
|
||||||
|
val notesService by kodein.instance<NotesService>()
|
||||||
|
|
||||||
|
get {
|
||||||
|
val userId = call.userId()
|
||||||
|
val notes = notesService.getNotes(userId)
|
||||||
|
call.respond(notes)
|
||||||
|
}
|
||||||
|
}
|
||||||
33
api/src/routing/RegisterController.kt
Normal file
33
api/src/routing/RegisterController.kt
Normal 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 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
30
api/src/routing/Routes.kt
Normal file
30
api/src/routing/Routes.kt
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
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.registerRoutes(kodein: Kodein) {
|
||||||
|
route("/login") {
|
||||||
|
this@registerRoutes.login(kodein)
|
||||||
|
}
|
||||||
|
|
||||||
|
route("/register") {
|
||||||
|
this@registerRoutes.register(kodein)
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticate {
|
||||||
|
route("/notes") {
|
||||||
|
this@registerRoutes.notes(kodein)
|
||||||
|
|
||||||
|
route("/{noteTitle}") {
|
||||||
|
this@registerRoutes.title(kodein)
|
||||||
|
|
||||||
|
route("/chapters/{chapterNumber}") {
|
||||||
|
this@registerRoutes.chapters(kodein)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
api/src/routing/TitleController.kt
Normal file
59
api/src/routing/TitleController.kt
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package be.vandewalleh.routing
|
||||||
|
|
||||||
|
import be.vandewalleh.extensions.*
|
||||||
|
import be.vandewalleh.services.NotesService
|
||||||
|
import io.ktor.application.call
|
||||||
|
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>()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package be.vandewalleh.services
|
package be.vandewalleh.services
|
||||||
|
|
||||||
|
import be.vandewalleh.tables.Chapters
|
||||||
import be.vandewalleh.tables.Notes
|
import be.vandewalleh.tables.Notes
|
||||||
import be.vandewalleh.tables.Tags
|
import be.vandewalleh.tables.Tags
|
||||||
import me.liuwj.ktorm.database.Database
|
import me.liuwj.ktorm.database.Database
|
||||||
@ -13,22 +14,20 @@ import java.time.format.DateTimeFormatter
|
|||||||
* service to handle database queries at the Notes level.
|
* service to handle database queries at the Notes level.
|
||||||
*/
|
*/
|
||||||
class NotesService(override val kodein: Kodein) : KodeinAware {
|
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
|
* returns a list of [NotesDTO] associated with the userId
|
||||||
*/
|
*/
|
||||||
fun getNotes(userId: Int): List<NotesDTO> {
|
fun getNotes(userId: Int): List<NotesDTO> = db.from(Notes)
|
||||||
val notes = db.from(Notes)
|
.select(Notes.id, Notes.title, Notes.updatedAt)
|
||||||
.select(Notes.id, Notes.title, Notes.updatedAt)
|
.where { Notes.userId eq userId }
|
||||||
.where { Notes.userId eq userId }
|
.orderBy(Notes.updatedAt.desc())
|
||||||
.orderBy(Notes.updatedAt.desc())
|
.map { row ->
|
||||||
.map { row ->
|
Notes.createEntity(row)
|
||||||
Notes.createEntity(row)
|
}
|
||||||
}
|
.toList()
|
||||||
.toList()
|
.map { note ->
|
||||||
|
|
||||||
return notes.map { note ->
|
|
||||||
val tags = db.from(Tags)
|
val tags = db.from(Tags)
|
||||||
.select(Tags.name)
|
.select(Tags.name)
|
||||||
.where { Tags.noteId eq note.id }
|
.where { Tags.noteId eq note.id }
|
||||||
@ -40,8 +39,67 @@ class NotesService(override val kodein: Kodein) : KodeinAware {
|
|||||||
NotesDTO(note.title, tags, updatedAt)
|
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)
|
data class NotesDTO(val title: String, val tags: List<String>, val updatedAt: String)
|
||||||
@ -10,4 +10,5 @@ import org.kodein.di.generic.singleton
|
|||||||
*/
|
*/
|
||||||
val serviceModule = Kodein.Module(name = "Services") {
|
val serviceModule = Kodein.Module(name = "Services") {
|
||||||
bind<NotesService>() with singleton { NotesService(this.kodein) }
|
bind<NotesService>() with singleton { NotesService(this.kodein) }
|
||||||
|
bind<UserService>() with singleton { UserService(this.kodein) }
|
||||||
}
|
}
|
||||||
@ -36,7 +36,8 @@
|
|||||||
"<span class="hljs-attribute">type</span>": <span class="hljs-value"><span class="hljs-string">"string"</span>
|
"<span class="hljs-attribute">type</span>": <span class="hljs-value"><span class="hljs-string">"string"</span>
|
||||||
</span>}
|
</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">¶</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">¶</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> <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>{
|
</div></div><div class="right"><div class="definition"><span class="method post">POST</span> <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">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>
|
"<span class="hljs-attribute">password</span>": <span class="hljs-value"><span class="hljs-string">"tortue"</span>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user