Skip to content

Commit

Permalink
Switch to Compose Web (#181)
Browse files Browse the repository at this point in the history
* Implement authentication

* Switch to Compose Web

* Switch to forked UUID library

Co-authored-by: hfhbd <[email protected]>
  • Loading branch information
hfhbd and hfhbd authored May 4, 2021
1 parent 86cd440 commit 11c3208
Show file tree
Hide file tree
Showing 37 changed files with 1,067 additions and 540 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@ Sample app to play with Jetpack Compose and Kotlin Multiplatform.

- Android using Jetpack Compose
- Desktop using Jetpack Compose
- Web using React
- Backend using Ktor
- Web using Jetpack Compose Web
- Backend using Ktor

## Authentication

The used authentication is for demo purpose only. Never use this implementation for a productive application.
7 changes: 3 additions & 4 deletions backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ kotlin {
val jvmMain by getting {
dependencies {
implementation("io.ktor:ktor-server-cio:$ktorVersion")
implementation("io.ktor:ktor-auth:$ktorVersion")
implementation("io.ktor:ktor-auth-jwt:$ktorVersion")

// Apache 2, https://github.com/hfhbd/RateLimit/releases/latest
implementation("app.softwork:ratelimit:0.0.8")
Expand All @@ -43,9 +43,8 @@ kotlin {
// todo: kotlin-time
implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion")

// Apache 2, https://github.com/cy6erGn0m/kotlinx-uuid/releases
implementation("org.jetbrains.kotlinx.experimental:ktor-server-uuid-jvm:0.0.3")
implementation("org.jetbrains.kotlinx.experimental:exposed-uuid-jvm:0.0.3")
// Apache 2, https://github.com/hfhbd/kotlinx-uuid/releases
implementation("app.softwork:kotlinx-uuid-exposed-jvm:0.0.4")

// EPL 1.0, https://github.com/qos-ch/logback/releases
runtimeOnly("ch.qos.logback:logback-classic:1.2.3")
Expand Down
69 changes: 69 additions & 0 deletions backend/src/jvmMain/kotlin/app/softwork/composetodo/JWTVerifier.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package app.softwork.composetodo

import app.softwork.composetodo.controller.*
import app.softwork.composetodo.dao.*
import app.softwork.composetodo.dao.User
import app.softwork.composetodo.dto.*
import com.auth0.jwt.*
import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.*
import com.auth0.jwt.impl.*
import com.auth0.jwt.interfaces.*
import io.ktor.auth.*
import io.ktor.auth.jwt.*
import kotlinx.datetime.*
import kotlinx.datetime.Clock
import kotlinx.uuid.*
import kotlin.time.*

@ExperimentalTime
data class JWTProvider(
val algorithm: Algorithm,
val issuer: String,
val audience: String,
val expireDuration: Duration
) {
val verifier: JWTVerifier = JWT
.require(algorithm)
.withAudience(audience)
.withIssuer(issuer)
.build()

suspend fun validate(credential: JWTCredential): User? =
if (audience in credential.payload.audience) {
credential.payload.subject.toUUIDOrNull()?.let { userID ->
UserController.find(userID)
}
} 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))
}


fun Token.Payload.build(algorithm: Algorithm): String = JWT.create()
.withIssuer(issuer)
.withSubject(subject.toString())
.withExpiresAt(expiredAt)
.withNotBefore(notBefore)
.withIssuedAt(issuedAt)
.withAudience(audience)
.sign(algorithm)

fun JWTCreator.Builder.withIssuedAt(createdAt: Instant) =
withClaim(PublicClaims.ISSUED_AT, createdAt.epochSeconds)

fun JWTCreator.Builder.withExpiresAt(expireAt: Instant) =
withClaim(PublicClaims.EXPIRES_AT, expireAt.epochSeconds)

fun JWTCreator.Builder.withNotBefore(notBefore: Instant) =
withClaim(PublicClaims.NOT_BEFORE, notBefore.epochSeconds)
}
22 changes: 15 additions & 7 deletions backend/src/jvmMain/kotlin/app/softwork/composetodo/Ktor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,26 @@ import io.ktor.http.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.util.*
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
import kotlinx.uuid.UUID
import kotlin.reflect.KProperty
import kotlinx.serialization.*
import kotlinx.serialization.builtins.*
import kotlinx.serialization.json.*
import kotlinx.uuid.*
import kotlin.reflect.*

suspend fun <T> ApplicationCall.respondJson(serializer: KSerializer<T>, json: Json = Json, data: suspend ApplicationCall.() -> T) =
suspend fun <T> ApplicationCall.respondJson(
serializer: KSerializer<T>,
json: Json = Json,
data: suspend ApplicationCall.() -> T
) =
respondText(contentType = ContentType.Application.Json) {
json.encodeToString(serializer, data())
}

suspend fun <T> ApplicationCall.respondJsonList(serializer: KSerializer<T>, json: Json = Json, data: suspend ApplicationCall.() -> List<T>) =
suspend fun <T> ApplicationCall.respondJsonList(
serializer: KSerializer<T>,
json: Json = Json,
data: suspend ApplicationCall.() -> List<T>
) =
respondText(contentType = ContentType.Application.Json) {
json.encodeToString(ListSerializer(serializer), data())
}
Expand Down
133 changes: 88 additions & 45 deletions backend/src/jvmMain/kotlin/app/softwork/composetodo/TodoModule.kt
Original file line number Diff line number Diff line change
@@ -1,24 +1,50 @@
package app.softwork.composetodo

import app.softwork.composetodo.controller.TodoController
import app.softwork.composetodo.controller.UserController
import app.softwork.composetodo.definitions.Todos
import app.softwork.composetodo.definitions.Users
import app.softwork.composetodo.dto.Todo
import app.softwork.composetodo.dto.User
import app.softwork.composetodo.controller.*
import app.softwork.composetodo.definitions.*
import app.softwork.composetodo.dto.*
import com.auth0.jwt.*
import com.auth0.jwt.algorithms.*
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 kotlinx.uuid.UUID
import kotlinx.uuid.ktor.uuid
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
import io.ktor.sessions.*
import kotlinx.datetime.*
import kotlinx.uuid.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.*
import kotlin.time.*

fun Application.TodoModule() {
install(DataConversion) {
uuid()
@ExperimentalTime
fun Application.TodoModule(db: Database, jwtProvider: JWTProvider) {

install(Authentication) {
basic("login") {
validate { (name, password) ->
UserController.findBy(name, password)
}
}
jwt {
verifier {
jwtProvider.verifier
}
validate { credentials ->
jwtProvider.validate(credentials)
}
}
}

install(Sessions) {
cookie<RefreshToken>("SESSION") {
cookie.path = "/refreshToken"
cookie.httpOnly = true
cookie.extensions["SameSite"] = "strict"
cookie.maxAgeInSeconds = 1.days.inSeconds.toLong()
}
}

transaction {
Expand All @@ -29,72 +55,89 @@ fun Application.TodoModule() {
get {
call.respondText { "API is online" }
}
route("/users") {
post {
call.respondJson(User.serializer()) {
val newUser = body(User.serializer())
UserController.createUser(newUser)

post("/users") {
call.respondJson(Token.serializer()) {
val newUser = body(User.New.serializer())
UserController.createUser(jwtProvider, newUser)
}
}

authenticate("login") {
get("/refreshToken") {
call.respondJson(Token.serializer()) {
val user = call.principal<app.softwork.composetodo.dao.User>()!!
call.sessions.set(RefreshToken(user.id.toString()))
jwtProvider.token(user)
}
}

route("/{userID}") {
get {
call.respondJson(User.serializer()) {
val userID: UUID by parameters
UserController(userID).getUser()
authenticate {
route("/refreshToken") {
delete {
call.sessions.clear<RefreshToken>()
call.respond(HttpStatusCode.OK)
}
}
put {
call.respondJson(User.serializer()) {
val userID: UUID by parameters
val toUpdate = body(User.serializer())
UserController(userID).update(toUpdate)

route("/me") {
get {
call.respondJson(User.serializer()) {
call.principal<app.softwork.composetodo.dao.User>()!!.toDTO()
}
}
}
delete {
with(call) {
val userID: UUID by parameters
UserController(userID).delete()
respond(HttpStatusCode.OK)
put {
call.respondJson(User.serializer()) {
val user = call.principal<app.softwork.composetodo.dao.User>()!!
val toUpdate = body(User.serializer())
UserController(user).update(toUpdate)
}
}
delete {
with(call) {
val user = call.principal<app.softwork.composetodo.dao.User>()!!
TodoController(user).deleteAll()
UserController(user).delete()
respond(HttpStatusCode.OK)
}
}
}

route("/todos") {
get {
call.respondJsonList(Todo.serializer()) {
val userID: UUID by parameters
TodoController(userID).todos()
val user = call.principal<app.softwork.composetodo.dao.User>()!!
TodoController(user).todos()
}
}
post {
call.respondJson(Todo.serializer()) {
val userID: UUID by parameters
val user = call.principal<app.softwork.composetodo.dao.User>()!!
val newTodo = body(Todo.serializer())
TodoController(userID).create(newTodo)
TodoController(user).create(newTodo)
}
}

route("/todoID") {
get("/{todoID}") {
call.respondJson(Todo.serializer()) {
val userID: UUID by parameters
val user = call.principal<app.softwork.composetodo.dao.User>()!!
val todoID: UUID by parameters
TodoController(userID).getTodo(todoID)
TodoController(user).getTodo(todoID)
}
}
put {
call.respondJson(Todo.serializer()) {
val userID: UUID by parameters
val user = call.principal<app.softwork.composetodo.dao.User>()!!
val todoID: UUID by parameters
val toUpdate = body(Todo.serializer())
TodoController(userID).update(todoID, toUpdate)
TodoController(user).update(todoID, toUpdate)
}
}
delete {
with(call) {
val userID: UUID by parameters
val user = call.principal<app.softwork.composetodo.dao.User>()!!
val todoID: UUID by parameters
TodoController(userID).delete(todoID)
TodoController(user).delete(todoID)
respond(HttpStatusCode.OK)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,19 @@
package app.softwork.composetodo.controller

import app.softwork.composetodo.toDTO
import app.softwork.composetodo.dao.Todo
import app.softwork.composetodo.dao.User
import kotlinx.datetime.toJavaLocalDateTime
import kotlinx.uuid.UUID
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction

class TodoController(userID: UUID) {
private val user = suspend { User[userID] }
private val todo: suspend (UUID) -> Todo = { id ->
user().todos.first { todo ->
todo.id.value == id
}
}
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(val user: User) {
suspend fun todos() = newSuspendedTransaction {
user().todos.map { it.toDTO() }
user.todos.map { it.toDTO() }
}

suspend fun create(newTodo: app.softwork.composetodo.dto.Todo) = newSuspendedTransaction {
val user = user()
Todo.new(newTodo.id) {
this.user = user
title = newTodo.title
Expand All @@ -30,19 +23,31 @@ class TodoController(userID: UUID) {
}

suspend fun getTodo(todoID: UUID) = newSuspendedTransaction {
todo(todoID).toDTO()
Todo.find {
Todos.id eq todoID and (Todos.user eq user.id)
}.first().toDTO()
}

suspend fun delete(todoID: UUID) = newSuspendedTransaction {
todo(todoID).delete()
Todo.find {
Todos.id eq todoID and (Todos.user eq user.id)
}.first().delete()
}

suspend fun update(todoID: UUID, update: app.softwork.composetodo.dto.Todo) =
newSuspendedTransaction {
todo(todoID).apply {
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() = newSuspendedTransaction {
user.todos.forEach {
it.delete()
}
}
}
Loading

0 comments on commit 11c3208

Please sign in to comment.