Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch to persistent CloudKit database #208

Merged
merged 2 commits into from
May 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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