From fce1cc0e9c63197895fa8fdf840213d82d826d90 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Mon, 20 Apr 2020 15:19:25 +0200 Subject: [PATCH 01/16] WIP --- api/src/controllers/ChaptersController.kt | 88 +++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 api/src/controllers/ChaptersController.kt diff --git a/api/src/controllers/ChaptersController.kt b/api/src/controllers/ChaptersController.kt new file mode 100644 index 0000000..ceb409b --- /dev/null +++ b/api/src/controllers/ChaptersController.kt @@ -0,0 +1,88 @@ +package be.vandewalleh.controllers + +import be.vandewalleh.entities.User +import be.vandewalleh.tables.Chapters +import be.vandewalleh.tables.Notes +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.routing.Route +import io.ktor.routing.post +import me.liuwj.ktorm.database.Database +import me.liuwj.ktorm.dsl.* +import me.liuwj.ktorm.entity.find +import me.liuwj.ktorm.entity.sequenceOf +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() + + private fun ApplicationCall.noteTitle(): String? { + return this.parameters["noteTitle"]!! + } + + private fun ApplicationCall.chapterNumber(): Int? { + return this.parameters["chapterNumber"]?.toIntOrNull() + } + + private fun ApplicationCall.chapterExists(): Boolean { + val user = user() + val title = noteTitle() ?: error("title missing") + val noteId = requestedNoteId() ?: error("") + + + + return db.from(Chapters) + .select(Chapters.id) + .where { Notes.userId eq user.id and (Notes.id eq noteId) } + .limit(0, 1) + .map { it[Notes.id]!! } + .firstOrNull() != null + } + + 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 data class PostRequestBody(val title: String, val content: String) + + override val route: Route.() -> Unit = { + post { + val noteId = call.requestedNoteId() + ?: return@post call.respondStatus(HttpStatusCode.NotFound) + + val chapterNumber = call.chapterNumber() + ?:return@post call.respondStatus(HttpStatusCode.BadRequest) + + val exists = + + + val (title, content) = call.receive() + + if() + + } + } +} From 8dd4fc9f4d8f500219197725bd9dd80f7d692da3 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Mon, 20 Apr 2020 15:41:26 +0200 Subject: [PATCH 02/16] Add user and notes services --- api/src/NotesApplication.kt | 2 + api/src/controllers/ChaptersController.kt | 3 +- api/src/services/NotesService.kt | 47 +++++++++++++++++++++++ api/src/services/Services.kt | 13 +++++++ api/src/services/UserService.kt | 28 ++++++++++++++ 5 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 api/src/services/NotesService.kt create mode 100644 api/src/services/Services.kt create mode 100644 api/src/services/UserService.kt diff --git a/api/src/NotesApplication.kt b/api/src/NotesApplication.kt index 5d3d97f..7f5c1bf 100644 --- a/api/src/NotesApplication.kt +++ b/api/src/NotesApplication.kt @@ -7,6 +7,7 @@ import be.vandewalleh.features.configurationFeature import be.vandewalleh.features.configurationModule import be.vandewalleh.features.features import be.vandewalleh.migrations.Migration +import be.vandewalleh.services.serviceModule import io.ktor.application.Application import io.ktor.application.log import io.ktor.auth.authenticate @@ -29,6 +30,7 @@ fun Application.module() { kodein = Kodein { import(controllerModule) import(configurationModule) + import(serviceModule) bind() with singleton { Migration(this.kodein) } bind() with singleton { Database.Companion.connect(this.instance()) } diff --git a/api/src/controllers/ChaptersController.kt b/api/src/controllers/ChaptersController.kt index ceb409b..54ff772 100644 --- a/api/src/controllers/ChaptersController.kt +++ b/api/src/controllers/ChaptersController.kt @@ -76,12 +76,11 @@ class ChaptersController(kodein: Kodein) : AuthCrudController("/notes/{noteTitle val chapterNumber = call.chapterNumber() ?:return@post call.respondStatus(HttpStatusCode.BadRequest) - val exists = + val exists = false val (title, content) = call.receive() - if() } } diff --git a/api/src/services/NotesService.kt b/api/src/services/NotesService.kt new file mode 100644 index 0000000..43eed75 --- /dev/null +++ b/api/src/services/NotesService.kt @@ -0,0 +1,47 @@ +package be.vandewalleh.services + +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.format.DateTimeFormatter + +/** + * service to handle database queries at the Notes level. + */ +class NotesService(override val kodein: Kodein) : KodeinAware { + val db by instance() + + /** + * returns a list of [NotesDTO] associated with the userId + */ + fun getNotes(userId: Int): List { + 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 -> + 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) + } + + } + +} + +data class NotesDTO(val title: String, val tags: List, val updatedAt: String) \ No newline at end of file diff --git a/api/src/services/Services.kt b/api/src/services/Services.kt new file mode 100644 index 0000000..fdb698c --- /dev/null +++ b/api/src/services/Services.kt @@ -0,0 +1,13 @@ +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() with singleton { NotesService(this.kodein) } +} \ No newline at end of file diff --git a/api/src/services/UserService.kt b/api/src/services/UserService.kt new file mode 100644 index 0000000..59dd579 --- /dev/null +++ b/api/src/services/UserService.kt @@ -0,0 +1,28 @@ +package be.vandewalleh.services + +import be.vandewalleh.tables.Users +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 + +/** + * service to handle database queries for users. + */ +class UserService(override val kodein: Kodein) : KodeinAware { + private val db by instance() + + /** + * 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() + } + +} \ No newline at end of file From d6f2489d505997cf4b1b5c9d7b9e977f2aee21df Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Mon, 20 Apr 2020 15:44:44 +0200 Subject: [PATCH 03/16] Change NotesController to use services --- api/src/controllers/AuthCrudController.kt | 7 ++++ api/src/controllers/NotesController.kt | 48 +++-------------------- 2 files changed, 12 insertions(+), 43 deletions(-) diff --git a/api/src/controllers/AuthCrudController.kt b/api/src/controllers/AuthCrudController.kt index ba57567..8aa7875 100644 --- a/api/src/controllers/AuthCrudController.kt +++ b/api/src/controllers/AuthCrudController.kt @@ -1,5 +1,6 @@ package be.vandewalleh.controllers +import be.vandewalleh.services.UserService import io.ktor.application.ApplicationCall import io.ktor.auth.UserIdPrincipal import io.ktor.auth.authenticate @@ -8,6 +9,7 @@ import io.ktor.routing.Route import io.ktor.routing.Routing import io.ktor.routing.route import org.kodein.di.Kodein +import org.kodein.di.generic.instance abstract class AuthCrudController( private val path: String, @@ -17,9 +19,14 @@ abstract class AuthCrudController( abstract val route: Route.() -> Unit + private val userService by instance() + fun ApplicationCall.userEmail(): String = this.principal()!!.name + fun ApplicationCall.userId(): Int = + userService.getUserId(userEmail())!! + override fun Routing.registerRoutes() { authenticate { route(path) { diff --git a/api/src/controllers/NotesController.kt b/api/src/controllers/NotesController.kt index 99dc7ec..d9a92a5 100644 --- a/api/src/controllers/NotesController.kt +++ b/api/src/controllers/NotesController.kt @@ -1,59 +1,21 @@ package be.vandewalleh.controllers -import be.vandewalleh.tables.Notes -import be.vandewalleh.tables.Tags -import be.vandewalleh.tables.Users +import be.vandewalleh.services.NotesService 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() - - private class ResponseItem(val title: String, val tags: List, val updatedAt: String) + private val notesService by kodein.instance() 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() - - 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) + val userId = call.userId() + val notes = notesService.getNotes(userId) + call.respond(notes) } } } From dc7f6b7b3a8513de0df9d4bcf066c4a513bb615a Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Mon, 20 Apr 2020 16:00:34 +0200 Subject: [PATCH 04/16] Use UserService --- api/src/controllers/UserController.kt | 53 +++++++-------------------- api/src/errors/Errors.kt | 7 ---- api/src/services/UserService.kt | 45 ++++++++++++++++++++++- 3 files changed, 58 insertions(+), 47 deletions(-) delete mode 100644 api/src/errors/Errors.kt diff --git a/api/src/controllers/UserController.kt b/api/src/controllers/UserController.kt index 0c69c41..fce3b8e 100644 --- a/api/src/controllers/UserController.kt +++ b/api/src/controllers/UserController.kt @@ -2,9 +2,8 @@ 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 be.vandewalleh.services.UserRegistrationDto +import be.vandewalleh.services.UserService import io.ktor.application.call import io.ktor.http.HttpStatusCode import io.ktor.locations.Location @@ -12,37 +11,26 @@ 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() - private val db by instance() + private val userService by instance() override fun Routing.registerRoutes() { - - post { data class Response(val token: String) val credential = call.receive() - 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) + val (email, password) = userService.getEmailAndPasswordFromUsername(credential.username) + ?: return@post call.respondStatus(HttpStatusCode.Unauthorized) if (!BCrypt.checkpw(credential.password, password)) { - return@post call.respond(HttpStatusCode.Unauthorized, ApiError.InvalidCredentialError) + return@post call.respondStatus(HttpStatusCode.Unauthorized) } return@post call.respond(Response(simpleJwt.sign(email))) @@ -51,29 +39,18 @@ class UserController(kodein: Kodein) : KodeinController(kodein) { post { data class Response(val message: String) - val user = call.receive() + val user = call.receive() - 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) - } + if (userService.userExists(user.username, user.email)) + return@post call.respond(HttpStatusCode.Conflict) 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() - } + userService.createUser( + UserRegistrationDto(user.username, user.email, hashedPassword) + ) - db.sequenceOf(Users).add(newUser) - - return@post call.respond(HttpStatusCode.Created, Response("User created successfully")) + return@post call.respondStatus(HttpStatusCode.Created) } } @@ -85,6 +62,4 @@ class UserController(kodein: Kodein) : KodeinController(kodein) { class Register } -} - -data class RegisterInfo(val username: String, val email: String, val password: String) \ No newline at end of file +} \ No newline at end of file diff --git a/api/src/errors/Errors.kt b/api/src/errors/Errors.kt deleted file mode 100644 index c917a24..0000000 --- a/api/src/errors/Errors.kt +++ /dev/null @@ -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") -} diff --git a/api/src/services/UserService.kt b/api/src/services/UserService.kt index 59dd579..67887d2 100644 --- a/api/src/services/UserService.kt +++ b/api/src/services/UserService.kt @@ -1,11 +1,15 @@ 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. @@ -25,4 +29,43 @@ class UserService(override val kodein: Kodein) : KodeinAware { .firstOrNull() } -} \ No newline at end of file + /** + * returns a user email and password from it's email if found or null + */ + fun getEmailAndPasswordFromUsername(username: String): Pair? { + 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) \ No newline at end of file From 2bb33297429d8a36113f9dc6ccbe2f5f9f7b794b Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Mon, 20 Apr 2020 16:03:47 +0200 Subject: [PATCH 05/16] Move and rename things --- api/src/NotesApplication.kt | 3 +-- api/src/controllers/ChaptersController.kt | 1 + api/src/controllers/Controllers.kt | 4 ++-- api/src/controllers/HealthCheckController.kt | 21 ------------------- api/src/controllers/NotesController.kt | 1 + ...sTitleController.kt => TitleController.kt} | 3 ++- api/src/controllers/UserController.kt | 1 + .../{ => base}/AuthCrudController.kt | 2 +- .../{ => base}/KodeinController.kt | 2 +- 9 files changed, 10 insertions(+), 28 deletions(-) delete mode 100644 api/src/controllers/HealthCheckController.kt rename api/src/controllers/{NotesTitleController.kt => TitleController.kt} (97%) rename api/src/controllers/{ => base}/AuthCrudController.kt (95%) rename api/src/controllers/{ => base}/KodeinController.kt (93%) diff --git a/api/src/NotesApplication.kt b/api/src/NotesApplication.kt index 7f5c1bf..0e0cf25 100644 --- a/api/src/NotesApplication.kt +++ b/api/src/NotesApplication.kt @@ -1,6 +1,6 @@ package be.vandewalleh -import be.vandewalleh.controllers.KodeinController +import be.vandewalleh.controllers.base.KodeinController import be.vandewalleh.controllers.controllerModule import be.vandewalleh.features.Feature import be.vandewalleh.features.configurationFeature @@ -10,7 +10,6 @@ import be.vandewalleh.migrations.Migration import be.vandewalleh.services.serviceModule import io.ktor.application.Application import io.ktor.application.log -import io.ktor.auth.authenticate import io.ktor.routing.routing import me.liuwj.ktorm.database.Database import org.kodein.di.Kodein diff --git a/api/src/controllers/ChaptersController.kt b/api/src/controllers/ChaptersController.kt index 54ff772..b55f05d 100644 --- a/api/src/controllers/ChaptersController.kt +++ b/api/src/controllers/ChaptersController.kt @@ -1,5 +1,6 @@ package be.vandewalleh.controllers +import be.vandewalleh.controllers.base.AuthCrudController import be.vandewalleh.entities.User import be.vandewalleh.tables.Chapters import be.vandewalleh.tables.Notes diff --git a/api/src/controllers/Controllers.kt b/api/src/controllers/Controllers.kt index 799361c..8bf78f7 100644 --- a/api/src/controllers/Controllers.kt +++ b/api/src/controllers/Controllers.kt @@ -1,5 +1,6 @@ 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 @@ -13,7 +14,6 @@ val controllerModule = Kodein.Module(name = "Controller") { bind() from setBinding() bind().inSet() with singleton { UserController(this.kodein) } - bind().inSet() with singleton { HealthCheckController(this.kodein) } bind().inSet() with singleton { NotesController(this.kodein) } - bind().inSet() with singleton { NotesTitleController(this.kodein) } + bind().inSet() with singleton { TitleController(this.kodein) } } \ No newline at end of file diff --git a/api/src/controllers/HealthCheckController.kt b/api/src/controllers/HealthCheckController.kt deleted file mode 100644 index 311ece3..0000000 --- a/api/src/controllers/HealthCheckController.kt +++ /dev/null @@ -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 { - call.respondText("pong") - } - } - - object Routes { - @Location("/ping") - class Ping - } -} \ No newline at end of file diff --git a/api/src/controllers/NotesController.kt b/api/src/controllers/NotesController.kt index d9a92a5..38df007 100644 --- a/api/src/controllers/NotesController.kt +++ b/api/src/controllers/NotesController.kt @@ -1,5 +1,6 @@ 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 diff --git a/api/src/controllers/NotesTitleController.kt b/api/src/controllers/TitleController.kt similarity index 97% rename from api/src/controllers/NotesTitleController.kt rename to api/src/controllers/TitleController.kt index fdb23ea..c38ddc0 100644 --- a/api/src/controllers/NotesTitleController.kt +++ b/api/src/controllers/TitleController.kt @@ -1,5 +1,6 @@ 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 @@ -22,7 +23,7 @@ import org.kodein.di.Kodein import org.kodein.di.generic.instance import java.time.LocalDateTime -class NotesTitleController(kodein: Kodein) : AuthCrudController("/notes/{noteTitle}", kodein) { +class TitleController(kodein: Kodein) : AuthCrudController("/notes/{noteTitle}", kodein) { private val db by kodein.instance() private fun ApplicationCall.noteTitle(): String? { diff --git a/api/src/controllers/UserController.kt b/api/src/controllers/UserController.kt index fce3b8e..25d3bd0 100644 --- a/api/src/controllers/UserController.kt +++ b/api/src/controllers/UserController.kt @@ -2,6 +2,7 @@ package be.vandewalleh.controllers import be.vandewalleh.auth.SimpleJWT import be.vandewalleh.auth.UsernamePasswordCredential +import be.vandewalleh.controllers.base.KodeinController import be.vandewalleh.services.UserRegistrationDto import be.vandewalleh.services.UserService import io.ktor.application.call diff --git a/api/src/controllers/AuthCrudController.kt b/api/src/controllers/base/AuthCrudController.kt similarity index 95% rename from api/src/controllers/AuthCrudController.kt rename to api/src/controllers/base/AuthCrudController.kt index 8aa7875..00ca994 100644 --- a/api/src/controllers/AuthCrudController.kt +++ b/api/src/controllers/base/AuthCrudController.kt @@ -1,4 +1,4 @@ -package be.vandewalleh.controllers +package be.vandewalleh.controllers.base import be.vandewalleh.services.UserService import io.ktor.application.ApplicationCall diff --git a/api/src/controllers/KodeinController.kt b/api/src/controllers/base/KodeinController.kt similarity index 93% rename from api/src/controllers/KodeinController.kt rename to api/src/controllers/base/KodeinController.kt index 5231c49..eb8e373 100644 --- a/api/src/controllers/KodeinController.kt +++ b/api/src/controllers/base/KodeinController.kt @@ -1,4 +1,4 @@ -package be.vandewalleh.controllers +package be.vandewalleh.controllers.base import io.ktor.application.ApplicationCall import io.ktor.http.HttpStatusCode From 5a9bdaca7389573ccdb57f913acb9450ce8403ff Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Mon, 20 Apr 2020 16:09:35 +0200 Subject: [PATCH 06/16] Move and rename more things --- api/src/controllers/ChaptersController.kt | 70 +------------------ api/src/controllers/NotesController.kt | 4 +- api/src/controllers/RegisterController.kt | 11 +++ api/src/controllers/TitleController.kt | 2 +- .../controllers/base/AuthCrudController.kt | 6 +- 5 files changed, 20 insertions(+), 73 deletions(-) create mode 100644 api/src/controllers/RegisterController.kt diff --git a/api/src/controllers/ChaptersController.kt b/api/src/controllers/ChaptersController.kt index b55f05d..1fab10d 100644 --- a/api/src/controllers/ChaptersController.kt +++ b/api/src/controllers/ChaptersController.kt @@ -1,20 +1,9 @@ package be.vandewalleh.controllers import be.vandewalleh.controllers.base.AuthCrudController -import be.vandewalleh.entities.User -import be.vandewalleh.tables.Chapters -import be.vandewalleh.tables.Notes -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.routing.Route -import io.ktor.routing.post +import io.ktor.routing.Routing import me.liuwj.ktorm.database.Database -import me.liuwj.ktorm.dsl.* -import me.liuwj.ktorm.entity.find -import me.liuwj.ktorm.entity.sequenceOf import org.kodein.di.Kodein import org.kodein.di.generic.instance @@ -29,60 +18,7 @@ class ChaptersController(kodein: Kodein) : AuthCrudController("/notes/{noteTitle return this.parameters["chapterNumber"]?.toIntOrNull() } - private fun ApplicationCall.chapterExists(): Boolean { - val user = user() - val title = noteTitle() ?: error("title missing") - val noteId = requestedNoteId() ?: error("") - - - - return db.from(Chapters) - .select(Chapters.id) - .where { Notes.userId eq user.id and (Notes.id eq noteId) } - .limit(0, 1) - .map { it[Notes.id]!! } - .firstOrNull() != null - } - - 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 data class PostRequestBody(val title: String, val content: String) - - override val route: Route.() -> Unit = { - post { - val noteId = call.requestedNoteId() - ?: return@post call.respondStatus(HttpStatusCode.NotFound) - - val chapterNumber = call.chapterNumber() - ?:return@post call.respondStatus(HttpStatusCode.BadRequest) - - val exists = false - - - val (title, content) = call.receive() - - - } + override fun Routing.registerAuthRoutes() { + TODO("Not yet implemented") } } diff --git a/api/src/controllers/NotesController.kt b/api/src/controllers/NotesController.kt index 38df007..cec3c8d 100644 --- a/api/src/controllers/NotesController.kt +++ b/api/src/controllers/NotesController.kt @@ -4,7 +4,7 @@ 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.Route +import io.ktor.routing.Routing import io.ktor.routing.get import org.kodein.di.Kodein import org.kodein.di.generic.instance @@ -12,7 +12,7 @@ import org.kodein.di.generic.instance class NotesController(kodein: Kodein) : AuthCrudController("/notes", kodein) { private val notesService by kodein.instance() - override val route: Route.() -> Unit = { + override fun Routing.registerAuthRoutes() { get { val userId = call.userId() val notes = notesService.getNotes(userId) diff --git a/api/src/controllers/RegisterController.kt b/api/src/controllers/RegisterController.kt new file mode 100644 index 0000000..52d6a23 --- /dev/null +++ b/api/src/controllers/RegisterController.kt @@ -0,0 +1,11 @@ +package be.vandewalleh.controllers + +import be.vandewalleh.controllers.base.KodeinController +import io.ktor.routing.Routing +import org.kodein.di.Kodein + +class RegisterController(kodein: Kodein): KodeinController(kodein){ + override fun Routing.registerRoutes() { + + } +} \ No newline at end of file diff --git a/api/src/controllers/TitleController.kt b/api/src/controllers/TitleController.kt index c38ddc0..3a60f95 100644 --- a/api/src/controllers/TitleController.kt +++ b/api/src/controllers/TitleController.kt @@ -59,7 +59,7 @@ class TitleController(kodein: Kodein) : AuthCrudController("/notes/{noteTitle}", private class PatchRequestBody(val title: String? = null, val tags: List? = null) - override val route: Route.() -> Unit = { + override fun Routing.registerAuthRoutes() { post { val title = call.noteTitle() ?: error("") val tags = call.receive().tags diff --git a/api/src/controllers/base/AuthCrudController.kt b/api/src/controllers/base/AuthCrudController.kt index 00ca994..a69d9fc 100644 --- a/api/src/controllers/base/AuthCrudController.kt +++ b/api/src/controllers/base/AuthCrudController.kt @@ -17,8 +17,6 @@ abstract class AuthCrudController( ) : KodeinController(kodein) { - abstract val route: Route.() -> Unit - private val userService by instance() fun ApplicationCall.userEmail(): String = @@ -27,10 +25,12 @@ abstract class AuthCrudController( fun ApplicationCall.userId(): Int = userService.getUserId(userEmail())!! + abstract fun Routing.registerAuthRoutes() + override fun Routing.registerRoutes() { authenticate { route(path) { - route() + this@registerRoutes.registerAuthRoutes() } } } From c387a2c4cf5144aa2a09457c409a7fd64f532488 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Mon, 20 Apr 2020 16:20:26 +0200 Subject: [PATCH 07/16] Move and rename more things --- api/src/controllers/ChaptersController.kt | 2 +- api/src/controllers/NotesController.kt | 2 +- api/src/controllers/RegisterController.kt | 5 ++-- api/src/controllers/TitleController.kt | 2 +- .../controllers/base/AuthCrudController.kt | 21 ++++---------- api/src/controllers/base/KodeinController.kt | 29 ++++++++++++++++--- 6 files changed, 35 insertions(+), 26 deletions(-) diff --git a/api/src/controllers/ChaptersController.kt b/api/src/controllers/ChaptersController.kt index 1fab10d..c42f2eb 100644 --- a/api/src/controllers/ChaptersController.kt +++ b/api/src/controllers/ChaptersController.kt @@ -18,7 +18,7 @@ class ChaptersController(kodein: Kodein) : AuthCrudController("/notes/{noteTitle return this.parameters["chapterNumber"]?.toIntOrNull() } - override fun Routing.registerAuthRoutes() { + override fun Routing.routes() { TODO("Not yet implemented") } } diff --git a/api/src/controllers/NotesController.kt b/api/src/controllers/NotesController.kt index cec3c8d..5fdacb9 100644 --- a/api/src/controllers/NotesController.kt +++ b/api/src/controllers/NotesController.kt @@ -12,7 +12,7 @@ import org.kodein.di.generic.instance class NotesController(kodein: Kodein) : AuthCrudController("/notes", kodein) { private val notesService by kodein.instance() - override fun Routing.registerAuthRoutes() { + override fun Routing.routes() { get { val userId = call.userId() val notes = notesService.getNotes(userId) diff --git a/api/src/controllers/RegisterController.kt b/api/src/controllers/RegisterController.kt index 52d6a23..f0b1b5d 100644 --- a/api/src/controllers/RegisterController.kt +++ b/api/src/controllers/RegisterController.kt @@ -4,8 +4,7 @@ import be.vandewalleh.controllers.base.KodeinController import io.ktor.routing.Routing import org.kodein.di.Kodein -class RegisterController(kodein: Kodein): KodeinController(kodein){ - override fun Routing.registerRoutes() { - +class RegisterController(kodein: Kodein) : KodeinController("", kodein) { + override fun Routing.routes() { } } \ No newline at end of file diff --git a/api/src/controllers/TitleController.kt b/api/src/controllers/TitleController.kt index 3a60f95..0e7d432 100644 --- a/api/src/controllers/TitleController.kt +++ b/api/src/controllers/TitleController.kt @@ -59,7 +59,7 @@ class TitleController(kodein: Kodein) : AuthCrudController("/notes/{noteTitle}", private class PatchRequestBody(val title: String? = null, val tags: List? = null) - override fun Routing.registerAuthRoutes() { + override fun Routing.routes() { post { val title = call.noteTitle() ?: error("") val tags = call.receive().tags diff --git a/api/src/controllers/base/AuthCrudController.kt b/api/src/controllers/base/AuthCrudController.kt index a69d9fc..b778f88 100644 --- a/api/src/controllers/base/AuthCrudController.kt +++ b/api/src/controllers/base/AuthCrudController.kt @@ -3,36 +3,25 @@ package be.vandewalleh.controllers.base import be.vandewalleh.services.UserService 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 import org.kodein.di.generic.instance abstract class AuthCrudController( - private val path: String, + path: String, override val kodein: Kodein ) : - KodeinController(kodein) { + KodeinController(path, kodein, auth = true) { private val userService by instance() + /** + * retrieves the user email from the JWT token + */ fun ApplicationCall.userEmail(): String = this.principal()!!.name fun ApplicationCall.userId(): Int = userService.getUserId(userEmail())!! - abstract fun Routing.registerAuthRoutes() - - override fun Routing.registerRoutes() { - authenticate { - route(path) { - this@registerRoutes.registerAuthRoutes() - } - } - } } - diff --git a/api/src/controllers/base/KodeinController.kt b/api/src/controllers/base/KodeinController.kt index eb8e373..a4c46c4 100644 --- a/api/src/controllers/base/KodeinController.kt +++ b/api/src/controllers/base/KodeinController.kt @@ -1,19 +1,40 @@ 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(override val kodein: Kodein) : KodeinAware { - +abstract class KodeinController( + private val path: String, + override val kodein: Kodein, + private val auth: Boolean = false +) : KodeinAware { /** - * Method that subtypes must override to register the handled [Routing] routes. + * Method that subtypes must override to declare their [Routing] routes. */ - abstract fun Routing.registerRoutes() + 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)) From 2a583ed3991cfc543e5661f668ad3f66e81005bf Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Mon, 20 Apr 2020 16:26:19 +0200 Subject: [PATCH 08/16] Split user controller into register and login --- api/src/controllers/Controllers.kt | 3 +- api/src/controllers/LoginController.kt | 39 ++++++++++++++ api/src/controllers/RegisterController.kt | 27 +++++++++- api/src/controllers/UserController.kt | 66 ----------------------- 4 files changed, 67 insertions(+), 68 deletions(-) create mode 100644 api/src/controllers/LoginController.kt delete mode 100644 api/src/controllers/UserController.kt diff --git a/api/src/controllers/Controllers.kt b/api/src/controllers/Controllers.kt index 8bf78f7..0ed5c15 100644 --- a/api/src/controllers/Controllers.kt +++ b/api/src/controllers/Controllers.kt @@ -13,7 +13,8 @@ import org.kodein.di.generic.singleton val controllerModule = Kodein.Module(name = "Controller") { bind() from setBinding() - bind().inSet() with singleton { UserController(this.kodein) } + bind().inSet() with singleton { RegisterController(this.kodein) } + bind().inSet() with singleton { LoginController(this.kodein) } bind().inSet() with singleton { NotesController(this.kodein) } bind().inSet() with singleton { TitleController(this.kodein) } } \ No newline at end of file diff --git a/api/src/controllers/LoginController.kt b/api/src/controllers/LoginController.kt new file mode 100644 index 0000000..176faf9 --- /dev/null +++ b/api/src/controllers/LoginController.kt @@ -0,0 +1,39 @@ +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() + private val userService by instance() + + data class TokenResponse(val token: String) + + override fun Routing.routes() { + post { + + val credential = call.receive() + + 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))) + } + } +} \ No newline at end of file diff --git a/api/src/controllers/RegisterController.kt b/api/src/controllers/RegisterController.kt index f0b1b5d..93aba9e 100644 --- a/api/src/controllers/RegisterController.kt +++ b/api/src/controllers/RegisterController.kt @@ -1,10 +1,35 @@ 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() -class RegisterController(kodein: Kodein) : KodeinController("", kodein) { override fun Routing.routes() { + post { + val user = call.receive() + + 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) + } } } \ No newline at end of file diff --git a/api/src/controllers/UserController.kt b/api/src/controllers/UserController.kt deleted file mode 100644 index 25d3bd0..0000000 --- a/api/src/controllers/UserController.kt +++ /dev/null @@ -1,66 +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.UserRegistrationDto -import be.vandewalleh.services.UserService -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 org.kodein.di.Kodein -import org.kodein.di.generic.instance -import org.mindrot.jbcrypt.BCrypt - -class UserController(kodein: Kodein) : KodeinController(kodein) { - private val simpleJwt by instance() - private val userService by instance() - - override fun Routing.registerRoutes() { - post { - data class Response(val token: String) - - val credential = call.receive() - - 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(Response(simpleJwt.sign(email))) - } - - post { - data class Response(val message: String) - - val user = call.receive() - - 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) - } - } - - object Routes { - @Location("/login") - class Login - - @Location("/register") - class Register - - } -} \ No newline at end of file From 1c8c7ba8d7bd884230cf18445b8812a8c7dde074 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Mon, 20 Apr 2020 16:28:55 +0200 Subject: [PATCH 09/16] Clean --- api/src/NotesApplication.kt | 7 +++---- api/src/migrations/Migration.kt | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/api/src/NotesApplication.kt b/api/src/NotesApplication.kt index 0e0cf25..b0ce2c1 100644 --- a/api/src/NotesApplication.kt +++ b/api/src/NotesApplication.kt @@ -31,7 +31,7 @@ fun Application.module() { import(configurationModule) import(serviceModule) - bind() with singleton { Migration(this.kodein) } + bind() with singleton { Migration(this.kodein) } bind() with singleton { Database.Companion.connect(this.instance()) } } @@ -39,9 +39,8 @@ fun Application.module() { log.debug(kodein.container.tree.bindings.description()) - // TODO, clean this (migration) - val feature by kodein.instance() - feature.execute() + val migration by kodein.instance() + migration.migrate() val controllers by kodein.instance>() diff --git a/api/src/migrations/Migration.kt b/api/src/migrations/Migration.kt index e661262..357465d 100644 --- a/api/src/migrations/Migration.kt +++ b/api/src/migrations/Migration.kt @@ -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() val flyway = Flyway.configure() .dataSource(dataSource) From c7b29f2d47441e9e517c35af653b889cd62991b6 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Mon, 20 Apr 2020 16:44:23 +0200 Subject: [PATCH 10/16] Remove ktor locations --- api/pom.xml | 5 ----- api/src/controllers/base/KodeinController.kt | 3 +-- api/src/features/Features.kt | 1 - api/src/features/LocationFeature.kt | 9 --------- 4 files changed, 1 insertion(+), 17 deletions(-) delete mode 100644 api/src/features/LocationFeature.kt diff --git a/api/pom.xml b/api/pom.xml index 8086402..4dc8a84 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -71,11 +71,6 @@ ktor-server-core ${ktor_version} - - io.ktor - ktor-locations - ${ktor_version} - io.ktor ktor-jackson diff --git a/api/src/controllers/base/KodeinController.kt b/api/src/controllers/base/KodeinController.kt index a4c46c4..c287205 100644 --- a/api/src/controllers/base/KodeinController.kt +++ b/api/src/controllers/base/KodeinController.kt @@ -28,8 +28,7 @@ abstract class KodeinController( this@registerRoutes.routes() } } - } - else { + } else { route(path) { this@registerRoutes.routes() } diff --git a/api/src/features/Features.kt b/api/src/features/Features.kt index 2ecaec2..59e4d77 100644 --- a/api/src/features/Features.kt +++ b/api/src/features/Features.kt @@ -6,7 +6,6 @@ import org.kodein.di.Kodein import org.kodein.di.KodeinAware fun Application.features() { - locationFeature() corsFeature() contentNegotiationFeature() authenticationModule() diff --git a/api/src/features/LocationFeature.kt b/api/src/features/LocationFeature.kt deleted file mode 100644 index 571246e..0000000 --- a/api/src/features/LocationFeature.kt +++ /dev/null @@ -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) -} \ No newline at end of file From cbee40f5e47c19d2a3205beb963feb0cad38f15e Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Mon, 20 Apr 2020 16:44:51 +0200 Subject: [PATCH 11/16] Remove dead code --- api/src/NotesApplication.kt | 1 - api/src/features/Features.kt | 6 ------ 2 files changed, 7 deletions(-) diff --git a/api/src/NotesApplication.kt b/api/src/NotesApplication.kt index b0ce2c1..c9d1d57 100644 --- a/api/src/NotesApplication.kt +++ b/api/src/NotesApplication.kt @@ -2,7 +2,6 @@ package be.vandewalleh import be.vandewalleh.controllers.base.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 diff --git a/api/src/features/Features.kt b/api/src/features/Features.kt index 59e4d77..97d1960 100644 --- a/api/src/features/Features.kt +++ b/api/src/features/Features.kt @@ -2,15 +2,9 @@ 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() { corsFeature() contentNegotiationFeature() authenticationModule() } - -abstract class Feature(override val kodein: Kodein) : KodeinAware { - abstract fun execute() -} \ No newline at end of file From 1f9923dc82a2a93f36a8fce8cfa3717d36753893 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Mon, 20 Apr 2020 19:21:52 +0200 Subject: [PATCH 12/16] WIP --- api-doc/users/index.apib | 2 +- api/src/NotesApplication.kt | 25 ++- api/src/controllers/ChaptersController.kt | 24 --- api/src/controllers/Controllers.kt | 20 -- api/src/controllers/LoginController.kt | 39 ---- api/src/controllers/NotesController.kt | 22 --- api/src/controllers/RegisterController.kt | 35 ---- api/src/controllers/TitleController.kt | 171 ------------------ .../controllers/base/AuthCrudController.kt | 27 --- api/src/controllers/base/KodeinController.kt | 41 ----- .../extensions/ApplicationCallExtensions.kt | 35 ++++ api/src/extensions/ParametersExtensions.kt | 22 +++ api/src/routing/ChaptersController.kt | 8 + api/src/routing/LoginController.kt | 35 ++++ api/src/routing/NotesController.kt | 20 ++ api/src/routing/RegisterController.kt | 33 ++++ api/src/routing/Routes.kt | 30 +++ api/src/routing/TitleController.kt | 59 ++++++ api/src/services/NotesService.kt | 82 +++++++-- api/src/services/Services.kt | 1 + public/index.html | 3 +- 21 files changed, 333 insertions(+), 401 deletions(-) delete mode 100644 api/src/controllers/ChaptersController.kt delete mode 100644 api/src/controllers/Controllers.kt delete mode 100644 api/src/controllers/LoginController.kt delete mode 100644 api/src/controllers/NotesController.kt delete mode 100644 api/src/controllers/RegisterController.kt delete mode 100644 api/src/controllers/TitleController.kt delete mode 100644 api/src/controllers/base/AuthCrudController.kt delete mode 100644 api/src/controllers/base/KodeinController.kt create mode 100644 api/src/extensions/ApplicationCallExtensions.kt create mode 100644 api/src/extensions/ParametersExtensions.kt create mode 100644 api/src/routing/ChaptersController.kt create mode 100644 api/src/routing/LoginController.kt create mode 100644 api/src/routing/NotesController.kt create mode 100644 api/src/routing/RegisterController.kt create mode 100644 api/src/routing/Routes.kt create mode 100644 api/src/routing/TitleController.kt diff --git a/api-doc/users/index.apib b/api-doc/users/index.apib index 126ce93..e4fd27c 100644 --- a/api-doc/users/index.apib +++ b/api-doc/users/index.apib @@ -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] diff --git a/api/src/NotesApplication.kt b/api/src/NotesApplication.kt index c9d1d57..125c687 100644 --- a/api/src/NotesApplication.kt +++ b/api/src/NotesApplication.kt @@ -1,14 +1,17 @@ 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.feature 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 me.liuwj.ktorm.database.Database import org.kodein.di.Kodein @@ -26,7 +29,6 @@ fun Application.module() { configurationFeature() kodein = Kodein { - import(controllerModule) import(configurationModule) import(serviceModule) @@ -41,11 +43,18 @@ fun Application.module() { val migration by kodein.instance() migration.migrate() - val controllers by kodein.instance>() - 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 { + return listOf(root) + root.children.flatMap { allRoutes(it) } } \ No newline at end of file diff --git a/api/src/controllers/ChaptersController.kt b/api/src/controllers/ChaptersController.kt deleted file mode 100644 index c42f2eb..0000000 --- a/api/src/controllers/ChaptersController.kt +++ /dev/null @@ -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() - - 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") - } -} diff --git a/api/src/controllers/Controllers.kt b/api/src/controllers/Controllers.kt deleted file mode 100644 index 0ed5c15..0000000 --- a/api/src/controllers/Controllers.kt +++ /dev/null @@ -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() - - bind().inSet() with singleton { RegisterController(this.kodein) } - bind().inSet() with singleton { LoginController(this.kodein) } - bind().inSet() with singleton { NotesController(this.kodein) } - bind().inSet() with singleton { TitleController(this.kodein) } -} \ No newline at end of file diff --git a/api/src/controllers/LoginController.kt b/api/src/controllers/LoginController.kt deleted file mode 100644 index 176faf9..0000000 --- a/api/src/controllers/LoginController.kt +++ /dev/null @@ -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() - private val userService by instance() - - data class TokenResponse(val token: String) - - override fun Routing.routes() { - post { - - val credential = call.receive() - - 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))) - } - } -} \ No newline at end of file diff --git a/api/src/controllers/NotesController.kt b/api/src/controllers/NotesController.kt deleted file mode 100644 index 5fdacb9..0000000 --- a/api/src/controllers/NotesController.kt +++ /dev/null @@ -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() - - override fun Routing.routes() { - get { - val userId = call.userId() - val notes = notesService.getNotes(userId) - call.respond(notes) - } - } -} diff --git a/api/src/controllers/RegisterController.kt b/api/src/controllers/RegisterController.kt deleted file mode 100644 index 93aba9e..0000000 --- a/api/src/controllers/RegisterController.kt +++ /dev/null @@ -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() - - override fun Routing.routes() { - post { - val user = call.receive() - - 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) - } - } -} \ No newline at end of file diff --git a/api/src/controllers/TitleController.kt b/api/src/controllers/TitleController.kt deleted file mode 100644 index 0e7d432..0000000 --- a/api/src/controllers/TitleController.kt +++ /dev/null @@ -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() - - 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) - - private class ChapterDto(val title: String, val content: String) - private class GetResponseBody(val tags: List, val chapters: List) - - private class PatchRequestBody(val title: String? = null, val tags: List? = null) - - override fun Routing.routes() { - post { - val title = call.noteTitle() ?: error("") - val tags = call.receive().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() - - // 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) - } - } -} \ No newline at end of file diff --git a/api/src/controllers/base/AuthCrudController.kt b/api/src/controllers/base/AuthCrudController.kt deleted file mode 100644 index b778f88..0000000 --- a/api/src/controllers/base/AuthCrudController.kt +++ /dev/null @@ -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() - - /** - * retrieves the user email from the JWT token - */ - fun ApplicationCall.userEmail(): String = - this.principal()!!.name - - fun ApplicationCall.userId(): Int = - userService.getUserId(userEmail())!! - -} diff --git a/api/src/controllers/base/KodeinController.kt b/api/src/controllers/base/KodeinController.kt deleted file mode 100644 index c287205..0000000 --- a/api/src/controllers/base/KodeinController.kt +++ /dev/null @@ -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)) - } -} diff --git a/api/src/extensions/ApplicationCallExtensions.kt b/api/src/extensions/ApplicationCallExtensions.kt new file mode 100644 index 0000000..de8ee00 --- /dev/null +++ b/api/src/extensions/ApplicationCallExtensions.kt @@ -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() + +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()!!.name + return userService.getUserId(email)!! +} + +private class Tags(val tags: List) + +suspend fun ApplicationCall.receiveTags(): List { + return receive().tags +} + +data class NotePatch(val tags: List?, val title: String?) + +suspend fun ApplicationCall.receiveNotePatch() = receive() \ No newline at end of file diff --git a/api/src/extensions/ParametersExtensions.kt b/api/src/extensions/ParametersExtensions.kt new file mode 100644 index 0000000..9b0b208 --- /dev/null +++ b/api/src/extensions/ParametersExtensions.kt @@ -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() + +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) +} \ No newline at end of file diff --git a/api/src/routing/ChaptersController.kt b/api/src/routing/ChaptersController.kt new file mode 100644 index 0000000..1fafeda --- /dev/null +++ b/api/src/routing/ChaptersController.kt @@ -0,0 +1,8 @@ +package be.vandewalleh.routing + +import io.ktor.routing.Routing +import org.kodein.di.Kodein + +fun Routing.chapters(kodein: Kodein) { + +} diff --git a/api/src/routing/LoginController.kt b/api/src/routing/LoginController.kt new file mode 100644 index 0000000..b51b755 --- /dev/null +++ b/api/src/routing/LoginController.kt @@ -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() + val userService by kodein.instance() + + data class TokenResponse(val token: String) + + post { + val credential = call.receive() + + 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))) + } + +} \ No newline at end of file diff --git a/api/src/routing/NotesController.kt b/api/src/routing/NotesController.kt new file mode 100644 index 0000000..bec6086 --- /dev/null +++ b/api/src/routing/NotesController.kt @@ -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() + + get { + val userId = call.userId() + val notes = notesService.getNotes(userId) + call.respond(notes) + } +} diff --git a/api/src/routing/RegisterController.kt b/api/src/routing/RegisterController.kt new file mode 100644 index 0000000..f36039f --- /dev/null +++ b/api/src/routing/RegisterController.kt @@ -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() + + post { + val user = call.receive() + + 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) + } +} \ No newline at end of file diff --git a/api/src/routing/Routes.kt b/api/src/routing/Routes.kt new file mode 100644 index 0000000..24341d4 --- /dev/null +++ b/api/src/routing/Routes.kt @@ -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) + } + } + } + } +} \ No newline at end of file diff --git a/api/src/routing/TitleController.kt b/api/src/routing/TitleController.kt new file mode 100644 index 0000000..0e994cb --- /dev/null +++ b/api/src/routing/TitleController.kt @@ -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() + + 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) + } +} diff --git a/api/src/services/NotesService.kt b/api/src/services/NotesService.kt index 43eed75..c88bef2 100644 --- a/api/src/services/NotesService.kt +++ b/api/src/services/NotesService.kt @@ -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() + private val db by instance() /** * returns a list of [NotesDTO] associated with the userId */ - fun getNotes(userId: Int): List { - 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 = 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) { + 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?, 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, val chapters: List) data class NotesDTO(val title: String, val tags: List, val updatedAt: String) \ No newline at end of file diff --git a/api/src/services/Services.kt b/api/src/services/Services.kt index fdb698c..6db6891 100644 --- a/api/src/services/Services.kt +++ b/api/src/services/Services.kt @@ -10,4 +10,5 @@ import org.kodein.di.generic.singleton */ val serviceModule = Kodein.Module(name = "Services") { bind() with singleton { NotesService(this.kodein) } + bind() with singleton { UserService(this.kodein) } } \ No newline at end of file diff --git a/public/index.html b/public/index.html index c840ea6..1e970fc 100644 --- a/public/index.html +++ b/public/index.html @@ -36,7 +36,8 @@ "type": "string" } } -}

Register a new user
POST/register


Authenticate user

Authenticate one user to access protected routes.

+}

Register a new user
POST/register


Authenticate user

Authenticate + one user to access protected routing.

POST http://localhost:5000/login
Requestsexample 1
Headers
Content-Type: application/json
Body
{
   "username": "babar",
   "password": "tortue"

From 69c35d0732e9cf237ac8cba731b561c595ae0cd2 Mon Sep 17 00:00:00 2001
From: Hubert Van De Walle 
Date: Tue, 21 Apr 2020 16:06:53 +0200
Subject: [PATCH 13/16] This is ugly..

---
 api/src/NotesApplication.kt           |  3 +
 api/src/auth/AuthenticationModule.kt  |  2 +-
 api/src/routing/ChaptersController.kt |  6 ++
 api/src/routing/LoginController.kt    | 20 +++---
 api/src/routing/NotesController.kt    | 11 ++--
 api/src/routing/RegisterController.kt |  2 +-
 api/src/routing/Routes.kt             | 28 ++-------
 api/src/routing/TitleController.kt    | 87 ++++++++++++++-------------
 8 files changed, 81 insertions(+), 78 deletions(-)

diff --git a/api/src/NotesApplication.kt b/api/src/NotesApplication.kt
index 125c687..c1c3cff 100644
--- a/api/src/NotesApplication.kt
+++ b/api/src/NotesApplication.kt
@@ -7,11 +7,14 @@ 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
diff --git a/api/src/auth/AuthenticationModule.kt b/api/src/auth/AuthenticationModule.kt
index 1a9de0f..f57ad63 100644
--- a/api/src/auth/AuthenticationModule.kt
+++ b/api/src/auth/AuthenticationModule.kt
@@ -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()
             verifier(simpleJwt.verifier)
             validate {
                 UserIdPrincipal(it.payload.getClaim("name").asString())
diff --git a/api/src/routing/ChaptersController.kt b/api/src/routing/ChaptersController.kt
index 1fafeda..76850b7 100644
--- a/api/src/routing/ChaptersController.kt
+++ b/api/src/routing/ChaptersController.kt
@@ -1,8 +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}") {
 
+        }
+    }
 }
diff --git a/api/src/routing/LoginController.kt b/api/src/routing/LoginController.kt
index b51b755..d776de1 100644
--- a/api/src/routing/LoginController.kt
+++ b/api/src/routing/LoginController.kt
@@ -9,6 +9,7 @@ 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
@@ -19,17 +20,20 @@ fun Routing.login(kodein: Kodein) {
 
     data class TokenResponse(val token: String)
 
-    post {
-        val credential = call.receive()
+    route("/login"){
+        post {
+            val credential = call.receive()
 
-        val (email, password) = userService.getEmailAndPasswordFromUsername(credential.username)
-            ?: return@post call.respond(HttpStatusCode.Unauthorized)
+            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)
+            if (!BCrypt.checkpw(credential.password, password)) {
+                return@post call.respond(HttpStatusCode.Unauthorized)
+            }
+
+            return@post call.respond(TokenResponse(simpleJwt.sign(email)))
         }
-
-        return@post call.respond(TokenResponse(simpleJwt.sign(email)))
     }
 
+
 }
\ No newline at end of file
diff --git a/api/src/routing/NotesController.kt b/api/src/routing/NotesController.kt
index bec6086..f4108ea 100644
--- a/api/src/routing/NotesController.kt
+++ b/api/src/routing/NotesController.kt
@@ -3,6 +3,7 @@ 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
@@ -12,9 +13,11 @@ import org.kodein.di.generic.instance
 fun Routing.notes(kodein: Kodein) {
     val notesService by kodein.instance()
 
-    get {
-        val userId = call.userId()
-        val notes = notesService.getNotes(userId)
-        call.respond(notes)
+    authenticate {
+        get("/notes") {
+            val userId = call.userId()
+            val notes = notesService.getNotes(userId)
+            call.respond(notes)
+        }
     }
 }
diff --git a/api/src/routing/RegisterController.kt b/api/src/routing/RegisterController.kt
index f36039f..8f32ae0 100644
--- a/api/src/routing/RegisterController.kt
+++ b/api/src/routing/RegisterController.kt
@@ -16,7 +16,7 @@ import org.mindrot.jbcrypt.BCrypt
 fun Routing.register(kodein: Kodein) {
     val userService by kodein.instance()
 
-    post {
+    post("/register") {
         val user = call.receive()
 
         if (userService.userExists(user.username, user.email))
diff --git a/api/src/routing/Routes.kt b/api/src/routing/Routes.kt
index 24341d4..31e6401 100644
--- a/api/src/routing/Routes.kt
+++ b/api/src/routing/Routes.kt
@@ -1,30 +1,12 @@
 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)
-                }
-            }
-        }
-    }
+    login(kodein)
+    register(kodein)
+    notes(kodein)
+    title(kodein)
+    chapters(kodein)
 }
\ No newline at end of file
diff --git a/api/src/routing/TitleController.kt b/api/src/routing/TitleController.kt
index 0e994cb..4fb4e95 100644
--- a/api/src/routing/TitleController.kt
+++ b/api/src/routing/TitleController.kt
@@ -3,6 +3,7 @@ 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.*
@@ -12,48 +13,52 @@ import org.kodein.di.generic.instance
 fun Routing.title(kodein: Kodein) {
     val notesService by kodein.instance()
 
-    post {
-        val userId = call.userId()
-        val title = call.parameters.noteTitle()
-        val tags = call.receiveTags()
-        val noteId = call.parameters.noteId(userId)
+    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)
+                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)
+            }
         }
-
-        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)
     }
 }

From ecf292bccfd5016c5c71fa5d7d4f18791acee8f8 Mon Sep 17 00:00:00 2001
From: Hubert Van De Walle 
Date: Tue, 21 Apr 2020 16:07:51 +0200
Subject: [PATCH 14/16] Update tests

---
 api/http/test.http | 32 ++++++++++++++++++++++++--------
 1 file changed, 24 insertions(+), 8 deletions(-)

diff --git a/api/http/test.http b/api/http/test.http
index 6af1fc7..3cca66d 100644
--- a/api/http/test.http
+++ b/api/http/test.http
@@ -7,25 +7,41 @@ 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
 Content-Type: application/json
 Authorization: Bearer {{token}}
 
-{
-  "tags": [
-    "Test",
-    "Dev"
-  ]
-}
+> {%
+client.test("Request executed successfully", function() {
+   client.assert(response.status === 200, "Response status is not 200");
+});
+%}
 
-### Create a note
+### 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");
+});
+%}

From 3ab0e395b3e024a08992b06cf2db956df3c524cb Mon Sep 17 00:00:00 2001
From: Hubert Van De Walle 
Date: Tue, 21 Apr 2020 16:19:01 +0200
Subject: [PATCH 15/16] Implement note creation + update tests

---
 api/http/test.http               | 11 +++++++++--
 api/src/services/NotesService.kt | 17 ++++++++++++++++-
 2 files changed, 25 insertions(+), 3 deletions(-)

diff --git a/api/http/test.http b/api/http/test.http
index 3cca66d..f80a543 100644
--- a/api/http/test.http
+++ b/api/http/test.http
@@ -25,13 +25,20 @@ client.test("Request executed successfully", function() {
 %}
 
 ### Create a note
-POST http://localhost:8081/notes/babar
+POST http://localhost:8081/notes/tortue
 Content-Type: application/json
 Authorization: Bearer {{token}}
 
+{
+    "tags": [
+        "Dev",
+        "Server"
+    ]
+}
+
 > {%
 client.test("Request executed successfully", function() {
-   client.assert(response.status === 200, "Response status is not 200");
+   client.assert(response.status === 201, "Response status is not 200");
 });
 %}
 
diff --git a/api/src/services/NotesService.kt b/api/src/services/NotesService.kt
index c88bef2..45ef13e 100644
--- a/api/src/services/NotesService.kt
+++ b/api/src/services/NotesService.kt
@@ -8,6 +8,7 @@ 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
 
 /**
@@ -47,7 +48,20 @@ class NotesService(override val kodein: Kodein) : KodeinAware {
         .firstOrNull()
 
     fun createNote(userId: Int, title: String, tags: List) {
-        TODO()
+        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 {
@@ -72,6 +86,7 @@ class NotesService(override val kodein: Kodein) : KodeinAware {
             if (title != null) {
                 db.update(Notes) {
                     it.title to title
+                    it.updatedAt to LocalDateTime.now()
                     where { it.id eq noteId }
                 }
             }

From 3a67e48ab03982dbb4b7106e90028ffd115dbc45 Mon Sep 17 00:00:00 2001
From: Hubert Van De Walle 
Date: Tue, 21 Apr 2020 16:30:55 +0200
Subject: [PATCH 16/16] Quick fix to handle bad requests

---
 api/src/features/ErrorFeature.kt | 17 +++++++++++++++++
 api/src/features/Features.kt     |  1 +
 2 files changed, 18 insertions(+)
 create mode 100644 api/src/features/ErrorFeature.kt

diff --git a/api/src/features/ErrorFeature.kt b/api/src/features/ErrorFeature.kt
new file mode 100644
index 0000000..c085ab2
--- /dev/null
+++ b/api/src/features/ErrorFeature.kt
@@ -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 { _ ->
+            call.respond(HttpStatusCode.BadRequest)
+        }
+    }
+}
\ No newline at end of file
diff --git a/api/src/features/Features.kt b/api/src/features/Features.kt
index 97d1960..09e5942 100644
--- a/api/src/features/Features.kt
+++ b/api/src/features/Features.kt
@@ -7,4 +7,5 @@ fun Application.features() {
     corsFeature()
     contentNegotiationFeature()
     authenticationModule()
+    handleErrors()
 }