Skip to content

Commit

Permalink
Switch to persistent CloudKit database (#208)
Browse files Browse the repository at this point in the history
Co-authored-by: hfhbd <[email protected]>
  • Loading branch information
hfhbd and hfhbd committed May 18, 2021
1 parent e60875f commit c619a2e
Show file tree
Hide file tree
Showing 20 changed files with 226 additions and 227 deletions.
16 changes: 4 additions & 12 deletions backend/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
kotlin("multiplatform")
kotlin("plugin.serialization")
application
}

Expand Down Expand Up @@ -35,27 +36,18 @@ kotlin {
// Apache 2, https://github.com/hfhbd/RateLimit/releases/latest
implementation("app.softwork:ratelimit:0.0.8")

// Apache 2, https://github.com/JetBrains/Exposed/releases/latest
val exposedVersion = "0.31.1"
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
// todo: kotlin-time
implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion")

// Apache 2, https://github.com/hfhbd/kotlinx-uuid/releases
implementation("app.softwork:kotlinx-uuid-exposed-jvm:0.0.5")
// Apache 2, https://github.com/hfhbd/cloudkitclient/releases/latest
implementation("app.softwork:cloudkitclient-core:0.0.7")

// EPL 1.0, https://github.com/qos-ch/logback/releases
runtimeOnly("ch.qos.logback:logback-classic:1.2.3")
// MPL 2.0 or EPL 1.0, https://github.com/h2database/h2database/releases/latest
runtimeOnly("com.h2database:h2:1.4.200")
}
}
val jvmTest by getting {
dependencies {
implementation(kotlin("test-junit"))
implementation("io.ktor:ktor-server-test-host:$ktorVersion")
implementation("app.softwork:cloudkitclient-testing:0.0.7")
}
}
}
Expand Down
52 changes: 44 additions & 8 deletions backend/src/jvmMain/kotlin/app/softwork/composetodo/DTO.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,52 @@
package app.softwork.composetodo

import app.softwork.composetodo.dao.Todo
import app.softwork.composetodo.dao.User
import kotlinx.datetime.toKotlinLocalDateTime
import app.softwork.cloudkitclient.values.*
import app.softwork.composetodo.dao.*
import kotlinx.uuid.*

fun Todo.toDTO() = app.softwork.composetodo.dto.Todo(
id = id.value,
title = title,
until = until?.toKotlinLocalDateTime(),
finished = finished
id = recordName.toUUID(),
title = fields.title.value,
until = fields.until?.value,
finished = fields.finished.value.toBoolean(),
recordChangeTag = recordChangeTag
)

fun app.softwork.composetodo.dto.Todo.toDAO(user: User) = Todo(
recordName = id.toString(),
fields = Todo.Fields(
title = Value.String(title),
finished = Value.String(finished.toString()),
until = until?.let { Value.DateTime(it) },
user = Value.Reference(user)
),
recordChangeTag = recordChangeTag
)

fun User.toDTO() = app.softwork.composetodo.dto.User(
id = id.value, firstName = firstName, lastName = lastName
username = recordName,
firstName = fields.firstName.value,
lastName = fields.lastName.value,
recordChangeTag = recordChangeTag!!
)

fun app.softwork.composetodo.dto.User.New.toDAO() = User(
recordName = username,

fields = User.Fields(
firstName = Value.String(firstName),
lastName = Value.String(lastName),
password = Value.String(password)
),
recordChangeTag = null
)

fun app.softwork.composetodo.dto.User.toDAO() = User(
recordName = username,
fields = User.Fields(
firstName = Value.String(firstName),
lastName = Value.String(lastName),
password = null
),
recordChangeTag = recordChangeTag
)
27 changes: 14 additions & 13 deletions backend/src/jvmMain/kotlin/app/softwork/composetodo/JWTVerifier.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import com.auth0.jwt.impl.*
import io.ktor.auth.jwt.*
import kotlinx.datetime.*
import kotlinx.datetime.Clock
import kotlinx.uuid.*
import kotlin.time.*

@ExperimentalTime
Expand All @@ -24,29 +23,31 @@ data class JWTProvider(
.withIssuer(issuer)
.build()

suspend fun validate(credential: JWTCredential, find: suspend (UUID) -> User?): User? =
suspend fun validate(credential: JWTCredential, find: suspend (String) -> User?): User? =
if (audience in credential.payload.audience) {
credential.payload.subject.toUUIDOrNull()?.let { userID ->
find(userID)
credential.payload.subject?.let { username ->
find(username)
}
} else null

fun token(user: User): Token {
val now = Clock.System.now()
return Token(Token.Payload(
issuer = issuer,
subject = user.id.value,
expiredAt = now + expireDuration,
notBefore = now,
issuedAt = now,
audience = audience
).build(algorithm))
return Token(
Token.Payload(
issuer = issuer,
subject = user.recordName,
expiredAt = now + expireDuration,
notBefore = now,
issuedAt = now,
audience = audience
).build(algorithm)
)
}


private fun Token.Payload.build(algorithm: Algorithm): String = JWT.create()
.withIssuer(issuer)
.withSubject(subject.toString())
.withSubject(subject)
.withExpiresAt(expiredAt)
.withNotBefore(notBefore)
.withIssuedAt(issuedAt)
Expand Down
29 changes: 12 additions & 17 deletions backend/src/jvmMain/kotlin/app/softwork/composetodo/TodoModule.kt
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
package app.softwork.composetodo

import app.softwork.cloudkitclient.*
import app.softwork.composetodo.controller.*
import app.softwork.composetodo.definitions.*
import app.softwork.composetodo.dto.*
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.auth.jwt.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.sessions.*
import kotlinx.uuid.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.*
import kotlin.time.*

@ExperimentalTime
fun Application.TodoModule(db: Database, jwtProvider: JWTProvider) {
fun Application.TodoModule(db: Client.Database, jwtProvider: JWTProvider) {
val userController = UserController(db = db)
val todoController = TodoController(db = db)

Expand Down Expand Up @@ -47,10 +46,6 @@ fun Application.TodoModule(db: Database, jwtProvider: JWTProvider) {
}
}

transaction(db) {
SchemaUtils.create(Users, Todos)
}

routing {
get {
call.respondText { "API is online" }
Expand All @@ -59,15 +54,16 @@ fun Application.TodoModule(db: Database, jwtProvider: JWTProvider) {
post("/users") {
call.respondJson(Token.serializer()) {
val newUser = body(User.New.serializer())
userController.createUser(jwtProvider, newUser)
require(newUser.password == newUser.passwordAgain)
userController.createUser(jwtProvider, newUser.toDAO())
}
}

authenticate("login") {
get("/refreshToken") {
call.respondJson(Token.serializer()) {
val user = call.principal<app.softwork.composetodo.dao.User>()!!
call.sessions.set(RefreshToken(user.id.toString()))
call.sessions.set(RefreshToken(user.recordName))
jwtProvider.token(user)
}
}
Expand All @@ -88,9 +84,8 @@ fun Application.TodoModule(db: Database, jwtProvider: JWTProvider) {
}
put {
call.respondJson(User.serializer()) {
val user = call.principal<app.softwork.composetodo.dao.User>()!!
val toUpdate = body(User.serializer())
userController.update(user, toUpdate)
userController.update(toUpdate.toDAO()).toDTO()
}
}
delete {
Expand All @@ -113,8 +108,8 @@ fun Application.TodoModule(db: Database, jwtProvider: JWTProvider) {
post {
call.respondJson(Todo.serializer()) {
val user = call.principal<app.softwork.composetodo.dao.User>()!!
val newTodo = body(Todo.serializer())
todoController.create(user, newTodo)
val newTodo = body(Todo.serializer()).toDAO(user)
todoController.create(newTodo).toDTO()
}
}

Expand All @@ -123,22 +118,22 @@ fun Application.TodoModule(db: Database, jwtProvider: JWTProvider) {
call.respondJson(Todo.serializer()) {
val user = call.principal<app.softwork.composetodo.dao.User>()!!
val todoID: UUID by parameters
todoController.getTodo(user, todoID)
todoController.getTodo(user, todoID)?.toDTO() ?: throw NotFoundException()
}
}
put {
call.respondJson(Todo.serializer()) {
val user = call.principal<app.softwork.composetodo.dao.User>()!!
val todoID: UUID by parameters
val toUpdate = body(Todo.serializer())
todoController.update(user, todoID, toUpdate)
todoController.update(user, todoID, toUpdate)?.toDTO() ?: throw NotFoundException()
}
}
delete {
with(call) {
val user = call.principal<app.softwork.composetodo.dao.User>()!!
val todoID: UUID by parameters
todoController.delete(user, todoID)
todoController.delete(user, todoID) ?: throw NotFoundException()
respond(HttpStatusCode.OK)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package app.softwork.composetodo.controller

import app.softwork.composetodo.dao.User
import app.softwork.composetodo.toDTO
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import app.softwork.cloudkitclient.*
import app.softwork.composetodo.*
import app.softwork.composetodo.dao.*

object AdminController {
suspend fun allUsers() = newSuspendedTransaction {
User.all().map { it.toDTO() }
}
class AdminController(private val db: Client.Database) {
suspend fun allUsers() = db.query(User).map { it.toDTO() }
}
Original file line number Diff line number Diff line change
@@ -1,53 +1,36 @@
package app.softwork.composetodo.controller

import app.softwork.cloudkitclient.*
import app.softwork.composetodo.*
import app.softwork.composetodo.dao.*
import app.softwork.composetodo.definitions.*
import kotlinx.datetime.*
import kotlinx.uuid.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.experimental.*

class TodoController(private val db: Database) {
suspend fun todos(user: User) = newSuspendedTransaction(db = db) {
user.todos.map { it.toDTO() }
}
class TodoController(private val db: Client.Database) {

suspend fun create(user: User, newTodo: app.softwork.composetodo.dto.Todo) = newSuspendedTransaction(db = db) {
Todo.new(newTodo.id) {
this.user = user
title = newTodo.title
until = newTodo.until?.toJavaLocalDateTime()
finished = newTodo.finished
}.toDTO()
}
suspend fun todos(user: User) = db.query(Todo) {
Todo.Fields::user eq user
}.map { it.toDTO() }

suspend fun getTodo(user: User, todoID: UUID) = newSuspendedTransaction(db = db) {
Todo.find {
Todos.id eq todoID and (Todos.user eq user.id)
}.first().toDTO()
suspend fun create(newTodo: Todo) = db.create(newTodo, Todo)

suspend fun getTodo(user: User, todoID: UUID) = db.read(todoID.toString(), Todo)?.takeIf {
it.fields.user.value.recordName == user.recordName
}

suspend fun delete(user: User, todoID: UUID) = newSuspendedTransaction(db = db) {
Todo.find {
Todos.id eq todoID and (Todos.user eq user.id)
}.first().delete()
suspend fun delete(user: User, todoID: UUID) = getTodo(user, todoID)?.let {
db.delete(it, Todo)
}

suspend fun update(user: User, todoID: UUID, update: app.softwork.composetodo.dto.Todo) =
newSuspendedTransaction(db = db) {
Todo.find {
Todos.id eq todoID and (Todos.user eq user.id)
}.first().apply {
title = update.title
until = update.until?.toJavaLocalDateTime()
finished = update.finished
}.toDTO()
}

suspend fun deleteAll(user: User) = newSuspendedTransaction(db = db) {
user.todos.forEach {
it.delete()
}
getTodo(user, todoID)?.toDTO()?.copy(
title = update.title,
until = update.until,
finished = update.finished
)?.toDAO(user)?.let { db.update(it, Todo) }

suspend fun deleteAll(user: User) = db.query(Todo) {
Todo.Fields::user eq user
}.forEach {
db.delete(it, Todo)
}
}
Loading

0 comments on commit c619a2e

Please sign in to comment.