165 lines
7.9 KiB
Kotlin
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)
|
|
}
|
|
}
|
|
}
|
|
}
|