Better seperation

This commit is contained in:
Hubert Van De Walle 2020-06-25 13:37:49 +02:00
parent 3306b16f91
commit 5b748e1c10
19 changed files with 148 additions and 266 deletions

View File

@ -1,77 +0,0 @@
# Register a new user
POST http://localhost:8081/user/login
Content-Type: application/json
{
"username": "{{username}}",
"password": "{{password}}"
}
> {%
client.global.set("token", response.body.token);
client.global.set("refreshToken", response.body.refreshToken);
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
});
%}
### Refresh token
POST http://localhost:8081/user/refresh_token
Content-Type: application/json
{
"refreshToken": "{{refreshToken}}"
}
> {%
client.test("Request executed successfully", function() {
client.global.set("token", response.body.token);
client.assert(response.status === 200, "Response status is not 200");
});
%}
### Get notes
GET http://localhost:8081/notes
Authorization: Bearer {{token}}
> {%
client.global.set("uuid", response.body[0].uuid);
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
});
%}
### Create note
POST http://localhost:8081/notes
Authorization: Bearer {{token}}
Content-Type: application/json
{
"title": "test",
"tags": [
"Some",
"Tags"
],
"chapters": [
{
"title": "Chapter 1",
"content": "# This is some content"
}
]
}
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 201, "Response status is not 201");
});
%}
### Get a note
GET http://localhost:8081/notes/{{uuid}}
Authorization: Bearer {{token}}
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
});
%}

View File

@ -7,7 +7,7 @@ database:
server:
host: 127.0.0.1
port: 8081
port: ${PORT:-8081}
cors: true
jwt:

View File

@ -1,10 +1,12 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<withJansi>true</withJansi>
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<pattern>%cyan(%d{YYYY-MM-dd HH:mm:ss.SSS}) [%thread] %highlight(%-5level) %magenta(%logger{36}) - %msg%n
</pattern>
</encoder>
</appender>
<root level="DEBUG">
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
<logger name="me.liuwj.ktorm.database" level="INFO"/>

View File

@ -3,11 +3,16 @@ package be.vandewalleh
import be.vandewalleh.features.Config
import be.vandewalleh.features.loadFeatures
import be.vandewalleh.migrations.Migration
import be.vandewalleh.routing.registerRoutes
import be.vandewalleh.routing.noteRoutes
import be.vandewalleh.routing.tagsRoute
import be.vandewalleh.routing.userRoutes
import com.sksamuel.hoplite.fp.valid
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import me.liuwj.ktorm.database.*
import org.kodein.di.Kodein
import org.kodein.di.description
import org.kodein.di.generic.instance
@ -50,7 +55,17 @@ fun Application.module(kodein: Kodein) {
loadFeatures(kodein)
routing {
registerRoutes(kodein)
route("/user") {
userRoutes(kodein)
}
authenticate {
route("/notes") {
noteRoutes(kodein)
}
route("/tags") {
tagsRoute(kodein)
}
}
}
}

View File

@ -7,10 +7,10 @@ import io.ktor.http.*
import io.ktor.response.*
suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) {
respond(status, """{"msg": "${status.description}"}""")
respond(status, """{"status": "${status.description}"}""")
}
/**
* @return the userId for the currently authenticated user
*/
fun ApplicationCall.userId() = principal<UserDbIdPrincipal>()!!.id
fun ApplicationCall.authenticatedUserId() = principal<UserDbIdPrincipal>()!!.id

View File

@ -1,8 +0,0 @@
package be.vandewalleh.extensions
import io.ktor.http.*
import java.util.*
fun Parameters.noteUuid(): UUID {
return UUID.fromString(this["noteUuid"])
}

View File

@ -60,6 +60,7 @@ private fun configureDatasource(kodein: Kodein): DataSource {
jdbcUrl = "jdbc:mariadb://$host:$port/$name"
username = dbConfig.username
password = dbConfig.password.value
connectionTimeout = 3000 // 3 seconds
}
return HikariDataSource(hikariConfig)

View File

@ -1,12 +1,11 @@
package be.vandewalleh.features
import am.ik.yavi.core.ViolationDetail
import be.vandewalleh.validation.ValidationException
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.utils.io.errors.*
import java.sql.SQLTransientConnectionException
fun Application.handleErrors() {
install(StatusPages) {
@ -17,12 +16,15 @@ fun Application.handleErrors() {
call.respond(HttpStatusCode.BadRequest)
}
exception<ValidationException> {
val error = ViolationError(it.details[0])
call.respond(HttpStatusCode.BadRequest, error)
call.respond(HttpStatusCode.BadRequest, ErrorResponse(it.error))
}
exception<SQLTransientConnectionException> {
val error = mapOf("error" to "It seems the server can't connect to the database")
call.respond(HttpStatusCode.InternalServerError, error)
}
}
}
class ViolationError(detail: ViolationDetail) {
val msg = detail.defaultMessage
}
class ValidationException(val error: String) : RuntimeException()
class ErrorResponse(val error: String)

View File

@ -0,0 +1,75 @@
package be.vandewalleh.routing
import be.vandewalleh.extensions.authenticatedUserId
import be.vandewalleh.extensions.respondStatus
import be.vandewalleh.features.ValidationException
import be.vandewalleh.services.NoteService
import be.vandewalleh.validation.noteValidator
import be.vandewalleh.validation.receiveValidated
import io.ktor.application.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.routing.*
import org.kodein.di.Kodein
import org.kodein.di.generic.instance
import java.util.*
fun Route.noteRoutes(kodein: Kodein) {
val noteService by kodein.instance<NoteService>()
post {
val userId = call.authenticatedUserId()
val note = call.receiveValidated(noteValidator)
val createdNote = noteService.create(userId, note)
call.respond(HttpStatusCode.Created, createdNote)
}
get {
val userId = call.authenticatedUserId()
val notes = noteService.findAll(userId)
call.respond(notes)
}
route("/{uuid}") {
fun ApplicationCall.userIdNoteIdPair(): Pair<Int, UUID> {
val userId = authenticatedUserId()
val uuid = parameters["uuid"]
val noteUuid = try {
UUID.fromString(uuid)
} catch (e: IllegalArgumentException) {
throw ValidationException("`$uuid` is not a valid UUID")
}
return userId to noteUuid
}
get {
val (userId, noteUuid) = call.userIdNoteIdPair()
val response = noteService.find(userId, noteUuid)
?: return@get call.respondStatus(HttpStatusCode.NotFound)
call.respond(response)
}
put {
val (userId, noteUuid) = call.userIdNoteIdPair()
val note = call.receiveValidated(noteValidator)
note.uuid = noteUuid
if (noteService.updateNote(userId, note))
call.respondStatus(HttpStatusCode.OK)
else call.respondStatus(HttpStatusCode.NotFound)
}
delete {
val (userId, noteUuid) = call.userIdNoteIdPair()
if (noteService.delete(userId, noteUuid))
call.respondStatus(HttpStatusCode.OK)
else
call.respondStatus(HttpStatusCode.NotFound)
}
}
}

View File

@ -1,17 +0,0 @@
package be.vandewalleh.routing
import be.vandewalleh.routing.notes.notes
import be.vandewalleh.routing.notes.tags
import be.vandewalleh.routing.notes.title
import be.vandewalleh.routing.user.auth
import be.vandewalleh.routing.user.user
import io.ktor.routing.*
import org.kodein.di.Kodein
fun Routing.registerRoutes(kodein: Kodein) {
user(kodein)
auth(kodein)
notes(kodein)
title(kodein)
tags(kodein)
}

View File

@ -1,20 +1,17 @@
package be.vandewalleh.routing.notes
package be.vandewalleh.routing
import be.vandewalleh.extensions.userId
import be.vandewalleh.extensions.authenticatedUserId
import be.vandewalleh.services.NoteService
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.response.*
import io.ktor.routing.*
import org.kodein.di.Kodein
import org.kodein.di.generic.instance
fun Routing.tags(kodein: Kodein) {
fun Route.tagsRoute(kodein: Kodein) {
val noteService by kodein.instance<NoteService>()
authenticate {
get("/tags") {
call.respond(noteService.getTags(call.userId()))
}
get {
call.respond(noteService.getTags(call.authenticatedUserId()))
}
}

View File

@ -1,11 +1,14 @@
package be.vandewalleh.routing.user
package be.vandewalleh.routing
import be.vandewalleh.auth.SimpleJWT
import be.vandewalleh.auth.UserDbIdPrincipal
import be.vandewalleh.auth.UsernamePasswordCredential
import be.vandewalleh.extensions.respondStatus
import be.vandewalleh.extensions.authenticatedUserId
import be.vandewalleh.features.PasswordHash
import be.vandewalleh.services.UserService
import be.vandewalleh.validation.receiveValidated
import be.vandewalleh.validation.registerValidator
import com.auth0.jwt.exceptions.JWTVerificationException
import io.ktor.application.*
import io.ktor.auth.*
@ -19,13 +22,25 @@ import org.kodein.di.generic.instance
data class RefreshToken(val refreshToken: String)
data class DualToken(val token: String, val refreshToken: String)
fun Routing.auth(kodein: Kodein) {
fun Route.userRoutes(kodein: Kodein) {
val authSimpleJwt by kodein.instance<SimpleJWT>("auth")
val refreshSimpleJwt by kodein.instance<SimpleJWT>("refresh")
val userService by kodein.instance<UserService>()
val passwordHash by kodein.instance<PasswordHash>()
post("/user/login") {
post {
val user = call.receiveValidated(registerValidator)
if (userService.exists(user.username))
return@post call.respondStatus(HttpStatusCode.Conflict)
val newUser = userService.create(user.username, user.password)
?: return@post call.respondStatus(HttpStatusCode.Conflict)
call.respond(HttpStatusCode.Created, newUser)
}
post("/login") {
val credential = call.receive<UsernamePasswordCredential>()
val user = userService.find(credential.username)
@ -42,7 +57,7 @@ fun Routing.auth(kodein: Kodein) {
return@post call.respond(response)
}
post("/user/refresh_token") {
post("/refresh_token") {
val token = call.receive<RefreshToken>().refreshToken
val id = try {
@ -63,12 +78,19 @@ fun Routing.auth(kodein: Kodein) {
}
authenticate {
get("/user/me") {
delete {
val userId = call.authenticatedUserId()
call.respondStatus(
if (userService.delete(userId)) HttpStatusCode.OK
else HttpStatusCode.NotFound
)
}
get("/me") {
val id = call.principal<UserDbIdPrincipal>()!!.id
val info = userService.find(id)
if (info != null) call.respond(mapOf("user" to info))
else call.respondStatus(HttpStatusCode.Unauthorized)
}
}
}

View File

@ -1,53 +0,0 @@
package be.vandewalleh.routing.notes
import be.vandewalleh.entities.Note
import be.vandewalleh.extensions.noteUuid
import be.vandewalleh.extensions.respondStatus
import be.vandewalleh.extensions.userId
import be.vandewalleh.services.NoteService
import io.ktor.application.call
import io.ktor.auth.authenticate
import io.ktor.http.HttpStatusCode
import io.ktor.request.*
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 noteService by kodein.instance<NoteService>()
authenticate {
route("/notes/{noteUuid}") {
get {
val userId = call.userId()
val noteUuid = call.parameters.noteUuid()
val response = noteService.find(userId, noteUuid)
?: return@get call.respondStatus(HttpStatusCode.NotFound)
call.respond(response)
}
put {
val userId = call.userId()
val noteUuid = call.parameters.noteUuid()
val note = call.receive<Note>()
note.uuid = noteUuid
if (noteService.updateNote(userId, note))
call.respondStatus(HttpStatusCode.OK)
else call.respondStatus(HttpStatusCode.NotFound)
}
delete {
val userId = call.userId()
val noteUuid = call.parameters.noteUuid()
if (noteService.delete(userId, noteUuid))
call.respondStatus(HttpStatusCode.OK)
else
call.respondStatus(HttpStatusCode.NotFound)
}
}
}
}

View File

@ -1,32 +0,0 @@
package be.vandewalleh.routing.notes
import be.vandewalleh.entities.Note
import be.vandewalleh.extensions.userId
import be.vandewalleh.services.NoteService
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.http.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import org.kodein.di.Kodein
import org.kodein.di.generic.instance
fun Routing.notes(kodein: Kodein) {
val noteService by kodein.instance<NoteService>()
authenticate {
get("/notes") {
val userId = call.userId()
val notes = noteService.findAll(userId)
call.respond(notes)
}
post("/notes") {
val userId = call.userId()
val note = call.receive<Note>()
val createdNote = noteService.create(userId, note)
call.respond(HttpStatusCode.Created, createdNote)
}
}
}

View File

@ -1,43 +0,0 @@
package be.vandewalleh.routing.user
import be.vandewalleh.extensions.respondStatus
import be.vandewalleh.extensions.userId
import be.vandewalleh.services.UserService
import be.vandewalleh.validation.receiveValidated
import be.vandewalleh.validation.user.registerValidator
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.routing.*
import org.kodein.di.Kodein
import org.kodein.di.generic.instance
fun Routing.user(kodein: Kodein) {
val userService by kodein.instance<UserService>()
route("/user") {
post {
val user = call.receiveValidated(registerValidator)
if (userService.exists(user.username))
return@post call.respondStatus(HttpStatusCode.Conflict)
val newUser = userService.create(user.username, user.password)
?: return@post call.respondStatus(HttpStatusCode.Conflict)
call.respond(HttpStatusCode.Created, newUser)
}
authenticate {
delete {
val status = if (userService.delete(call.userId()))
HttpStatusCode.OK
else
HttpStatusCode.NotFound
call.respondStatus(status)
}
}
}
}

View File

@ -26,7 +26,7 @@ class NoteService(override val kodein: Kodein) : KodeinAware {
suspend fun findAll(userId: Int): List<Note> {
val notes = launchIo {
db.sequenceOf(Notes, withReferences = false)
.filterColumns { it.columns - it.userId - it.content }
.filterColumns { listOf(it.uuid, it.title, it.updatedAt) }
.filter { it.userId eq userId }
.sortedByDescending { it.updatedAt }
.toList()
@ -41,7 +41,7 @@ class NoteService(override val kodein: Kodein) : KodeinAware {
.toList()
}
val tagsByUuid = allTags.groupBy({ it.note.uuid }, { it.name })
val tagsByUuid = allTags.groupByTo(HashMap(), { it.note.uuid }, { it.name })
notes.forEach {
val tags = tagsByUuid[it.uuid]

View File

@ -1,4 +1,4 @@
package be.vandewalleh.validation.user
package be.vandewalleh.validation
import am.ik.yavi.builder.ValidatorBuilder
import am.ik.yavi.builder.konstraint

View File

@ -1,14 +1,12 @@
package be.vandewalleh.validation
import am.ik.yavi.core.Validator
import am.ik.yavi.core.ViolationDetail
import be.vandewalleh.features.ValidationException
import io.ktor.application.*
import io.ktor.request.*
suspend inline fun <reified T : Any> ApplicationCall.receiveValidated(validator: Validator<T>): T {
val value: T = receive()
validator.validate(value).throwIfInvalid { ValidationException(it.details()) }
validator.validate(value).throwIfInvalid { ValidationException(it.details()[0].defaultMessage) }
return value
}
data class ValidationException(val details: List<ViolationDetail>) : RuntimeException()

View File

@ -1,7 +1,7 @@
package unit.validation
import be.vandewalleh.entities.User
import be.vandewalleh.validation.user.registerValidator
import be.vandewalleh.validation.registerValidator
import org.amshove.kluent.*
import org.junit.jupiter.api.*
import utils.firstInvalid