Skip to content

Commit

Permalink
Reshuffle and CORS
Browse files Browse the repository at this point in the history
  • Loading branch information
jvorhauer committed Feb 14, 2024
1 parent 221d70c commit 9d9d768
Show file tree
Hide file tree
Showing 20 changed files with 239 additions and 247 deletions.
5 changes: 5 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ indent_style = space
insert_final_newline = true
max_line_length = 166
tab_width = 2

[{*.kt,kts}]
ij_kotlin_imports_layout = java.**,|,javax.**,|,akka.**,|,com.**,|,org.**,|,*,|,blog.**
ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ dependencies {
implementation("io.ktor:ktor-server-call-logging-jvm")
implementation("io.ktor:ktor-server-request-validation")
implementation("io.ktor:ktor-server-status-pages")
implementation("io.ktor:ktor-server-cors")
implementation("io.ktor:ktor-serialization-jackson")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.16.1")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.1")
Expand Down
28 changes: 15 additions & 13 deletions src/main/kotlin/blog/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,42 @@ package blog
import akka.actor.typed.ActorSystem
import akka.actor.typed.Behavior
import akka.actor.typed.javadsl.Behaviors
import com.typesafe.config.ConfigFactory
import io.github.config4k.extract
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.routing.*
import io.sentry.Sentry
import blog.model.Konfig
import blog.model.loginRoute
import blog.model.notesRoute
import blog.model.tasksRoute
import blog.model.usersRoute
import blog.module.authentication
import blog.module.content
import blog.route.info
import blog.module.http
import blog.module.logging
import blog.module.status
import blog.module.validation
import blog.read.Reader
import blog.route.loginRoute
import blog.route.notesRoute
import blog.route.tasksRoute
import blog.route.usersRoute
import blog.read.info
import blog.write.Processor
import com.typesafe.config.ConfigFactory
import io.github.config4k.extract
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.routing.*
import io.sentry.Sentry

const val pid: String = "27"

object Main {
private val kfg: Konfig = ConfigFactory.load("application.conf").extract("jwt")

private val behavior: Behavior<Void> = Behaviors.setup { ctx ->
embeddedServer(Netty, port = 8080, watchPaths = listOf("classes")) {
embeddedServer(Netty, port = 8080) {
val reader = Reader()
val processor = ctx.spawn(Processor.create(pid, reader), "konomas")
val scheduler = ctx.system.scheduler()

environment.monitor.subscribe(ServerReady) { reader.setServerReady() }

http()
content()
logging()
authentication(kfg)
Expand Down
48 changes: 48 additions & 0 deletions src/main/kotlin/blog/model/Note.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ import akka.pattern.StatusReply
import org.owasp.encoder.Encode
import java.time.LocalDateTime
import java.time.ZoneId
import akka.actor.typed.Scheduler
import akka.actor.typed.javadsl.AskPattern
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.coroutines.future.await
import blog.read.Reader

data class CreateNoteRequest(val title: String, val body: String) {
fun toCommand(user: Long, replyTo: ActorRef<StatusReply<NoteResponse>>) = CreateNote(user, title, body, replyTo)
Expand Down Expand Up @@ -61,3 +71,41 @@ data class NoteResponse(
val title: String,
val body: String
)

fun Route.notesRoute(processor: ActorRef<Command>, reader: Reader, scheduler: Scheduler, kfg: Konfig) =
authenticate(kfg.realm) {
route("/api/notes") {
post {
val cnr = call.receive<CreateNoteRequest>()
val userId = user(call) ?: return@post call.respond(HttpStatusCode.Unauthorized, "Unauthorized")
AskPattern.ask(processor, { rt -> cnr.toCommand(userId, rt) }, timeout, scheduler).await().let {
if (it.isSuccess) {
call.response.header("Location", "/api/notes/${it.value.id}")
call.respond(HttpStatusCode.Created, it.value)
} else {
call.respond(HttpStatusCode.BadRequest, it.error.localizedMessage)
}
}
}
get {
val rows = call.request.queryParameters["rows"]?.toInt() ?: 10
val start = call.request.queryParameters["start"]?.toInt() ?: 0
call.respond(reader.allNotes(rows, start).map { it.toResponse() })
}
get("{id?}") {
val id = call.parameters["id"]?.toLong() ?: return@get call.respond(HttpStatusCode.NotFound, "note id not specified")
val note: Note = reader.find(id) ?: return@get call.respond(HttpStatusCode.NotFound, "note not found for $id")
call.respond(note.toResponse())
}
delete("{id?}") {
val id = call.parameters["id"]?.toLong() ?: return@delete call.respond(HttpStatusCode.NotFound, "note id not specified")
reader.findNote(id) ?: return@delete call.respond(HttpStatusCode.NotFound, "note not found for $id")
AskPattern.ask(processor, { rt -> DeleteNote(id, rt) }, timeout, scheduler).await().let {
when {
it.isSuccess -> call.respond(HttpStatusCode.OK, mapOf("note" to id))
else -> call.respond(HttpStatusCode.InternalServerError, it.error.localizedMessage)
}
}
}
}
}
66 changes: 65 additions & 1 deletion src/main/kotlin/blog/model/Task.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
package blog.model

import java.time.LocalDateTime
import akka.Done
import akka.actor.typed.ActorRef
import akka.actor.typed.Scheduler
import akka.actor.typed.javadsl.AskPattern
import akka.pattern.StatusReply
import java.time.LocalDateTime
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.coroutines.future.await
import blog.read.Reader

enum class TaskStatus {
TODO, DOING, REVIEW, DONE
Expand All @@ -27,6 +37,10 @@ data class Task(
status = tu.status ?: this.status
)
fun toResponse() = TaskResponse(id, user, title, body, DTF.format(due), status.name)

companion object {
val clazz = Task::class
}
}

data class CreateTaskRequest(val title: String, val body: String, val due: LocalDateTime): Request {
Expand Down Expand Up @@ -97,3 +111,53 @@ data class TaskResponse(
val due: String,
val status: String
)

fun Route.tasksRoute(processor: ActorRef<Command>, reader: Reader, scheduler: Scheduler, kfg: Konfig) =
authenticate(kfg.realm) {
route("/api/tasks") {
post {
val ctr = call.receive<CreateTaskRequest>()
val userId = user(call) ?: return@post call.respond(HttpStatusCode.Unauthorized, "Unauthorized")
AskPattern.ask(processor, { rt -> ctr.toCommand(userId, rt) }, timeout, scheduler).await().let {
when {
it.isSuccess -> {
call.response.header("Location", "/api/tasks/${it.value.id}")
call.respond(HttpStatusCode.Created, it.value)
}
else -> call.respond(HttpStatusCode.BadRequest, it.error.localizedMessage)
}
}
}
get {
val rows = call.request.queryParameters["rows"]?.toInt() ?: 10
val start = call.request.queryParameters["start"]?.toInt() ?: 0
call.respond(reader.allTasks(rows, start).map { it.toResponse() })
}
get("{id?}") {
val id = call.parameters["id"]?.toLong() ?: return@get call.respond(HttpStatusCode.NotFound, "no task id specified")
(reader.find<Task>(id) ?: return@get call.respond(HttpStatusCode.NotFound, "task not found for $id")).let {
call.respond(it.toResponse())
}
}
put {
val utr = call.receive<UpdateTaskRequest>()
val userId = user(call) ?: return@put call.respond(HttpStatusCode.Unauthorized, "Unauthorized")
AskPattern.ask(processor, { rt -> utr.toCommand(userId, rt) }, timeout, scheduler).await().let {
when {
it.isSuccess -> call.respond(HttpStatusCode.OK, it.value)
else -> call.respond(HttpStatusCode.BadRequest, it.error.localizedMessage)
}
}
}
delete("{id?}") {
val id = call.parameters["id"]?.toLong() ?: return@delete call.respond(HttpStatusCode.NotFound, "no task id specified")
reader.find<Task>(id) ?: return@delete call.respond(HttpStatusCode.NotFound, "task not found for $id")
AskPattern.ask(processor, { rt -> DeleteTask(id, rt) }, timeout, scheduler).await().let {
when {
it.isSuccess -> call.respond(HttpStatusCode.OK, mapOf("task" to id))
else -> call.respond(HttpStatusCode.InternalServerError, it.error.localizedMessage)
}
}
}
}
}
63 changes: 61 additions & 2 deletions src/main/kotlin/blog/model/User.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
package blog.model

import java.time.Duration
import java.time.Instant
import java.time.ZoneId
import akka.actor.typed.ActorRef
import akka.actor.typed.Scheduler
import akka.actor.typed.javadsl.AskPattern
import akka.pattern.StatusReply
import blog.read.Reader
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import org.owasp.encoder.Encode
import java.time.ZoneId
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.coroutines.future.await
import blog.read.Reader

data class RegisterUserRequest(
val email: String,
Expand Down Expand Up @@ -75,3 +88,49 @@ data class UserResponse(
val notes: List<NoteResponse>,
val tasks: List<TaskResponse>,
)

fun Route.loginRoute(reader: Reader, kfg: Konfig): Route =
route("/api/login") {
post {
val loginRequest = call.receive<LoginRequest>()
val user: User = reader.findUserByEmail(loginRequest.username) ?: return@post call.respond(HttpStatusCode.Unauthorized, "user not found")
val token: String = JWT.create()
.withAudience(kfg.audience)
.withClaim("uid", user.id)
.withExpiresAt(Instant.now().plusMillis(kfg.expiresIn))
.withIssuer(kfg.issuer)
.sign(Algorithm.HMAC256(kfg.secret))
call.respond(hashMapOf("token" to token))
}
}

fun Route.usersRoute(processor: ActorRef<Command>, reader: Reader, scheduler: Scheduler, kfg: Konfig): Route =
route("/api/users") {
get {
val start = call.request.queryParameters["start"]?.toInt() ?: 0
val rows = call.request.queryParameters["rows"]?.toInt() ?: 10
call.respond(reader.allUsers(rows, start).map { it.toResponse(reader) })
}
get("{id?}") {
val id = call.parameters["id"] ?: return@get call.respondText("no user id specified", status = HttpStatusCode.BadRequest)
val user: User = reader.find(id.toLong()) ?: return@get call.respondText("user not found for $id", status = HttpStatusCode.NotFound)
call.respond(user.toResponse(reader))
}
authenticate(kfg.realm) {
get("/tasks") {
val userId = user(call) ?: return@get call.respond(HttpStatusCode.Unauthorized, "Unauthorized")
call.respond(reader.findTasksForUser(userId).map { it.toResponse() })
}
}
post {
val cnu = call.receive<RegisterUserRequest>()
AskPattern.ask(processor, { rt -> cnu.toCommand(rt) }, Duration.ofMinutes(1), scheduler).await().let {
if (it.isSuccess) {
call.response.header("Location", "/api/users/${it.value.id}")
call.respond(HttpStatusCode.Created, it.value)
} else {
call.respond(HttpStatusCode.BadRequest, it.error.localizedMessage)
}
}
}
}
8 changes: 7 additions & 1 deletion src/main/kotlin/blog/model/model.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import java.net.InetAddress
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.security.SecureRandom
import java.time.Duration
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.Locale
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*

interface Request : Serializable
interface Command : Serializable
Expand All @@ -27,7 +31,6 @@ object Hasher {
}
fun gravatarize(s: String): String = s.trim().lowercase().hashed()


val DTF: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
fun now(): LocalDateTime = LocalDateTime.now()

Expand Down Expand Up @@ -55,3 +58,6 @@ data class Konfig(
val issuer: String,
val expiresIn: Long = 1000L * 60L * 60L * 24L
)

val timeout: Duration = Duration.ofSeconds(10)
fun user(call: ApplicationCall): Long? = call.principal<JWTPrincipal>()?.payload?.getClaim("uid")?.asLong()
18 changes: 18 additions & 0 deletions src/main/kotlin/blog/module/Http.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package blog.module

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.cors.routing.*

fun Application.http() {
install(CORS) {
anyHost()
allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Delete)
allowHeader(HttpHeaders.Authorization)
allowHeader(HttpHeaders.ContentType)
allowHeader(HttpHeaders.Accept)
allowNonSimpleContentTypes = true
allowCredentials = true
}
}
3 changes: 1 addition & 2 deletions src/main/kotlin/blog/module/Logging.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import org.slf4j.event.Level
fun Application.logging() {
install(CallLogging) {
level = Level.INFO
filter { it.request.path().startsWith("/") }

filter { it.request.path().startsWith("/api") }
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package blog.route
package blog.read

import blog.model.TaskStatus
import blog.read.Reader
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/blog/read/Reader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class Reader(
fun findUser(id: Long): User? = users[id]
fun findUserByEmail(email: String): User? = users.values.find { it.email == email }
fun exists(email: String): Boolean = findUserByEmail(email) != null
fun canAuthenticate(email: String, password: String): Boolean = users.values.find { it.email == email && it.password == password } != null
private fun canAuthenticate(email: String, password: String): Boolean = users.values.find { it.email == email && it.password == password } != null
fun canNotAuthenticate(email: String, password: String): Boolean = !canAuthenticate(email, password)
fun allUsers(rows: Int = 10, start: Int = 0): List<User> = users.values.drop(start * rows).take(rows)

Expand Down
29 changes: 0 additions & 29 deletions src/main/kotlin/blog/route/Login.kt

This file was deleted.

Loading

0 comments on commit 9d9d768

Please sign in to comment.