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.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.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("", VendorCtrl::create, Roles(Role.Explicit("ROLE_VENDOR_CREATE"))) post("/batch", VendorCtrl::createBatch, Roles(Role.Explicit("ROLE_VENDOR_CREATE"))) get("/{id}", VendorCtrl::get, Roles(Role.Explicit("ROLE_VENDOR_VIEW", "ROLE_VENDOR_CREATE"))) post( "/getAll", VendorCtrl::getAll, Roles(Role.Explicit("ROLE_VENDOR_VIEW", "ROLE_VENDOR_CREATE")) ) get( "quotes/{id}", VendorCtrl::getQuotes, Roles(Role.Explicit("ROLE_QUOTE_VIEW", "ROLE_QUOTE_CREATE", "ROLE_VENDOR_VIEW")) ) get("pos/{id}", VendorCtrl::getPos, Roles(Role.Explicit("ROLE_PO_VIEW", "ROLE_PO_CREATE`"))) put("/rate/{id}/{rating}", VendorCtrl::rate, Roles(Role.Explicit("ROLE_VENDOR_CREATE"))) put("/{id}", VendorCtrl::update, Roles(Role.Explicit("ROLE_VENDOR_CREATE"))) } path("/incoming") { post("", IncomingInventoryCtrl::create, Roles(Role.Explicit("ROLE_INVENTORY_CREATE"))) get("/next", IncomingInventoryCtrl::getNextNum, Roles(Role.Explicit("ROLE_INVENTORY_CREATE"))) get( "/{id}", IncomingInventoryCtrl::get, Roles(Role.Explicit("ROLE_INVENTORY_VIEW", "ROLE_INVENTORY_CREATE")) ) put("/{id}", IncomingInventoryCtrl::update, Roles(Role.Explicit("ROLE_INVENTORY_CREATE"))) post( "/getAll", IncomingInventoryCtrl::getAll, Roles(Role.Explicit("ROLE_INVENTORY_CREATE", "ROLE_INVENTORY_VIEW")) ) } path("/outgoing") { post("", OutgoingInventoryCtrl::create, Roles(Role.Explicit("ROLE_INVENTORY_CREATE"))) get("/next", OutgoingInventoryCtrl::getNextNum, Roles(Role.Explicit("ROLE_INVENTORY_CREATE"))) get( "/{id}", OutgoingInventoryCtrl::get, Roles(Role.Explicit("ROLE_INVENTORY_VIEW", "ROLE_INVENTORY_CREATE")) ) put("/{id}", OutgoingInventoryCtrl::update, Roles(Role.Explicit("ROLE_INVENTORY_CREATE"))) post( "/getAll", OutgoingInventoryCtrl::getAll, Roles(Role.Explicit("ROLE_INVENTORY_CREATE", "ROLE_INVENTORY_VIEW")) ) } path("/invoice") { post("", InvoiceCtrl::create, Roles(Role.Explicit("ROLE_INVOICE_CREATE"))) get("/next", InvoiceCtrl::getNextNum, Roles(Role.Explicit("ROLE_INVOICE_CREATE"))) get( "/{id}", InvoiceCtrl::get, Roles(Role.Explicit("ROLE_INVOICE_VIEW", "ROLE_INVOICE_CREATE")) ) put("/{id}", InvoiceCtrl::update, Roles(Role.Explicit("ROLE_INVOICE_CREATE"))) post( "/getAll", InvoiceCtrl::getAll, Roles(Role.Explicit("ROLE_INVOICE_CREATE", "ROLE_INVOICE_VIEW")) ) } path("/payment") { post("", PaymentCtrl::create, Roles(Role.Explicit("ROLE_PAYMENT_CREATE"))) get( "/{id}", PaymentCtrl::get, Roles(Role.Explicit("ROLE_PAYMENT_VIEW", "ROLE_PAYMENT_CREATE")) ) put("/{id}", PaymentCtrl::update, Roles(Role.Explicit("ROLE_PAYMENT_CREATE"))) post( "/getAll", PaymentCtrl::getAll, Roles(Role.Explicit("ROLE_PAYMENT_CREATE", "ROLE_PAYMENT_VIEW")) ) delete("/{id}", PaymentCtrl::delete, Roles(Role.Explicit("ROLE_PAYMENT_CREATE"))) } path("/po") { get("/next", PurchaseOrderCtrl::getNextNum, Roles(Role.Explicit("ROLE_PO_CREATE"))) post("", PurchaseOrderCtrl::create, Roles(Role.Explicit("ROLE_PO_CREATE"))) post("/batch", PurchaseOrderCtrl::createBatch, Roles(Role.Explicit("ROLE_PO_CREATE"))) post( "/getAll", PurchaseOrderCtrl::getAll, Roles(Role.Explicit("ROLE_PO_CREATE", "ROLE_PO_VIEW", "ROLE_VENDOR_CREATE")) ) get( "/{id}", PurchaseOrderCtrl::get, Roles(Role.Explicit("ROLE_PO_CREATE", "ROLE_PO_VIEW", "ROLE_QUOTE_CREATE")) ) put("/{id}", PurchaseOrderCtrl::update, Roles(Role.Explicit("ROLE_PO_CREATE"))) put("/approve/{id}", PurchaseOrderCtrl::approve, Roles(Role.Explicit())) put("/reject/{id}", PurchaseOrderCtrl::reject, Roles(Role.Explicit())) get("/refQuote/{id}", PurchaseOrderCtrl::quoteReference, Roles(Role.Explicit("ROLE_PO_CREATE"))) } path("/quote") { get("/next", QuotationCtrl::getNextNum, Roles(Role.Explicit("ROLE_QUOTE_CREATE"))) post("", QuotationCtrl::create, Roles(Role.Explicit("ROLE_QUOTE_CREATE"))) post("/batch", QuotationCtrl::createBatch, Roles(Role.Explicit("ROLE_QUOTE_CREATE"))) post( "/getAll", QuotationCtrl::getAll, Roles(Role.Explicit("ROLE_QUOTE_CREATE", "ROLE_QUOTE_VIEW")) ) get("/{id}", QuotationCtrl::get, Roles(Role.Explicit("ROLE_QUOTE_VIEW", "ROLE_QUOTE_CREATE"))) put("/{id}", QuotationCtrl::update, Roles(Role.Explicit("ROLE_QUOTE_CREATE"))) delete("/{id}", QuotationCtrl::delete, Roles(Role.Explicit("ROLE_QUOTE_CREATE"))) } path("/product") { post("", ProductCtrl::create, Roles(Role.Explicit("ROLE_PRODUCT_CREATE"))) put("/{id}", ProductCtrl::update, Roles(Role.Explicit("ROLE_PRODUCT_CREATE"))) delete("/{id}", ProductCtrl::delete, Roles(Role.Explicit("ROLE_PRODUCT_CREATE"))) patch("/{id}", ProductCtrl::patch, Roles(Role.Explicit("ROLE_PRODUCT_CREATE"))) post("/getAll", ProductCtrl::getAll, Roles(Role.Explicit("ROLE_PRODUCT_VIEW"))) get("/{id}", ProductCtrl::get, Roles(Role.Explicit("ROLE_PRODUCT_VIEW"))) } path("/doc") { post("", DocumentCtrl::create, Roles(Role.Explicit("ROLE_DOC_CREATE"))) //why type and refid are clubbed ?? get( "/{type}/{refId}", DocumentCtrl::getWithRefId, Roles(Role.Explicit("ROLE_DOC_VIEW", "ROLE_PRODUCT_CREATE")) ) get("/{id}", DocumentCtrl::get, Roles(Role.Explicit("ROLE_DOC_VIEW", "ROLE_PRODUCT_CREATE"))) get( "/print/{id}", DocumentCtrl::print, Roles(Role.Explicit("ROLE_DOC_CREATE", "ROLE_DOC_VIEW")) ) delete("/{id}", DocumentCtrl::delete, Roles(Role.Explicit("ROLE_DOC_CREATE"))) } path("/reqForQuote") { post( "", RequestForQuote::create, Roles(Role.Explicit("ROLE_QUOTE_CREATE", "ROLE_PO_CREATE", "ROLE_RFQ_CREATE")) ) get( "/{id}", RequestForQuote::get, Roles(Role.Explicit("ROLE_RFQ_CREATE", "ROLE_RFQ_VIEW", "ROLE_QUOTE_VIEW", "ROLE_PO_VIEW")) ) put( "/{id}", RequestForQuote::update, Roles(Role.Explicit("ROLE_QUOTE_CREATE", "ROLE_PO_CREATE", "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()