package com.restapi import com.fasterxml.jackson.databind.JsonMappingException import com.restapi.config.* import com.restapi.config.AppConfig.Companion.appConfig import com.restapi.config.Auth.validateAuthToken import com.restapi.controllers.* import com.restapi.domain.DataNotFoundException import com.restapi.domain.Product import com.restapi.domain.Session import com.restapi.domain.Session.currentTenant import com.restapi.domain.Session.currentUser import com.restapi.domain.Session.objectMapper import com.restapi.domain.Session.setAuthorizedUser import com.restapi.domain.Session.signPayload import io.ebean.DataIntegrityException import io.ebean.DuplicateKeyException import io.javalin.Javalin import io.javalin.apibuilder.ApiBuilder.* import io.javalin.http.ContentType import io.javalin.http.Context import io.javalin.http.UnauthorizedResponse import io.javalin.http.util.NaiveRateLimit import io.javalin.http.util.RateLimitUtil import io.javalin.json.JavalinJackson import org.checkerframework.dataflow.qual.Pure import org.jose4j.jwt.consumer.InvalidJwtException import org.slf4j.LoggerFactory import java.security.MessageDigest import java.time.LocalDateTime import java.util.* import java.util.concurrent.TimeUnit import kotlin.jvm.optionals.getOrDefault fun main(args: Array) { val logger = LoggerFactory.getLogger("api") val adminRole = Role.Standard(Action.ADMIN) val viewRole = Role.Standard(Action.VIEW) val createRole = Role.Standard(Action.CREATE) val updateRole = Role.Standard(Action.UPDATE) val approveOrRejectRole = Role.Standard(Action.APPROVE) //todo, create roles in keycloak based on entity and actions //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 if (appConfig.corsEnabled()) { cfg.plugins.enableCors { container -> container.add { it.allowHost( "http://localhost:5173", *appConfig.corsHosts().toTypedArray() ) } } } cfg.http.defaultContentType = ContentType.JSON cfg.compression.gzipOnly() cfg.jsonMapper(JavalinJackson(objectMapper)) cfg.accessManager(AppAccessManager()) } .routes { path("/auth") { get("/endpoint", Auth::endPoint) get("/init", Auth::init) get("/code", Auth::code) get("/keys", Auth::keys) post("/refresh", Auth::refreshToken) } before("/api/*") { ctx -> NaiveRateLimit.requestPerTimeUnit( ctx, appConfig.rateLimit().getOrDefault(30), TimeUnit.MINUTES ) val authToken = ctx.getAuthHeader() ?: throw UnauthorizedResponse() //there are 2 scenarios, 1) auth user for admin 2) non user for flow, we need to handle both setAuthorizedUser(validateAuthToken(authToken = authToken)) if (appConfig.enforcePayloadEncryption()) { //todo: decrypt the request from user } } after("/api/*") { val md = MessageDigest.getInstance("SHA-512") md.update((it.result() ?: "").toByteArray()) val aMessageDigest = md.digest() val outEncoded: String = Base64.getEncoder().encodeToString(aMessageDigest) it.header("X-Checksum", outEncoded) it.header("X-Signature", signPayload(outEncoded)) if (appConfig.enforcePayloadEncryption()) { //todo: encrypt and send the response back to user } } path("/api") { post("/audit/{action}") { logger.warn("User ${currentUser()} of tenant ${currentTenant()} has performed ${it.pathParam("action")} @ ${LocalDateTime.now()}") it.json(mapOf("status" to true)) } path("/vendor"){ path("/"){ post("", Vendor::create, Roles(Role.Explicit(listOf("ROLE_VENDOR_CREATE", "ROLE_ADMIN")))) get("", Vendor::get, Roles(Role.Explicit(listOf("ROLE_VENDOR_VIEW", "ROLE_VENDOR_CREATE", "ROLE_ADMIN")))) get("quotes/{id}", Vendor::getQuotes, Roles(Role.Explicit(listOf("ROLE_ADMIN", "ROLE_QUOTE_VIEW", "ROLE_QUOTE_CREATE", "ROLE_VENDOR_VIEW")))) get("pos/{id}", Vendor::getPos, Roles(Role.Explicit(listOf("ROLE_ADMIN", "ROLE_PO_VIEW", "ROLE_PO_CREATE`")))) put("/rate/{id}/{rating}", Vendor::rate, Roles(Role.Explicit(listOf("ROLE_VENDOR_CREATE")))) } path("/po"){ post("", PurchaseOrder::create, Roles(Role.Explicit(listOf("ROLE_PO_CREATE", "ROLE_ADMIN")))) get("/{id}", PurchaseOrder::get, Roles(Role.Explicit(listOf("ROLE_PO_CREATE", "ROLE_PO_VIEW", "ROLE_QUOTE_CREATE")))) put("/approve/{id}", PurchaseOrder::approve, Roles(Role.Explicit(listOf("ROLE_ADMIN", "ROLE_APPROVE")))) put("/reject/{id}", PurchaseOrder::reject, Roles(Role.Explicit(listOf("ROLE_ADMIN", "ROLE_APPROVE")))) get("/refQuote/{id}", PurchaseOrder::quoteReference, Roles(Role.Explicit(listOf("ROLE_PO_CREATE", "ROLE_PO_VIEW")))) } path("/quote"){ post("", Quotation::create, Roles(Role.Explicit(listOf("ROLE_QUOTE_CREATE", "ROLE_ADMIN")))) get("/{id}", Quotation::get, Roles(Role.Explicit(listOf("ROLE_QUOTE_VIEW", "ROLE_ADMIN", "ROLE_PO_CREATE", "ROLE_QUOTE_CREATE")))) get("/po/{id}", Quotation::generatePO, Roles(Role.Explicit(listOf("ROLE_ADMIN", "ROLE_PO_CRETE")))) get("/rfq/{rfqNum}", Quotation::reqForQuote, Roles(Role.Explicit(listOf("ROLE_QUOTE_CREATE", "ROLE_QUOTE_VIEW")))) delete("/{id}", Quotation::delete, Roles(Role.Explicit(listOf("ROLE_QUOTE_CREATE", "ROLE_ADMIN")))) } path("/product"){ post("", ProductCtrl::create, Roles(Role.Explicit(listOf("ROLE_PRODUCT_CREATE", "ROLE_ADMIN")))) get("/{id}", ProductCtrl::get, Roles(Role.Explicit(listOf("ROLE_PRODUCT_VIEW", "ROLE_ADMIN")))) put("/{id}", ProductCtrl::update, Roles(Role.Explicit(listOf("ROLE_PRODUCT_UPDATE", "ROLE_ADMIN")))) patch("/{id}", ProductCtrl::patch, Roles(Role.Explicit(listOf("ROLE_PRODUCT_UPDATE", "ROLE_ADMIN")))) delete("/{id}", ProductCtrl::delete, Roles(Role.Explicit(listOf("ROLE_PRODUCT_DELETE", "ROLE_ADMIN")))) get("") {ctx -> ctx.json(Session.database.find(Product::class.java).findList())} } path("/doc"){ post("", Document::create, Roles(Role.Explicit(listOf("ROLE_DOC_CREATE", "ROLE_ADMIN")))) //why type and refid are clubbed ?? get("/{type}/{refId}", Document::getWithRefId, Roles(Role.Explicit(listOf("ROLE_DOC_VIEW", "ROLE_ADMIN", "ROLE_PRODUCT_CREATE")))) get("/{id}", Document::get, Roles(Role.Explicit(listOf("ROLE_DOC_VIEW", "ROLE_ADMIN", "ROLE_PRODUCT_CREATE")))) get("/print/{id}", Document::print, Roles(Role.Explicit(listOf("ROLE_DOC_CREATE", "ROLE_DOC_VIEW")))) delete("/{id}", Document::delete, Roles(Role.Explicit(listOf("ROLE_DOC_CREATE")))) } path("/reqForQuote"){ post("", ReqForQuote::create, Roles(Role.Explicit(listOf("ROLE_RFQ_CREATE")))) get("/{id}", ReqForQuote::get, Roles(Role.Explicit(listOf("ROLE_RFQ_CREATE", "ROLE_RFQ_VIEW")))) put("/{id}", ReqForQuote::update, Roles(Role.Explicit(listOf("ROLE_RFQ_CREATE")))) } } post("/script/database/{name}", Entities::executeStoredProcedure, Roles(adminRole, Role.DbOps)) post("/script/{file}/{name}", Entities::executeScript, Roles(adminRole, Role.DbOps)) get("/{entity}/{id}", Entities::view, Roles(adminRole, viewRole)) post("/{entity}/query/{id}", Entities::sqlQueryById, Roles(adminRole, viewRole)) post("/{entity}/query", Entities::sqlQueryRaw, Roles(adminRole, viewRole)) post("/{entity}", Entities::create, Roles(adminRole, createRole)) put("/{entity}/approve/{id}", Entities::approve, Roles(adminRole, approveOrRejectRole)) put("/{entity}/reject/{id}", Entities::reject, Roles(adminRole, approveOrRejectRole)) put("/{entity}/{action}/{id}", Entities::action, Roles(adminRole, Role.Entity)) put("/{entity}/{id}", Entities::update, Roles(adminRole, updateRole)) patch("/{entity}/{id}", Entities::patch, Roles(adminRole, updateRole)) delete("/{entity}/{id}", Entities::delete, Roles(adminRole, Role.Standard(Action.DELETE))) } } .exception(DuplicateKeyException::class.java, Exceptions.dupKeyExceptionHandler) .exception(DataIntegrityException::class.java, Exceptions.dataIntegrityException) .exception(DataNotFoundException::class.java, Exceptions.dataNotFoundException) .exception(IllegalArgumentException::class.java, Exceptions.illegalArgumentException) .exception(JsonMappingException::class.java, Exceptions.jsonMappingException) .exception(InvalidJwtException::class.java, Exceptions.invalidJwtException) .start(appConfig.portNumber()) } private fun Context.getAuthHeader() = header("Authorization") ?.replace("Bearer ", "") ?.replace("Bearer: ", "") ?.trim()