Skip to content

Commit

Permalink
bits of shuffling and minor improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
jvorhauer committed Feb 26, 2024
1 parent b894698 commit 905a8aa
Show file tree
Hide file tree
Showing 20 changed files with 312 additions and 93 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ dependencies {

testImplementation("com.typesafe.akka:akka-persistence-testkit_2.13:2.9.1")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:1.9.22")
testImplementation("org.assertj:assertj-core:3.11.1")
testImplementation("org.assertj:assertj-core:3.25.2")
testImplementation(platform("org.junit:junit-bom:5.10.2"))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("io.ktor:ktor-server-tests-jvm")
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/blog/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ 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.config.Konfig
import blog.model.loginRoute
import blog.model.notesRoute
import blog.model.tasksRoute
Expand Down
19 changes: 19 additions & 0 deletions src/main/kotlin/blog/config/Konfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package blog.config

data class Jwt(
val secret: String,
val audience: String,
val realm: String,
val issuer: String,
val expiresIn: Long = 1000L * 60L * 60L * 24L
)

data class Server(
val port: Int,
val host: String
)

data class Konfig(
val jwt: Jwt,
val server: Server
)
27 changes: 17 additions & 10 deletions src/main/kotlin/blog/model/Note.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package blog.model

import java.time.ZoneId
import java.time.ZonedDateTime
import akka.Done
import akka.actor.typed.ActorRef
Expand All @@ -15,14 +14,15 @@ import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.coroutines.future.await
import blog.config.Konfig
import blog.read.Reader

data class CreateNoteRequest(val title: String, val body: String) {
fun toCommand(user: String, replyTo: ActorRef<StatusReply<NoteResponse>>) = CreateNote(user, title.encode(), body.encode(), replyTo)
fun toCommand(user: String, replyTo: ActorRef<StatusReply<NoteResponse>>) = CreateNote(user, title.encode, body.encode, replyTo)
}

data class UpdateNoteRequest(val id: String, val title: String?, val body: String?) {
fun toCommand(user: String, rt: ActorRef<StatusReply<NoteResponse>>) = UpdateNote(user, id, title.mencode(), body.mencode(), rt)
fun toCommand(user: String, rt: ActorRef<StatusReply<NoteResponse>>) = UpdateNote(user, id, title.mencode, body.mencode, rt)
}

data class CreateNote(
Expand All @@ -44,7 +44,7 @@ data class DeleteNote(val id: String, val rt: ActorRef<StatusReply<Done>>): Comm
}

data class NoteCreated(val id: String, val user: String, val title: String, val body: String) : Event {
fun toEntity() = Note(id, user, title, slugify(title), body)
fun toEntity() = Note(id, user, title, title.slug, body)
fun toResponse() = this.toEntity().toResponse()
}

Expand All @@ -58,17 +58,24 @@ data class Note(
val title: String,
val slug: String,
val body: String,
val created: ZonedDateTime = TSID.from(id).instant.atZone(ZoneId.of("CET")),
val created: ZonedDateTime = TSID.from(id).instant.atZone(CET),
val updated: ZonedDateTime = znow()
): Entity {
constructor(id: String, user: String, title: String, body: String): this(id, user, title, slugify(title), body)
fun update(nu: NoteUpdated): Note = this.copy(title = nu.title ?: this.title, slug = slugify(nu.title ?: this.title), body = nu.body ?: this.body)
fun toResponse() = NoteResponse(id, user, DTF.format(created), title, slug, body)
constructor(id: String, user: String, title: String, body: String): this(id, user, title, title.slug, body)
fun update(nu: NoteUpdated): Note = this.copy(
title = nu.title ?: this.title, slug = nu.title?.slug ?: this.slug, body = nu.body ?: this.body, updated = znow()
)
fun toResponse() = NoteResponse(id, user, DTF.format(created), DTF.format(updated), title, slug, body)

override fun equals(other: Any?): Boolean = equals(this, other)
override fun hashCode(): Int = id.hashCode()
}

data class NoteResponse(
val id: String,
val user: String,
val created: String,
val updated: String,
val title: String,
val slug: String,
val body: String
Expand All @@ -79,7 +86,7 @@ fun Route.notesRoute(processor: ActorRef<Command>, reader: Reader, scheduler: Sc
route("/api/notes") {
post {
val cnr = call.receive<CreateNoteRequest>()
val userId = user(call) ?: return@post call.respond(HttpStatusCode.Unauthorized, "Unauthorized")
val userId = userIdFromJWT(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}")
Expand All @@ -101,7 +108,7 @@ fun Route.notesRoute(processor: ActorRef<Command>, reader: Reader, scheduler: Sc
}
put {
val unr = call.receive<UpdateNoteRequest>()
val userId = user(call) ?: return@put call.respond(HttpStatusCode.Unauthorized, "Unauthorized")
val userId = userIdFromJWT(call) ?: return@put call.respond(HttpStatusCode.Unauthorized, "Unauthorized")
AskPattern.ask(processor, { rt -> unr.toCommand(userId, rt) }, timeout, scheduler).await().let {
when {
it.isSuccess -> call.respond(HttpStatusCode.OK, it.value)
Expand Down
8 changes: 4 additions & 4 deletions src/main/kotlin/blog/model/State.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package blog.model
import java.io.Serializable

data class State(
private val users: Map<String, User> = mapOf(),
private val notes: Map<String, Note> = mapOf(),
private val tasks: Map<String, Task> = mapOf(),
private val tags : Map<String, Tag> = mapOf(),
private val users: Map<String, User> = HashMap(9),
private val notes: Map<String, Note> = HashMap(9),
private val tasks: Map<String, Task> = HashMap(9),
private val tags : Map<String, Tag> = HashMap(9),
private val recovered: Boolean = false
): Serializable {
fun save(u: User) : State = this.copy(users = this.users.minus(u.id).plus(u.id to u))
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/blog/model/Tag.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ data class Tag(override val id: String, val label: String): Entity {
fun toResponse() = TagResponse(id, label)
}

data class CreateTagReq(val label: String) {
data class CreateTagRequest(val label: String) {
fun toCommand(replyTo: ActorRef<StatusReply<TagResponse>>) = CreateTag(nextId(), label, replyTo)
}

Expand Down
29 changes: 15 additions & 14 deletions src/main/kotlin/blog/model/Task.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package blog.model

import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
import akka.Done
import akka.actor.typed.ActorRef
Expand All @@ -16,6 +15,7 @@ import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.coroutines.future.await
import blog.config.Konfig
import blog.read.Reader

enum class TaskStatus {
Expand All @@ -31,28 +31,28 @@ data class Task(
val due: LocalDateTime,
val status: TaskStatus = TaskStatus.TODO,
val private: Boolean = true,
val created: ZonedDateTime = TSID.from(id).instant.atZone(ZoneId.of("CET"))
val created: ZonedDateTime = TSID.from(id).instant.atZone(CET),
val updated: ZonedDateTime = znow()
) : Entity {
fun update(tu: TaskUpdated): Task = this.copy(
title = tu.title ?: this.title,
slug = slugify(tu.title ?: this.title),
slug = tu.title?.slug ?: this.slug,
body = tu.body ?: this.body,
due = tu.due ?: this.due,
status = tu.status ?: this.status
status = tu.status ?: this.status,
updated = znow()
)
fun toResponse() = TaskResponse(id,DTF.format(created), user, title, body, DTF.format(due), status.name)

companion object {
val clazz = Task::class
}
fun toResponse() = TaskResponse(id, created.fmt, updated.fmt, user, title, body, due.fmt, status.name)
override fun equals(other: Any?): Boolean = equals(this, other)
override fun hashCode(): Int = id.hashCode()
}

data class CreateTaskRequest(val title: String, val body: String, val due: LocalDateTime): Request {
fun toCommand(user: String, replyTo: ActorRef<StatusReply<TaskResponse>>) = CreateTask(user, title.encode(), body.encode(), due, replyTo)
fun toCommand(user: String, replyTo: ActorRef<StatusReply<TaskResponse>>) = CreateTask(user, title.encode, body.encode, due, replyTo)
}

data class UpdateTaskRequest(val id: String, val title: String?, val body: String?, val due: LocalDateTime?, val status: TaskStatus?): Request {
fun toCommand(user: String, replyTo: ActorRef<StatusReply<TaskResponse>>) = UpdateTask(user, id, title.mencode(), body.mencode(), due, status, replyTo)
fun toCommand(user: String, replyTo: ActorRef<StatusReply<TaskResponse>>) = UpdateTask(user, id, title.mencode, body.mencode, due, status, replyTo)
}


Expand Down Expand Up @@ -91,7 +91,7 @@ data class TaskCreated(
val body: String,
val due: LocalDateTime
) : Event {
fun toEntity() = Task(id, user, title, slugify(title), body, due)
fun toEntity() = Task(id, user, title, title.slug, body, due)
fun toResponse() = toEntity().toResponse()
}

Expand All @@ -110,6 +110,7 @@ data class TaskDeleted(val id: String): Event
data class TaskResponse(
val id: String,
val created: String,
val updated: String,
val user: String,
val title: String,
val body: String,
Expand All @@ -122,7 +123,7 @@ fun Route.tasksRoute(processor: ActorRef<Command>, reader: Reader, scheduler: Sc
route("/api/tasks") {
post {
val ctr = call.receive<CreateTaskRequest>()
val userId = user(call) ?: return@post call.respond(HttpStatusCode.Unauthorized, "Unauthorized")
val userId = userIdFromJWT(call) ?: return@post call.respond(HttpStatusCode.Unauthorized, "Unauthorized")
ask(processor, { rt -> ctr.toCommand(userId, rt) }, timeout, scheduler).await().let {
when {
it.isSuccess -> {
Expand All @@ -146,7 +147,7 @@ fun Route.tasksRoute(processor: ActorRef<Command>, reader: Reader, scheduler: Sc
}
put {
val utr = call.receive<UpdateTaskRequest>()
val userId = user(call) ?: return@put call.respond(HttpStatusCode.Unauthorized, "Unauthorized")
val userId = userIdFromJWT(call) ?: return@put call.respond(HttpStatusCode.Unauthorized, "Unauthorized")
ask(processor, { rt -> utr.toCommand(userId, rt) }, timeout, scheduler).await().let {
when {
it.isSuccess -> call.respond(HttpStatusCode.OK, it.value)
Expand Down
60 changes: 49 additions & 11 deletions src/main/kotlin/blog/model/User.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package blog.model

import java.time.Duration
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
import akka.actor.typed.ActorRef
import akka.actor.typed.Scheduler
Expand All @@ -21,6 +19,7 @@ import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.coroutines.future.await
import blog.config.Konfig
import blog.read.Reader

data class RegisterUserRequest(
Expand All @@ -31,7 +30,15 @@ data class RegisterUserRequest(
fun toCommand(replyTo: ActorRef<StatusReply<User>>): CreateUser = CreateUser(this, replyTo)
}

data class LoginRequest(val username: String, val password: String)
data class UpdateUserRequest(
val name: String?,
val password: String?
): Request {
fun toCommand(id: String, replyTo: ActorRef<StatusReply<User>>) = UpdateUser(id, name, password, replyTo)
}

data class LoginRequest(val username: String, val password: String): Request

// Commands

data class CreateUser(
Expand All @@ -43,7 +50,16 @@ data class CreateUser(
) : Command {
constructor(rur: RegisterUserRequest, replyTo: ActorRef<StatusReply<User>>) : this(rur.email, rur.name, rur.password, replyTo)

fun toEvent() = UserCreated(id, email, name.encode(), password.hashed())
fun toEvent() = UserCreated(id, email, name.encode, password.hashed)
}

data class UpdateUser(
val id: String,
val name: String?,
val password: String?,
val replyTo: ActorRef<StatusReply<User>>,
): Command {
fun toEvent() = UserUpdated(id, name, password?.hashed)
}

// Events
Expand All @@ -57,8 +73,13 @@ data class UserCreated(
fun toEntity(): User = User(id, email, name, password)
}

data class UserUpdated(
val id: String,
val name: String?,
val password: String?
): Event

data class UserDeleted(val id: String) : Event
data class UserDeletedByEmail(val email: String) : Event

// Entitites

Expand All @@ -67,8 +88,9 @@ data class User(
val email: String,
val name: String,
val password: String,
val gravatar: String = gravatarize(email),
val joined: ZonedDateTime = TSID.from(id).instant.atZone(ZoneId.of("CET"))
val gravatar: String = email.gravatar,
val joined: ZonedDateTime = TSID.from(id).instant.atZone(CET),
val updated: ZonedDateTime = znow()
) : Entity {
fun toResponse(reader: Reader): UserResponse = UserResponse(
id,
Expand All @@ -79,6 +101,11 @@ data class User(
reader.findNotesForUser(id).map(Note::toResponse),
reader.findTasksForUser(id).map(Task::toResponse)
)
fun update(uu: UserUpdated): User = this.copy(
name = uu.name ?: this.name, password = uu.password ?: this.password, updated = znow()
)
override fun hashCode(): Int = id.hashCode()
override fun equals(other: Any?): Boolean = equals(this, other)
}

// Responses
Expand Down Expand Up @@ -120,18 +147,29 @@ fun Route.usersRoute(processor: ActorRef<Command>, reader: Reader, scheduler: Sc
}
authenticate(kfg.jwt.realm) {
get("/tasks") {
val userId = user(call) ?: return@get call.respondText("Unauthorized", status = Unauthorized)
val userId = userIdFromJWT(call) ?: return@get call.respondText("Unauthorized", status = Unauthorized)
call.respond(reader.findTasksForUser(userId).map { it.toResponse() })
}
get("/me") {
val id = user(call) ?: return@get call.respondText("Unauthorized", status = Unauthorized)
val id = userIdFromJWT(call) ?: return@get call.respondText("Unauthorized", status = Unauthorized)
val user = reader.findUser(id) ?: return@get call.respondText("user not found for $id", status = NotFound)
call.respond(user.toResponse(reader))
}
get("/notes") {
val userId = user(call) ?: return@get call.respond(Unauthorized, "Unauthorized")
val userId = userIdFromJWT(call) ?: return@get call.respond(Unauthorized, "Unauthorized")
call.respond(reader.findNotesForUser(userId).sortedBy { it.created }.map { it.toResponse() })
}
put {
val userId = userIdFromJWT(call) ?: return@put call.respond(Unauthorized, "Unauthorized")
val uur = call.receive<UpdateUserRequest>()
ask(processor, { rt -> uur.toCommand(userId, rt) }, timeout, scheduler).await().let {
if (it.isSuccess) {
call.respond(it.value.toResponse(reader))
} else {
call.respondText(it.error.message ?: "unknown error", status = BadRequest)
}
}
}
}
get("{id?}") {
val id = call.parameters["id"] ?: return@get call.respondText("no user id specified", status = BadRequest)
Expand All @@ -140,7 +178,7 @@ fun Route.usersRoute(processor: ActorRef<Command>, reader: Reader, scheduler: Sc
}
post {
val cnu = call.receive<RegisterUserRequest>()
ask(processor, { rt -> cnu.toCommand(rt) }, Duration.ofMinutes(1), scheduler).await().let {
ask(processor, { rt -> cnu.toCommand(rt) }, timeout, scheduler).await().let {
if (it.isSuccess) {
call.response.header("Location", "/api/users/${it.value.id}")
call.respond(HttpStatusCode.Created, hashMapOf("token" to createJwtToken(it.value.id, kfg)))
Expand Down
Loading

0 comments on commit 905a8aa

Please sign in to comment.