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": { "data": {
"number": "KA01MU0556" "number": "TN36BA5009"
}, },
"uniqueIdentifier": "KA01MU0556" "uniqueIdentifier": "TN36BA5009"
} }
### create row, with autogenerated identifier ### create row, with autogenerated identifier

View File

@ -6,7 +6,7 @@ plugins {
application application
} }
group = "com.readymixerp" group = "com.basuvaraj"
version = "1.0-SNAPSHOT" version = "1.0-SNAPSHOT"
repositories { repositories {
@ -29,6 +29,7 @@ dependencies {
implementation("org.bitbucket.b_c:jose4j:0.9.3") implementation("org.bitbucket.b_c:jose4j:0.9.3")
implementation("org.slf4j:slf4j-simple:2.0.7") implementation("org.slf4j:slf4j-simple:2.0.7")
implementation("redis.clients:jedis:5.0.2") 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") api ("net.cactusthorn.config:config-core:0.81")
kapt("net.cactusthorn.config:config-compiler:0.81") kapt("net.cactusthorn.config:config-compiler:0.81")
kapt("io.ebean:kotlin-querybean-generator:13.23.2") 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" 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.databind.JsonMappingException
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import com.restapi.config.AppConfig.Companion.appConfig import com.restapi.config.AppConfig.Companion.appConfig
import com.restapi.config.Auth
import com.restapi.config.Auth.getAuthEndpoint import com.restapi.config.Auth.getAuthEndpoint
import com.restapi.config.AuthEndpoint import com.restapi.config.Auth.parseAuthToken
import com.restapi.domain.DataModel import com.restapi.controllers.Entities
import com.restapi.domain.DataNotFoundException 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.database
import com.restapi.domain.Session.findByEntityAndId
import com.restapi.domain.Session.nextUniqId
import com.restapi.domain.Session.objectMapper import com.restapi.domain.Session.objectMapper
import com.restapi.domain.Session.redis import com.restapi.domain.Session.redis
import com.restapi.domain.Session.setAuthorizedUser import com.restapi.domain.Session.setAuthorizedUser
import io.ebean.CallableSql import io.ebean.CallableSql
import io.ebean.DuplicateKeyException import io.ebean.DuplicateKeyException
import io.ebean.RawSqlBuilder
import io.javalin.Javalin import io.javalin.Javalin
import io.javalin.apibuilder.ApiBuilder.* import io.javalin.apibuilder.ApiBuilder.*
import io.javalin.http.* import io.javalin.http.*
import io.javalin.http.util.NaiveRateLimit
import io.javalin.http.util.RateLimitUtil
import io.javalin.json.JavalinJackson import io.javalin.json.JavalinJackson
import io.javalin.security.AccessManager
import io.javalin.security.RouteRole
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.net.URI import java.net.URI
import java.net.URLEncoder import java.net.URLEncoder
@ -32,11 +30,14 @@ import java.net.http.HttpRequest
import java.net.http.HttpRequest.BodyPublishers import java.net.http.HttpRequest.BodyPublishers
import java.net.http.HttpResponse.BodyHandlers import java.net.http.HttpResponse.BodyHandlers
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.time.LocalDateTime import java.util.concurrent.TimeUnit
import kotlin.jvm.optionals.getOrDefault
fun main(args: Array<String>) { fun main(args: Array<String>) {
val logger = LoggerFactory.getLogger("api") val logger = LoggerFactory.getLogger("api")
//ratelimit based on IP Only
RateLimitUtil.keyFunction = { ctx -> ctx.header("X-Forwarded-For")?.split(",")?.get(0) ?: ctx.ip() }
Javalin Javalin
.create { cfg -> .create { cfg ->
cfg.http.generateEtags = true cfg.http.generateEtags = true
@ -52,7 +53,8 @@ fun main(args: Array<String>) {
} }
cfg.http.defaultContentType = ContentType.JSON cfg.http.defaultContentType = ContentType.JSON
cfg.compression.gzipOnly() cfg.compression.gzipOnly()
cfg.jsonMapper(JavalinJackson(Session.objectMapper)) cfg.jsonMapper(JavalinJackson(objectMapper))
cfg.accessManager(AppAccessManager())
} }
.routes { .routes {
@ -95,110 +97,49 @@ fun main(args: Array<String>) {
} }
} }
before("/api/*") { ctx -> before("/api/*") { ctx ->
//validate, auth token //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 //allow only alpha, numeric, hypen, underscore, dot in paths
val regex = Regex("^[a-zA-Z0-9\\-_\\.]+$") val regex = Regex("^[a-zA-Z0-9\\-_\\.]+$")
ctx.path().split("/").dropWhile { it.isEmpty() } ctx.path().split("/")
.dropWhile { it.isEmpty() }
.forEach { .forEach {
if (!it.matches(regex)) { if (!it.matches(regex)) {
throw IllegalArgumentException() throw IllegalArgumentException()
} }
} }
val at = ctx.header("Authorization")?.replace("Bearer ", "")?.replace("Bearer: ", "")?.trim() val authToken = ctx.header("Authorization")?.replace("Bearer ", "")
?: throw UnauthorizedResponse() ?.replace("Bearer: ", "")
val pt = Auth.parseAuthToken(authToken = at) ?.trim() ?: throw UnauthorizedResponse()
setAuthorizedUser(pt) logger.warn("authToken = $authToken")
setAuthorizedUser(parseAuthToken(authToken = authToken))
} }
path("/api") { path("/api") {
post("/execute/{name}") { post("/execute/{name}", Entities::executeStoredProcedure, Roles(Role.DbOps))
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"))
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( put("/{entity}/approve/{id}", Entities::approve, Roles(Role.Standard(Action.APPROVE)))
database.find(DataModel::class.java) put("/{entity}/reject/{id}", Entities::reject, Roles(Role.Standard(Action.APPROVE)))
.setRawSql( put("/{entity}/{action}/{id}", Entities::action, Roles(Role.Entity))
RawSqlBuilder.parse(querySql).create()
).apply {
sql.params.forEach { (t, u) ->
setParameter(t, u)
}
}
.findList()
)
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)))
} }
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()
}
}
} }
.exception(DuplicateKeyException::class.java) { _, ctx -> .exception(DuplicateKeyException::class.java) { _, ctx ->
ctx.json( ctx.json(
@ -231,10 +172,19 @@ fun main(args: Array<String>) {
.start(appConfig.portNumber()) .start(appConfig.portNumber())
} }
data class Query(
val sql: String, enum class Action {
val params: Map<String, Any> 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 { private fun getFormDataAsString(formData: Map<String, String>): String {
val formBodyBuilder = StringBuilder() val formBodyBuilder = StringBuilder()
@ -249,4 +199,3 @@ private fun getFormDataAsString(formData: Map<String, String>): String {
return formBodyBuilder.toString() 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") @Key("app.cache.redis_uri")
fun redisUri(): Optional<String> fun redisUri(): Optional<String>
@Key("app.network.rate_limit")
fun rateLimit(): Optional<Int>
companion object { companion object {
val appConfig: AppConfig = ConfigFactory.builder().build().create(AppConfig::class.java) 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 currentUser() = currentUser.get().userName
fun currentTenant() = currentUser.get().tenant
fun Database.findByEntityAndId(entity: String, id: String): DataModel { fun Database.findByEntityAndId(entity: String, id: String): DataModel {
return find(DataModel::class.java) 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.WhoCreated
import io.ebean.annotation.WhoModified import io.ebean.annotation.WhoModified
import java.time.LocalDateTime import java.time.LocalDateTime
import javax.persistence.Entity import javax.persistence.*
import javax.persistence.GeneratedValue
import javax.persistence.Id
import javax.persistence.MappedSuperclass
import javax.persistence.Version
data class Comments(val text: String = "", val by: String = "", val at: LocalDateTime = LocalDateTime.now()) 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() 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 @Entity
@Index(unique = true, name = "entity_unique_id", columnNames = ["entity_name", "unique_identifier", "tenant_id"]) @Index(unique = true, name = "entity_unique_id", columnNames = ["entity_name", "unique_identifier", "tenant_id"])
open class DataModel : BaseModel() { 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)
}
}