Merge branch 'refactor/services'

This commit is contained in:
Hubert Van De Walle 2020-04-21 16:32:04 +02:00
commit ff01ab2af3
29 changed files with 527 additions and 466 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

@ -7,25 +7,48 @@ Content-Type: application/json
"password": "test"
}
> {% client.global.set("token", response.body.token); %}
> {%
client.global.set("token", response.body.token);
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
});
%}
### Get notes
GET http://localhost:8081/notes
Authorization: Bearer {{token}}
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
});
%}
### Create a note
POST http://localhost:8081/notes/babar
POST http://localhost:8081/notes/tortue
Content-Type: application/json
Authorization: Bearer {{token}}
{
"tags": [
"Test",
"Dev"
]
"tags": [
"Dev",
"Server"
]
}
### Create a note
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 201, "Response status is not 200");
});
%}
### Read a note
GET http://localhost:8081/notes/babar
Content-Type: application/json
Authorization: Bearer {{token}}
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
});
%}

View File

@ -71,11 +71,6 @@
<artifactId>ktor-server-core</artifactId>
<version>${ktor_version}</version>
</dependency>
<dependency>
<groupId>io.ktor</groupId>
<artifactId>ktor-locations</artifactId>
<version>${ktor_version}</version>
</dependency>
<dependency>
<groupId>io.ktor</groupId>
<artifactId>ktor-jackson</artifactId>

View File

@ -1,15 +1,20 @@
package be.vandewalleh
import be.vandewalleh.controllers.KodeinController
import be.vandewalleh.controllers.controllerModule
import be.vandewalleh.features.Feature
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.auth.authenticate
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
@ -27,10 +32,10 @@ fun Application.module() {
configurationFeature()
kodein = Kodein {
import(controllerModule)
import(configurationModule)
import(serviceModule)
bind<Feature>() with singleton { Migration(this.kodein) }
bind<Migration>() with singleton { Migration(this.kodein) }
bind<Database>() with singleton { Database.Companion.connect(this.instance<DataSource>()) }
}
@ -38,15 +43,21 @@ fun Application.module() {
log.debug(kodein.container.tree.bindings.description())
// TODO, clean this (migration)
val feature by kodein.instance<Feature>()
feature.execute()
val controllers by kodein.instance<Set<KodeinController>>()
val migration by kodein.instance<Migration>()
migration.migrate()
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,31 +0,0 @@
package be.vandewalleh.controllers
import io.ktor.application.ApplicationCall
import io.ktor.auth.UserIdPrincipal
import io.ktor.auth.authenticate
import io.ktor.auth.principal
import io.ktor.routing.Route
import io.ktor.routing.Routing
import io.ktor.routing.route
import org.kodein.di.Kodein
abstract class AuthCrudController(
private val path: String,
override val kodein: Kodein
) :
KodeinController(kodein) {
abstract val route: Route.() -> Unit
fun ApplicationCall.userEmail(): String =
this.principal<UserIdPrincipal>()!!.name
override fun Routing.registerRoutes() {
authenticate {
route(path) {
route()
}
}
}
}

View File

@ -1,19 +0,0 @@
package be.vandewalleh.controllers
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 { UserController(this.kodein) }
bind<KodeinController>().inSet() with singleton { HealthCheckController(this.kodein) }
bind<KodeinController>().inSet() with singleton { NotesController(this.kodein) }
bind<KodeinController>().inSet() with singleton { NotesTitleController(this.kodein) }
}

View File

@ -1,21 +0,0 @@
package be.vandewalleh.controllers
import io.ktor.application.call
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respondText
import io.ktor.routing.Routing
import org.kodein.di.Kodein
class HealthCheckController(kodein: Kodein) : KodeinController(kodein) {
override fun Routing.registerRoutes() {
get<Routes.Ping> {
call.respondText("pong")
}
}
object Routes {
@Location("/ping")
class Ping
}
}

View File

@ -1,21 +0,0 @@
package be.vandewalleh.controllers
import io.ktor.application.ApplicationCall
import io.ktor.http.HttpStatusCode
import io.ktor.response.respond
import io.ktor.routing.Routing
import org.kodein.di.Kodein
import org.kodein.di.KodeinAware
abstract class KodeinController(override val kodein: Kodein) : KodeinAware {
/**
* Method that subtypes must override to register the handled [Routing] routes.
*/
abstract fun Routing.registerRoutes()
suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) {
this.respond(status, mapOf("message" to status.description))
}
}

View File

@ -1,59 +0,0 @@
package be.vandewalleh.controllers
import be.vandewalleh.tables.Notes
import be.vandewalleh.tables.Tags
import be.vandewalleh.tables.Users
import io.ktor.application.call
import io.ktor.response.respond
import io.ktor.response.respondText
import io.ktor.routing.Route
import io.ktor.routing.get
import me.liuwj.ktorm.database.Database
import me.liuwj.ktorm.dsl.*
import org.kodein.di.Kodein
import org.kodein.di.generic.instance
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
class NotesController(kodein: Kodein) : AuthCrudController("/notes", kodein) {
private val db by kodein.instance<Database>()
private class ResponseItem(val title: String, val tags: List<String>, val updatedAt: String)
override val route: Route.() -> Unit = {
get {
val email = call.userEmail()
val list = db.from(Notes)
.leftJoin(Users, on = Users.id eq Notes.userId)
.select(Notes.id, Notes.title, Notes.updatedAt)
.where { Users.email eq email }
.orderBy(Notes.updatedAt.desc())
.map { row ->
Notes.createEntity(row)
}
.toList()
val response = mutableListOf<ResponseItem>()
list.forEach { note ->
val tags = db.from(Tags)
.select(Tags.name)
.where { Tags.noteId eq note.id }
.map { it[Tags.name]!! }
.toList()
val updatedAt = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(note.updatedAt)
val item = ResponseItem(
title = note.title,
tags = tags,
updatedAt = updatedAt
)
response += item
}
call.respond(response)
}
}
}

View File

@ -1,170 +0,0 @@
package be.vandewalleh.controllers
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 NotesTitleController(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 val route: Route.() -> Unit = {
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,90 +0,0 @@
package be.vandewalleh.controllers
import be.vandewalleh.auth.SimpleJWT
import be.vandewalleh.auth.UsernamePasswordCredential
import be.vandewalleh.entities.User
import be.vandewalleh.errors.ApiError
import be.vandewalleh.tables.Users
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.Location
import io.ktor.locations.post
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.routing.Routing
import me.liuwj.ktorm.database.Database
import me.liuwj.ktorm.dsl.*
import me.liuwj.ktorm.entity.add
import me.liuwj.ktorm.entity.sequenceOf
import org.kodein.di.Kodein
import org.kodein.di.generic.instance
import org.mindrot.jbcrypt.BCrypt
import java.time.LocalDateTime
class UserController(kodein: Kodein) : KodeinController(kodein) {
private val simpleJwt by instance<SimpleJWT>()
private val db by instance<Database>()
override fun Routing.registerRoutes() {
post<Routes.Login> {
data class Response(val token: String)
val credential = call.receive<UsernamePasswordCredential>()
val (email, password) = db.from(Users)
.select(Users.email, Users.password)
.where { Users.username eq credential.username }
.map { row -> row[Users.email]!! to row[Users.password]!! }
.firstOrNull()
?: return@post call.respond(HttpStatusCode.Unauthorized, ApiError.InvalidCredentialError)
if (!BCrypt.checkpw(credential.password, password)) {
return@post call.respond(HttpStatusCode.Unauthorized, ApiError.InvalidCredentialError)
}
return@post call.respond(Response(simpleJwt.sign(email)))
}
post<Routes.Register> {
data class Response(val message: String)
val user = call.receive<RegisterInfo>()
val exists = db.from(Users)
.select()
.where { (Users.username eq user.username) or (Users.email eq user.email) }
.any()
if (exists) {
return@post call.respond(HttpStatusCode.Conflict, ApiError.ExistingUserError)
}
val hashedPassword = BCrypt.hashpw(user.password, BCrypt.gensalt())
val newUser = User {
this.username = user.username
this.email = user.email
this.password = hashedPassword
this.createdAt = LocalDateTime.now()
}
db.sequenceOf(Users).add(newUser)
return@post call.respond(HttpStatusCode.Created, Response("User created successfully"))
}
}
object Routes {
@Location("/login")
class Login
@Location("/register")
class Register
}
}
data class RegisterInfo(val username: String, val email: String, val password: String)

View File

@ -1,7 +0,0 @@
package be.vandewalleh.errors
sealed class ApiError(val message: String){
object InvalidCredentialError : ApiError("Invalid credentials")
object ExistingUserError : ApiError("User already exists")
object DeletedUserError : ApiError("User has been deleted")
}

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,17 @@
package be.vandewalleh.features
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.StatusPages
import io.ktor.http.HttpStatusCode
import io.ktor.response.respond
import io.ktor.utils.io.errors.IOException
fun Application.handleErrors() {
install(StatusPages) {
exception<IOException> { _ ->
call.respond(HttpStatusCode.BadRequest)
}
}
}

View File

@ -2,16 +2,10 @@ package be.vandewalleh.features
import be.vandewalleh.auth.authenticationModule
import io.ktor.application.Application
import org.kodein.di.Kodein
import org.kodein.di.KodeinAware
fun Application.features() {
locationFeature()
corsFeature()
contentNegotiationFeature()
authenticationModule()
}
abstract class Feature(override val kodein: Kodein) : KodeinAware {
abstract fun execute()
handleErrors()
}

View File

@ -1,9 +0,0 @@
package be.vandewalleh.features
import io.ktor.application.Application
import io.ktor.application.install
import io.ktor.locations.Locations
fun Application.locationFeature() {
install(Locations)
}

View File

@ -1,14 +1,14 @@
package be.vandewalleh.migrations
import be.vandewalleh.features.Feature
import org.flywaydb.core.Flyway
import org.kodein.di.Kodein
import org.kodein.di.KodeinAware
import org.kodein.di.generic.instance
import javax.sql.DataSource
class Migration(override val kodein: Kodein) : Feature(kodein) {
class Migration(override val kodein: Kodein) : KodeinAware {
override fun execute() {
fun migrate() {
val dataSource by instance<DataSource>()
val flyway = Flyway.configure()
.dataSource(dataSource)

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

@ -0,0 +1,39 @@
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 io.ktor.routing.route
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)
route("/login"){
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)))
}
}
}

View File

@ -0,0 +1,23 @@
package be.vandewalleh.routing
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
fun Routing.notes(kodein: Kodein) {
val notesService by kodein.instance<NotesService>()
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

@ -0,0 +1,120 @@
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
import me.liuwj.ktorm.dsl.*
import org.kodein.di.Kodein
import org.kodein.di.KodeinAware
import org.kodein.di.generic.instance
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
/**
* service to handle database queries at the Notes level.
*/
class NotesService(override val kodein: Kodein) : KodeinAware {
private val db by instance<Database>()
/**
* returns a list of [NotesDTO] associated with the userId
*/
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 }
.map { it[Tags.name]!! }
.toList()
val updatedAt = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(note.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>) {
db.useTransaction {
val noteId = db.insertAndGenerateKey(Notes) {
it.title to title
it.userId to userId
it.updatedAt to LocalDateTime.now()
}
tags.forEach { tagName ->
db.insert(Tags) {
it.name to tagName
it.noteId to noteId
}
}
}
}
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
it.updatedAt to LocalDateTime.now()
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

@ -0,0 +1,14 @@
package be.vandewalleh.services
import org.kodein.di.Kodein
import org.kodein.di.generic.bind
import org.kodein.di.generic.singleton
/**
* [Kodein] controller module containing the app services
*/
val serviceModule = Kodein.Module(name = "Services") {
bind<NotesService>() with singleton { NotesService(this.kodein) }
bind<UserService>() with singleton { UserService(this.kodein) }
}

View File

@ -0,0 +1,71 @@
package be.vandewalleh.services
import be.vandewalleh.entities.User
import be.vandewalleh.tables.Users
import me.liuwj.ktorm.database.Database
import me.liuwj.ktorm.dsl.*
import me.liuwj.ktorm.entity.add
import me.liuwj.ktorm.entity.sequenceOf
import org.kodein.di.Kodein
import org.kodein.di.KodeinAware
import org.kodein.di.generic.instance
import java.time.LocalDateTime
/**
* service to handle database queries for users.
*/
class UserService(override val kodein: Kodein) : KodeinAware {
private val db by instance<Database>()
/**
* returns a user ID if present or null
*/
fun getUserId(userEmail: String): Int? {
return db.from(Users)
.select(Users.id)
.where { Users.email eq userEmail }
.limit(0, 1)
.map { it[Users.id] }
.firstOrNull()
}
/**
* returns a user email and password from it's email if found or null
*/
fun getEmailAndPasswordFromUsername(username: String): Pair<String, String>? {
return db.from(Users)
.select(Users.email, Users.password)
.where { Users.username eq username }
.limit(0, 1)
.map { row -> row[Users.email]!! to row[Users.password]!! }
.firstOrNull()
}
fun userExists(username: String, email: String): Boolean {
return db.from(Users)
.select(Users.id)
.where { (Users.username eq username) or (Users.email eq email) }
.limit(0, 1)
.firstOrNull() != null
}
/**
* create a new user
* password should already be hashed
*/
fun createUser(user: UserRegistrationDto) {
db.useTransaction {
val newUser = User {
this.username = user.username
this.email = user.email
this.password = user.password
this.createdAt = LocalDateTime.now()
}
db.sequenceOf(Users).add(newUser)
}
}
}
data class UserRegistrationDto(val username: String, val email: String, val password: String)

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>