diff --git a/.idea/misc.xml b/.idea/misc.xml index fd4e49e..0cae666 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -4,10 +4,13 @@ - - - + + + + + + diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 887deb1..f427e6d 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -3,5 +3,6 @@ + \ No newline at end of file diff --git a/src/main/kotlin/com/restapi/Main.kt b/src/main/kotlin/com/restapi/Main.kt index 304f35b..35bd882 100644 --- a/src/main/kotlin/com/restapi/Main.kt +++ b/src/main/kotlin/com/restapi/Main.kt @@ -5,12 +5,15 @@ import com.restapi.config.* import com.restapi.config.AppConfig.Companion.appConfig import com.restapi.config.Auth.validateAuthToken import com.restapi.controllers.Entities +import com.restapi.domain.AnonSession import com.restapi.domain.DataNotFoundException +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 com.restapi.domain.TenantModel import io.ebean.DataIntegrityException import io.ebean.DuplicateKeyException import io.javalin.Javalin @@ -60,6 +63,43 @@ fun main(args: Array) { } .routes { path("/auth") { + get("/session") { + //a simple session to keep track of anon users + val at = it.getAuthHeader() + val tenant = Session.database.find(TenantModel::class.java) + .where() + .eq("domain",it.host()) + .findOne() ?: throw UnauthorizedResponse() + + if(at == null){ + //new session + val s = AnonSession().apply { + sessionId = UUID.randomUUID().toString() + firstSeenAt = LocalDateTime.now() + lastSeenAt = LocalDateTime.now() + tenantId = tenant.name + headerMap = it.headerMap() + } + Session.database.save(s) + it.json(s) + } else { + val s = Session.database.find(AnonSession::class.java) + .where() + .eq("sessionId", at) + .findOne() ?: throw UnauthorizedResponse() + + + Session.database.save( + s.apply { + lastSeenAt = LocalDateTime.now() + headerMap = it.headerMap() + } + ) + + it.json(s) + } + + } get("/endpoint", Auth::endPoint) get("/init", Auth::init) get("/code", Auth::code) @@ -74,10 +114,10 @@ fun main(args: Array) { TimeUnit.MINUTES ) - val authToken = ctx.header("Authorization") - ?.replace("Bearer ", "") - ?.replace("Bearer: ", "") - ?.trim() ?: throw UnauthorizedResponse() + 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)) @@ -96,11 +136,12 @@ fun main(args: Array) { it.header("X-Signature", signPayload(outEncoded)) if (appConfig.enforcePayloadEncryption()) { - //todo:, encrypt and set the response back to user + //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)) @@ -132,3 +173,8 @@ fun main(args: Array) { .start(appConfig.portNumber()) } +private fun Context.getAuthHeader() = header("Authorization") + ?.replace("Bearer ", "") + ?.replace("Bearer: ", "") + ?.trim() + diff --git a/src/main/kotlin/com/restapi/config/AppConfig.kt b/src/main/kotlin/com/restapi/config/AppConfig.kt index 47f7deb..f26b2ca 100644 --- a/src/main/kotlin/com/restapi/config/AppConfig.kt +++ b/src/main/kotlin/com/restapi/config/AppConfig.kt @@ -81,6 +81,21 @@ interface AppConfig { @Key("app.security.public_key") fun publicKey(): Optional + @Key("app.locate.api_key") + fun locateApiKey(): Optional + + @Key("app.s3.url") + fun s3Url(): Optional + + @Key("app.s3.bucket") + fun s3Bucket(): Optional + + @Key("app.s3.access_key") + fun s3AccessKey(): Optional + + @Key("app.s3.secret_key") + fun s3SecretKey(): Optional + companion object { val appConfig: AppConfig = ConfigFactory.builder().build().create(AppConfig::class.java) } diff --git a/src/main/kotlin/com/restapi/config/Auth.kt b/src/main/kotlin/com/restapi/config/Auth.kt index c86be02..07acb5c 100644 --- a/src/main/kotlin/com/restapi/config/Auth.kt +++ b/src/main/kotlin/com/restapi/config/Auth.kt @@ -2,6 +2,7 @@ package com.restapi.config import com.fasterxml.jackson.module.kotlin.readValue import com.restapi.config.AppConfig.Companion.appConfig +import com.restapi.domain.AnonSession import com.restapi.domain.Session import com.restapi.domain.Session.objectMapper import io.javalin.http.BadRequestResponse @@ -74,6 +75,23 @@ object Auth { fun validateAuthToken(authToken: String, skipValidate: Boolean = false): AuthUser { + + //check if this is anon session + val anonSession = Session.database.find(AnonSession::class.java) + .where() + .eq("sessionId", authToken) + .findOne() + + if (anonSession != null) { + return AuthUser( + userName = authToken, + tenant = anonSession.tenantId, + roles = emptyList(), + token = authToken, + expiry = LocalDateTime.now().plusDays(1) + ) + } + // Validate the JWT and process it to the Claims val jwtClaims = if (skipValidate) jwtConsumerSkipValidate.process(authToken) else jwtConsumer.process(authToken) val userId = jwtClaims.jwtClaims.claimsMap["preferred_username"] as String diff --git a/src/main/kotlin/com/restapi/domain/models.kt b/src/main/kotlin/com/restapi/domain/models.kt index 50f5f60..25f230e 100644 --- a/src/main/kotlin/com/restapi/domain/models.kt +++ b/src/main/kotlin/com/restapi/domain/models.kt @@ -5,17 +5,8 @@ import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.annotation.JsonDeserialize import io.ebean.Model -import io.ebean.annotation.DbArray -import io.ebean.annotation.DbDefault -import io.ebean.annotation.DbJsonB +import io.ebean.annotation.* import io.ebean.annotation.Index -import io.ebean.annotation.Platform -import io.ebean.annotation.SoftDelete -import io.ebean.annotation.TenantId -import io.ebean.annotation.WhenCreated -import io.ebean.annotation.WhenModified -import io.ebean.annotation.WhoCreated -import io.ebean.annotation.WhoModified import java.time.LocalDateTime import javax.persistence.* @@ -48,7 +39,6 @@ abstract class BaseModel : Model() { var modifiedAt: LocalDateTime? = null - @WhoCreated var createdBy: String = "" @@ -61,6 +51,7 @@ abstract class BaseModel : Model() { @DbDefault("0") var currentApprovalLevel: Int = 0 + @DbDefault("0") var requiredApprovalLevels: Int = 0 @@ -110,11 +101,17 @@ open class AuditLog : BaseTenantModel() { var uniqueIdentifier: String = "" @DbJsonB - @Index(definition = "create index audit_log_values_idx on audit_log using GIN (data)", platforms = [Platform.POSTGRES]) + @Index( + definition = "create index audit_log_values_idx on audit_log using GIN (data)", + platforms = [Platform.POSTGRES] + ) var data: Map = hashMapOf() @DbJsonB - @Index(definition = "create index audit_log_changes_idx on audit_log using GIN (changes)", platforms = [Platform.POSTGRES]) + @Index( + definition = "create index audit_log_changes_idx on audit_log using GIN (changes)", + platforms = [Platform.POSTGRES] + ) var changes: Map = hashMapOf() } @@ -171,7 +168,7 @@ enum class JobType { @Entity @Index(unique = true, name = "sql_unique_id", columnNames = ["entity_name", "sql_id", "tenant_id"]) -open class SqlModel : BaseTenantModel(){ +open class SqlModel : BaseTenantModel() { @JsonDeserialize(using = SafeStringDeserializer::class) var sqlId: String = "" @@ -181,6 +178,7 @@ open class SqlModel : BaseTenantModel(){ @Column(columnDefinition = "text") var sql: String = "" } + @Entity open class JobModel : BaseTenantModel() { @Index(unique = true) @@ -216,6 +214,19 @@ open class DataModel : BaseTenantModel() { } +@Entity +@Index(unique = true, name = "unique_session_id", columnNames = ["session_id"]) +open class AnonSession : BaseTenantModel() { + + var sessionId: String? = null + var ip: String? = null + var firstSeenAt: LocalDateTime? = null + var lastSeenAt: LocalDateTime? = null + + @DbJsonB + var headerMap: Map = hashMapOf() +} + class SafeStringDeserializer : JsonDeserializer() { private val regex = Regex("^[a-zA-Z0-9\\-_\\.]+$") diff --git a/src/main/kotlin/com/restapi/integ/ExternalAPI.kt b/src/main/kotlin/com/restapi/integ/ExternalAPI.kt new file mode 100644 index 0000000..f2179f8 --- /dev/null +++ b/src/main/kotlin/com/restapi/integ/ExternalAPI.kt @@ -0,0 +1,190 @@ +package com.restapi.integ + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import com.restapi.config.AppConfig +import io.minio.MinioClient +import io.minio.UploadObjectArgs +import org.apache.http.client.methods.HttpGet +import org.apache.http.client.utils.URIBuilder +import org.apache.http.impl.client.HttpClients +import org.apache.http.message.BasicNameValuePair +import org.apache.http.util.EntityUtils +import org.slf4j.LoggerFactory +import java.io.File + +val logger = LoggerFactory.getLogger("ExternalAPI") + +object S3 { + private val s3Url = AppConfig.appConfig.s3Url().orElse("s3.amazonaws.com") + private val minioClient: MinioClient by lazy { + MinioClient.builder() + .endpoint("https://$s3Url") + .credentials( + AppConfig.appConfig.s3AccessKey().orElseThrow(), + AppConfig.appConfig.s3SecretKey().orElseThrow() + ).build() + } + + fun uploadFilesToS3(f: File, name: String): Result { + val objStore: String = AppConfig.appConfig.s3Bucket().orElseThrow() + logger.warn("Trying to upload Object ${f.absolutePath} -> $objStore") + + try { + minioClient.uploadObject( + UploadObjectArgs.builder() + .bucket(objStore) + .`object`(name) + .filename(f.absolutePath) + .build() + ) + return Result.success("https://${objStore}.${s3Url}/$name") + } catch (e: Exception) { + logger.warn("Error in file Upload ${f.absolutePath} ==> ${e.message} => Objstore[${objStore}]") + return Result.failure(e) + } + } + +} + + +data class LoqateFullAddrResponse( + @JsonProperty("Items") + val items: List +) { + data class Item( + @JsonProperty("AdminAreaCode") + val adminAreaCode: String, + @JsonProperty("AdminAreaName") + val adminAreaName: String, + @JsonProperty("Barcode") + val barcode: String, + @JsonProperty("Block") + val block: String, + @JsonProperty("BuildingName") + val buildingName: String, + @JsonProperty("BuildingNumber") + val buildingNumber: String, + @JsonProperty("City") + val city: String, + @JsonProperty("Company") + val company: String, + @JsonProperty("CountryIso2") + val countryIso2: String, + @JsonProperty("CountryIso3") + val countryIso3: String, + @JsonProperty("CountryIsoNumber") + val countryIsoNumber: String, + @JsonProperty("CountryName") + val countryName: String, + @JsonProperty("DataLevel") + val dataLevel: String, + @JsonProperty("Department") + val department: String, + @JsonProperty("District") + val district: String, + @JsonProperty("DomesticId") + val domesticId: String, + @JsonProperty("Field1") + val field1: String, + @JsonProperty("Field10") + val field10: String, + @JsonProperty("Field11") + val field11: String, + @JsonProperty("Field12") + val field12: String, + @JsonProperty("Field13") + val field13: String, + @JsonProperty("Field14") + val field14: String, + @JsonProperty("Field15") + val field15: String, + @JsonProperty("Field16") + val field16: String, + @JsonProperty("Field17") + val field17: String, + @JsonProperty("Field18") + val field18: String, + @JsonProperty("Field19") + val field19: String, + @JsonProperty("Field2") + val field2: String, + @JsonProperty("Field20") + val field20: String, + @JsonProperty("Field3") + val field3: String, + @JsonProperty("Field4") + val field4: String, + @JsonProperty("Field5") + val field5: String, + @JsonProperty("Field6") + val field6: String, + @JsonProperty("Field7") + val field7: String, + @JsonProperty("Field8") + val field8: String, + @JsonProperty("Field9") + val field9: String, + @JsonProperty("Id") + val id: String, + @JsonProperty("Label") + val label: String, + @JsonProperty("Language") + val language: String, + @JsonProperty("LanguageAlternatives") + val languageAlternatives: String, + @JsonProperty("Line1") + val line1: String, + @JsonProperty("Line2") + val line2: String, + @JsonProperty("Line3") + val line3: String, + @JsonProperty("Line4") + val line4: String, + @JsonProperty("Line5") + val line5: String, + @JsonProperty("Neighbourhood") + val neighbourhood: String, + @JsonProperty("POBoxNumber") + val pOBoxNumber: String, + @JsonProperty("PostalCode") + val postalCode: String, + @JsonProperty("Province") + val province: String, + @JsonProperty("ProvinceCode") + val provinceCode: String, + @JsonProperty("ProvinceName") + val provinceName: String, + @JsonProperty("SecondaryStreet") + val secondaryStreet: String, + @JsonProperty("SortingNumber1") + val sortingNumber1: String, + @JsonProperty("SortingNumber2") + val sortingNumber2: String, + @JsonProperty("Street") + val street: String, + @JsonProperty("SubBuilding") + val subBuilding: String, + @JsonProperty("Type") + val type: String + ) +} + +object LoqateAPI { + //todo: add search? + fun findFullAddr(id: String): LoqateFullAddrResponse { + //https://api.addressy.com/Capture/Interactive/Retrieve/v1.2/ + return HttpClients.createDefault().use { h -> + val key = AppConfig.appConfig.locateApiKey().orElseThrow() + val ub = URIBuilder("https://api.addressy.com/Capture/Interactive/Retrieve/v1.2/json3.ws") + .setParameters( + BasicNameValuePair("Key", key), + BasicNameValuePair("Id", id) + ).build() + h.execute(HttpGet(ub)).use { + jacksonObjectMapper().readValue(EntityUtils.toString(it.entity)) + } + } + } +} \ No newline at end of file