bolts/src/main/kotlin/Main.kt

165 lines
7.9 KiB
Kotlin

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<EntityDefinition>.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)
}
}
}
}