From 57c7ed4523510a82f3397bcfb8f0e29d7cf552c6 Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Tue, 25 Jun 2024 12:22:46 +0200 Subject: [PATCH] Addig user management to ZeServer Secret server token and header needed for admin. Otherwise you have to know / guess the user uuid --- .gitignore | 4 +- .../de/berlindroid/zekompanion/server/Main.kt | 22 ++++ .../zekompanion/server/routers/UserRouter.kt | 123 ++++++++++++++++++ .../zekompanion/server/user/UserRepository.kt | 87 +++++++++++++ 4 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt create mode 100644 zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt diff --git a/.gitignore b/.gitignore index dc8e5e70..5693d901 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt index fd2ea587..28e4a565 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt @@ -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.* @@ -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) { val keyPassword = try { @@ -32,6 +40,8 @@ fun main(args: Array) { val serverPort = extractServerPort(args, keyStore) println("Serving on port $serverPort.") + val users = UserRepository.load() + embeddedServer( Tomcat, environment = applicationEngineEnvironment { @@ -45,10 +55,22 @@ fun main(args: Array) { 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) } } }, diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt new file mode 100644 index 00000000..bebe64d8 --- /dev/null +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt @@ -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.withParameter( + key: String, + block: suspend PipelineContext.(value: String) -> Unit, +) { + if (call.parameters.contains(key)) { + val value = call.parameters[key] + if (value != null) { + block(value) + } + } +} + +suspend fun PipelineContext.ifAuthorized(block: suspend PipelineContext.() -> 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() ?: 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() ?: 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}") + } + } diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt new file mode 100644 index 00000000..216c3833 --- /dev/null +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt @@ -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 = 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 { + 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 + } +}