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: server:
host: 127.0.0.1 host: 127.0.0.1
port: 8081 port: ${PORT:-8081}
cors: true cors: true
jwt: jwt:

View File

@ -1,10 +1,12 @@
<configuration> <configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<withJansi>true</withJansi>
<encoder> <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> </encoder>
</appender> </appender>
<root level="DEBUG"> <root level="INFO">
<appender-ref ref="STDOUT"/> <appender-ref ref="STDOUT"/>
</root> </root>
<logger name="me.liuwj.ktorm.database" level="INFO"/> <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.Config
import be.vandewalleh.features.loadFeatures import be.vandewalleh.features.loadFeatures
import be.vandewalleh.migrations.Migration 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.application.*
import io.ktor.auth.*
import io.ktor.routing.* import io.ktor.routing.*
import io.ktor.server.engine.* import io.ktor.server.engine.*
import io.ktor.server.netty.* import io.ktor.server.netty.*
import me.liuwj.ktorm.database.*
import org.kodein.di.Kodein import org.kodein.di.Kodein
import org.kodein.di.description import org.kodein.di.description
import org.kodein.di.generic.instance import org.kodein.di.generic.instance
@ -50,7 +55,17 @@ fun Application.module(kodein: Kodein) {
loadFeatures(kodein) loadFeatures(kodein)
routing { 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.* import io.ktor.response.*
suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) { suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) {
respond(status, """{"msg": "${status.description}"}""") respond(status, """{"status": "${status.description}"}""")
} }
/** /**
* @return the userId for the currently authenticated user * @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" jdbcUrl = "jdbc:mariadb://$host:$port/$name"
username = dbConfig.username username = dbConfig.username
password = dbConfig.password.value password = dbConfig.password.value
connectionTimeout = 3000 // 3 seconds
} }
return HikariDataSource(hikariConfig) return HikariDataSource(hikariConfig)

View File

@ -1,12 +1,11 @@
package be.vandewalleh.features package be.vandewalleh.features
import am.ik.yavi.core.ViolationDetail
import be.vandewalleh.validation.ValidationException
import io.ktor.application.* import io.ktor.application.*
import io.ktor.features.* import io.ktor.features.*
import io.ktor.http.* import io.ktor.http.*
import io.ktor.response.* import io.ktor.response.*
import io.ktor.utils.io.errors.* import io.ktor.utils.io.errors.*
import java.sql.SQLTransientConnectionException
fun Application.handleErrors() { fun Application.handleErrors() {
install(StatusPages) { install(StatusPages) {
@ -17,12 +16,15 @@ fun Application.handleErrors() {
call.respond(HttpStatusCode.BadRequest) call.respond(HttpStatusCode.BadRequest)
} }
exception<ValidationException> { exception<ValidationException> {
val error = ViolationError(it.details[0]) call.respond(HttpStatusCode.BadRequest, ErrorResponse(it.error))
call.respond(HttpStatusCode.BadRequest, 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) { class ValidationException(val error: String) : RuntimeException()
val msg = detail.defaultMessage 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 be.vandewalleh.services.NoteService
import io.ktor.application.* import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.response.* import io.ktor.response.*
import io.ktor.routing.* import io.ktor.routing.*
import org.kodein.di.Kodein import org.kodein.di.Kodein
import org.kodein.di.generic.instance import org.kodein.di.generic.instance
fun Routing.tags(kodein: Kodein) { fun Route.tagsRoute(kodein: Kodein) {
val noteService by kodein.instance<NoteService>() val noteService by kodein.instance<NoteService>()
authenticate { get {
get("/tags") { call.respond(noteService.getTags(call.authenticatedUserId()))
call.respond(noteService.getTags(call.userId()))
}
} }
} }

View File

@ -1,11 +1,14 @@
package be.vandewalleh.routing.user package be.vandewalleh.routing
import be.vandewalleh.auth.SimpleJWT import be.vandewalleh.auth.SimpleJWT
import be.vandewalleh.auth.UserDbIdPrincipal import be.vandewalleh.auth.UserDbIdPrincipal
import be.vandewalleh.auth.UsernamePasswordCredential import be.vandewalleh.auth.UsernamePasswordCredential
import be.vandewalleh.extensions.respondStatus import be.vandewalleh.extensions.respondStatus
import be.vandewalleh.extensions.authenticatedUserId
import be.vandewalleh.features.PasswordHash import be.vandewalleh.features.PasswordHash
import be.vandewalleh.services.UserService import be.vandewalleh.services.UserService
import be.vandewalleh.validation.receiveValidated
import be.vandewalleh.validation.registerValidator
import com.auth0.jwt.exceptions.JWTVerificationException import com.auth0.jwt.exceptions.JWTVerificationException
import io.ktor.application.* import io.ktor.application.*
import io.ktor.auth.* import io.ktor.auth.*
@ -19,13 +22,25 @@ import org.kodein.di.generic.instance
data class RefreshToken(val refreshToken: String) data class RefreshToken(val refreshToken: String)
data class DualToken(val token: String, 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 authSimpleJwt by kodein.instance<SimpleJWT>("auth")
val refreshSimpleJwt by kodein.instance<SimpleJWT>("refresh") val refreshSimpleJwt by kodein.instance<SimpleJWT>("refresh")
val userService by kodein.instance<UserService>() val userService by kodein.instance<UserService>()
val passwordHash by kodein.instance<PasswordHash>() 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 credential = call.receive<UsernamePasswordCredential>()
val user = userService.find(credential.username) val user = userService.find(credential.username)
@ -42,7 +57,7 @@ fun Routing.auth(kodein: Kodein) {
return@post call.respond(response) return@post call.respond(response)
} }
post("/user/refresh_token") { post("/refresh_token") {
val token = call.receive<RefreshToken>().refreshToken val token = call.receive<RefreshToken>().refreshToken
val id = try { val id = try {
@ -63,12 +78,19 @@ fun Routing.auth(kodein: Kodein) {
} }
authenticate { 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 id = call.principal<UserDbIdPrincipal>()!!.id
val info = userService.find(id) val info = userService.find(id)
if (info != null) call.respond(mapOf("user" to info)) if (info != null) call.respond(mapOf("user" to info))
else call.respondStatus(HttpStatusCode.Unauthorized) 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> { suspend fun findAll(userId: Int): List<Note> {
val notes = launchIo { val notes = launchIo {
db.sequenceOf(Notes, withReferences = false) 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 } .filter { it.userId eq userId }
.sortedByDescending { it.updatedAt } .sortedByDescending { it.updatedAt }
.toList() .toList()
@ -41,7 +41,7 @@ class NoteService(override val kodein: Kodein) : KodeinAware {
.toList() .toList()
} }
val tagsByUuid = allTags.groupBy({ it.note.uuid }, { it.name }) val tagsByUuid = allTags.groupByTo(HashMap(), { it.note.uuid }, { it.name })
notes.forEach { notes.forEach {
val tags = tagsByUuid[it.uuid] 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.ValidatorBuilder
import am.ik.yavi.builder.konstraint import am.ik.yavi.builder.konstraint

View File

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

View File

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