more stuff

This commit is contained in:
gowthaman.b 2023-11-11 11:38:58 +05:30
parent 1e3d9aa1f1
commit dc0a59fcf2
11 changed files with 335 additions and 109 deletions

View File

@ -5,9 +5,9 @@ Authorization: set-auth-token
{
"data": {
"number": "KA01MU0556"
"number": "TN36BA5009"
},
"uniqueIdentifier": "KA01MU0556"
"uniqueIdentifier": "TN36BA5009"
}
### create row, with autogenerated identifier

View File

@ -6,7 +6,7 @@ plugins {
application
}
group = "com.readymixerp"
group = "com.basuvaraj"
version = "1.0-SNAPSHOT"
repositories {
@ -29,6 +29,7 @@ dependencies {
implementation("org.bitbucket.b_c:jose4j:0.9.3")
implementation("org.slf4j:slf4j-simple:2.0.7")
implementation("redis.clients:jedis:5.0.2")
implementation("org.jetbrains.kotlin:kotlin-scripting-jsr223:1.9.0")
api ("net.cactusthorn.config:config-core:0.81")
kapt("net.cactusthorn.config:config-compiler:0.81")
kapt("io.ebean:kotlin-querybean-generator:13.23.2")

View File

@ -9,4 +9,4 @@ plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
}
rootProject.name = "rmc_modules_api"
rootProject.name = "rest_api"

View File

@ -0,0 +1,15 @@
package com.restapi
import io.javalin.http.Context
import io.javalin.http.Handler
import io.javalin.security.AccessManager
import io.javalin.security.RouteRole
import org.slf4j.LoggerFactory
class AppAccessManager : AccessManager {
private val logger = LoggerFactory.getLogger("Access")
override fun manage(handler: Handler, ctx: Context, routeRoles: Set<RouteRole>) {
logger.warn("access {}, {}", ctx.pathParamMap(), routeRoles)
handler.handle(ctx)
}
}

View File

@ -4,26 +4,24 @@ import AuthTokenResponse
import com.fasterxml.jackson.databind.JsonMappingException
import com.fasterxml.jackson.module.kotlin.readValue
import com.restapi.config.AppConfig.Companion.appConfig
import com.restapi.config.Auth
import com.restapi.config.Auth.getAuthEndpoint
import com.restapi.config.AuthEndpoint
import com.restapi.domain.DataModel
import com.restapi.config.Auth.parseAuthToken
import com.restapi.controllers.Entities
import com.restapi.domain.DataNotFoundException
import com.restapi.domain.Session
import com.restapi.domain.Session.creatSeq
import com.restapi.domain.Session.database
import com.restapi.domain.Session.findByEntityAndId
import com.restapi.domain.Session.nextUniqId
import com.restapi.domain.Session.objectMapper
import com.restapi.domain.Session.redis
import com.restapi.domain.Session.setAuthorizedUser
import io.ebean.CallableSql
import io.ebean.DuplicateKeyException
import io.ebean.RawSqlBuilder
import io.javalin.Javalin
import io.javalin.apibuilder.ApiBuilder.*
import io.javalin.http.*
import io.javalin.http.util.NaiveRateLimit
import io.javalin.http.util.RateLimitUtil
import io.javalin.json.JavalinJackson
import io.javalin.security.AccessManager
import io.javalin.security.RouteRole
import org.slf4j.LoggerFactory
import java.net.URI
import java.net.URLEncoder
@ -32,11 +30,14 @@ import java.net.http.HttpRequest
import java.net.http.HttpRequest.BodyPublishers
import java.net.http.HttpResponse.BodyHandlers
import java.nio.charset.StandardCharsets
import java.time.LocalDateTime
import java.util.concurrent.TimeUnit
import kotlin.jvm.optionals.getOrDefault
fun main(args: Array<String>) {
val logger = LoggerFactory.getLogger("api")
//ratelimit based on IP Only
RateLimitUtil.keyFunction = { ctx -> ctx.header("X-Forwarded-For")?.split(",")?.get(0) ?: ctx.ip() }
Javalin
.create { cfg ->
cfg.http.generateEtags = true
@ -52,7 +53,8 @@ fun main(args: Array<String>) {
}
cfg.http.defaultContentType = ContentType.JSON
cfg.compression.gzipOnly()
cfg.jsonMapper(JavalinJackson(Session.objectMapper))
cfg.jsonMapper(JavalinJackson(objectMapper))
cfg.accessManager(AppAccessManager())
}
.routes {
@ -95,110 +97,49 @@ fun main(args: Array<String>) {
}
}
before("/api/*") { ctx ->
//validate, auth token
NaiveRateLimit.requestPerTimeUnit(ctx, appConfig.rateLimit().getOrDefault(30), TimeUnit.MINUTES) // throws if rate limit is exceeded
//allow only alpha, numeric, hypen, underscore, dot in paths
val regex = Regex("^[a-zA-Z0-9\\-_\\.]+$")
ctx.path().split("/").dropWhile { it.isEmpty() }
ctx.path().split("/")
.dropWhile { it.isEmpty() }
.forEach {
if (!it.matches(regex)) {
throw IllegalArgumentException()
}
}
val at = ctx.header("Authorization")?.replace("Bearer ", "")?.replace("Bearer: ", "")?.trim()
?: throw UnauthorizedResponse()
val pt = Auth.parseAuthToken(authToken = at)
val authToken = ctx.header("Authorization")?.replace("Bearer ", "")
?.replace("Bearer: ", "")
?.trim() ?: throw UnauthorizedResponse()
setAuthorizedUser(pt)
logger.warn("authToken = $authToken")
setAuthorizedUser(parseAuthToken(authToken = authToken))
}
path("/api") {
post("/execute/{name}") {
val name = it.pathParam("name")
val params = it.bodyAsClass<Map<String, Any>>()
val placeholders = (0..params.entries.size).joinToString(",") { "?" }
val sql = "{call $name($placeholders)}"
val cs: CallableSql = database.createCallableSql(sql)
params.entries.forEachIndexed { index, entry ->
cs.setParameter(index + 1, entry.value)
}
database.execute(cs)
}
get("/{entity}/{id}") {
it.json(
database.findByEntityAndId(it.pathParam("entity"), it.pathParam("id"))
)
}
post("/{entity}/query/{id}") {
val sql = it.bodyAsClass<Query>()
val query = database.findByEntityAndId(it.pathParam("entity"), it.pathParam("id"))
post("/execute/{name}", Entities::executeStoredProcedure, Roles(Role.DbOps))
val querySql = query.data["sql"] as String? ?: throw NotFoundResponse()
get("/{entity}/{id}", Entities::view, Roles(Role.Standard(Action.VIEW)))
post("/{entity}/query/{id}", Entities::sqlQueryId, Roles(Role.Standard(Action.VIEW)))
post("/{entity}/query", Entities::sqlQueryRaw, Roles(Role.Standard(Action.VIEW)))
post("/{entity}", Entities::create, Roles(Role.Standard(Action.CREATE)))
it.json(
database.find(DataModel::class.java)
.setRawSql(
RawSqlBuilder.parse(querySql).create()
).apply {
sql.params.forEach { (t, u) ->
setParameter(t, u)
}
}
.findList()
)
put("/{entity}/approve/{id}", Entities::approve, Roles(Role.Standard(Action.APPROVE)))
put("/{entity}/reject/{id}", Entities::reject, Roles(Role.Standard(Action.APPROVE)))
put("/{entity}/{action}/{id}", Entities::action, Roles(Role.Entity))
}
post("/{entity}/query") {
val sql = it.bodyAsClass<Query>()
it.json(
database.find(DataModel::class.java)
.setRawSql(
RawSqlBuilder.parse(sql.sql).create()
).apply {
sql.params.forEach { (t, u) ->
setParameter(t, u)
}
}
.findList()
)
}
post("/{entity}") {
val entity = it.pathParam("entity")
val seqCreated = creatSeq(entity)
logger.debug("sequence created for $entity? = $seqCreated")
database.save(
it.bodyAsClass<DataModel>().apply {
this.entityName = entity
if (this.uniqueIdentifier.isEmpty()) {
this.uniqueIdentifier = nextUniqId(entity)
}
}
)
}
put("/{entity}/{id}") {
val e = database.findByEntityAndId(it.pathParam("entity"), it.pathParam("id"))
val newData = it.bodyAsClass<Map<String, Any>>()
e.data.putAll(newData)
e.update()
}
patch("/{entity}/{id}") {
val e = database.findByEntityAndId(it.pathParam("entity"), it.pathParam("id"))
val pv = it.bodyAsClass<PatchValue>()
e.data[pv.key] = pv.value;
e.update()
}
delete("/{entity}/{id}") {
val id = it.pathParam("id")
val e = database.findByEntityAndId(it.pathParam("entity"), it.pathParam("id"))
e.deletedBy = Session.currentUser()
e.deletedOn = LocalDateTime.now()
e.update()
e.delete()
}
put("/{entity}/{id}", Entities::update, Roles(Role.Standard(Action.UPDATE)))
patch("/{entity}/{id}", Entities::patch, Roles(Role.Standard(Action.UPDATE)))
delete("/{entity}/{id}", Entities::delete, Roles(Role.Standard(Action.DELETE)))
}
}
.exception(DuplicateKeyException::class.java) { _, ctx ->
ctx.json(
@ -231,10 +172,19 @@ fun main(args: Array<String>) {
.start(appConfig.portNumber())
}
data class Query(
val sql: String,
val params: Map<String, Any>
)
enum class Action {
CREATE, VIEW, UPDATE, DELETE, APPROVE
}
sealed class Role {
open class Standard(vararg val action: Action) : Role()
data object Entity : Role()
data object DbOps : Role()
}
open class Roles(vararg val roles: Role) : RouteRole
private fun getFormDataAsString(formData: Map<String, String>): String {
val formBodyBuilder = StringBuilder()
@ -249,4 +199,3 @@ private fun getFormDataAsString(formData: Map<String, String>): String {
return formBodyBuilder.toString()
}
data class PatchValue(val key: String, val value: Any)

View File

@ -63,6 +63,9 @@ interface AppConfig {
@Key("app.cache.redis_uri")
fun redisUri(): Optional<String>
@Key("app.network.rate_limit")
fun rateLimit(): Optional<Int>
companion object {
val appConfig: AppConfig = ConfigFactory.builder().build().create(AppConfig::class.java)
}

View File

@ -0,0 +1,119 @@
package com.restapi.controllers
import com.restapi.domain.DataModel
import com.restapi.domain.Session
import com.restapi.domain.Session.database
import com.restapi.domain.Session.findByEntityAndId
import io.ebean.CallableSql
import io.ebean.RawSqlBuilder
import io.javalin.http.Context
import io.javalin.http.NotFoundResponse
import io.javalin.http.bodyAsClass
import org.slf4j.LoggerFactory
import java.time.LocalDateTime
data class PatchValue(val key: String, val value: Any)
data class Query(
val sql: String,
val params: Map<String, Any>
)
object Entities {
private val logger = LoggerFactory.getLogger("Entities")
fun delete(ctx: Context) {
val e = database.findByEntityAndId(ctx.pathParam("entity"), ctx.pathParam("id"))
e.deletedBy = Session.currentUser()
e.deletedOn = LocalDateTime.now()
e.update()
e.delete()
}
fun patch(ctx: Context) {
val e = database.findByEntityAndId(ctx.pathParam("entity"), ctx.pathParam("id"))
val pv = ctx.bodyAsClass<PatchValue>()
e.data[pv.key] = pv.value;
e.update()
}
fun update(ctx: Context) {
val e = database.findByEntityAndId(ctx.pathParam("entity"), ctx.pathParam("id"))
val newData = ctx.bodyAsClass<Map<String, Any>>()
e.data.putAll(newData)
e.update()
}
fun action(ctx: Context) {}
fun approve(ctx: Context) {}
fun reject(ctx: Context) {}
fun executeStoredProcedure(ctx: Context) {
val name = ctx.pathParam("name")
val params = ctx.bodyAsClass<Map<String, Any>>()
val placeholders = (0..params.entries.size + 1).joinToString(",") { "?" }
val sql = "{call $name($placeholders)}"
val cs: CallableSql = database.createCallableSql(sql)
params.entries.forEachIndexed { index, entry ->
cs.setParameter(index + 1, entry.value)
}
cs.setParameter(params.entries.size + 1, Session.currentTenant())
database.execute(cs)
}
fun sqlQueryRaw(ctx: Context) {
val sql = ctx.bodyAsClass<Query>()
ctx.json(
database.find(DataModel::class.java)
.setRawSql(
RawSqlBuilder.parse(sql.sql).create()
).apply {
sql.params.forEach { (t, u) ->
setParameter(t, u)
}
}
.findList()
)
}
fun sqlQueryId(ctx: Context) {
val sql = ctx.bodyAsClass<Query>()
val query = database.findByEntityAndId(ctx.pathParam("entity"), ctx.pathParam("id"))
val querySql = query.data["sql"] as String? ?: throw NotFoundResponse()
ctx.json(
database.find(DataModel::class.java)
.setRawSql(
RawSqlBuilder.parse(querySql).create()
).apply {
sql.params.forEach { (t, u) ->
setParameter(t, u)
}
}
.findList()
)
}
fun view(it: Context) {
it.json(
database.findByEntityAndId(it.pathParam("entity"), it.pathParam("id"))
)
}
fun create(ctx: Context) {
val entity = ctx.pathParam("entity")
val seqCreated = Session.creatSeq(entity)
logger.debug("sequence created for $entity? = $seqCreated")
database.save(
ctx.bodyAsClass<DataModel>().apply {
this.entityName = entity
if (this.uniqueIdentifier.isEmpty()) {
this.uniqueIdentifier = Session.nextUniqId(entity)
}
}
)
}
}

View File

@ -46,6 +46,7 @@ object Session {
}
fun currentUser() = currentUser.get().userName
fun currentTenant() = currentUser.get().tenant
fun Database.findByEntityAndId(entity: String, id: String): DataModel {
return find(DataModel::class.java)

View File

@ -16,11 +16,7 @@ import io.ebean.annotation.WhenModified
import io.ebean.annotation.WhoCreated
import io.ebean.annotation.WhoModified
import java.time.LocalDateTime
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id
import javax.persistence.MappedSuperclass
import javax.persistence.Version
import javax.persistence.*
data class Comments(val text: String = "", val by: String = "", val at: LocalDateTime = LocalDateTime.now())
@ -62,6 +58,95 @@ abstract class BaseModel : Model() {
var comments: MutableList<Comments> = arrayListOf()
}
@Entity
open class TenantModel : BaseModel() {
var name: String = ""
var domain: String = ""
var mobile: List<String> = emptyList()
var emails: List<String> = emptyList()
@DbJsonB
var preferences: MutableMap<String, Any> = hashMapOf()
}
enum class AuditType {
CREATE, UPDATE, DELETE, VIEW
}
open class AuditLog : BaseModel() {
var auditType: AuditType = AuditType.CREATE
var entity: String = ""
var uniqueIdentifier: String = ""
@DbJsonB
@Index(definition = "create index audit_log_values_idx on audit_log using GIN (data) ", platforms = [Platform.POSTGRES])
var data: Map<String, Any> = hashMapOf()
@DbJsonB
@Index(definition = "create index audit_log_changes_idx on audit_log using GIN (changes) ", platforms = [Platform.POSTGRES])
var changes: Map<String, Any> = hashMapOf()
}
@Entity
open class EntityModel : BaseModel() {
@Index(unique = true)
@JsonDeserialize(using = SafeStringDeserializer::class)
var name: String = ""
//a kts script that will return true/false along with errors before saving
var preSaveScript: String = ""
//a kts script that will do something ... returns void
var postSaveScript: String = ""
//this will create extra actions/roles in keycloak
//the default actions are create, update, view, delete
@DbArray
var actions: List<String> = emptyList()
//allow only these fields, if this is empty, then all fields are allowed
@DbArray
var allowedFields: List<String> = emptyList()
//when an entity is saved/updated audit logs will be populated, when this is empty, all fields are logged
@DbArray
var auditLogFields: List<String> = emptyList()
@DbJsonB
var preferences: MutableMap<String, Any> = hashMapOf()
//if '0' then its auto saved, no approval steps are required, for further steps,
//a user needs to have ROLE_ENTITY_APPROVE_LEVEL1, ROLE_ENTITY_APPROVE_LEVEL2 roles
var approvalLevels: Int = 0
}
enum class JobFrequencyType {
SPECIFIC, EVERY, CRON
}
enum class JobType {
SCRIPT, DB
}
@Entity
open class JobModel : BaseModel() {
@Index(unique = true)
var jobName: String = ""
@Enumerated(EnumType.STRING)
var jobType: JobType = JobType.SCRIPT
var jobPath: String = ""
@DbArray
var tenants: List<String> = emptyList()
@Enumerated(EnumType.STRING)
var jobFrequencyType = JobFrequencyType.EVERY
var frequency: String = "1h"
}
@Entity
@Index(unique = true, name = "entity_unique_id", columnNames = ["entity_name", "unique_identifier", "tenant_id"])
open class DataModel : BaseModel() {

View File

@ -0,0 +1,10 @@
package com.restapi.integ
object Jobs {
fun runJobs(){
//wake up every minute
//see jobs that are to be run and run them
//not very accurate so use for simple jobs,
// sometimes they might fail and will not be attempted to re-run
}
}

View File

@ -0,0 +1,43 @@
package com.restapi.integ
import javax.script.Invocable
import javax.script.ScriptEngineManager
object Scripting {
const val a = "1"
@JvmStatic
fun main(args: Array<String>) {
k()
}
fun k(){
val engine = ScriptEngineManager().getEngineByExtension("kts")!!
val res1 = engine.eval("""
fun fn(x: Int) = x + 2
val obj = object {
fun fn1(x: Int) = x + 3
}
obj""".trimIndent())
println(res1)
val invocator = engine as? Invocable
println(invocator)
try {
println(invocator!!.invokeFunction("fn1", 3))
} catch (e: NoSuchMethodException) {
println(e)
}
val res2 = invocator!!.invokeFunction("fn", 3)
println(res2)
val res3 = invocator.invokeMethod(res1, "fn1", 3)
println(res3)
}
}