import dev.mduchene.bolts.persistence.Database import dev.mduchene.bolts.persistence.DatabaseConfig import dev.mduchene.bolts.entity.EntityDefinition import dev.mduchene.bolts.entity.EntityDefinitionRepository import dev.mduchene.bolts.entity.EntityField import dev.mduchene.bolts.entity.FieldType import dev.mduchene.bolts.user.LoginService import dev.mduchene.bolts.user.SessionRepository import dev.mduchene.bolts.user.UserRepository import io.javalin.Javalin import io.javalin.http.HttpStatus fun main() { val database = Database(DatabaseConfig.fromEnvironment()) database.initialize() val users = UserRepository(database) val loginService = LoginService(users, SessionRepository(database)) val entities = EntityDefinitionRepository(database) loginService.initializeAdmin() Javalin.create { config -> config.routes.apply { get("/") { ctx -> ctx.result("Hello World") } post("/api/login") { ctx -> val username = ctx.formParam("username").orEmpty() val password = ctx.formParam("password").orEmpty() val session = loginService.authenticate(username, password) if (session != null) { ctx.contentType("application/json") .result( """{"username":"${session.user.username.toJsonString()}","role":"${session.user.role.toJsonString()}","token":"${session.token}"}""", ) } else { ctx.status(HttpStatus.UNAUTHORIZED) .contentType("application/json") .result("""{"message":"Invalid username or password"}""") } } get("/api/users") { ctx -> val token = ctx.header("Authorization")?.removePrefix("Bearer ") val currentUser = loginService.userForToken(token) if (currentUser?.role != "admin") { ctx.status(HttpStatus.FORBIDDEN) .contentType("application/json") .result("""{"message":"Admin access required"}""") return@get } val result = users.findAll().joinToString(prefix = "[", postfix = "]") { user -> """{"id":${user.id},"username":"${user.username.toJsonString()}","role":"${user.role.toJsonString()}"}""" } ctx.contentType("application/json").result(result) } get("/api/entity-definitions") { ctx -> if (!ctx.requireAdmin(loginService)) return@get ctx.contentType("application/json").result(entities.findAll().toJson()) } post("/api/entity-definitions") { ctx -> if (!ctx.requireAdmin(loginService)) return@post val name = ctx.requiredFormParam("name") ?: return@post ctx.status(HttpStatus.CREATED).contentType("application/json") .result(entities.create(name).toJson()) } patch("/api/entity-definitions/{entityId}") { ctx -> if (!ctx.requireAdmin(loginService)) return@patch val id = ctx.pathParam("entityId").toLongOrNull() val name = ctx.requiredFormParam("name") if (id == null || name == null) return@patch val entity = entities.update(id, name) if (entity == null) ctx.notFound() else ctx.contentType("application/json").result(entity.toJson()) } delete("/api/entity-definitions/{entityId}") { ctx -> if (!ctx.requireAdmin(loginService)) return@delete val id = ctx.pathParam("entityId").toLongOrNull() if (id == null || !entities.delete(id)) ctx.notFound() else ctx.status(HttpStatus.NO_CONTENT) } post("/api/entity-definitions/{entityId}/fields") { ctx -> if (!ctx.requireAdmin(loginService)) return@post val entityId = ctx.pathParam("entityId").toLongOrNull() val name = ctx.requiredFormParam("name") val type = ctx.formParam("type")?.let(FieldType::from) if (entityId == null || name == null || type == null) { if (type == null) ctx.badRequest("A valid field type is required") return@post } val field = entities.createField(entityId, name, type) if (field == null) ctx.notFound() else { ctx.status(HttpStatus.CREATED).contentType("application/json").result(field.toJson()) } } patch("/api/entity-definitions/{entityId}/fields/{fieldId}") { ctx -> if (!ctx.requireAdmin(loginService)) return@patch val entityId = ctx.pathParam("entityId").toLongOrNull() val fieldId = ctx.pathParam("fieldId").toLongOrNull() val name = ctx.requiredFormParam("name") val type = ctx.formParam("type")?.let(FieldType::from) if (entityId == null || fieldId == null || name == null || type == null) { if (type == null) ctx.badRequest("A valid field type is required") return@patch } val field = entities.updateField(entityId, fieldId, name, type) if (field == null) ctx.notFound() else ctx.contentType("application/json").result(field.toJson()) } delete("/api/entity-definitions/{entityId}/fields/{fieldId}") { ctx -> if (!ctx.requireAdmin(loginService)) return@delete val entityId = ctx.pathParam("entityId").toLongOrNull() val fieldId = ctx.pathParam("fieldId").toLongOrNull() if (entityId == null || fieldId == null || !entities.deleteField(entityId, fieldId)) { ctx.notFound() } else { ctx.status(HttpStatus.NO_CONTENT) } } } }.start(System.getenv("SERVER_PORT")?.toIntOrNull() ?: 7070) } private fun io.javalin.http.Context.requireAdmin(loginService: LoginService): Boolean { val token = header("Authorization")?.removePrefix("Bearer ") if (loginService.userForToken(token)?.role == "admin") return true status(HttpStatus.FORBIDDEN).contentType("application/json").result("""{"message":"Admin access required"}""") return false } private fun io.javalin.http.Context.requiredFormParam(name: String): String? { val value = formParam(name)?.trim()?.takeIf(String::isNotEmpty) if (value == null) badRequest("$name is required") return value } private fun io.javalin.http.Context.badRequest(message: String) { status(HttpStatus.BAD_REQUEST).contentType("application/json") .result("""{"message":"${message.toJsonString()}"}""") } private fun io.javalin.http.Context.notFound() { status(HttpStatus.NOT_FOUND).contentType("application/json").result("""{"message":"Not found"}""") } private fun List.toJson() = joinToString(prefix = "[", postfix = "]") { it.toJson() } private fun EntityDefinition.toJson() = """{"id":$id,"name":"${name.toJsonString()}","fields":${fields.joinToString(prefix = "[", postfix = "]") { it.toJson() }}}""" private fun EntityField.toJson() = """{"id":$id,"name":"${name.toJsonString()}","type":"$type"}""" private fun String.toJsonString() = buildString { for (character in this@toJsonString) { when (character) { '\\' -> append("\\\\") '"' -> append("\\\"") '\b' -> append("\\b") '\u000C' -> append("\\f") '\n' -> append("\\n") '\r' -> append("\\r") '\t' -> append("\\t") else -> if (character.code < 0x20) { append("\\u%04x".format(character.code)) } else { append(character) } } } }