Skip to content

Commit

Permalink
Addig user management to ZeServer
Browse files Browse the repository at this point in the history
Secret server token and header needed for admin. Otherwise you have to know / guess the user uuid
  • Loading branch information
mariobodemann authored and Mario Bodemann committed Jun 25, 2024
1 parent 165c74c commit 57c7ed4
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 1 deletion.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -559,4 +559,6 @@ obj/
# End of https://www.toptal.com/developers/gitignore/api/python,android,androidstudio,intellij+all,windows,linux,macos,sublimetext,java,kotlin,circuitpython
/zeapp/app/release/output-metadata.json
/zeapp/app/debug/output-metadata.json
/zeapp/versions.properties
i/zeapp/versions.properties

*.db
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ package de.berlindroid.zekompanion.server
import de.berlindroid.zekompanion.server.routers.imageBin
import de.berlindroid.zekompanion.server.routers.imagePng
import de.berlindroid.zekompanion.server.routers.index
import de.berlindroid.zekompanion.server.routers.adminCreateUser
import de.berlindroid.zekompanion.server.routers.adminDeleteUser
import de.berlindroid.zekompanion.server.routers.adminListUsers
import de.berlindroid.zekompanion.server.routers.getUser
import de.berlindroid.zekompanion.server.routers.updateUser
import de.berlindroid.zekompanion.server.user.UserRepository
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
Expand All @@ -20,6 +26,8 @@ private const val LOCAL_TLS_PORT = 8443
private const val SSL_PASSWORD_ENV = "SSL_CERTIFICATE_PASSWORD"
private const val KEYSTORE_RESOURCE_FILE = "/tmp/keystore.jks"

private const val USER_DB_FILE = "/tmp/user.db"


fun main(args: Array<String>) {
val keyPassword = try {
Expand All @@ -32,6 +40,8 @@ fun main(args: Array<String>) {
val serverPort = extractServerPort(args, keyStore)
println("Serving on port $serverPort.")

val users = UserRepository.load()

embeddedServer(
Tomcat,
environment = applicationEngineEnvironment {
Expand All @@ -45,10 +55,22 @@ fun main(args: Array<String>) {
routing {
staticResources("/", "static") {
index()
exclude { file ->
file.path.endsWith("db")
}
}

imageBin()
imagePng()

// Callable from ZeFlasher only?
adminCreateUser(users)
adminListUsers(users)
adminDeleteUser(users)

// TODO: Check if callable from ZeBadge (no ssl)
updateUser(users)
getUser(users)
}
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package de.berlindroid.zekompanion.server.routers

import de.berlindroid.zekompanion.server.user.User
import de.berlindroid.zekompanion.server.user.UserRepository
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.call
import io.ktor.server.request.header
import io.ktor.server.request.receiveNullable
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
import io.ktor.server.routing.Route
import io.ktor.server.routing.delete
import io.ktor.server.routing.get
import io.ktor.server.routing.post
import io.ktor.server.routing.put
import io.ktor.util.pipeline.PipelineContext


suspend fun PipelineContext<Unit, ApplicationCall>.withParameter(
key: String,
block: suspend PipelineContext<Unit, ApplicationCall>.(value: String) -> Unit,
) {
if (call.parameters.contains(key)) {
val value = call.parameters[key]
if (value != null) {
block(value)
}
}
}

suspend fun PipelineContext<Unit, ApplicationCall>.ifAuthorized(block: suspend PipelineContext<Unit, ApplicationCall>.() -> Unit) {
val authHeader = call.request.header("ZeAuth")
val authEnv = System.getenv("ZESERVER_AUTH_TOKEN")
if (authEnv.isEmpty() || authHeader == null || authEnv != authHeader) {
call.respondText(status = HttpStatusCode.Forbidden, text = "Forbidden")
} else {
block()
}
}

fun Route.adminCreateUser(users: UserRepository) =
post("/api/user/") {
runCatching {
ifAuthorized {
val newUser = call.receiveNullable<User>() ?: throw IllegalArgumentException("No user payload found.")
val uuidAdded = users.createUser(newUser)

if (uuidAdded != null) {
call.respond(status = HttpStatusCode.Created, users.getUser(uuidAdded)!!)
} else {
call.respondText("invalid", status = HttpStatusCode.Forbidden)
}

}
}.onFailure {
it.printStackTrace()
call.respondText("Error: ${it.message}")
}
}

fun Route.adminListUsers(users: UserRepository) =
get("/api/user/") {
runCatching {
ifAuthorized {
call.respond(status = HttpStatusCode.OK, users.getUsers())
}
}.onFailure {
it.printStackTrace()
call.respondText("Error: ${it.message}")
}
}

fun Route.adminDeleteUser(users: UserRepository) =
delete("/api/user/{UUID}") {
runCatching {
ifAuthorized {
withParameter("UUID") { uuid ->
call.respond(status = HttpStatusCode.OK, users.deleteUser(uuid))
}
}
}.onFailure {
it.printStackTrace()
call.respondText("Error: ${it.message}")
}
}

fun Route.updateUser(users: UserRepository) =
put("/api/user/{UUID}") {
runCatching {
withParameter("UUID") { uuid ->
val newUser = call.receiveNullable<User>() ?: throw IllegalArgumentException("No user payload found.")
val userUpdated = users.updateUser(newUser.copy(uuid = uuid))

if (userUpdated) {
call.respondText(text = "OK")
} else {
call.respondText("invalid", status = HttpStatusCode.NotAcceptable)
}
}
}.onFailure {
it.printStackTrace()
call.respondText("Error: ${it.message}", status = HttpStatusCode.NotAcceptable)
}
}

fun Route.getUser(users: UserRepository) =
get("/api/user/{UUID}") {
runCatching {
withParameter("UUID") { uuid ->
val user = users.getUser(uuid)
if (user != null) {
call.respond(status = HttpStatusCode.OK, user)
} else {
call.respondText(status = HttpStatusCode.NotFound, text = "Not Found.")
}
}
call.respondText(status = HttpStatusCode.UnprocessableEntity, text = "No UUID.")
}.onFailure {
it.printStackTrace()
call.respondText("Error: ${it.message}")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package de.berlindroid.zekompanion.server.user

import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.File
import java.io.FileNotFoundException
import java.util.UUID

private const val DB_FILENAME = "./user.db"

@Serializable
data class User(
val name: String? = null,
val iconUrl: String? = null,
val uuid: String? = null,
)

class UserRepository private constructor(
private val users: MutableList<User> = mutableListOf(),
) {
companion object {
fun load(): UserRepository = try {
UserRepository(
users = Json.decodeFromString(File(DB_FILENAME).readText()),
)
} catch (notFound: FileNotFoundException) {
UserRepository()
}

fun save(repo: UserRepository) = File(DB_FILENAME).writer().use {
it.write(Json.encodeToString(repo.users))
}
}

fun createUser(user: User): String? {
val existingUser = users.find { it.uuid == user.uuid }
if (existingUser != null || user.uuid != null) {
return null
}

val uuid = UUID.randomUUID().toString()
users.add(user.copy(uuid = uuid))

save(this)

return uuid
}

fun getUser(uuid: String): User? {
return users.find { it.uuid == uuid }
}

fun getUsers(): List<User> {
return users.toList()
}

fun updateUser(newUser: User): Boolean {
val index = users.indexOfFirst { it.uuid == newUser.uuid }
if (index < 0) {
return false
}

if (newUser.uuid == null) {
return false
} else {
users[index] = newUser
}

save(this)

return true
}

fun deleteUser(uuid: String): Boolean {
val user = users.find { it.uuid == uuid }
if (user == null) {
return false
}

users.remove(user)

save(this)

return true
}
}

0 comments on commit 57c7ed4

Please sign in to comment.