diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index dd84ea782..320592668 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -12,6 +12,7 @@ A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
+
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
@@ -24,15 +25,17 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- - OS: [e.g. iOS]
- - Browser [e.g. chrome, safari]
- - Version [e.g. 22]
+
+- OS: [e.g. iOS]
+- Browser [e.g. chrome, safari]
+- Version [e.g. 22]
**Smartphone (please complete the following information):**
- - Device: [e.g. iPhone6]
- - OS: [e.g. iOS8.1]
- - Browser [e.g. stock browser, safari]
- - Version [e.g. 22]
+
+- Device: [e.g. iPhone6]
+- OS: [e.g. iOS8.1]
+- Browser [e.g. stock browser, safari]
+- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
diff --git a/.github/workflows/dev-otc.yml b/.github/workflows/dev-otc.yml
index 54dc68967..0436c8904 100644
--- a/.github/workflows/dev-otc.yml
+++ b/.github/workflows/dev-otc.yml
@@ -1,8 +1,8 @@
-name: Build, Test, and Deploy otc (DEV env) services for specific partner
+name: Build, Test, and Deploy otc (DEV env) services for specific partner
on:
-# push:
-# branches:
-# - dev
+ # push:
+ # branches:
+ # - dev
workflow_dispatch:
inputs:
@@ -14,7 +14,7 @@ on:
jobs:
build:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
strategy:
matrix:
java: [ 17 ]
@@ -107,7 +107,7 @@ jobs:
echo "password=$server_pass" >> $GITHUB_OUTPUT
- name: Build
- run: |
+ run: |
mvn -pl common -am -B -T 1C clean install -Potc
mvn -pl wallet,bc-gateway -amd -B -T 1C clean install -Potc
diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml
index ff6a92555..9ca8609d7 100644
--- a/.github/workflows/dev.yml
+++ b/.github/workflows/dev.yml
@@ -7,7 +7,7 @@ on:
jobs:
build:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
strategy:
matrix:
java: [ 21 ]
@@ -21,11 +21,11 @@ jobs:
distribution: 'adopt'
java-package: jdk
java-version: ${{ matrix.java }}
-# cache: maven
+ # cache: maven
- name: Build
- run: mvn -B -T 1C clean install
+ run: mvn -B clean install
- name: Run Tests
- run: mvn -B -T 1C -Dskip.unit.tests=false surefire:test
+ run: mvn -B -Dskip.unit.tests=false surefire:test
- name: Build Docker images
env:
TAG: dev
diff --git a/.github/workflows/main-otc.yml b/.github/workflows/main-otc.yml
index 81bffbb29..b515c0237 100644
--- a/.github/workflows/main-otc.yml
+++ b/.github/workflows/main-otc.yml
@@ -1,8 +1,8 @@
-name: Build, Test, and Deploy otc (PRD env) services for specific partner
+name: Build, Test, and Deploy otc (PRD env) services for specific partner
on:
-# push:
-# branches:
-# - main
+ # push:
+ # branches:
+ # - main
workflow_dispatch:
inputs:
@@ -14,7 +14,7 @@ on:
jobs:
build:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
strategy:
matrix:
java: [ 17 ]
@@ -107,7 +107,7 @@ jobs:
echo "password=$server_pass" >> $GITHUB_OUTPUT
- name: Build
- run: |
+ run: |
mvn -pl common -am -B -T 1C clean install -Potc
mvn -pl wallet,bc-gateway -amd -B -T 1C clean install -Potc
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index e1187e2e3..a72b0f314 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -7,7 +7,7 @@ on:
jobs:
build:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
strategy:
matrix:
java: [ 21 ]
@@ -21,7 +21,7 @@ jobs:
distribution: 'adopt'
java-package: jdk
java-version: ${{ matrix.java }}
-# cache: maven
+ # cache: maven
- name: Build
run: mvn -B -T 1C clean install
- name: Run Tests
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 6c3ae6cb3..145809528 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -6,7 +6,7 @@ on:
jobs:
build:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
strategy:
matrix:
java: [ 21 ]
@@ -20,11 +20,11 @@ jobs:
distribution: 'adopt'
java-package: jdk
java-version: ${{ matrix.java }}
-# cache: maven
+ # cache: maven
- name: Build
- run: mvn -B -T 1C clean install -Potc
+ run: mvn -B clean install -Potc
- name: Run Tests
- run: mvn -B -T 1C -Dskip.unit.tests=false surefire:test
+ run: mvn -B -Dskip.unit.tests=false surefire:test
- name: Build Docker images
env:
TAG: pr
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index eb644660a..28bb270ad 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -5,10 +5,15 @@ on:
# branches:
# - dev
workflow_dispatch:
-
+# inputs:
+# partner_name:
+# type: string
+# description: 'The name of the partner (provided during workflow execution)'
+# required: true
+# default: default
jobs:
build:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
strategy:
matrix:
java: [ 17 ]
diff --git a/README.md b/README.md
index f15258b05..174fa8211 100644
--- a/README.md
+++ b/README.md
@@ -63,7 +63,6 @@ KEYCLOAK_FRONTEND_URL=http://localhost:8083/auth
KEYCLOAK_ADMIN_URL=http://localhost:8083/auth
KEYCLOAK_VERIFY_REDIRECT_URL=http://localhost:8080/verify
KEYCLOAK_FORGOT_REDIRECT_URL=http://localhost:8080/forgot
-PREFERENCES=preferences-demo.yml
WHITELIST_REGISTER_ENABLED=true
WHITELIST_LOGIN_ENABLED=true
WALLET_BACKUP_ENABLED=false
@@ -80,7 +79,6 @@ TAG=debug
| SMTP_PASS | SMTP password used by keycloak to send emails for various operations (e.g. user verification, reset password) |
| API_KEY_CLIENT_SECRET | In order to access the api key feature, please follow the steps below:1. Go to Keycloak admin panel located at http://localhost:8083/auth/admin/master/console/#/realms/opex/clients
2. Login with the username and password you provided in the `.env` file (KEYCLOAK_ADMIN_USERNAME and KEYCLOAK_ADMIN_PASSWORD)
3. Go to `clients` section on the left menu 4. Click on `opex-api-key` client 5. In the credentials section, click on `Regenerate Secret` button 6. Copy the generated secret and paste it into this section |
| KEYCLOAK_FRONTEND_URL
KEYCLOAK_ADMIN_URL
KEYCLOAK_VERIFY_REDIRECT_URL
KEYCLOAK_FORGOT_REDIRECT_URL | Replace `localhost` with your server's IP if you're not running on local machine. Do not change the rest. |
-| PREFERENCES | Points to a file containing seed data used to by modules to initialize their databases. An example of this file is provided and is available inside the root directory (preferences-demo.yml). It's deprecated and will be removed soon |
| WHITELIST_REGISTER_ENABLED | Allows registration only for whitelisted emails |
| WHITELIST_LOGIN_ENABLED | Allows login only for whitelisted emails |
| WALLET_BACKUP_ENABLED | Enables wallet data backup to google drive folder. In order to use this feature, you need to have `drive-key.json` file (obtained from google drive API panel) in the root directory of project |
diff --git a/accountant/accountant-app/pom.xml b/accountant/accountant-app/pom.xml
index f722891eb..67649ff75 100644
--- a/accountant/accountant-app/pom.xml
+++ b/accountant/accountant-app/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
@@ -64,10 +64,6 @@
micrometer-registry-prometheus
runtime
-
- co.nilin.opex.utility
- preferences
-
org.testcontainers
testcontainers
diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/AccountantApp.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/AccountantApp.kt
index 02bc1d88d..ad5272a2c 100644
--- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/AccountantApp.kt
+++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/AccountantApp.kt
@@ -1,7 +1,6 @@
package co.nilin.opex.accountant.app
import org.springframework.boot.autoconfigure.SpringBootApplication
-import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication
import org.springframework.context.annotation.ComponentScan
diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/AppConfig.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/AppConfig.kt
index f63ed9800..8b4e26e95 100644
--- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/AppConfig.kt
+++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/AppConfig.kt
@@ -1,10 +1,6 @@
package co.nilin.opex.accountant.app.config
import co.nilin.opex.accountant.app.listener.*
-import co.nilin.opex.accountant.app.listener.AccountantEventListener
-import co.nilin.opex.accountant.app.listener.AccountantTempEventListener
-import co.nilin.opex.accountant.app.listener.AccountantTradeListener
-import co.nilin.opex.accountant.app.listener.OrderListener
import co.nilin.opex.accountant.core.api.FeeCalculator
import co.nilin.opex.accountant.core.api.FinancialActionJobManager
import co.nilin.opex.accountant.core.api.OrderManager
@@ -14,10 +10,6 @@ import co.nilin.opex.accountant.core.service.OrderManagerImpl
import co.nilin.opex.accountant.core.service.TradeManagerImpl
import co.nilin.opex.accountant.core.spi.*
import co.nilin.opex.accountant.ports.kafka.listener.consumer.*
-import co.nilin.opex.accountant.ports.kafka.listener.consumer.EventKafkaListener
-import co.nilin.opex.accountant.ports.kafka.listener.consumer.OrderKafkaListener
-import co.nilin.opex.accountant.ports.kafka.listener.consumer.TempEventKafkaListener
-import co.nilin.opex.accountant.ports.kafka.listener.consumer.TradeKafkaListener
import co.nilin.opex.accountant.ports.kafka.listener.spi.FAResponseListener
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
@@ -147,7 +139,8 @@ class AppConfig {
@Autowired
fun configureTempEventListener(
tempEventKafkaListener: TempEventKafkaListener,
- accountantTempEventListener: AccountantTempEventListener) {
+ accountantTempEventListener: AccountantTempEventListener
+ ) {
tempEventKafkaListener.addListener(accountantTempEventListener)
}
diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/InitializeService.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/InitializeService.kt
index 20d751529..8e9b73c94 100644
--- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/InitializeService.kt
+++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/InitializeService.kt
@@ -1,61 +1,18 @@
package co.nilin.opex.accountant.app.config
-import co.nilin.opex.accountant.ports.postgres.dao.PairConfigRepository
-import co.nilin.opex.accountant.ports.postgres.dao.PairFeeConfigRepository
-import co.nilin.opex.accountant.ports.postgres.dao.UserLevelRepository
-import co.nilin.opex.accountant.ports.postgres.model.PairFeeConfigModel
-import co.nilin.opex.utility.preferences.Preferences
-import kotlinx.coroutines.reactor.awaitSingleOrNull
import kotlinx.coroutines.runBlocking
-import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.DependsOn
import org.springframework.stereotype.Component
import javax.annotation.PostConstruct
@Component
@DependsOn("postgresConfig")
-class InitializeService(
- private val pairConfigRepository: PairConfigRepository,
- private val pairFeeConfigRepository: PairFeeConfigRepository,
- private val userLevelRepository: UserLevelRepository,
-) {
-
- @Autowired
- private lateinit var preferences: Preferences
+class InitializeService{
@PostConstruct
fun init() = runBlocking {
- preferences.userLevels.forEach {
- userLevelRepository.insert(it).awaitSingleOrNull()
- }
-
- preferences.markets.map {
- val pair = it.pair ?: "${it.leftSide}_${it.rightSide}"
- val leftSideCurrency = preferences.currencies.first { c -> it.leftSide == c.symbol }
- val rightSideCurrency = preferences.currencies.first { c -> it.rightSide == c.symbol }
- val leftSideFraction = (it.leftSideFraction ?: leftSideCurrency.precision)
- val rightSideFraction = (it.rightSideFraction ?: rightSideCurrency.precision)
- pairConfigRepository.insert(
- pair,
- it.leftSide,
- it.rightSide,
- leftSideFraction,
- rightSideFraction
- ).awaitSingleOrNull()
- it.feeConfigs.forEach { f ->
- runCatching {
- pairFeeConfigRepository.save(
- PairFeeConfigModel(
- null,
- pair,
- f.direction,
- f.userLevel,
- f.makerFee,
- f.takerFee
- )
- ).awaitSingleOrNull()
- }
- }
- }
+ // addUserLevels()
+ // addPairConfigs()
+ // addPairFeeConfigs
}
}
diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/SecurityConfig.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/SecurityConfig.kt
new file mode 100644
index 000000000..dd1dc06dd
--- /dev/null
+++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/SecurityConfig.kt
@@ -0,0 +1,18 @@
+package co.nilin.opex.accountant.app.config
+
+import org.springframework.context.annotation.Bean
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.web.server.SecurityWebFilterChain
+
+@EnableWebFluxSecurity
+class SecurityConfig {
+
+ @Bean
+ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+ http.csrf().disable()
+ .authorizeExchange()
+ .anyExchange().permitAll()
+ return http.build()
+ }
+}
\ No newline at end of file
diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/data/PairFeeResponse.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/data/PairFeeResponse.kt
index cbfb4c63f..6ac8d804b 100644
--- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/data/PairFeeResponse.kt
+++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/data/PairFeeResponse.kt
@@ -3,7 +3,7 @@ package co.nilin.opex.accountant.app.data
import java.math.BigDecimal
data class PairFeeResponse(
- val pair:String,
+ val pair: String,
val direction: String,
val userLevel: String,
val makerFee: BigDecimal,
diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/KycLevelUpdatedListener.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/KycLevelUpdatedListener.kt
index b7ff38ba7..98ae6147b 100644
--- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/KycLevelUpdatedListener.kt
+++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/KycLevelUpdatedListener.kt
@@ -3,11 +3,11 @@ package co.nilin.opex.accountant.app.listener
import co.nilin.opex.accountant.core.inout.KycLevelUpdatedEvent
import co.nilin.opex.accountant.ports.kafka.listener.spi.KycLevelUpdatedEventListener
import co.nilin.opex.accountant.ports.postgres.impl.UserLevelLoaderImpl
-import org.slf4j.LoggerFactory
-import org.springframework.stereotype.Component
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import org.slf4j.LoggerFactory
+import org.springframework.stereotype.Component
@Component
class KycLevelUpdatedListener(val userLevelLoaderImpl: UserLevelLoaderImpl) : KycLevelUpdatedEventListener {
@@ -18,8 +18,10 @@ class KycLevelUpdatedListener(val userLevelLoaderImpl: UserLevelLoaderImpl) : Ky
return "KycLevelUpdatedListener"
}
- override fun onEvent(event: KycLevelUpdatedEvent,
- partition: Int, offset: Long, timestamp: Long, eventId: String) {
+ override fun onEvent(
+ event: KycLevelUpdatedEvent,
+ partition: Int, offset: Long, timestamp: Long, eventId: String
+ ) {
logger.info("==========================================================================")
logger.info("Incoming UserLevelUpdated event: $event")
logger.info("==========================================================================")
diff --git a/accountant/accountant-app/src/test/resources/application.yml b/accountant/accountant-app/src/test/resources/application.yml
index 005d0e363..6fa2654d1 100644
--- a/accountant/accountant-app/src/test/resources/application.yml
+++ b/accountant/accountant-app/src/test/resources/application.yml
@@ -38,7 +38,7 @@ management:
web:
base-path: /actuator
exposure:
- include: ["health", "metrics"]
+ include: [ "health", "metrics" ]
endpoint:
health:
show-details: always
diff --git a/accountant/accountant-core/pom.xml b/accountant/accountant-core/pom.xml
index fe9705d0c..4ef1fd63d 100644
--- a/accountant/accountant-core/pom.xml
+++ b/accountant/accountant-core/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FinancialActionJobManagerImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FinancialActionJobManagerImpl.kt
index f6c25d076..0dce40930 100644
--- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FinancialActionJobManagerImpl.kt
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FinancialActionJobManagerImpl.kt
@@ -3,7 +3,9 @@ package co.nilin.opex.accountant.core.service
import co.nilin.opex.accountant.core.api.FinancialActionJobManager
import co.nilin.opex.accountant.core.model.FinancialAction
import co.nilin.opex.accountant.core.model.FinancialActionStatus
-import co.nilin.opex.accountant.core.spi.*
+import co.nilin.opex.accountant.core.spi.FinancialActionLoader
+import co.nilin.opex.accountant.core.spi.FinancialActionPersister
+import co.nilin.opex.accountant.core.spi.WalletProxy
import co.nilin.opex.utility.error.data.OpexException
import org.slf4j.LoggerFactory
import org.springframework.web.reactive.function.client.WebClientResponseException
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserLevelLoader.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserLevelLoader.kt
index b1e2c3173..1946ee4fd 100644
--- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserLevelLoader.kt
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserLevelLoader.kt
@@ -6,6 +6,6 @@ interface UserLevelLoader {
suspend fun load(uuid: String): String
- suspend fun update(uuid: String,userLevel:KycLevel)
+ suspend fun update(uuid: String, userLevel: KycLevel)
}
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-eventlistener-kafka/pom.xml b/accountant/accountant-ports/accountant-eventlistener-kafka/pom.xml
index a27cadb8f..52c433ffe 100644
--- a/accountant/accountant-ports/accountant-eventlistener-kafka/pom.xml
+++ b/accountant/accountant-ports/accountant-eventlistener-kafka/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
diff --git a/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/OrderRequestEvent.kt b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/OrderRequestEvent.kt
index 04876ee6f..f4d99e7b6 100644
--- a/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/OrderRequestEvent.kt
+++ b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/OrderRequestEvent.kt
@@ -2,4 +2,4 @@ package co.nilin.opex.accountant.ports.kafka.listener.inout
import co.nilin.opex.matching.engine.core.model.Pair
-abstract class OrderRequestEvent(val ouid:String, val uuid: String, val pair: Pair)
\ No newline at end of file
+abstract class OrderRequestEvent(val ouid: String, val uuid: String, val pair: Pair)
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/pom.xml b/accountant/accountant-ports/accountant-persister-postgres/pom.xml
index 15d35337e..b90ec1cf5 100644
--- a/accountant/accountant-ports/accountant-persister-postgres/pom.xml
+++ b/accountant/accountant-ports/accountant-persister-postgres/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/FinancialActionRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/FinancialActionRepository.kt
index fa04c317d..3139f647d 100644
--- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/FinancialActionRepository.kt
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/FinancialActionRepository.kt
@@ -41,10 +41,12 @@ interface FinancialActionRepository : ReactiveCrudRepository, status: FinancialActionStatus): Mono
- @Query("""
+ @Query(
+ """
select * from fi_actions fi
where status = 'CREATED'
and (parent_id is null or 'ERROR' != (select pfi.status from fi_actions pfi where pfi.id = fi.parent_id))
- """)
+ """
+ )
fun findReadyToProcess(of: Pageable): Flow
}
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionPersisterImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionPersisterImpl.kt
index b347977cc..86d9dcd89 100644
--- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionPersisterImpl.kt
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionPersisterImpl.kt
@@ -3,7 +3,6 @@ package co.nilin.opex.accountant.ports.postgres.impl
import co.nilin.opex.accountant.core.model.FinancialAction
import co.nilin.opex.accountant.core.model.FinancialActionStatus
import co.nilin.opex.accountant.core.spi.FinancialActionPersister
-import co.nilin.opex.accountant.core.spi.JsonMapper
import co.nilin.opex.accountant.ports.postgres.dao.FinancialActionErrorRepository
import co.nilin.opex.accountant.ports.postgres.dao.FinancialActionRepository
import co.nilin.opex.accountant.ports.postgres.dao.FinancialActionRetryRepository
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/TempEventPersisterImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/TempEventPersisterImpl.kt
index 8bfa95402..2aed7a703 100644
--- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/TempEventPersisterImpl.kt
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/TempEventPersisterImpl.kt
@@ -6,7 +6,6 @@ import co.nilin.opex.accountant.ports.postgres.dao.TempEventRepository
import co.nilin.opex.accountant.ports.postgres.model.TempEventModel
import co.nilin.opex.matching.engine.core.eventh.events.CoreEvent
import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.module.kotlin.readValue
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.reactive.awaitFirstOrNull
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserLevelLoaderImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserLevelLoaderImpl.kt
index ac2817040..4ecd0b13f 100644
--- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserLevelLoaderImpl.kt
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserLevelLoaderImpl.kt
@@ -9,8 +9,10 @@ import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.springframework.stereotype.Component
@Component
-class UserLevelLoaderImpl(private val userLevelMapperRepository: UserLevelMapperRepository,
- private val userLevelRepository: UserLevelRepository) : UserLevelLoader {
+class UserLevelLoaderImpl(
+ private val userLevelMapperRepository: UserLevelMapperRepository,
+ private val userLevelRepository: UserLevelRepository
+) : UserLevelLoader {
override suspend fun load(uuid: String): String {
val mapper = userLevelMapperRepository.findByUuid(uuid).awaitSingleOrNull()
@@ -21,14 +23,23 @@ class UserLevelLoaderImpl(private val userLevelMapperRepository: UserLevelMapper
userLevelRepository.findByLevel(userLevel.name).awaitSingleOrNull()?.let {
userLevelMapperRepository.findByUuid(uuid).awaitSingleOrNull()
- ?.let { userLevelMapperRepository.save(UserLevelMapperModel(it.id, it.uuid, userLevel.name)).awaitSingleOrNull() }
- ?: run { userLevelMapperRepository.save(UserLevelMapperModel(null, uuid, userLevel.name)).awaitSingleOrNull() }
- }?:
- run {
- userLevelRepository.insert(userLevel.name) .awaitSingleOrNull()
+ ?.let {
+ userLevelMapperRepository.save(UserLevelMapperModel(it.id, it.uuid, userLevel.name))
+ .awaitSingleOrNull()
+ }
+ ?: run {
+ userLevelMapperRepository.save(UserLevelMapperModel(null, uuid, userLevel.name)).awaitSingleOrNull()
+ }
+ } ?: run {
+ userLevelRepository.insert(userLevel.name).awaitSingleOrNull()
userLevelMapperRepository.findByUuid(uuid).awaitSingleOrNull()
- ?.let { userLevelMapperRepository.save(UserLevelMapperModel(it.id, it.uuid, userLevel.name)).awaitSingleOrNull() }
- ?: run { userLevelMapperRepository.save(UserLevelMapperModel(null, uuid, userLevel.name)).awaitSingleOrNull() }
+ ?.let {
+ userLevelMapperRepository.save(UserLevelMapperModel(it.id, it.uuid, userLevel.name))
+ .awaitSingleOrNull()
+ }
+ ?: run {
+ userLevelMapperRepository.save(UserLevelMapperModel(null, uuid, userLevel.name)).awaitSingleOrNull()
+ }
}
}
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FinancialActionModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FinancialActionModel.kt
index 59d10c675..a8b79712a 100644
--- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FinancialActionModel.kt
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FinancialActionModel.kt
@@ -4,7 +4,6 @@ import co.nilin.opex.accountant.core.model.FinancialActionCategory
import co.nilin.opex.accountant.core.model.FinancialActionStatus
import co.nilin.opex.accountant.core.model.WalletType
import org.springframework.data.annotation.Id
-import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.math.BigDecimal
import java.time.LocalDateTime
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserLevelMapperModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserLevelMapperModel.kt
index f2f2f08f3..af4c5512e 100644
--- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserLevelMapperModel.kt
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserLevelMapperModel.kt
@@ -5,7 +5,7 @@ import org.springframework.data.relational.core.mapping.Table
@Table("user_level_mapper")
data class UserLevelMapperModel(
- @Id val id: Long?,
- val uuid: String,
- val userLevel: String
+ @Id val id: Long?,
+ val uuid: String,
+ val userLevel: String
)
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql b/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql
index beeddfa88..c541c8e59 100644
--- a/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql
@@ -1,119 +1,276 @@
CREATE TABLE IF NOT EXISTS orders
(
- id SERIAL PRIMARY KEY,
- ouid VARCHAR(72) NOT NULL UNIQUE,
- uuid VARCHAR(72) NOT NULL,
- pair VARCHAR(72) NOT NULL,
- matching_engine_id INTEGER,
- maker_fee DECIMAL NOT NULL,
- taker_fee DECIMAL NOT NULL,
- left_side_fraction DECIMAL NOT NULL,
- right_side_fraction DECIMAL NOT NULL,
- user_level VARCHAR(20) NOT NULL,
- direction VARCHAR(20) NOT NULL,
- match_constraint VARCHAR(30) NOT NULL,
- order_type VARCHAR(30) NOT NULL,
- price DECIMAL NOT NULL,
- quantity DECIMAL NOT NULL,
- filled_quantity DECIMAL NOT NULL,
- orig_price DECIMAL NOT NULL,
- orig_quantity DECIMAL NOT NULL,
- filled_orig_quantity DECIMAL NOT NULL,
- first_transfer_amount DECIMAL NOT NULL,
- remained_transfer_amount DECIMAL NOT NULL,
- status INTEGER NOT NULL,
- agent VARCHAR(20),
- ip VARCHAR(11),
- create_date TIMESTAMP NOT NULL
-);
+ id
+ SERIAL
+ PRIMARY
+ KEY,
+ ouid
+ VARCHAR
+(
+ 72
+) NOT NULL UNIQUE,
+ uuid VARCHAR
+(
+ 72
+) NOT NULL,
+ pair VARCHAR
+(
+ 72
+) NOT NULL,
+ matching_engine_id INTEGER,
+ maker_fee DECIMAL NOT NULL,
+ taker_fee DECIMAL NOT NULL,
+ left_side_fraction DECIMAL NOT NULL,
+ right_side_fraction DECIMAL NOT NULL,
+ user_level VARCHAR
+(
+ 20
+) NOT NULL,
+ direction VARCHAR
+(
+ 20
+) NOT NULL,
+ match_constraint VARCHAR
+(
+ 30
+) NOT NULL,
+ order_type VARCHAR
+(
+ 30
+) NOT NULL,
+ price DECIMAL NOT NULL,
+ quantity DECIMAL NOT NULL,
+ filled_quantity DECIMAL NOT NULL,
+ orig_price DECIMAL NOT NULL,
+ orig_quantity DECIMAL NOT NULL,
+ filled_orig_quantity DECIMAL NOT NULL,
+ first_transfer_amount DECIMAL NOT NULL,
+ remained_transfer_amount DECIMAL NOT NULL,
+ status INTEGER NOT NULL,
+ agent VARCHAR
+(
+ 20
+),
+ ip VARCHAR
+(
+ 11
+),
+ create_date TIMESTAMP NOT NULL
+ );
CREATE TABLE IF NOT EXISTS fi_actions
(
- id SERIAL PRIMARY KEY,
- uuid VARCHAR(72) NOT NULL UNIQUE,
- parent_id INTEGER,
- event_type VARCHAR(72) NOT NULL,
- pointer VARCHAR(72) NOT NULL,
- symbol VARCHAR(36) NOT NULL,
- amount DECIMAL NOT NULL,
- sender VARCHAR(36) NOT NULL,
- sender_wallet_type VARCHAR(36) NOT NULL,
- receiver VARCHAR(36) NOT NULL,
- receiver_wallet_type VARCHAR(36) NOT NULL,
- agent VARCHAR(20),
- ip VARCHAR(11),
- create_date TIMESTAMP NOT NULL,
- status VARCHAR(20)
-);
+ id
+ SERIAL
+ PRIMARY
+ KEY,
+ uuid
+ VARCHAR
+(
+ 72
+) NOT NULL UNIQUE,
+ parent_id INTEGER,
+ event_type VARCHAR
+(
+ 72
+) NOT NULL,
+ pointer VARCHAR
+(
+ 72
+) NOT NULL,
+ symbol VARCHAR
+(
+ 36
+) NOT NULL,
+ amount DECIMAL NOT NULL,
+ sender VARCHAR
+(
+ 36
+) NOT NULL,
+ sender_wallet_type VARCHAR
+(
+ 36
+) NOT NULL,
+ receiver VARCHAR
+(
+ 36
+) NOT NULL,
+ receiver_wallet_type VARCHAR
+(
+ 36
+) NOT NULL,
+ agent VARCHAR
+(
+ 20
+),
+ ip VARCHAR
+(
+ 11
+),
+ create_date TIMESTAMP NOT NULL,
+ status VARCHAR
+(
+ 20
+)
+ );
CREATE INDEX IF NOT EXISTS idx_fi_actions_symbol ON fi_actions(symbol);
CREATE INDEX IF NOT EXISTS idx_fi_event_type ON fi_actions(event_type);
CREATE INDEX IF NOT EXISTS idx_fi_actions_status ON fi_actions(status);
CREATE INDEX IF NOT EXISTS idx_fi_actions_pointer ON fi_actions(pointer);
ALTER TABLE fi_actions
- ADD COLUMN IF NOT EXISTS category_name VARCHAR(36);
+ ADD COLUMN IF NOT EXISTS category_name VARCHAR (36);
CREATE TABLE IF NOT EXISTS fi_action_retry
(
- id SERIAL PRIMARY KEY,
- fa_id INTEGER NOT NULL UNIQUE REFERENCES fi_actions (id),
- retries INTEGER NOT NULL DEFAULT 0,
+ id
+ SERIAL
+ PRIMARY
+ KEY,
+ fa_id
+ INTEGER
+ NOT
+ NULL
+ UNIQUE
+ REFERENCES
+ fi_actions
+(
+ id
+),
+ retries INTEGER NOT NULL DEFAULT 0,
next_run_time TIMESTAMP NOT NULL,
- is_resolved BOOLEAN NOT NULL DEFAULT false,
- has_given_up BOOLEAN NOT NULL DEFAULT false
-);
+ is_resolved BOOLEAN NOT NULL DEFAULT false,
+ has_given_up BOOLEAN NOT NULL DEFAULT false
+ );
CREATE TABLE IF NOT EXISTS fi_action_error
(
- id SERIAL PRIMARY KEY,
- fa_id INTEGER NOT NULL REFERENCES fi_actions (id),
- error TEXT NOT NULL,
- message TEXT NOT NULL,
- body TEXT,
- retry_id INTEGER REFERENCES fi_action_retry (id),
- date TIMESTAMP NOT NULL DEFAULT CURRENT_DATE
-);
+ id
+ SERIAL
+ PRIMARY
+ KEY,
+ fa_id
+ INTEGER
+ NOT
+ NULL
+ REFERENCES
+ fi_actions
+(
+ id
+),
+ error TEXT NOT NULL,
+ message TEXT NOT NULL,
+ body TEXT,
+ retry_id INTEGER REFERENCES fi_action_retry
+(
+ id
+),
+ date TIMESTAMP NOT NULL DEFAULT CURRENT_DATE
+ );
CREATE TABLE IF NOT EXISTS pair_config
(
- pair VARCHAR(72) PRIMARY KEY,
- left_side_wallet_symbol VARCHAR(36) NOT NULL,
- right_side_wallet_symbol VARCHAR(36) NOT NULL,
- left_side_fraction DECIMAL NOT NULL,
- right_side_fraction DECIMAL NOT NULL,
- UNIQUE (left_side_wallet_symbol, right_side_wallet_symbol)
-);
+ pair VARCHAR
+(
+ 72
+) PRIMARY KEY,
+ left_side_wallet_symbol VARCHAR
+(
+ 36
+) NOT NULL,
+ right_side_wallet_symbol VARCHAR
+(
+ 36
+) NOT NULL,
+ left_side_fraction DECIMAL NOT NULL,
+ right_side_fraction DECIMAL NOT NULL,
+ UNIQUE
+(
+ left_side_wallet_symbol,
+ right_side_wallet_symbol
+)
+ );
CREATE TABLE IF NOT EXISTS user_level
(
- level VARCHAR(36) PRIMARY KEY
-);
+ level VARCHAR
+(
+ 36
+) PRIMARY KEY
+ );
CREATE TABLE IF NOT EXISTS pair_fee_config
(
- id SERIAL PRIMARY KEY,
- pair_config_id VARCHAR(72) NOT NULL REFERENCES pair_config (pair),
- direction VARCHAR(36) NOT NULL,
- user_level VARCHAR(36) NOT NULL REFERENCES user_level (level),
- maker_fee DECIMAL NOT NULL,
- taker_fee DECIMAL NOT NULL,
- UNIQUE (direction, user_level, pair_config_id)
-);
+ id
+ SERIAL
+ PRIMARY
+ KEY,
+ pair_config_id
+ VARCHAR
+(
+ 72
+) NOT NULL REFERENCES pair_config
+(
+ pair
+),
+ direction VARCHAR
+(
+ 36
+) NOT NULL,
+ user_level VARCHAR
+(
+ 36
+) NOT NULL REFERENCES user_level
+(
+ level
+),
+ maker_fee DECIMAL NOT NULL,
+ taker_fee DECIMAL NOT NULL,
+ UNIQUE
+(
+ direction,
+ user_level,
+ pair_config_id
+)
+ );
CREATE TABLE IF NOT EXISTS user_level_mapper
(
- id SERIAL PRIMARY KEY,
- uuid VARCHAR(36) NOT NULL UNIQUE,
- user_level VARCHAR(36) NOT NULL REFERENCES user_level (level)
-);
+ id
+ SERIAL
+ PRIMARY
+ KEY,
+ uuid
+ VARCHAR
+(
+ 36
+) NOT NULL UNIQUE,
+ user_level VARCHAR
+(
+ 36
+) NOT NULL REFERENCES user_level
+(
+ level
+)
+ );
CREATE TABLE IF NOT EXISTS temp_events
(
- id SERIAL PRIMARY KEY,
- ouid VARCHAR(72) NOT NULL,
- event_type VARCHAR(72) NOT NULL,
- event_body TEXT NOT NULL,
- event_date TIMESTAMP NOT NULL
-);
+ id
+ SERIAL
+ PRIMARY
+ KEY,
+ ouid
+ VARCHAR
+(
+ 72
+) NOT NULL,
+ event_type VARCHAR
+(
+ 72
+) NOT NULL,
+ event_body TEXT NOT NULL,
+ event_date TIMESTAMP NOT NULL
+ );
COMMIT;
diff --git a/accountant/accountant-ports/accountant-submitter-kafka/pom.xml b/accountant/accountant-ports/accountant-submitter-kafka/pom.xml
index 2759d98c3..353a37eba 100644
--- a/accountant/accountant-ports/accountant-submitter-kafka/pom.xml
+++ b/accountant/accountant-ports/accountant-submitter-kafka/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
diff --git a/accountant/accountant-ports/accountant-wallet-proxy/pom.xml b/accountant/accountant-ports/accountant-wallet-proxy/pom.xml
index 3819743c8..17fecd3ba 100644
--- a/accountant/accountant-ports/accountant-wallet-proxy/pom.xml
+++ b/accountant/accountant-ports/accountant-wallet-proxy/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
diff --git a/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/config/WebClientConfig.kt b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/config/WebClientConfig.kt
index b559aa504..2ba1c3c02 100644
--- a/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/config/WebClientConfig.kt
+++ b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/config/WebClientConfig.kt
@@ -5,7 +5,6 @@ import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalanc
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
-import org.springframework.http.client.reactive.ReactorClientHttpConnector
import org.springframework.web.reactive.function.client.WebClient
import org.zalando.logbook.Logbook
import org.zalando.logbook.netty.LogbookClientHandler
diff --git a/accountant/pom.xml b/accountant/pom.xml
index 15916c8d3..6eded39f6 100644
--- a/accountant/pom.xml
+++ b/accountant/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
@@ -82,11 +82,6 @@
error-handler
${error-hanlder.version}
-
- co.nilin.opex.utility
- preferences
- ${preferences.version}
-
org.springframework.cloud
spring-cloud-dependencies
diff --git a/api/api-app/pom.xml b/api/api-app/pom.xml
index d27d6e876..b2a2e11aa 100644
--- a/api/api-app/pom.xml
+++ b/api/api-app/pom.xml
@@ -51,6 +51,11 @@
co.nilin.opex.api.ports.binance
api-binance-rest
+
+ co.nilin.opex.api.ports.opex
+ api-opex-rest
+ 1.0.1-beta.7
+
co.nilin.opex.api.ports.postgres
api-persister-postgres
@@ -64,10 +69,6 @@
org.springframework.cloud
spring-cloud-starter-vault-config
-
- co.nilin.opex.utility
- preferences
-
io.micrometer
micrometer-registry-prometheus
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/ApiApp.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/ApiApp.kt
index 35df759a8..e1c0b542a 100644
--- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/ApiApp.kt
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/ApiApp.kt
@@ -2,10 +2,8 @@ package co.nilin.opex.api.app
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
-import org.springframework.cache.annotation.EnableCaching
import org.springframework.context.annotation.ComponentScan
import org.springframework.scheduling.annotation.EnableScheduling
-import springfox.documentation.swagger2.annotations.EnableSwagger2
@SpringBootApplication
@ComponentScan("co.nilin.opex")
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/InitializeService.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/InitializeService.kt
index c390d73a1..b28ce1e11 100644
--- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/InitializeService.kt
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/InitializeService.kt
@@ -1,28 +1,16 @@
package co.nilin.opex.api.app.config
-import co.nilin.opex.api.ports.postgres.dao.SymbolMapRepository
-import co.nilin.opex.api.ports.postgres.model.SymbolMapModel
-import co.nilin.opex.utility.preferences.Preferences
import jakarta.annotation.PostConstruct
-import kotlinx.coroutines.reactor.awaitSingleOrNull
import kotlinx.coroutines.runBlocking
-import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.DependsOn
import org.springframework.stereotype.Component
@Component
@DependsOn("postgresConfig")
-class InitializeService(private val symbolMapRepository: SymbolMapRepository) {
-
- @Autowired
- private lateinit var preferences: Preferences
+class InitializeService {
@PostConstruct
fun init() = runBlocking {
- preferences.markets.map {
- val pair = it.pair ?: "${it.leftSide}_${it.rightSide}"
- val items = it.aliases.map { a -> SymbolMapModel(null, pair, a.key, a.alias) }
- runCatching { symbolMapRepository.saveAll(items).collectList().awaitSingleOrNull() }
- }
+ // Add symbol maps
}
}
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/controller/APIKeyController.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/controller/APIKeyController.kt
index ab8bcd624..7a616d2d0 100644
--- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/controller/APIKeyController.kt
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/controller/APIKeyController.kt
@@ -7,14 +7,7 @@ import co.nilin.opex.api.ports.binance.util.jwtAuthentication
import co.nilin.opex.api.ports.binance.util.tokenValue
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
-import org.springframework.web.bind.annotation.DeleteMapping
-import org.springframework.web.bind.annotation.GetMapping
-import org.springframework.web.bind.annotation.PathVariable
-import org.springframework.web.bind.annotation.PostMapping
-import org.springframework.web.bind.annotation.PutMapping
-import org.springframework.web.bind.annotation.RequestBody
-import org.springframework.web.bind.annotation.RequestMapping
-import org.springframework.web.bind.annotation.RestController
+import org.springframework.web.bind.annotation.*
import java.security.Principal
@RestController
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/APIKeyResponse.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/APIKeyResponse.kt
index dfe733f83..b2832019f 100644
--- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/APIKeyResponse.kt
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/APIKeyResponse.kt
@@ -1,7 +1,6 @@
package co.nilin.opex.api.app.data
import java.time.LocalDateTime
-import java.util.*
data class APIKeyResponse(
val label: String,
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/proxy/AuthProxy.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/proxy/AuthProxy.kt
index 5de6ae557..6dcb89d25 100644
--- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/proxy/AuthProxy.kt
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/proxy/AuthProxy.kt
@@ -12,12 +12,12 @@ import org.springframework.web.reactive.function.client.bodyToMono
@Component
class AuthProxy(
- private val client: WebClient,
@Value("\${app.auth.token-url}")
private val tokenUrl: String
) {
private val logger = LoggerFactory.getLogger(AuthProxy::class.java)
+ private val client = WebClient.create()
suspend fun exchangeToken(clientSecret: String, token: String): AccessTokenResponse {
val body = BodyInserters.fromFormData("client_id", "opex-api-key")
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/service/APIKeyServiceImpl.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/service/APIKeyServiceImpl.kt
index 034808af0..a95ae30f5 100644
--- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/service/APIKeyServiceImpl.kt
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/service/APIKeyServiceImpl.kt
@@ -25,7 +25,6 @@ import java.util.concurrent.TimeUnit
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
-import kotlin.math.log
@Service
class APIKeyServiceImpl(
diff --git a/api/api-app/src/main/resources/application-binance.yml b/api/api-app/src/main/resources/application-binance.yml
new file mode 100644
index 000000000..65104684b
--- /dev/null
+++ b/api/api-app/src/main/resources/application-binance.yml
@@ -0,0 +1,4 @@
+app:
+ auth:
+ cert-url: http://opex-auth/auth/realms/opex/protocol/openid-connect/certs
+ token-url: http://opex-auth/auth/realms/opex/protocol/openid-connect/token
\ No newline at end of file
diff --git a/api/api-app/src/main/resources/application.yml b/api/api-app/src/main/resources/application.yml
index 3833dc8a4..64c273602 100644
--- a/api/api-app/src/main/resources/application.yml
+++ b/api/api-app/src/main/resources/application.yml
@@ -103,8 +103,8 @@ app:
opex-bc-gateway:
url: http://opex-bc-gateway
auth:
- cert-url: http://opex-auth/auth/realms/opex/protocol/openid-connect/certs
- token-url: http://opex-auth/auth/realms/opex/protocol/openid-connect/token
+ cert-url: http://keycloak:8080/realms/opex/protocol/openid-connect/certs
+ token-url: http://keycloak:8080/realms/opex/protocol/openid-connect/token
api-key-client:
secret: ${API_KEY_CLIENT_SECRET}
binance:
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/Amount.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/Amount.kt
new file mode 100644
index 000000000..8bf4ce101
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/Amount.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+
+data class Amount(val currency: CurrencyCommand, val amount: BigDecimal)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/CurrencyCommand.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/CurrencyCommand.kt
new file mode 100644
index 000000000..5b0cadda5
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/CurrencyCommand.kt
@@ -0,0 +1,39 @@
+package co.nilin.opex.api.core.inout
+
+
+import java.math.BigDecimal
+import java.util.*
+
+data class CurrencyCommand(
+ var symbol: String,
+ var uuid: String? = UUID.randomUUID().toString(),
+ var name: String,
+ var precision: BigDecimal,
+ var title: String? = null,
+ var alias: String? = null,
+ var icon: String? = null,
+ var isTransitive: Boolean? = false,
+ var isActive: Boolean? = true,
+ var sign: String? = null,
+ var description: String? = null,
+ var shortDescription: String? = null,
+ var withdrawAllowed: Boolean? = false,
+ var depositAllowed: Boolean? = false,
+ var externalUrl: String? = null,
+ var gateways: List? = null,
+ var availableGatewayType: String? = null,
+ var order: Int? = null
+
+) {
+ fun updateTo(newData: CurrencyCommand): CurrencyCommand {
+ return newData.apply {
+ this.uuid = uuid
+ this.symbol = symbol
+ }
+ }
+
+
+}
+
+
+data class CurrenciesCommand(var currencies: List?)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/CurrencyData.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/CurrencyData.kt
new file mode 100644
index 000000000..42c85fdcb
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/CurrencyData.kt
@@ -0,0 +1,24 @@
+package co.nilin.opex.api.core.inout
+
+
+import java.math.BigDecimal
+import java.util.*
+
+data class CurrencyData(
+ var symbol: String,
+ var uuid: String? = UUID.randomUUID().toString(),
+ var name: String,
+ var precision: BigDecimal,
+ var title: String? = null,
+ var alias: String? = null,
+ var icon: String? = null,
+ var isTransitive: Boolean? = false,
+ var isActive: Boolean? = true,
+ var sign: String? = null,
+ var description: String? = null,
+ var shortDescription: String? = null,
+ var externalUrl: String? = null,
+ var order: Int? = null,
+ var maxOrder : BigDecimal? = null,
+
+ )
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositHistoryResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositHistoryResponse.kt
new file mode 100644
index 000000000..3dd89eec2
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositHistoryResponse.kt
@@ -0,0 +1,30 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+import java.util.*
+
+data class DepositHistoryResponse(
+ val id: Long,
+ val uuid: String,
+ val currency: String,
+ val amount: BigDecimal,
+ val network: String?,
+ val note: String?,
+ val transactionRef: String?,
+ val sourceAddress: String?,
+ val status: DepositStatus,
+ val type: DepositType,
+ val attachment: String?,
+ val createDate: Date?,
+ val transferMethod: TransferMethod?
+)
+
+enum class DepositType {
+
+ ON_CHAIN, OFF_CHAIN
+}
+
+enum class DepositStatus {
+
+ PROCESSING, DONE, INVALID
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/GatewayCommand.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/GatewayCommand.kt
new file mode 100644
index 000000000..5089471da
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/GatewayCommand.kt
@@ -0,0 +1,90 @@
+package co.nilin.opex.api.core.inout
+
+import com.fasterxml.jackson.annotation.JsonSubTypes
+import com.fasterxml.jackson.annotation.JsonTypeInfo
+import java.math.BigDecimal
+import java.util.*
+
+enum class GatewayType() {
+ OnChain, OffChain
+}
+
+@JsonTypeInfo(
+ use = JsonTypeInfo.Id.NAME,
+ include = JsonTypeInfo.As.PROPERTY,
+ property = "type"
+)
+@JsonSubTypes(
+ JsonSubTypes.Type(value = OffChainGatewayCommand::class, name = "OffChain"),
+ JsonSubTypes.Type(value = OnChainGatewayCommand::class, name = "OnChain"),
+)
+open abstract class CurrencyGatewayCommand(
+ open var currencySymbol: String? = null,
+ open var gatewayUuid: String? = UUID.randomUUID().toString(),
+ open var isActive: Boolean?,
+ open var withdrawFee: BigDecimal? = BigDecimal.ZERO,
+ open var withdrawAllowed: Boolean? = true,
+ open var depositAllowed: Boolean? = true,
+ open var depositMin: BigDecimal? = BigDecimal.ZERO,
+ open var depositMax: BigDecimal? = BigDecimal.ZERO,
+ open var withdrawMin: BigDecimal? = BigDecimal.ZERO,
+ open var withdrawMax: BigDecimal? = BigDecimal.ZERO,
+)
+
+data class OffChainGatewayCommand(
+ var transferMethod: TransferMethod,
+ override var currencySymbol: String? = null,
+ override var gatewayUuid: String? = UUID.randomUUID().toString(),
+ override var isActive: Boolean? = true,
+ override var withdrawFee: BigDecimal? = BigDecimal.ZERO,
+ override var withdrawAllowed: Boolean? = true,
+ override var depositAllowed: Boolean? = true,
+ override var depositMin: BigDecimal? = BigDecimal.ZERO,
+ override var depositMax: BigDecimal? = BigDecimal.ZERO,
+ override var withdrawMin: BigDecimal? = BigDecimal.ZERO,
+ override var withdrawMax: BigDecimal? = BigDecimal.ZERO,
+) : CurrencyGatewayCommand(
+ currencySymbol,
+ gatewayUuid,
+ isActive,
+ withdrawFee,
+ withdrawAllowed,
+ depositAllowed,
+ depositMin,
+ depositMax,
+ withdrawMin,
+ withdrawMax
+)
+
+data class OnChainGatewayCommand(
+
+ var implementationSymbol: String? = null,
+ var tokenName: String? = null,
+ var tokenAddress: String? = null,
+ var isToken: Boolean? = false,
+ var decimal: Int,
+ var chain: String,
+ override var currencySymbol: String? = null,
+ override var gatewayUuid: String? = UUID.randomUUID().toString(),
+ override var isActive: Boolean? = true,
+ override var withdrawFee: BigDecimal? = BigDecimal.ZERO,
+ override var withdrawAllowed: Boolean? = true,
+ override var depositAllowed: Boolean? = true,
+ override var depositMin: BigDecimal? = BigDecimal.ZERO,
+ override var depositMax: BigDecimal? = BigDecimal.ZERO,
+ override var withdrawMin: BigDecimal? = BigDecimal.ZERO,
+ override var withdrawMax: BigDecimal? = BigDecimal.ZERO,
+) : CurrencyGatewayCommand(
+ currencySymbol,
+ gatewayUuid,
+ isActive,
+ withdrawFee,
+ withdrawAllowed,
+ depositAllowed,
+ depositMin,
+ depositMax,
+ withdrawMin,
+ withdrawMax
+)
+
+
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderData.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderData.kt
new file mode 100644
index 000000000..c451aace9
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderData.kt
@@ -0,0 +1,22 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+import java.util.*
+
+data class OrderData(
+ val symbol: String,
+ val orderId: Long,
+ val orderType: MatchingOrderType,
+ val side: OrderDirection,
+ val price: BigDecimal,
+ val quantity: BigDecimal,
+ val quoteQuantity: BigDecimal,
+ val executedQuantity: BigDecimal,
+ val takerFee: BigDecimal,
+ val makerFee: BigDecimal,
+ val status: Int,
+ val appearance: Int,
+ val createDate: LocalDateTime,
+ val updateDate: LocalDateTime,
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderEnums.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderEnums.kt
index aba4d3351..bac8c0752 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderEnums.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderEnums.kt
@@ -6,16 +6,42 @@ enum class TimeInForce {
FOK, //Fill or Kill, An order will expire if the full order cannot be filled upon execution.
}
-enum class OrderStatus(val code: Int) {
+enum class OrderStatus(val code: Int, val orderOfAppearance: Int) {
- REQUESTED(0),
- NEW(1), //The order has been accepted by the engine.
- PARTIALLY_FILLED(4), //A part of the order has been filled.
- FILLED(5), //The order has been completed.
- CANCELED(2), //The order has been canceled by the user.
- REJECTED(3), //The order was not accepted by the engine and not processed.
- EXPIRED(6); //The order was canceled according to the order type's rules (e.g. LIMIT FOK orders with no fill, LIMIT IOC or MARKET orders that partially fill) or by the exchange, (e.g. orders canceled during liquidation, orders canceled during maintenance)
+ REQUESTED(0, 0),
+ NEW(1, 1), //The order has been accepted by the engine.
+ PARTIALLY_FILLED(4, 2), //A part of the order has been filled.
+ FILLED(5, 3), //The order has been completed.
+ CANCELED(2, 3), //The order has been canceled by the user.
+ REJECTED(3, 3), //The order was not accepted by the engine and not processed.
+ EXPIRED(
+ 6,
+ 3
+ ); //The order was canceled according to the order type's rules (e.g. LIMIT FOK orders with no fill, LIMIT IOC or MARKET orders that partially fill) or by the exchange, (e.g. orders canceled during liquidation, orders canceled during maintenance)
+ fun comesBefore(status: OrderStatus?): Boolean {
+ if (status == null)
+ return false
+ return orderOfAppearance < status.orderOfAppearance
+ }
+
+ fun comesAfter(status: OrderStatus?): Boolean {
+ if (status == null)
+ return false
+ return orderOfAppearance > status.orderOfAppearance
+ }
+
+ fun isOpenOrder(): Boolean {
+ return this == NEW || this == PARTIALLY_FILLED
+ }
+
+ companion object {
+ fun fromCode(code: Int?): OrderStatus? {
+ if (code == null)
+ return null
+ return values().find { it.code == code }
+ }
+ }
fun isWorking(): Boolean {
return listOf(NEW, PARTIALLY_FILLED).contains(this)
}
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairConfig.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairConfig.kt
new file mode 100644
index 000000000..d1741a047
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairConfig.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+
+class PairConfig(
+ val pair: String,
+ val leftSideWalletSymbol: String,
+ val rightSideWalletSymbol: String,
+ val leftSideFraction: BigDecimal,
+ val rightSideFraction: BigDecimal
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairConfigResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairConfigResponse.kt
new file mode 100644
index 000000000..da44479aa
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairConfigResponse.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+
+data class PairConfigResponse(
+ val pair: String,
+ val leftSideWalletSymbol: String,
+ val rightSideWalletSymbol: String,
+ val leftSideFraction: BigDecimal,
+ val rightSideFraction: BigDecimal
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairInfoResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairInfoResponse.kt
index f7e3fd12c..d6c00fe0e 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairInfoResponse.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairInfoResponse.kt
@@ -4,8 +4,10 @@ import java.math.BigDecimal
data class PairInfoResponse(
val pair: String,
- val leftSideWalletSymbol: String,
- val rightSideWalletSymbol: String,
- val leftSideFraction: BigDecimal,
- val rightSideFraction: BigDecimal
+ val baseAsset: String,
+ val quoteAsset: String,
+ val isAvailable: Boolean,
+ val minOrder : BigDecimal,
+ val maxOrder : BigDecimal,
+ val orderTypes : String,
)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairSetting.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairSetting.kt
new file mode 100644
index 000000000..b3d7d5946
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairSetting.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+class PairSetting(
+ val pair: String,
+ val isAvailable: Boolean,
+ val minOrder : BigDecimal,
+ val maxOrder : BigDecimal,
+ val orderTypes : String,
+ val updateDate: LocalDateTime? = null,
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/QuoteCurrency.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/QuoteCurrency.kt
new file mode 100644
index 000000000..0ae3a77ff
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/QuoteCurrency.kt
@@ -0,0 +1,9 @@
+package co.nilin.opex.api.core.inout
+
+import java.time.LocalDateTime
+
+data class QuoteCurrency(
+ val currency: String,
+ val isActive: Boolean = false,
+ var lastUpdateDate: LocalDateTime = LocalDateTime.now(),
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/RequestDepositBody.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/RequestDepositBody.kt
new file mode 100644
index 000000000..3168eb16b
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/RequestDepositBody.kt
@@ -0,0 +1,14 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+
+data class RequestDepositBody(
+ val symbol: String,
+ val receiverUuid: String,
+ val receiverWalletType: WalletType,
+ val amount: BigDecimal,
+ val description: String?,
+ val transferRef: String?,
+ val gatewayUuid: String?,
+ val chain: String?,
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/RequestWithdrawBody.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/RequestWithdrawBody.kt
new file mode 100644
index 000000000..e2a41b977
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/RequestWithdrawBody.kt
@@ -0,0 +1,14 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+
+data class RequestWithdrawBody(
+ val currency: String,
+ val amount: BigDecimal,
+ val destSymbol: String?,
+ val destAddress: String,
+ val destNetwork: String?,
+ val destNote: String?,
+ val description: String?,
+ val gatewayUuid: String?
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/SubmitVoucherResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/SubmitVoucherResponse.kt
new file mode 100644
index 000000000..73f2ba3c8
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/SubmitVoucherResponse.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+
+data class SubmitVoucherResponse(
+ val amount: BigDecimal,
+ val currency: String,
+ var issuer: String?,
+ var description: String? = null
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/SwapResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/SwapResponse.kt
new file mode 100644
index 000000000..efea1245f
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/SwapResponse.kt
@@ -0,0 +1,17 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+data class SwapResponse(
+ var reserveNumber: String,
+ var sourceSymbol: String,
+ var destSymbol: String,
+ var uuid: String,
+ var sourceAmount: BigDecimal,
+ var reservedDestAmount: BigDecimal,
+ var reserveDate: LocalDateTime? = LocalDateTime.now(),
+ var expDate: LocalDateTime? = null,
+ var status: ReservedStatus? = null,
+ val rate: BigDecimal? = null,
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/Trade.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/Trade.kt
index 6212380ba..31ca765d9 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/Trade.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/Trade.kt
@@ -1,6 +1,7 @@
package co.nilin.opex.api.core.inout
import java.math.BigDecimal
+import java.time.LocalDateTime
import java.util.*
data class Trade(
@@ -12,7 +13,7 @@ data class Trade(
val quoteQuantity: BigDecimal,
val commission: BigDecimal,
val commissionAsset: String,
- val time: Date,
+ val time: LocalDateTime,
val isBuyer: Boolean,
val isMaker: Boolean,
val isBestMatch: Boolean,
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransactionHistoryResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransactionHistoryResponse.kt
deleted file mode 100644
index 5b03de954..000000000
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransactionHistoryResponse.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package co.nilin.opex.api.core.inout
-
-import java.math.BigDecimal
-
-data class TransactionHistoryResponse(
- val id: Long,
- val currency: String,
- val amount: BigDecimal,
- val description: String?,
- val ref: String?,
- val date: Long
-)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransactionSummary.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransactionSummary.kt
new file mode 100644
index 000000000..d0494d022
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransactionSummary.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+
+data class TransactionSummary(
+ val currency: String,
+ val amount: BigDecimal,
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransferMethod.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransferMethod.kt
new file mode 100644
index 000000000..3b88d5ae0
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransferMethod.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.api.core.inout
+
+enum class TransferMethod {
+ CARD, SHEBA, IPG, EXCHANGE , MANUALLY , VOUCHER
+}
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransferResult.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransferResult.kt
new file mode 100644
index 000000000..f6a3657b6
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransferResult.kt
@@ -0,0 +1,15 @@
+package co.nilin.opex.api.core.inout
+
+data class TransferResult(
+ val date: Long,
+ val sourceUuid: String,
+ val sourceWalletType: WalletType,
+ val sourceBalanceBeforeAction: Amount,
+ val sourceBalanceAfterAction: Amount,
+ val amount: Amount,
+ val destUuid: String,
+ val destWalletType: WalletType,
+ val receivedAmount: Amount,
+ val sourceWallet: Long? = null,
+ val destWallet: Long? = null
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionCategory.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionCategory.kt
new file mode 100644
index 000000000..690a0c411
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionCategory.kt
@@ -0,0 +1,17 @@
+package co.nilin.opex.api.core.inout
+
+enum class UserTransactionCategory {
+
+ TRADE,
+ DEPOSIT,
+ DEPOSIT_TO, // for admin using DEPOSIT_MANUALLY
+ WITHDRAW_FROM, // for admin using DEPOSIT_MANUALLY
+ WITHDRAW,
+ FEE,
+ SWAP,
+ REFERRAL_COMMISSION,
+ REFERRAL_KYC_REWARD,
+ REFERENT_COMMISSION,
+ KYC_ACCEPTED_REWARD,
+ SYSTEM
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionHistory.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionHistory.kt
new file mode 100644
index 000000000..155dc7d2f
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionHistory.kt
@@ -0,0 +1,15 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+data class UserTransactionHistory(
+ val id: String,
+ val userId: String,
+ val currency: String,
+ val balance: BigDecimal,
+ val balanceChange: BigDecimal,
+ val category: UserTransactionCategory,
+ val description: String?,
+ val date: LocalDateTime
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionRequest.kt
new file mode 100644
index 000000000..814642140
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionRequest.kt
@@ -0,0 +1,19 @@
+package co.nilin.opex.api.core.inout
+
+data class UserTransactionRequest(
+ val userId: String? = null,
+ val currency: String?,
+ val sourceSymbol: String?,
+ val destSymbol: String?,
+ val category: UserTransactionCategory?,
+ val startTime: Long? = null,
+ val endTime: Long? = null,
+ val limit: Int? = 10,
+ val offset: Int? = 0,
+ val ascendingByTime: Boolean = false,
+ val status: ReservedStatus? = ReservedStatus.Committed
+)
+
+enum class ReservedStatus {
+ Created, Expired, Committed,
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WalletType.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WalletType.kt
new file mode 100644
index 000000000..adc7a6b4c
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WalletType.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.api.core.inout
+
+enum class WalletType {
+
+ MAIN, EXCHANGE, CASHOUT
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawActionResult.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawActionResult.kt
new file mode 100644
index 000000000..c53810e10
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawActionResult.kt
@@ -0,0 +1,4 @@
+package co.nilin.opex.api.core.inout
+
+class WithdrawActionResult(val withdrawId: Long, val status: WithdrawStatus) {
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawHistoryResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawHistoryResponse.kt
index 3d0d6607a..83b3cdd69 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawHistoryResponse.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawHistoryResponse.kt
@@ -1,14 +1,14 @@
package co.nilin.opex.api.core.inout
import java.math.BigDecimal
+import java.time.LocalDateTime
data class WithdrawHistoryResponse(
- val withdrawId: Long?,
+ val withdrawId: Long,
val uuid: String,
val amount: BigDecimal,
val currency: String,
- val acceptedFee: BigDecimal,
- val appliedFee: BigDecimal?,
+ val appliedFee: BigDecimal,
val destAmount: BigDecimal?,
val destSymbol: String?,
val destAddress: String?,
@@ -16,7 +16,15 @@ data class WithdrawHistoryResponse(
var destNote: String?,
var destTransactionRef: String?,
val statusReason: String?,
- val status: String,
- val createDate: Long,
- val acceptDate: Long?
+ val status: WithdrawStatus,
+ var applicator: String?,
+ var withdrawType: WithdrawType,
+ var attachment: String?,
+ val createDate: LocalDateTime,
+ val lastUpdateDate: LocalDateTime?,
+ var transferMethod: TransferMethod?,
)
+
+enum class WithdrawType {
+ CARD_TO_CARD, SHEBA, ON_CHAIN , OFF_CHAIN
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawResponse.kt
new file mode 100644
index 000000000..6ae3caf87
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawResponse.kt
@@ -0,0 +1,26 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+class WithdrawResponse(
+ val withdrawId: Long,
+ val uuid: String,
+ val amount: BigDecimal,
+ val currency: String,
+ val appliedFee: BigDecimal,
+ val destAmount: BigDecimal?,
+ val destSymbol: String?,
+ val destAddress: String?,
+ val destNetwork: String?,
+ var destNote: String?,
+ var destTransactionRef: String?,
+ val statusReason: String?,
+ val status: WithdrawStatus,
+ var applicator: String?,
+ var withdrawType: WithdrawType,
+ var attachment: String?,
+ val createDate: LocalDateTime,
+ val lastUpdateDate: LocalDateTime?,
+ var transferMethod: TransferMethod?,
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawStatus.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawStatus.kt
new file mode 100644
index 000000000..b063165f1
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawStatus.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.api.core.inout
+
+enum class WithdrawStatus {
+
+ CREATED,
+ PROCESSING,
+ CANCELED,
+ REJECTED,
+ DONE
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AccountantProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AccountantProxy.kt
index c6a32e2ab..6885d6c23 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AccountantProxy.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AccountantProxy.kt
@@ -1,11 +1,11 @@
package co.nilin.opex.api.core.spi
import co.nilin.opex.api.core.inout.PairFeeResponse
-import co.nilin.opex.api.core.inout.PairInfoResponse
+import co.nilin.opex.api.core.inout.PairConfigResponse
interface AccountantProxy {
- suspend fun getPairConfigs(): List
+ suspend fun getPairConfigs(): List
suspend fun getFeeConfigs(): List
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/BlockchainGatewayProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/BlockchainGatewayProxy.kt
index cd99d1064..abc249a26 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/BlockchainGatewayProxy.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/BlockchainGatewayProxy.kt
@@ -10,6 +10,7 @@ interface BlockchainGatewayProxy {
suspend fun getDepositDetails(refs: List): List
- suspend fun getCurrencyImplementations(currency: String? = null): List
+// suspend fun getCurrencyImplementations(currency: String? = null): List
+
}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MarketDataProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MarketDataProxy.kt
index 00b3d2abf..f58daca30 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MarketDataProxy.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MarketDataProxy.kt
@@ -2,7 +2,6 @@ package co.nilin.opex.api.core.spi
import co.nilin.opex.api.core.inout.*
import co.nilin.opex.common.utils.Interval
-import java.time.LocalDateTime
interface MarketDataProxy {
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MarketUserDataProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MarketUserDataProxy.kt
index 7a4e6f59b..828a0c210 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MarketUserDataProxy.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MarketUserDataProxy.kt
@@ -1,6 +1,9 @@
package co.nilin.opex.api.core.spi
+import co.nilin.opex.api.core.inout.MatchingOrderType
import co.nilin.opex.api.core.inout.Order
+import co.nilin.opex.api.core.inout.OrderData
+import co.nilin.opex.api.core.inout.OrderDirection
import co.nilin.opex.api.core.inout.Trade
import java.security.Principal
import java.util.*
@@ -27,4 +30,42 @@ interface MarketUserDataProxy {
endTime: Date?,
limit: Int?
): List
+
+ suspend fun getOrderHistory(
+ uuid : String,
+ symbol: String?,
+ startTime: Long?,
+ endTime: Long?,
+ orderType: MatchingOrderType?,
+ direction: OrderDirection?,
+ limit: Int?,
+ offset: Int?,
+ ): List
+
+ suspend fun getOrderHistoryCount(
+ uuid : String,
+ symbol: String?,
+ startTime: Long?,
+ endTime: Long?,
+ orderType: MatchingOrderType?,
+ direction: OrderDirection?,
+ ): Long
+
+ suspend fun getTradeHistory(
+ uuid : String,
+ symbol: String?,
+ startTime: Long?,
+ endTime: Long?,
+ direction: OrderDirection?,
+ limit: Int?,
+ offset: Int?,
+ ): List
+
+ suspend fun getTradeHistoryCount(
+ uuid : String,
+ symbol: String?,
+ startTime: Long?,
+ endTime: Long?,
+ direction: OrderDirection?,
+ ): Long
}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MatchingGatewayProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MatchingGatewayProxy.kt
index 906aec319..c615e3d3b 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MatchingGatewayProxy.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MatchingGatewayProxy.kt
@@ -4,6 +4,8 @@ import co.nilin.opex.api.core.inout.MatchConstraint
import co.nilin.opex.api.core.inout.MatchingOrderType
import co.nilin.opex.api.core.inout.OrderDirection
import co.nilin.opex.api.core.inout.OrderSubmitResult
+import co.nilin.opex.api.core.inout.PairConfigResponse
+import co.nilin.opex.api.core.inout.PairSetting
import java.math.BigDecimal
interface MatchingGatewayProxy {
@@ -27,4 +29,6 @@ interface MatchingGatewayProxy {
symbol: String,
token: String?
): OrderSubmitResult?
+
+ suspend fun getPairSettings(): List
}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/WalletProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/WalletProxy.kt
index 85a628217..7f83cb162 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/WalletProxy.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/WalletProxy.kt
@@ -1,9 +1,6 @@
package co.nilin.opex.api.core.spi
-import co.nilin.opex.api.core.inout.OwnerLimitsResponse
-import co.nilin.opex.api.core.inout.TransactionHistoryResponse
-import co.nilin.opex.api.core.inout.Wallet
-import co.nilin.opex.api.core.inout.WithdrawHistoryResponse
+import co.nilin.opex.api.core.inout.*
interface WalletProxy {
@@ -14,26 +11,118 @@ interface WalletProxy {
suspend fun getOwnerLimits(uuid: String?, token: String?): OwnerLimitsResponse
suspend fun getDepositTransactions(
- uuid: String,
- token: String?,
- coin: String?,
- startTime: Long?,
- endTime: Long?,
- limit: Int,
- offset: Int,
- ascendingByTime: Boolean?
- ): List
+ uuid: String,
+ token: String,
+ currency: String?,
+ startTime: Long?,
+ endTime: Long?,
+ limit: Int,
+ offset: Int,
+ ascendingByTime: Boolean?,
+ ): List
+
+ suspend fun getDepositTransactionsCount(
+ uuid: String,
+ token: String,
+ currency: String?,
+ startTime: Long?,
+ endTime: Long?,
+ ): Long
suspend fun getWithdrawTransactions(
- uuid: String,
- token: String?,
- coin: String?,
- startTime: Long?,
- endTime: Long?,
- limit: Int,
- offset: Int,
- ascendingByTime: Boolean?
+ uuid: String,
+ token: String,
+ currency: String?,
+ startTime: Long?,
+ endTime: Long?,
+ limit: Int,
+ offset: Int,
+ ascendingByTime: Boolean?,
): List
+ suspend fun getWithdrawTransactionsCount(
+ uuid: String,
+ token: String,
+ currency: String?,
+ startTime: Long?,
+ endTime: Long?,
+ ): Long
+
+ suspend fun getTransactions(
+ uuid: String,
+ token: String,
+ currency: String?,
+ category: UserTransactionCategory?,
+ startTime: Long?,
+ endTime: Long?,
+ limit: Int,
+ offset: Int,
+ ascendingByTime: Boolean?,
+ ): List
+
+ suspend fun getTransactionsCount(
+ uuid: String,
+ token: String,
+ currency: String?,
+ category: UserTransactionCategory?,
+ startTime: Long?,
+ endTime: Long?,
+ ): Long
+
+ suspend fun getGateWays(
+ includeOffChainGateways: Boolean,
+ includeOnChainGateways: Boolean,
+ ): List
+
+ suspend fun getCurrencies(): List
+
+ suspend fun getUserTradeTransactionSummary(
+ uuid: String,
+ token: String,
+ startTime: Long?,
+ endTime: Long?,
+ limit: Int?,
+ ): List
+
+ suspend fun getUserDepositSummary(
+ uuid: String,
+ token: String,
+ startTime: Long?,
+ endTime: Long?,
+ limit: Int?,
+ ): List
+
+ suspend fun getUserWithdrawSummary(
+ uuid: String,
+ token: String,
+ startTime: Long?,
+ endTime: Long?,
+ limit: Int?,
+ ): List
+
+ suspend fun deposit(
+ request: RequestDepositBody
+ ): TransferResult?
+
+ suspend fun requestWithdraw(
+ token: String,
+ request: RequestWithdrawBody
+ ): WithdrawActionResult
+
+ suspend fun cancelWithdraw(
+ token: String,
+ withdrawId: Long
+ ): Void?
+
+ suspend fun findWithdraw(
+ token: String,
+ withdrawId: Long
+ ): WithdrawResponse
+
+ suspend fun submitVoucher(code: String, token: String): SubmitVoucherResponse
+
+ suspend fun getQuoteCurrencies(): List
+ suspend fun getSwapTransactions(token: String, request: UserTransactionRequest): List
+ suspend fun getSwapTransactionsCount(token: String, request: UserTransactionRequest): Long
}
\ No newline at end of file
diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/ErrorHandlerConfig.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/ErrorHandlerConfig.kt
index bc321e9e0..48154d4a7 100644
--- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/ErrorHandlerConfig.kt
+++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/ErrorHandlerConfig.kt
@@ -3,7 +3,7 @@ package co.nilin.opex.api.ports.binance.config
import co.nilin.opex.utility.error.EnableOpexErrorHandler
import org.springframework.context.annotation.Configuration
-@Configuration
+@Configuration("binanceErrorHandlerConfig")
@EnableOpexErrorHandler
class ErrorHandlerConfig {
diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/RestConfig.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/RestConfig.kt
index 1357932d2..3fc377c72 100644
--- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/RestConfig.kt
+++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/RestConfig.kt
@@ -7,7 +7,7 @@ import org.springframework.format.Formatter
import org.springframework.web.server.WebFilter
import java.util.*
-@Configuration
+@Configuration("binanceRestConfig")
class RestConfig {
@Bean
fun dateFormatter(): Formatter? {
diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/SecurityConfig.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/SecurityConfig.kt
index c818de467..6e504eddd 100644
--- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/SecurityConfig.kt
+++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/SecurityConfig.kt
@@ -1,10 +1,11 @@
package co.nilin.opex.api.ports.binance.config
import co.nilin.opex.api.core.spi.APIKeyFilter
+import co.nilin.opex.common.security.ReactiveCustomJwtConverter
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
-import org.springframework.security.config.Customizer
+import org.springframework.http.HttpMethod
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.SecurityWebFiltersOrder
import org.springframework.security.config.web.server.ServerHttpSecurity
@@ -15,7 +16,7 @@ import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.server.WebFilter
@EnableWebFluxSecurity
-@Configuration
+@Configuration("binanceSecurityConfig")
class SecurityConfig(
private val webClient: WebClient,
private val apiKeyFilter: APIKeyFilter,
@@ -39,11 +40,21 @@ class SecurityConfig(
.pathMatchers("/v3/klines").permitAll()
.pathMatchers("/socket").permitAll()
.pathMatchers("/v1/landing/**").permitAll()
- .pathMatchers("/**").hasAuthority("SCOPE_trust")
+ .pathMatchers(HttpMethod.POST, "/v3/order").hasAuthority("PERM_order:write")
+ .pathMatchers(HttpMethod.DELETE, "/v3/order").hasAuthority("PERM_order:write")
+
+ // Opex endpoints
+ .pathMatchers("/opex/v1/deposit/**").hasAuthority("DEPOSIT_deposit:write")
+ .pathMatchers(HttpMethod.POST, "/opex/v1/order").hasAuthority("PERM_order:write")
+ .pathMatchers(HttpMethod.PUT, "/opex/v1/order").hasAuthority("PERM_order:write")
+ .pathMatchers(HttpMethod.POST, "/opex/v1/withdraw").hasAuthority("PERM_withdraw:write")
+ .pathMatchers(HttpMethod.PUT, "/opex/v1/withdraw").hasAuthority("PERM_withdraw:write")
+ .pathMatchers("/opex/v1/voucher").hasAuthority("PERM_voucher:submit")
+ .pathMatchers("/opex/v1/market/**").permitAll()
.anyExchange().authenticated()
}
.addFilterBefore(apiKeyFilter as WebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
- .oauth2ResourceServer { it.jwt(Customizer.withDefaults()) }
+ .oauth2ResourceServer { it.jwt { jwt -> jwt.jwtAuthenticationConverter(ReactiveCustomJwtConverter()) } }
.build()
}
@@ -51,7 +62,7 @@ class SecurityConfig(
@Throws(Exception::class)
fun reactiveJwtDecoder(): ReactiveJwtDecoder? {
return NimbusReactiveJwtDecoder.withJwkSetUri(jwkUrl)
- .webClient(webClient)
+ .webClient(WebClient.create())
.build()
}
}
diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/WebClientConfig.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/WebClientConfig.kt
index d07ec5c8c..63a5e11aa 100644
--- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/WebClientConfig.kt
+++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/WebClientConfig.kt
@@ -10,7 +10,7 @@ import reactor.netty.http.client.HttpClient
import reactor.netty.resources.ConnectionProvider
import java.time.Duration
-@Configuration
+@Configuration("binanceWebClientConfig")
class WebClientConfig {
@Bean
diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/MarketController.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/MarketController.kt
index d4dc0b3d5..c83f41bac 100644
--- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/MarketController.kt
+++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/MarketController.kt
@@ -19,7 +19,7 @@ import java.math.BigDecimal
import java.security.Principal
import java.time.ZoneId
-@RestController
+@RestController("binanceMarketController")
class MarketController(
private val accountantProxy: AccountantProxy,
private val marketDataProxy: MarketDataProxy,
@@ -176,28 +176,28 @@ class MarketController(
}
// Custom service
- @GetMapping("/v3/currencyInfo")
- suspend fun getNetworks(@RequestParam(required = false) currency: String?): List {
- return blockchainGatewayProxy.getCurrencyImplementations(currency)
- .groupBy { it.currency }
- .toList()
- .map { pair ->
- CurrencyNetworkResponse(
- pair.first.symbol,
- pair.first.name,
- pair.second.map {
- CurrencyNetwork(
- it.chain.name,
- it.implCurrency.symbol,
- it.withdrawMin,
- it.withdrawFee,
- it.token,
- it.tokenAddress
- )
- }
- )
- }
- }
+// @GetMapping("/v3/currencyInfo")
+// suspend fun getNetworks(@RequestParam(required = false) currency: String?): List {
+// return blockchainGatewayProxy.getCurrencyImplementations(currency)
+// .groupBy { it.currency }
+// .toList()
+// .map { pair ->
+// CurrencyNetworkResponse(
+// pair.first.symbol,
+// pair.first.name,
+// pair.second.map {
+// CurrencyNetwork(
+// it.chain.name,
+// it.implCurrency.symbol,
+// it.withdrawMin,
+// it.withdrawFee,
+// it.token,
+// it.tokenAddress
+// )
+// }
+// )
+// }
+// }
// Custom service
@GetMapping("/v3/currencyInfo/quotes")
diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/WalletController.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/WalletController.kt
index 0b21b8399..2a857546c 100644
--- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/WalletController.kt
+++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/WalletController.kt
@@ -1,27 +1,21 @@
package co.nilin.opex.api.ports.binance.controller
-import co.nilin.opex.api.core.inout.DepositDetails
-import co.nilin.opex.api.core.inout.TransactionHistoryResponse
import co.nilin.opex.api.core.spi.*
-import co.nilin.opex.api.ports.binance.data.*
+import co.nilin.opex.api.ports.binance.data.AssetResponse
+import co.nilin.opex.api.ports.binance.data.AssetsEstimatedValue
+import co.nilin.opex.api.ports.binance.data.AssignAddressResponse
+import co.nilin.opex.api.ports.binance.data.PairFeeResponse
import co.nilin.opex.api.ports.binance.util.jwtAuthentication
import co.nilin.opex.api.ports.binance.util.tokenValue
import co.nilin.opex.common.OpexError
-import co.nilin.opex.common.utils.Interval
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
import org.springframework.web.bind.annotation.GetMapping
-import org.springframework.web.bind.annotation.PostMapping
-import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.math.BigDecimal
-import java.time.Instant
-import java.time.LocalDateTime
-import java.time.ZoneId
-import java.util.*
-@RestController
+@RestController("walletBinanceController")
class WalletController(
private val walletProxy: WalletProxy,
private val symbolMapper: SymbolMapper,
@@ -48,153 +42,151 @@ class WalletController(
return AssignAddressResponse(address[0].address, coin, network, "", "")
}
- @GetMapping("/v1/capital/deposit/hisrec")
- suspend fun getDepositTransactions(
- @RequestParam(required = false)
- coin: String?,
- @RequestParam("network", required = false)
- status: Int?,
- @RequestParam(required = false)
- startTime: Long?,
- @RequestParam(required = false)
- endTime: Long?,
- @RequestParam(required = false)
- offset: Int?,
- @RequestParam(required = false)
- limit: Int?,
- @RequestParam(required = false)
- recvWindow: Long?, //The value cannot be greater than 60000
- @RequestParam
- timestamp: Long,
- @RequestParam(required = false)
- ascendingByTime: Boolean? = false,
- @CurrentSecurityContext securityContext: SecurityContext
- ): List {
- val validLimit = limit ?: 1000
- val deposits = walletProxy.getDepositTransactions(
- securityContext.jwtAuthentication().name,
- securityContext.jwtAuthentication().tokenValue(),
- coin,
- startTime ?: null,
- endTime ?: null,
- if (validLimit > 1000 || validLimit < 1) 1000 else validLimit,
- offset ?: 0,
- ascendingByTime
- )
- if (deposits.isEmpty())
- return emptyList()
+// @GetMapping("/v1/capital/deposit/hisrec")
+// suspend fun getDepositTransactions(
+// @RequestParam(required = false)
+// coin: String?,
+// @RequestParam("network", required = false)
+// status: Int?,
+// @RequestParam(required = false)
+// startTime: Long?,
+// @RequestParam(required = false)
+// endTime: Long?,
+// @RequestParam(required = false)
+// offset: Int?,
+// @RequestParam(required = false)
+// limit: Int?,
+// @RequestParam(required = false)
+// recvWindow: Long?, //The value cannot be greater than 60000
+// @RequestParam
+// timestamp: Long,
+// @RequestParam(required = false)
+// ascendingByTime: Boolean? = false,
+// @CurrentSecurityContext securityContext: SecurityContext
+// ): List {
+// val validLimit = limit ?: 1000
+// val deposits = walletProxy.getDepositTransactions(
+// securityContext.jwtAuthentication().name,
+// securityContext.jwtAuthentication().tokenValue(),
+// coin,
+// startTime ?: null,
+// endTime ?: null,
+// if (validLimit > 1000 || validLimit < 1) 1000 else validLimit,
+// offset ?: 0,
+// ascendingByTime
+// )
+// if (deposits.isEmpty())
+// return emptyList()
+//
+// val details = bcGatewayProxy.getDepositDetails(deposits.filterNot { it.ref.isNullOrBlank() }.map { it.ref!! })
+// return matchDepositsAndDetails(deposits, details)
+// }
- val details = bcGatewayProxy.getDepositDetails(deposits.filterNot { it.ref.isNullOrBlank() }.map { it.ref!! })
- return matchDepositsAndDetails(deposits, details)
- }
-
- @GetMapping("/v1/capital/withdraw/history")
- suspend fun getWithdrawTransactions(
- @RequestParam(required = false)
- coin: String,
- @RequestParam(required = false)
- withdrawOrderId: String?,
- @RequestParam("status", required = false)
- withdrawStatus: Int?,
- @RequestParam(required = false)
- offset: Int?,
- @RequestParam(required = false)
- limit: Int?,
- @RequestParam(required = false)
- startTime: Long?,
- @RequestParam(required = false)
- endTime: Long?,
- @RequestParam(required = false)
- ascendingByTime: Boolean? = false,
- @RequestParam(required = false)
- recvWindow: Long?, //The value cannot be greater than 60000
- @RequestParam
- timestamp: Long,
- @CurrentSecurityContext securityContext: SecurityContext
- ): List {
- val validLimit = limit ?: 1000
- val response = walletProxy.getWithdrawTransactions(
- securityContext.jwtAuthentication().name,
- securityContext.jwtAuthentication().tokenValue(),
- coin,
- startTime ?: null,
- endTime ?: null,
- if (validLimit > 1000 || validLimit < 1) 1000 else validLimit,
- offset ?: 0,
- ascendingByTime
- )
- return response.map {
- val status = when (it.status) {
- "CREATED" -> 0
- "DONE" -> 1
- "REJECTED" -> 2
- else -> -1
- }
-
- WithdrawResponse(
- it.destAddress ?: "0x0",
- it.amount,
- LocalDateTime.ofInstant(Instant.ofEpochMilli(it.createDate), ZoneId.systemDefault())
- .toString()
- .replace("T", " "),
- it.destSymbol ?: "",
- it.withdrawId?.toString() ?: "",
- "",
- it.destNetwork ?: "",
- 1,
- status,
- it.appliedFee.toString(),
- 3,
- it.destTransactionRef ?: it.withdrawId.toString(),
- if (status == 1 && it.acceptDate != null) it.acceptDate!! else it.createDate
- )
- }
- }
+// @GetMapping("/v1/capital/withdraw/history")
+// suspend fun getWithdrawTransactions(
+// @RequestParam(required = false)
+// coin: String,
+// @RequestParam(required = false)
+// withdrawOrderId: String?,
+// @RequestParam("status", required = false)
+// withdrawStatus: Int?,
+// @RequestParam(required = false)
+// offset: Int?,
+// @RequestParam(required = false)
+// limit: Int?,
+// @RequestParam(required = false)
+// startTime: Long?,
+// @RequestParam(required = false)
+// endTime: Long?,
+// @RequestParam(required = false)
+// ascendingByTime: Boolean? = false,
+// @RequestParam(required = false)
+// recvWindow: Long?, //The value cannot be greater than 60000
+// @RequestParam
+// timestamp: Long,
+// @CurrentSecurityContext securityContext: SecurityContext
+// ): List {
+// val validLimit = limit ?: 1000
+// val response = walletProxy.getWithdrawTransactions(
+// securityContext.jwtAuthentication().name,
+// securityContext.jwtAuthentication().tokenValue(),
+// coin,
+// startTime ?: null,
+// endTime ?: null,
+// if (validLimit > 1000 || validLimit < 1) 1000 else validLimit,
+// offset ?: 0,
+// ascendingByTime
+// )
+// return response.map {
+// val status = when (it.status) {
+// "CREATED" -> 0
+// "DONE" -> 1
+// "REJECTED" -> 2
+// else -> -1
+// }
+//
+// WithdrawResponse(
+// it.destAddress ?: "0x0",
+// it.amount,
+// it.createDate,
+// it.destSymbol ?: "",
+// it.withdrawId?.toString() ?: "",
+// "",
+// it.destNetwork ?: "",
+// 1,
+// status,
+// it.appliedFee.toString(),
+// 3,
+// it.destTransactionRef ?: it.withdrawId.toString(),
+// if (status == 1 && it.acceptDate != null) it.acceptDate!! else it.createDate,
+// it.transferMethod
+// )
+// }
+// }
- @PostMapping("/v2/capital/withdraw/history")
- suspend fun getWithdrawTransactionsV2(
- @RequestBody withdrawRequest: WithDrawRequest,
- @CurrentSecurityContext securityContext: SecurityContext
- ): List {
- val validLimit = withdrawRequest.limit ?: 1000
- val response = walletProxy.getWithdrawTransactions(
- securityContext.jwtAuthentication().name,
- securityContext.jwtAuthentication().tokenValue(),
- withdrawRequest.coin,
- withdrawRequest.startTime ?: null,
- withdrawRequest.endTime ?: null,
- if (validLimit > 1000 || validLimit < 1) 1000 else validLimit,
- withdrawRequest.offset ?: 0,
- withdrawRequest.ascendingByTime
- )
- return response.map {
- val status = when (it.status) {
- "CREATED" -> 0
- "DONE" -> 1
- "REJECTED" -> 2
- else -> -1
- }
-
- WithdrawResponse(
- it.destAddress ?: "0x0",
- it.amount,
- LocalDateTime.ofInstant(Instant.ofEpochMilli(it.createDate), ZoneId.systemDefault())
- .toString()
- .replace("T", " "),
- it.destSymbol ?: "",
- it.withdrawId?.toString() ?: "",
- "",
- it.destNetwork ?: "",
- 1,
- status,
- it.appliedFee.toString(),
- 3,
- it.destTransactionRef ?: it.withdrawId.toString(),
- if (status == 1 && it.acceptDate != null) it.acceptDate!! else it.createDate
- )
- }
- }
+// @PostMapping("/v2/capital/withdraw/history")
+// suspend fun getWithdrawTransactionsV2(
+// @RequestBody withdrawRequest: WithDrawRequest,
+// @CurrentSecurityContext securityContext: SecurityContext
+// ): List {
+// val validLimit = withdrawRequest.limit ?: 1000
+// val response = walletProxy.getWithdrawTransactions(
+// securityContext.jwtAuthentication().name,
+// securityContext.jwtAuthentication().tokenValue(),
+// withdrawRequest.coin,
+// withdrawRequest.startTime ?: null,
+// withdrawRequest.endTime ?: null,
+// if (validLimit > 1000 || validLimit < 1) 1000 else validLimit,
+// withdrawRequest.offset ?: 0,
+// withdrawRequest.ascendingByTime
+// )
+// return response.map {
+// val status = when (it.status) {
+// "CREATED" -> 0
+// "DONE" -> 1
+// "REJECTED" -> 2
+// else -> -1
+// }
+//
+// WithdrawResponse(
+// it.destAddress ?: "0x0",
+// it.amount,
+// it.createDate,
+// it.destSymbol ?: "",
+// it.withdrawId?.toString() ?: "",
+// "",
+// it.destNetwork ?: "",
+// 1,
+// status,
+// it.appliedFee.toString(),
+// 3,
+// it.destTransactionRef ?: it.withdrawId.toString(),
+// if (status == 1 && it.acceptDate != null) it.acceptDate!! else it.createDate,
+// it.transferMethod
+// )
+// }
+// }
@GetMapping("/v1/asset/tradeFee")
suspend fun getPairFees(
@@ -303,30 +295,30 @@ class WalletController(
return AssetsEstimatedValue(value, quoteAsset.uppercase(), zeroAssets)
}
- private fun matchDepositsAndDetails(
- deposits: List,
- details: List
- ): List {
- val detailMap = details.associateBy { it.hash }
- return deposits.associateWith {
- detailMap[it.ref]
- }.mapNotNull { (deposit, detail) ->
- detail?.let {
- DepositResponse(
- deposit.amount,
- deposit.currency,
- detail.chain,
- 1,
- detail.address,
- null,
- deposit.ref ?: deposit.id.toString(),
- deposit.date,
- 1,
- "1/1",
- "1/1",
- deposit.date
- )
- }
- }
- }
+// private fun matchDepositsAndDetails(
+// deposits: List,
+// details: List
+// ): List {
+// val detailMap = details.associateBy { it.hash }
+// return deposits.associateWith {
+// detailMap[it.ref]
+// }.mapNotNull { (deposit, detail) ->
+// detail?.let {
+// DepositResponse(
+// deposit.amount,
+// deposit.currency,
+// detail.chain,
+// 1,
+// detail.address,
+// null,
+// deposit.ref ?: deposit.id.toString(),
+// deposit.date,
+// 1,
+// "1/1",
+// "1/1",
+// deposit.date
+// )
+// }
+// }
+// }
}
diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/data/NewOrderResponse.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/data/NewOrderResponse.kt
index 69bbe87a2..cc50e652f 100644
--- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/data/NewOrderResponse.kt
+++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/data/NewOrderResponse.kt
@@ -4,7 +4,6 @@ import co.nilin.opex.api.core.inout.OrderSide
import co.nilin.opex.api.core.inout.OrderStatus
import co.nilin.opex.api.core.inout.OrderType
import co.nilin.opex.api.core.inout.TimeInForce
-import co.nilin.opex.api.ports.binance.controller.AccountController
import com.fasterxml.jackson.annotation.JsonInclude
import java.math.BigDecimal
import java.util.*
diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/data/TradeResponse.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/data/TradeResponse.kt
index 05c2d28aa..5ef611cfa 100644
--- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/data/TradeResponse.kt
+++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/data/TradeResponse.kt
@@ -2,6 +2,7 @@ package co.nilin.opex.api.ports.binance.data
import com.fasterxml.jackson.annotation.JsonInclude
import java.math.BigDecimal
+import java.time.LocalDateTime
import java.util.*
@JsonInclude(JsonInclude.Include.NON_NULL)
@@ -15,7 +16,7 @@ data class TradeResponse(
val quoteQty: BigDecimal,
val commission: BigDecimal,
val commissionAsset: String,
- val time: Date,
+ val time: LocalDateTime,
val isBuyer: Boolean,
val isMaker: Boolean,
val isBestMatch: Boolean
diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/data/WithDrawRequest.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/data/WithDrawRequest.kt
index 4b32b5080..00cccfaa3 100644
--- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/data/WithDrawRequest.kt
+++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/data/WithDrawRequest.kt
@@ -1,18 +1,17 @@
package co.nilin.opex.api.ports.binance.data
import com.fasterxml.jackson.annotation.JsonProperty
-import org.springframework.web.bind.annotation.RequestParam
data class WithDrawRequest(
- var coin: String?,
- var withdrawOrderId: String?,
- @JsonProperty("status")
- var withdrawStatus: Int?,
- var offset: Int?,
- var limit: Int?,
- var startTime: Long?,
- var endTime: Long?,
- var ascendingByTime: Boolean? = false,
- var recvWindow: Long?, //The value cannot be greater than 60000
- var timestamp: Long,
+ var coin: String?,
+ var withdrawOrderId: String?,
+ @JsonProperty("status")
+ var withdrawStatus: Int?,
+ var offset: Int?,
+ var limit: Int?,
+ var startTime: Long?,
+ var endTime: Long?,
+ var ascendingByTime: Boolean? = false,
+ var recvWindow: Long?, //The value cannot be greater than 60000
+ var timestamp: Long,
)
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/config/ErrorHandlerConfig.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/config/ErrorHandlerConfig.kt
index 9a1e1afd5..d512dbc55 100644
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/config/ErrorHandlerConfig.kt
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/config/ErrorHandlerConfig.kt
@@ -3,7 +3,7 @@ package co.nilin.opex.api.ports.opex.config
import co.nilin.opex.utility.error.EnableOpexErrorHandler
import org.springframework.context.annotation.Configuration
-@Configuration
+@Configuration("opexErrorHandlerConfig")
@EnableOpexErrorHandler
class ErrorHandlerConfig {
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/config/RestConfig.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/config/RestConfig.kt
index 1416783c2..5fe320f8b 100644
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/config/RestConfig.kt
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/config/RestConfig.kt
@@ -7,7 +7,7 @@ import org.springframework.format.Formatter
import org.springframework.web.server.WebFilter
import java.util.*
-@Configuration
+@Configuration("opexRestConfig")
class RestConfig {
@Bean
fun dateFormatter(): Formatter? {
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/config/SecurityConfig.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/config/SecurityConfig.kt
index f42735a79..d74c85756 100644
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/config/SecurityConfig.kt
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/config/SecurityConfig.kt
@@ -1,57 +1,49 @@
package co.nilin.opex.api.ports.opex.config
import co.nilin.opex.api.core.spi.APIKeyFilter
-import org.springframework.beans.factory.annotation.Autowired
+import co.nilin.opex.common.security.ReactiveCustomJwtConverter
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Profile
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
-import org.springframework.security.config.web.server.SecurityWebFiltersOrder
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.web.reactive.function.client.WebClient
-import org.springframework.web.server.WebFilter
-
-@EnableWebFluxSecurity
-class SecurityConfig(private val webClient: WebClient) {
+//@EnableWebFluxSecurity
+//@EnableMethodSecurity
+//@Configuration("opexSecurityConfig")
+class SecurityConfig(
+ private val apiKeyFilter: APIKeyFilter,
@Value("\${app.auth.cert-url}")
- private lateinit var jwkUrl: String
-
- @Autowired
- private lateinit var apiKeyFilter: APIKeyFilter
+ private val jwkUrl: String
+) {
- @Bean
- fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain? {
- http.csrf().disable()
- .authorizeExchange()
- .pathMatchers("/actuator/**").permitAll()
- .pathMatchers("/swagger-ui/**").permitAll()
- .pathMatchers("/swagger-resources/**").permitAll()
- .pathMatchers("/v2/api-docs").permitAll()
- .pathMatchers("/v3/depth").permitAll()
- .pathMatchers("/v3/trades").permitAll()
- .pathMatchers("/v3/ticker/**").permitAll()
- .pathMatchers("/v3/exchangeInfo").permitAll()
- .pathMatchers("/v3/currencyInfo/**").permitAll()
- .pathMatchers("/v3/klines").permitAll()
- .pathMatchers("/socket").permitAll()
- .pathMatchers("/v1/landing/**").permitAll()
- .pathMatchers("/**").hasAuthority("SCOPE_trust")
- .anyExchange().authenticated()
- .and()
- .addFilterBefore(apiKeyFilter as WebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
- .oauth2ResourceServer()
- .jwt()
- return http.build()
+ //@Bean
+ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+ return http.csrf { it.disable() }
+ .authorizeExchange {
+ it.pathMatchers("/actuator/**").permitAll()
+ .pathMatchers("/swagger-ui/**").permitAll()
+ .pathMatchers("/opex/v1/market/**").permitAll()
+ .pathMatchers("/opex/v1/order/**").hasAuthority("PERM_order:write")
+ .pathMatchers("/**").hasAuthority("SCOPE_trust")
+ .anyExchange().authenticated()
+ }
+// .addFilterBefore(apiKeyFilter as WebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
+ .oauth2ResourceServer { it.jwt { jwt -> jwt.jwtAuthenticationConverter(ReactiveCustomJwtConverter()) } }
+ .build()
}
- @Bean
+ //@Bean
@Throws(Exception::class)
fun reactiveJwtDecoder(): ReactiveJwtDecoder? {
return NimbusReactiveJwtDecoder.withJwkSetUri(jwkUrl)
- .webClient(webClient)
+ .webClient(WebClient.builder().build())
.build()
}
}
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/config/WebClientConfig.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/config/WebClientConfig.kt
index 7da7c0aad..09ccc6f79 100644
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/config/WebClientConfig.kt
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/config/WebClientConfig.kt
@@ -1,26 +1,22 @@
package co.nilin.opex.api.ports.opex.config
-import org.springframework.cloud.client.ServiceInstance
-import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer
-import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction
+import org.springframework.cloud.client.loadbalancer.LoadBalanced
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
-import org.springframework.http.client.reactive.ReactorClientHttpConnector
import org.springframework.web.reactive.function.client.WebClient
-import org.zalando.logbook.Logbook
-import org.zalando.logbook.netty.LogbookClientHandler
-import reactor.netty.http.client.HttpClient
-@Configuration
+@Configuration("opexWebClientConfig")
class WebClientConfig {
@Bean
- fun webClient(loadBalancerFactory: ReactiveLoadBalancer.Factory, logbook: Logbook): WebClient {
- val client = HttpClient.create().doOnConnected { it.addHandlerLast(LogbookClientHandler(logbook)) }
+ @LoadBalanced
+ fun webClientBuilder(): WebClient.Builder {
return WebClient.builder()
- //.clientConnector(ReactorClientHttpConnector(client))
- .filter(ReactorLoadBalancerExchangeFilterFunction(loadBalancerFactory, emptyList()))
- .build()
+ }
+
+ @Bean
+ fun webClient(webclientBuilder: WebClient.Builder): WebClient {
+ return webclientBuilder.build()
}
}
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/AccountController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/AccountController.kt
deleted file mode 100644
index 2fef5ab05..000000000
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/AccountController.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package co.nilin.opex.api.ports.opex.controller
-
-import co.nilin.opex.api.core.inout.*
-import co.nilin.opex.api.core.spi.MarketUserDataProxy
-import co.nilin.opex.api.core.spi.MatchingGatewayProxy
-import co.nilin.opex.api.core.spi.SymbolMapper
-import co.nilin.opex.api.core.spi.WalletProxy
-import org.springframework.web.bind.annotation.*
-
-
-@RestController
-class AccountController(
-
-) {}
\ No newline at end of file
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/DepositController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/DepositController.kt
new file mode 100644
index 000000000..fd055219b
--- /dev/null
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/DepositController.kt
@@ -0,0 +1,19 @@
+package co.nilin.opex.api.ports.opex.controller
+
+import co.nilin.opex.api.core.inout.RequestDepositBody
+import co.nilin.opex.api.core.inout.TransferResult
+import co.nilin.opex.api.core.spi.WalletProxy
+import org.springframework.web.bind.annotation.PostMapping
+import org.springframework.web.bind.annotation.RequestBody
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+@RequestMapping("/opex/v1/deposit")
+class DepositController(private val walletProxy: WalletProxy) {
+
+ @PostMapping
+ suspend fun deposit(@RequestBody request: RequestDepositBody): TransferResult? {
+ return walletProxy.deposit(request)
+ }
+}
\ No newline at end of file
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/MarketController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/MarketController.kt
new file mode 100644
index 000000000..291268fe3
--- /dev/null
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/MarketController.kt
@@ -0,0 +1,255 @@
+package co.nilin.opex.api.ports.opex.controller
+
+import co.nilin.opex.api.core.inout.*
+import co.nilin.opex.api.core.spi.*
+import co.nilin.opex.api.ports.opex.data.MarketInfoResponse
+import co.nilin.opex.api.ports.opex.data.MarketStatResponse
+import co.nilin.opex.api.ports.opex.data.OrderBookResponse
+import co.nilin.opex.api.ports.opex.data.RecentTradeResponse
+import co.nilin.opex.common.OpexError
+import co.nilin.opex.common.utils.Interval
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+import org.springframework.web.bind.annotation.*
+import java.math.BigDecimal
+import java.time.ZoneId
+
+@RestController("opexMarketController")
+@RequestMapping("/opex/v1/market")
+class MarketController(
+ private val accountantProxy: AccountantProxy,
+ private val marketStatProxy: MarketStatProxy,
+ private val marketDataProxy: MarketDataProxy,
+ private val walletProxy: WalletProxy,
+ private val matchingGatewayProxy: MatchingGatewayProxy,
+) {
+ private val orderBookValidLimits = arrayListOf(5, 10, 20, 50, 100, 500, 1000, 5000)
+ private val validDurations = arrayListOf("24h", "7d", "1M")
+
+ @GetMapping("/currency")
+ suspend fun getCurrencies(): List {
+ return walletProxy.getCurrencies()
+ }
+
+ @GetMapping("/pair")
+ suspend fun getPairs(): List {
+ val pairSettings = matchingGatewayProxy.getPairSettings().associateBy { it.pair }
+
+ return accountantProxy.getPairConfigs().mapNotNull { config ->
+ pairSettings[config.pair]?.run {
+ PairInfoResponse(
+ pair = config.pair,
+ baseAsset = config.leftSideWalletSymbol,
+ quoteAsset = config.rightSideWalletSymbol,
+ isAvailable = isAvailable,
+ minOrder = minOrder,
+ maxOrder = maxOrder,
+ orderTypes = orderTypes
+ )
+ }
+ }
+ }
+
+ @GetMapping("/currency/gateway")
+ suspend fun getCurrencyGateways(
+ @RequestParam(defaultValue = "true") includeOffChainGateways: Boolean,
+ @RequestParam(defaultValue = "true") includeOnChainGateways: Boolean,
+ ): List {
+ return walletProxy.getGateWays(includeOffChainGateways, includeOnChainGateways)
+ }
+
+ @GetMapping("/pair/fee")
+ suspend fun getPairFees(): List {
+ return accountantProxy.getFeeConfigs()
+ }
+
+ @GetMapping("/stats")
+ suspend fun getMarketStats(
+ @RequestParam interval: String,
+ @RequestParam(required = false) limit: Int?
+ ): MarketStatResponse = coroutineScope {
+ val intervalEnum = Interval.findByLabel(interval) ?: Interval.Week
+ val validLimit = getValidLimit(limit)
+
+ val mostIncreased = async {
+ marketStatProxy.getMostIncreasedInPricePairs(intervalEnum, validLimit)
+ }
+
+ val mostDecreased = async {
+ marketStatProxy.getMostDecreasedInPricePairs(intervalEnum, validLimit)
+ }
+
+ val highestVolume = async {
+ marketStatProxy.getHighestVolumePair(intervalEnum)
+ }
+
+ val mostTrades = async {
+ marketStatProxy.getTradeCountPair(intervalEnum)
+ }
+
+ MarketStatResponse(
+ mostIncreased.await(),
+ mostDecreased.await(),
+ highestVolume.await(),
+ mostTrades.await()
+ )
+ }
+
+ @GetMapping("/info")
+ suspend fun getMarketInfo(@RequestParam interval: String): MarketInfoResponse {
+ val intervalEnum = Interval.findByLabel(interval) ?: Interval.ThreeMonth
+ return MarketInfoResponse(
+ marketDataProxy.countActiveUsers(intervalEnum),
+ marketDataProxy.countTotalOrders(intervalEnum),
+ marketDataProxy.countTotalTrades(intervalEnum),
+ )
+ }
+
+ @GetMapping("/depth")
+ suspend fun orderBook(
+ @RequestParam
+ symbol: String,
+ @RequestParam(required = false)
+ limit: Int? // Default 100; max 5000. Valid limits:[5, 10, 20, 50, 100, 500, 1000, 5000]
+ ): OrderBookResponse {
+ val validLimit = limit ?: 100
+ if (!orderBookValidLimits.contains(validLimit))
+ OpexError.InvalidLimitForOrderBook.exception()
+
+ val mappedBidOrders = ArrayList>()
+ val mappedAskOrders = ArrayList>()
+
+ val bidOrders = marketDataProxy.openBidOrders(symbol, validLimit)
+ val askOrders = marketDataProxy.openAskOrders(symbol, validLimit)
+
+ bidOrders.forEach {
+ val mapped = arrayListOf().apply {
+ add(it.price ?: BigDecimal.ZERO)
+ add(it.quantity ?: BigDecimal.ZERO)
+ }
+ mappedBidOrders.add(mapped)
+ }
+
+ askOrders.forEach {
+ val mapped = arrayListOf().apply {
+ add(it.price ?: BigDecimal.ZERO)
+ add(it.quantity ?: BigDecimal.ZERO)
+ }
+ mappedAskOrders.add(mapped)
+ }
+
+ val lastOrder = marketDataProxy.lastOrder(symbol)
+ return OrderBookResponse(lastOrder?.orderId ?: -1, mappedBidOrders, mappedAskOrders)
+ }
+
+ @GetMapping("/trades")
+ suspend fun recentTrades(
+ @RequestParam
+ symbol: String,
+ @RequestParam(required = false)
+ limit: Int? // Default 500; max 1000.
+ ): List {
+ val validLimit = limit ?: 500
+ if (validLimit !in 1..1000)
+ OpexError.InvalidLimitForRecentTrades.exception()
+
+ return marketDataProxy.recentTrades(symbol, validLimit)
+ .map {
+ RecentTradeResponse(
+ it.id,
+ it.price,
+ it.quantity,
+ it.quoteQuantity,
+ it.time.time,
+ it.isMakerBuyer,
+ it.isBestMatch
+ )
+ }
+ }
+
+ @GetMapping("/ticker/{duration:24h|7d|1M}")
+ suspend fun priceChange(
+ @PathVariable duration: String,
+ @RequestParam(required = false) symbol: String?,
+ @RequestParam(required = false) quote: String?
+ ): List {
+ if (!validDurations.contains(duration))
+ OpexError.InvalidPriceChangeDuration.exception()
+
+ val interval = Interval.findByLabel(duration) ?: Interval.Week
+
+ val result = if (symbol.isNullOrEmpty())
+ marketDataProxy.getTradeTickerData(interval).toMutableList()
+ else
+ arrayListOf(marketDataProxy.getTradeTickerDataBySymbol(symbol, interval))
+
+ result.forEach {
+ val parts = it.symbol?.split("_")
+ if (parts != null && parts.size == 2) {
+ it.base = parts[0].uppercase()
+ it.quote = parts[1].uppercase()
+ }
+ }
+
+ return if (quote.isNullOrEmpty()) result else result.filter { it.quote.equals(quote, true) }
+ }
+
+ @GetMapping("/ticker/price")
+ suspend fun priceTicker(@RequestParam(required = false) symbol: String?): List {
+ return marketDataProxy.lastPrice(symbol)
+ }
+
+ @GetMapping("/currencyInfo/quotes")
+ suspend fun getQuoteCurrencies(): List {
+ return walletProxy.getQuoteCurrencies().map { it.currency }
+ }
+
+ @GetMapping("/klines")
+ suspend fun klines(
+ @RequestParam
+ symbol: String,
+ @RequestParam
+ interval: String,
+ @RequestParam(required = false)
+ startTime: Long?,
+ @RequestParam(required = false)
+ endTime: Long?,
+ @RequestParam(required = false)
+ limit: Int? // Default 500; max 1000.
+ ): List> {
+ val validLimit = limit ?: 500
+ if (validLimit !in 1..1000)
+ throw OpexError.InvalidLimitForRecentTrades.exception()
+
+ val i = Interval.findByLabel(interval) ?: throw OpexError.InvalidInterval.exception()
+
+ val list = ArrayList>()
+ marketDataProxy.getCandleInfo(symbol, "${i.duration} ${i.unit}", startTime, endTime, validLimit)
+ .forEach {
+ list.add(
+ arrayListOf(
+ it.openTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(),
+ it.open.toString(),
+ it.high.toString(),
+ it.low.toString(),
+ it.close.toString(),
+ it.volume.toString(),
+ it.closeTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(),
+ it.quoteAssetVolume.toString(),
+ it.trades,
+ it.takerBuyBaseAssetVolume.toString(),
+ it.takerBuyQuoteAssetVolume.toString(),
+ "0.0"
+ )
+ )
+ }
+ return list
+ }
+
+ private fun getValidLimit(limit: Int?): Int = when {
+ limit == null -> 100
+ limit > 1000 -> 1000
+ limit < 1 -> 1
+ else -> limit
+ }
+}
\ No newline at end of file
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/OrderController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/OrderController.kt
new file mode 100644
index 000000000..57289f3da
--- /dev/null
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/OrderController.kt
@@ -0,0 +1,226 @@
+package co.nilin.opex.api.ports.opex.controller
+
+import co.nilin.opex.api.core.inout.*
+import co.nilin.opex.api.core.spi.MarketUserDataProxy
+import co.nilin.opex.api.core.spi.MatchingGatewayProxy
+import co.nilin.opex.api.ports.opex.data.CancelOrderResponse
+import co.nilin.opex.api.ports.opex.data.NewOrderResponse
+import co.nilin.opex.api.ports.opex.data.QueryOrderResponse
+import co.nilin.opex.api.ports.opex.util.*
+import co.nilin.opex.common.OpexError
+import io.swagger.annotations.ApiParam
+import org.springframework.security.core.annotation.CurrentSecurityContext
+import org.springframework.security.core.context.SecurityContext
+import org.springframework.web.bind.annotation.*
+import java.math.BigDecimal
+import java.security.Principal
+import java.time.ZoneId
+import java.util.*
+
+@RestController
+@RequestMapping("/opex/v1/order")
+class OrderController(
+ val queryHandler: MarketUserDataProxy,
+ val matchingGatewayProxy: MatchingGatewayProxy,
+) {
+ @PostMapping
+ suspend fun createNewOrder(
+ @RequestParam
+ symbol: String,
+ @RequestParam
+ side: OrderSide,
+ @RequestParam
+ type: OrderType,
+ @RequestParam(required = false)
+ timeInForce: TimeInForce?,
+ @RequestParam(required = false)
+ quantity: BigDecimal?,
+ @RequestParam(required = false)
+ quoteOrderQty: BigDecimal?,
+ @RequestParam(required = false)
+ price: BigDecimal?,
+ @ApiParam(value = "Used with STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, and TAKE_PROFIT_LIMIT orders.")
+ @RequestParam(required = false)
+ stopPrice: BigDecimal?,
+ @CurrentSecurityContext securityContext: SecurityContext
+ ): NewOrderResponse {
+ validateNewOrderParams(type, price, quantity, timeInForce, stopPrice, quoteOrderQty)
+
+ matchingGatewayProxy.createNewOrder(
+ securityContext.jwtAuthentication().name,
+ symbol,
+ price ?: BigDecimal.ZERO,
+ quantity ?: BigDecimal.ZERO,
+ side.asOrderDirection(),
+ timeInForce?.asMatchConstraint(),
+ type.asMatchingOrderType(),
+ "*",
+ securityContext.jwtAuthentication().tokenValue()
+ )
+ return NewOrderResponse(symbol)
+ }
+
+ @PutMapping
+ suspend fun cancelOrder(
+ principal: Principal,
+ @RequestParam
+ symbol: String,
+ @RequestParam(required = false)
+ orderId: Long?,
+ @RequestParam(required = false)
+ origClientOrderId: String?,
+ @CurrentSecurityContext securityContext: SecurityContext
+ ): CancelOrderResponse {
+ if (orderId == null && origClientOrderId == null)
+ throw OpexError.BadRequest.exception("'orderId' or 'origClientOrderId' must be sent")
+
+ val order = queryHandler.queryOrder(principal, symbol, orderId, origClientOrderId)
+ ?: throw OpexError.OrderNotFound.exception()
+
+ val response = CancelOrderResponse(
+ symbol,
+ origClientOrderId,
+ orderId,
+ -1,
+ null,
+ order.price,
+ order.quantity,
+ order.executedQuantity,
+ order.accumulativeQuoteQty,
+ OrderStatus.CANCELED,
+ order.constraint.asTimeInForce(),
+ order.type.asOrderType(),
+ order.direction.asOrderSide()
+ )
+
+ if (order.status == OrderStatus.CANCELED)
+ return response
+
+ if (order.status.equalsAny(OrderStatus.REJECTED, OrderStatus.EXPIRED, OrderStatus.FILLED))
+ throw OpexError.CancelOrderNotAllowed.exception()
+
+ matchingGatewayProxy.cancelOrder(
+ order.ouid,
+ principal.name,
+ order.orderId ?: 0,
+ symbol,
+ securityContext.jwtAuthentication().tokenValue()
+ )
+ return response
+ }
+
+ @GetMapping
+ suspend fun queryOrder(
+ principal: Principal,
+ @RequestParam
+ symbol: String,
+ @RequestParam(required = false)
+ orderId: Long?,
+ @RequestParam(required = false)
+ origClientOrderId: String?,
+ ): QueryOrderResponse {
+ return queryHandler.queryOrder(principal, symbol, orderId, origClientOrderId)
+ ?.asQueryOrderResponse()
+ ?.apply { this.symbol = symbol }
+ ?: throw OpexError.OrderNotFound.exception()
+ }
+
+ @GetMapping("/open")
+ suspend fun fetchOpenOrders(
+ principal: Principal,
+ @RequestParam(required = false)
+ symbol: String?,
+ @RequestParam(required = false)
+ limit: Int?
+ ): List {
+ return queryHandler.openOrders(principal, symbol, limit).map {
+ it.asQueryOrderResponse().apply { symbol?.let { s -> this.symbol = s } }
+ }
+ }
+
+ private fun validateNewOrderParams(
+ type: OrderType,
+ price: BigDecimal?,
+ quantity: BigDecimal?,
+ timeInForce: TimeInForce?,
+ stopPrice: BigDecimal?,
+ quoteOrderQty: BigDecimal?,
+ ) {
+ when (type) {
+ OrderType.LIMIT -> {
+ checkDecimal(price, "price")
+ checkDecimal(quantity, "quantity")
+ checkNull(timeInForce, "timeInForce")
+ }
+
+ OrderType.MARKET -> {
+ if (quantity == null)
+ checkDecimal(quoteOrderQty, "quoteOrderQty")
+ else
+ checkDecimal(quantity, "quantity")
+ }
+
+ OrderType.STOP_LOSS -> {
+ checkDecimal(quantity, "quantity")
+ checkDecimal(stopPrice, "stopPrice")
+ }
+
+ OrderType.STOP_LOSS_LIMIT -> {
+ checkDecimal(price, "price")
+ checkDecimal(quantity, "quantity")
+ checkDecimal(stopPrice, "stopPrice")
+ checkNull(timeInForce, "timeInForce")
+ }
+
+ OrderType.TAKE_PROFIT -> {
+ checkDecimal(quantity, "quantity")
+ checkDecimal(stopPrice, "stopPrice")
+ }
+
+ OrderType.TAKE_PROFIT_LIMIT -> {
+ checkDecimal(price, "price")
+ checkDecimal(quantity, "quantity")
+ checkDecimal(stopPrice, "stopPrice")
+ checkNull(timeInForce, "timeInForce")
+ }
+
+ OrderType.LIMIT_MAKER -> {
+ checkDecimal(price, "price")
+ checkDecimal(quantity, "quantity")
+ }
+ }
+ }
+
+ private fun checkDecimal(decimal: BigDecimal?, paramName: String) {
+ if (decimal == null || decimal <= BigDecimal.ZERO)
+ throw OpexError.InvalidRequestParam.exception("Parameter '$paramName' is either missing or invalid")
+ }
+
+ private fun checkNull(obj: Any?, paramName: String) {
+ if (obj == null)
+ throw OpexError.InvalidRequestParam.exception("Parameter '$paramName' is either missing or invalid")
+ }
+
+ private fun Order.asQueryOrderResponse() = QueryOrderResponse(
+ symbol,
+ ouid,
+ orderId ?: 0,
+ -1,
+ "",
+ price,
+ quantity,
+ executedQuantity,
+ accumulativeQuoteQty,
+ status,
+ constraint.asTimeInForce(),
+ type.asOrderType(),
+ direction.asOrderSide(),
+ null,
+ null,
+ Date.from(createDate.atZone(ZoneId.systemDefault()).toInstant()),
+ Date.from(updateDate.atZone(ZoneId.systemDefault()).toInstant()),
+ status.isWorking(),
+ quoteQuantity
+ )
+
+}
\ No newline at end of file
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserHistoryController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserHistoryController.kt
new file mode 100644
index 000000000..8ea58b43a
--- /dev/null
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserHistoryController.kt
@@ -0,0 +1,272 @@
+package co.nilin.opex.api.ports.opex.controller
+
+import co.nilin.opex.api.core.inout.*
+import co.nilin.opex.api.core.spi.MarketUserDataProxy
+import co.nilin.opex.api.core.spi.WalletProxy
+import co.nilin.opex.api.ports.opex.data.OrderDataResponse
+import co.nilin.opex.api.ports.opex.util.jwtAuthentication
+import co.nilin.opex.api.ports.opex.util.toResponse
+import co.nilin.opex.api.ports.opex.util.tokenValue
+import org.springframework.security.core.annotation.CurrentSecurityContext
+import org.springframework.security.core.context.SecurityContext
+import org.springframework.web.bind.annotation.*
+
+@RestController
+@RequestMapping("/opex/v1/user")
+class UserHistoryController(
+ private val marketUserDataProxy: MarketUserDataProxy,
+ private val walletProxy: WalletProxy,
+) {
+
+ @GetMapping("/history/order")
+ suspend fun getOrderHistory(
+ @RequestParam symbol: String?,
+ @RequestParam startTime: Long?,
+ @RequestParam endTime: Long?,
+ @RequestParam orderType: MatchingOrderType?,
+ @RequestParam direction: OrderDirection?,
+ @RequestParam limit: Int?,
+ @RequestParam offset: Int?,
+ @CurrentSecurityContext securityContext: SecurityContext,
+ ): List {
+ return marketUserDataProxy.getOrderHistory(
+ securityContext.authentication.name,
+ symbol,
+ startTime,
+ endTime,
+ orderType,
+ direction,
+ limit ?: 10,
+ offset ?: 0,
+ ).map { it.toResponse() }
+ }
+
+ @GetMapping("/history/order/count")
+ suspend fun getOrderHistoryCount(
+ @RequestParam symbol: String?,
+ @RequestParam startTime: Long?,
+ @RequestParam endTime: Long?,
+ @RequestParam orderType: MatchingOrderType?,
+ @RequestParam direction: OrderDirection?,
+ @CurrentSecurityContext securityContext: SecurityContext,
+ ): Long {
+ return marketUserDataProxy.getOrderHistoryCount(
+ securityContext.authentication.name,
+ symbol,
+ startTime,
+ endTime,
+ orderType,
+ direction,
+ )
+ }
+
+ @GetMapping("/history/trade")
+ suspend fun getTradeHistory(
+ @RequestParam symbol: String?,
+ @RequestParam startTime: Long?,
+ @RequestParam endTime: Long?,
+ @RequestParam direction: OrderDirection?,
+ @RequestParam limit: Int?,
+ @RequestParam offset: Int?,
+ @CurrentSecurityContext securityContext: SecurityContext,
+ ): List {
+ return marketUserDataProxy.getTradeHistory(
+ securityContext.authentication.name, symbol, startTime, endTime, direction, limit ?: 10, offset ?: 0
+ )
+ }
+
+ @GetMapping("/history/trade/count")
+ suspend fun getTradeHistoryCount(
+ @RequestParam symbol: String?,
+ @RequestParam startTime: Long?,
+ @RequestParam endTime: Long?,
+ @RequestParam direction: OrderDirection?,
+ @CurrentSecurityContext securityContext: SecurityContext,
+ ): Long {
+ return marketUserDataProxy.getTradeHistoryCount(
+ securityContext.authentication.name, symbol, startTime, endTime, direction
+ )
+ }
+
+ @GetMapping("/history/withdraw")
+ suspend fun getWithdrawHistory(
+ @RequestParam currency: String?,
+ @RequestParam startTime: Long?,
+ @RequestParam endTime: Long?,
+ @RequestParam limit: Int?,
+ @RequestParam offset: Int?,
+ @RequestParam ascendingByTime: Boolean?,
+ @CurrentSecurityContext securityContext: SecurityContext,
+ ): List {
+ return walletProxy.getWithdrawTransactions(
+ securityContext.jwtAuthentication().name,
+ securityContext.jwtAuthentication().tokenValue(),
+ currency,
+ startTime,
+ endTime,
+ limit ?: 10,
+ offset ?: 0,
+ ascendingByTime,
+ )
+ }
+
+ @GetMapping("/history/withdraw/count")
+ suspend fun getWithdrawHistoryCount(
+ @RequestParam currency: String?,
+ @RequestParam startTime: Long?,
+ @RequestParam endTime: Long?,
+ @CurrentSecurityContext securityContext: SecurityContext,
+ ): Long {
+ return walletProxy.getWithdrawTransactionsCount(
+ securityContext.jwtAuthentication().name,
+ securityContext.jwtAuthentication().tokenValue(),
+ currency,
+ startTime,
+ endTime,
+ )
+ }
+
+ @GetMapping("/history/deposit")
+ suspend fun getDepositHistory(
+ @RequestParam currency: String?,
+ @RequestParam startTime: Long?,
+ @RequestParam endTime: Long?,
+ @RequestParam limit: Int?,
+ @RequestParam offset: Int?,
+ @RequestParam ascendingByTime: Boolean?,
+ @CurrentSecurityContext securityContext: SecurityContext,
+ ): List {
+ return walletProxy.getDepositTransactions(
+ securityContext.jwtAuthentication().name,
+ securityContext.jwtAuthentication().tokenValue(),
+ currency,
+ startTime,
+ endTime,
+ limit ?: 10,
+ offset ?: 0,
+ ascendingByTime,
+ )
+ }
+
+ @GetMapping("/history/deposit/count")
+ suspend fun getDepositHistoryCount(
+ @RequestParam currency: String?,
+ @RequestParam startTime: Long?,
+ @RequestParam endTime: Long?,
+ @CurrentSecurityContext securityContext: SecurityContext,
+ ): Long {
+ return walletProxy.getDepositTransactionsCount(
+ securityContext.jwtAuthentication().name,
+ securityContext.jwtAuthentication().tokenValue(),
+ currency,
+ startTime,
+ endTime,
+ )
+ }
+
+ @GetMapping("/history/transaction")
+ suspend fun getTransactionHistory(
+ @RequestParam currency: String?,
+ @RequestParam category: UserTransactionCategory?,
+ @RequestParam startTime: Long?,
+ @RequestParam endTime: Long?,
+ @RequestParam limit: Int?,
+ @RequestParam offset: Int?,
+ @RequestParam ascendingByTime: Boolean?,
+ @CurrentSecurityContext securityContext: SecurityContext,
+ ): List {
+ return walletProxy.getTransactions(
+ securityContext.jwtAuthentication().name,
+ securityContext.jwtAuthentication().tokenValue(),
+ currency,
+ category,
+ startTime,
+ endTime,
+ limit ?: 10,
+ offset ?: 0,
+ ascendingByTime,
+ )
+ }
+
+ @GetMapping("/history/transaction/count")
+ suspend fun getTransactionHistoryCount(
+ @RequestParam currency: String?,
+ @RequestParam category: UserTransactionCategory?,
+ @RequestParam startTime: Long?,
+ @RequestParam endTime: Long?,
+ @CurrentSecurityContext securityContext: SecurityContext,
+ ): Long {
+ return walletProxy.getTransactionsCount(
+ securityContext.jwtAuthentication().name,
+ securityContext.jwtAuthentication().tokenValue(),
+ currency,
+ category,
+ startTime,
+ endTime,
+ )
+ }
+
+ @GetMapping("/summary/trade")
+ suspend fun getTradeTransactionSummary(
+ @RequestParam startTime: Long?,
+ @RequestParam endTime: Long?,
+ @RequestParam limit: Int?,
+ @CurrentSecurityContext securityContext: SecurityContext,
+ ): List {
+ return walletProxy.getUserTradeTransactionSummary(
+ securityContext.jwtAuthentication().name,
+ securityContext.jwtAuthentication().tokenValue(),
+ startTime,
+ endTime,
+ limit,
+ )
+ }
+
+ @GetMapping("/summary/deposit")
+ suspend fun getDepositSummary(
+ @RequestParam startTime: Long?,
+ @RequestParam endTime: Long?,
+ @RequestParam limit: Int?,
+ @CurrentSecurityContext securityContext: SecurityContext,
+ ): List {
+ return walletProxy.getUserDepositSummary(
+ securityContext.jwtAuthentication().name,
+ securityContext.jwtAuthentication().tokenValue(),
+ startTime,
+ endTime,
+ limit,
+ )
+ }
+
+ @GetMapping("/summary/withdraw")
+ suspend fun getWithdrawSummary(
+ @RequestParam startTime: Long?,
+ @RequestParam endTime: Long?,
+ @RequestParam limit: Int?,
+ @CurrentSecurityContext securityContext: SecurityContext,
+ ): List {
+ return walletProxy.getUserWithdrawSummary(
+ securityContext.jwtAuthentication().name,
+ securityContext.jwtAuthentication().tokenValue(),
+ startTime,
+ endTime,
+ limit,
+ )
+ }
+
+ @PostMapping("/history/swap")
+ suspend fun getSwapHistory(
+ @CurrentSecurityContext securityContext: SecurityContext,
+ @RequestBody request: UserTransactionRequest
+ ): List {
+ return walletProxy.getSwapTransactions(securityContext.jwtAuthentication().tokenValue(), request)
+ }
+
+ @PostMapping("/history/swap/count")
+ suspend fun getSwapHistoryCount(
+ @CurrentSecurityContext securityContext: SecurityContext,
+ @RequestBody request: UserTransactionRequest
+ ): Long {
+ return walletProxy.getSwapTransactionsCount(securityContext.jwtAuthentication().tokenValue(), request)
+ }
+}
\ No newline at end of file
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/VoucherController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/VoucherController.kt
new file mode 100644
index 000000000..44605ec35
--- /dev/null
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/VoucherController.kt
@@ -0,0 +1,25 @@
+package co.nilin.opex.api.ports.opex.controller
+
+import co.nilin.opex.api.core.inout.SubmitVoucherResponse
+import co.nilin.opex.api.core.spi.WalletProxy
+import co.nilin.opex.api.ports.opex.util.jwtAuthentication
+import co.nilin.opex.api.ports.opex.util.tokenValue
+import org.springframework.security.core.annotation.CurrentSecurityContext
+import org.springframework.security.core.context.SecurityContext
+import org.springframework.web.bind.annotation.PathVariable
+import org.springframework.web.bind.annotation.PutMapping
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+@RequestMapping("/opex/v1/voucher")
+class VoucherController(private val walletProxy: WalletProxy) {
+
+ @PutMapping("/{code}")
+ suspend fun submitVoucher(
+ @PathVariable code: String,
+ @CurrentSecurityContext securityContext: SecurityContext
+ ): SubmitVoucherResponse {
+ return walletProxy.submitVoucher(code, securityContext.jwtAuthentication().tokenValue())
+ }
+}
\ No newline at end of file
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/WalletController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/WalletController.kt
new file mode 100644
index 000000000..007f0375b
--- /dev/null
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/WalletController.kt
@@ -0,0 +1,48 @@
+package co.nilin.opex.api.ports.opex.controller
+
+import co.nilin.opex.api.core.inout.OwnerLimitsResponse
+import co.nilin.opex.api.core.spi.WalletProxy
+import co.nilin.opex.api.ports.opex.data.AssetResponse
+import co.nilin.opex.api.ports.opex.util.jwtAuthentication
+import co.nilin.opex.api.ports.opex.util.tokenValue
+import org.springframework.security.core.annotation.CurrentSecurityContext
+import org.springframework.security.core.context.SecurityContext
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RequestParam
+import org.springframework.web.bind.annotation.RestController
+
+@RestController("walletOpexController")
+@RequestMapping("/opex/v1/wallet")
+class WalletController(
+ private val walletProxy: WalletProxy,
+) {
+
+ @GetMapping("/asset")
+ suspend fun getUserAssets(
+ @CurrentSecurityContext securityContext: SecurityContext,
+ @RequestParam(required = false) symbol: String?,
+ ): List {
+ val auth = securityContext.jwtAuthentication()
+ val result = arrayListOf()
+
+ if (symbol != null) {
+ val wallet = walletProxy.getWallet(auth.name, auth.tokenValue(), symbol.uppercase())
+ result.add(AssetResponse(wallet.asset, wallet.balance, wallet.locked, wallet.withdraw))
+ } else {
+ result.addAll(
+ walletProxy.getWallets(auth.name, auth.tokenValue())
+ .map { AssetResponse(it.asset, it.balance, it.locked, it.withdraw) }
+ )
+ }
+ return result
+ }
+
+ @GetMapping("/limits")
+ suspend fun getWalletOwnerLimits(@CurrentSecurityContext securityContext: SecurityContext): OwnerLimitsResponse {
+ return walletProxy.getOwnerLimits(
+ securityContext.jwtAuthentication().name,
+ securityContext.jwtAuthentication().tokenValue(),
+ )
+ }
+}
\ No newline at end of file
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/WithdrawController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/WithdrawController.kt
new file mode 100644
index 000000000..aa2e6a0b7
--- /dev/null
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/WithdrawController.kt
@@ -0,0 +1,51 @@
+package co.nilin.opex.api.ports.opex.controller
+
+import co.nilin.opex.api.core.inout.RequestWithdrawBody
+import co.nilin.opex.api.core.inout.WithdrawActionResult
+import co.nilin.opex.api.core.inout.WithdrawResponse
+import co.nilin.opex.api.core.spi.WalletProxy
+import co.nilin.opex.api.ports.opex.util.jwtAuthentication
+import co.nilin.opex.api.ports.opex.util.tokenValue
+import org.springframework.security.core.annotation.CurrentSecurityContext
+import org.springframework.security.core.context.SecurityContext
+import org.springframework.web.bind.annotation.*
+
+@RestController
+@RequestMapping("/opex/v1/withdraw")
+class WithdrawController(
+ private val walletProxy: WalletProxy,
+) {
+
+ @PostMapping
+ suspend fun requestWithdraw(
+ @CurrentSecurityContext securityContext: SecurityContext,
+ @RequestBody request: RequestWithdrawBody
+ ): WithdrawActionResult? {
+ return walletProxy.requestWithdraw(
+ securityContext.jwtAuthentication().tokenValue(),
+ request
+ )
+ }
+
+ @PutMapping("/{withdrawId}/cancel")
+ suspend fun cancelWithdraw(
+ @CurrentSecurityContext securityContext: SecurityContext,
+ @PathVariable withdrawId: Long
+ ) {
+ walletProxy.cancelWithdraw(
+ securityContext.jwtAuthentication().tokenValue(),
+ withdrawId
+ )
+ }
+
+ @GetMapping("/{withdrawId}")
+ suspend fun findWithdraw(
+ @CurrentSecurityContext securityContext: SecurityContext,
+ @PathVariable withdrawId: Long
+ ): WithdrawResponse {
+ return walletProxy.findWithdraw(
+ securityContext.jwtAuthentication().tokenValue(),
+ withdrawId
+ )
+ }
+}
\ No newline at end of file
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/AssetResponse.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/AssetResponse.kt
new file mode 100644
index 000000000..e852bc4fd
--- /dev/null
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/AssetResponse.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.api.ports.opex.data
+
+import java.math.BigDecimal
+
+data class AssetResponse(
+ val asset: String,
+ var free: BigDecimal,
+ var locked: BigDecimal,
+ var withdrawing: BigDecimal,
+)
\ No newline at end of file
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/CancelOrderResponse.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/CancelOrderResponse.kt
new file mode 100644
index 000000000..29e46426c
--- /dev/null
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/CancelOrderResponse.kt
@@ -0,0 +1,25 @@
+package co.nilin.opex.api.ports.opex.data
+
+import co.nilin.opex.api.core.inout.OrderSide
+import co.nilin.opex.api.core.inout.OrderStatus
+import co.nilin.opex.api.core.inout.OrderType
+import co.nilin.opex.api.core.inout.TimeInForce
+import com.fasterxml.jackson.annotation.JsonInclude
+import java.math.BigDecimal
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class CancelOrderResponse(
+ val symbol: String,
+ val origClientOrderId: String?,
+ val orderId: Long?,
+ val orderListId: Long, //Unless OCO, value will be -1
+ val clientOrderId: String?,
+ val price: BigDecimal?,
+ val origQty: BigDecimal?,
+ val executedQty: BigDecimal?,
+ val cummulativeQuoteQty: BigDecimal?,
+ val status: OrderStatus?,
+ val timeInForce: TimeInForce?,
+ val type: OrderType?,
+ val side: OrderSide?
+)
\ No newline at end of file
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/MarketInfoResponse.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/MarketInfoResponse.kt
new file mode 100644
index 000000000..01b1a0c11
--- /dev/null
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/MarketInfoResponse.kt
@@ -0,0 +1,7 @@
+package co.nilin.opex.api.ports.opex.data
+
+data class MarketInfoResponse(
+ val activeUsers: Long,
+ val totalOrders: Long,
+ val totalTrades: Long
+)
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/MarketStatResponse.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/MarketStatResponse.kt
new file mode 100644
index 000000000..bcdef449d
--- /dev/null
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/MarketStatResponse.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.api.ports.opex.data
+
+import co.nilin.opex.api.core.inout.PriceStat
+import co.nilin.opex.api.core.inout.TradeVolumeStat
+
+data class MarketStatResponse(
+ val mostIncreasedPrice: List,
+ val mostDecreasedPrice: List,
+ val mostVolume: TradeVolumeStat?,
+ val mostTrades: TradeVolumeStat?
+)
\ No newline at end of file
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/NewOrderResponse.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/NewOrderResponse.kt
new file mode 100644
index 000000000..15aa2d3c5
--- /dev/null
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/NewOrderResponse.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.api.ports.opex.data
+
+import com.fasterxml.jackson.annotation.JsonInclude
+import java.util.*
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class NewOrderResponse(
+ val symbol: String,
+ val date: Date = Date()
+)
\ No newline at end of file
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/OrderBookResponse.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/OrderBookResponse.kt
new file mode 100644
index 000000000..6ba08c2fc
--- /dev/null
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/OrderBookResponse.kt
@@ -0,0 +1,9 @@
+package co.nilin.opex.api.ports.opex.data
+
+import java.math.BigDecimal
+
+data class OrderBookResponse(
+ val lastUpdateId: Long,
+ val bids: List>, // Inner list -> [0]: PRICE, [1]: QTY
+ val asks: List> // Inner list -> [0]: PRICE, [1]: QTY
+)
\ No newline at end of file
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/OrderDataResponse.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/OrderDataResponse.kt
new file mode 100644
index 000000000..269b95352
--- /dev/null
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/OrderDataResponse.kt
@@ -0,0 +1,24 @@
+package co.nilin.opex.api.ports.opex.data
+
+import co.nilin.opex.api.core.inout.MatchingOrderType
+import co.nilin.opex.api.core.inout.OrderDirection
+import co.nilin.opex.api.core.inout.OrderStatus
+import java.math.BigDecimal
+import java.time.LocalDateTime
+import java.util.*
+
+data class OrderDataResponse(
+ val symbol: String,
+ val orderId: Long,
+ val orderType: MatchingOrderType,
+ val side: OrderDirection,
+ val price: BigDecimal,
+ val quantity: BigDecimal,
+ val quoteQuantity: BigDecimal,
+ val executedQuantity: BigDecimal,
+ val takerFee: BigDecimal,
+ val makerFee: BigDecimal,
+ val status: OrderStatus,
+ val createDate: LocalDateTime,
+ val updateDate: LocalDateTime,
+)
\ No newline at end of file
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/QueryOrderResponse.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/QueryOrderResponse.kt
new file mode 100644
index 000000000..044fd8347
--- /dev/null
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/QueryOrderResponse.kt
@@ -0,0 +1,32 @@
+package co.nilin.opex.api.ports.opex.data
+
+import co.nilin.opex.api.core.inout.OrderSide
+import co.nilin.opex.api.core.inout.OrderStatus
+import co.nilin.opex.api.core.inout.OrderType
+import co.nilin.opex.api.core.inout.TimeInForce
+import com.fasterxml.jackson.annotation.JsonInclude
+import java.math.BigDecimal
+import java.util.*
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class QueryOrderResponse(
+ var symbol: String,
+ val ouid: String,
+ val orderId: Long,
+ val orderListId: Long, //Unless part of an OCO, the value will always be -1.
+ val clientOrderId: String,
+ val price: BigDecimal,
+ val origQty: BigDecimal,
+ val executedQty: BigDecimal,
+ val cummulativeQuoteQty: BigDecimal,
+ val status: OrderStatus,
+ val timeInForce: TimeInForce,
+ val type: OrderType,
+ val side: OrderSide,
+ val stopPrice: BigDecimal?,
+ val icebergQty: BigDecimal?,
+ val time: Date,
+ val updateTime: Date,
+ val isWorking: Boolean,
+ val origQuoteOrderQty: BigDecimal
+)
\ No newline at end of file
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/RecentTradeResponse.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/RecentTradeResponse.kt
new file mode 100644
index 000000000..f49ab4669
--- /dev/null
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/RecentTradeResponse.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.api.ports.opex.data
+
+import java.math.BigDecimal
+
+data class RecentTradeResponse(
+ val id: Long,
+ val price: BigDecimal,
+ val qty: BigDecimal,
+ val quoteQty: BigDecimal,
+ val time: Long,
+ val isBuyerMaker: Boolean,
+ val isBestMatch: Boolean
+)
\ No newline at end of file
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/util/ConvertorExtenstions.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/util/ConvertorExtenstions.kt
new file mode 100644
index 000000000..ca232e74b
--- /dev/null
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/util/ConvertorExtenstions.kt
@@ -0,0 +1,23 @@
+package co.nilin.opex.api.ports.opex.util
+
+import co.nilin.opex.api.core.inout.OrderData
+import co.nilin.opex.api.core.inout.OrderStatus
+import co.nilin.opex.api.ports.opex.data.OrderDataResponse
+
+fun OrderData.toResponse(): OrderDataResponse {
+ return OrderDataResponse(
+ symbol = this.symbol,
+ orderId = this.orderId,
+ orderType = this.orderType,
+ side = this.side,
+ price = this.price,
+ quantity = this.quantity,
+ quoteQuantity = this.quoteQuantity,
+ executedQuantity = this.executedQuantity,
+ takerFee = this.takerFee,
+ makerFee = this.makerFee,
+ status = OrderStatus.fromCode(this.status) ?: OrderStatus.REJECTED,
+ createDate = this.createDate,
+ updateDate = this.updateDate,
+ )
+}
\ No newline at end of file
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/util/EnumExtensions.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/util/EnumExtensions.kt
new file mode 100644
index 000000000..4363a192e
--- /dev/null
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/util/EnumExtensions.kt
@@ -0,0 +1,54 @@
+package co.nilin.opex.api.ports.opex.util
+
+import co.nilin.opex.api.core.inout.*
+
+fun OrderSide.asOrderDirection(): OrderDirection {
+ if (this == OrderSide.BUY)
+ return OrderDirection.BID
+ return OrderDirection.ASK
+}
+
+fun OrderDirection.asOrderSide(): OrderSide {
+ if (this == OrderDirection.BID)
+ return OrderSide.BUY
+ return OrderSide.SELL
+}
+
+fun TimeInForce.asMatchConstraint(): MatchConstraint {
+ return when (this) {
+ TimeInForce.GTC -> MatchConstraint.GTC
+ TimeInForce.IOC -> MatchConstraint.IOC
+ TimeInForce.FOK -> MatchConstraint.FOK
+ }
+}
+
+fun MatchConstraint.asTimeInForce(): TimeInForce {
+ return when (this) {
+ MatchConstraint.GTC -> TimeInForce.GTC
+ MatchConstraint.IOC -> TimeInForce.IOC
+ MatchConstraint.FOK -> TimeInForce.FOK
+ else -> TimeInForce.GTC
+ }
+}
+
+fun OrderType.asMatchingOrderType(): MatchingOrderType {
+ return when (this) {
+ OrderType.LIMIT -> MatchingOrderType.LIMIT_ORDER
+ OrderType.MARKET -> MatchingOrderType.MARKET_ORDER
+ else -> MatchingOrderType.LIMIT_ORDER
+ }
+}
+
+fun MatchingOrderType.asOrderType(): OrderType {
+ return when (this) {
+ MatchingOrderType.LIMIT_ORDER -> OrderType.LIMIT
+ MatchingOrderType.MARKET_ORDER -> OrderType.MARKET
+ }
+}
+
+fun > R.equalsAny(vararg equals: R): Boolean {
+ for (e in equals)
+ if (this == e)
+ return true
+ return false
+}
\ No newline at end of file
diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/APIKeyModel.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/APIKeyModel.kt
index 77f4bf4c5..f33c9cd65 100644
--- a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/APIKeyModel.kt
+++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/APIKeyModel.kt
@@ -4,7 +4,7 @@ import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.time.LocalDateTime
-import java.util.UUID
+import java.util.*
@Table("api_key")
data class APIKeyModel(
diff --git a/api/api-ports/api-persister-postgres/src/main/resources/schema.sql b/api/api-ports/api-persister-postgres/src/main/resources/schema.sql
index 1d4db9878..5f4a12367 100644
--- a/api/api-ports/api-persister-postgres/src/main/resources/schema.sql
+++ b/api/api-ports/api-persister-postgres/src/main/resources/schema.sql
@@ -1,23 +1,54 @@
CREATE TABLE IF NOT EXISTS symbol_maps
(
- id SERIAL PRIMARY KEY,
- symbol VARCHAR(72) NOT NULL,
- alias_key VARCHAR(72) NOT NULL,
- alias VARCHAR(72) NOT NULL,
- UNIQUE (symbol, alias_key, alias)
-);
+ id
+ SERIAL
+ PRIMARY
+ KEY,
+ symbol
+ VARCHAR
+(
+ 72
+) NOT NULL,
+ alias_key VARCHAR
+(
+ 72
+) NOT NULL,
+ alias VARCHAR
+(
+ 72
+) NOT NULL,
+ UNIQUE
+(
+ symbol,
+ alias_key,
+ alias
+)
+ );
CREATE TABLE IF NOT EXISTS api_key
(
- id SERIAL PRIMARY KEY,
- user_id VARCHAR(36) NOT NULL,
- label VARCHAR(200) NOT NULL,
- access_token TEXT NOT NULL,
- refresh_token TEXT NOT NULL,
- expiration_time TIMESTAMP,
- allowed_ips TEXT,
- token_expiration_time TIMESTAMP NOT NULL,
- key VARCHAR(36) NOT NULL UNIQUE,
- is_enabled BOOLEAN NOT NULL DEFAULT true,
- is_expired BOOLEAN NOT NULL DEFAULT false
-);
+ id
+ SERIAL
+ PRIMARY
+ KEY,
+ user_id
+ VARCHAR
+(
+ 36
+) NOT NULL,
+ label VARCHAR
+(
+ 200
+) NOT NULL,
+ access_token TEXT NOT NULL,
+ refresh_token TEXT NOT NULL,
+ expiration_time TIMESTAMP,
+ allowed_ips TEXT,
+ token_expiration_time TIMESTAMP NOT NULL,
+ key VARCHAR
+(
+ 36
+) NOT NULL UNIQUE,
+ is_enabled BOOLEAN NOT NULL DEFAULT true,
+ is_expired BOOLEAN NOT NULL DEFAULT false
+ );
diff --git a/api/api-ports/api-persister-postgres/src/test/kotlin/co/nilin/opex/api/ports/postgres/impl/sample/Samples.kt b/api/api-ports/api-persister-postgres/src/test/kotlin/co/nilin/opex/api/ports/postgres/impl/sample/Samples.kt
index b6fc9e909..82e2905ba 100644
--- a/api/api-ports/api-persister-postgres/src/test/kotlin/co/nilin/opex/api/ports/postgres/impl/sample/Samples.kt
+++ b/api/api-ports/api-persister-postgres/src/test/kotlin/co/nilin/opex/api/ports/postgres/impl/sample/Samples.kt
@@ -1,9 +1,6 @@
package co.nilin.opex.api.ports.postgres.impl.sample
import co.nilin.opex.api.ports.postgres.model.SymbolMapModel
-import java.security.Principal
-import java.time.LocalDateTime
-import java.time.ZoneOffset
object VALID {
diff --git a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/data/TransactionRequest.kt b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/data/TransactionRequest.kt
index 33a6640f4..b1d3debb5 100644
--- a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/data/TransactionRequest.kt
+++ b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/data/TransactionRequest.kt
@@ -1,10 +1,10 @@
package co.nilin.opex.api.ports.proxy.data
data class TransactionRequest(
- val coin: String?,
- val startTime: Long?=null,
- val endTime: Long?=null,
- val limit: Int,
- val offset: Int,
- val ascendingByTime: Boolean? = false
+ val currency: String?,
+ val startTime: Long? = null,
+ val endTime: Long? = null,
+ val limit: Int?,
+ val offset: Int?,
+ val ascendingByTime: Boolean? = false
)
\ No newline at end of file
diff --git a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/AccountantProxyImpl.kt b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/AccountantProxyImpl.kt
index 18db21431..c7ca50130 100644
--- a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/AccountantProxyImpl.kt
+++ b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/AccountantProxyImpl.kt
@@ -1,7 +1,7 @@
package co.nilin.opex.api.ports.proxy.impl
import co.nilin.opex.api.core.inout.PairFeeResponse
-import co.nilin.opex.api.core.inout.PairInfoResponse
+import co.nilin.opex.api.core.inout.PairConfigResponse
import co.nilin.opex.api.core.spi.AccountantProxy
import co.nilin.opex.api.ports.proxy.config.ProxyDispatchers
import co.nilin.opex.common.utils.LoggerDelegate
@@ -22,7 +22,7 @@ class AccountantProxyImpl(private val webClient: WebClient) : AccountantProxy {
@Value("\${app.accountant.url}")
private lateinit var baseUrl: String
- override suspend fun getPairConfigs(): List {
+ override suspend fun getPairConfigs(): List {
logger.info("fetching pair configs")
return withContext(ProxyDispatchers.general) {
webClient.get()
@@ -30,7 +30,7 @@ class AccountantProxyImpl(private val webClient: WebClient) : AccountantProxy {
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus({ t -> t.isError }, { it.createException() })
- .bodyToFlux()
+ .bodyToFlux()
.collectList()
.awaitSingle()
}
diff --git a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/BlockchainGatewayProxyImpl.kt b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/BlockchainGatewayProxyImpl.kt
index bc3fa6814..a7d53ac6f 100644
--- a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/BlockchainGatewayProxyImpl.kt
+++ b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/BlockchainGatewayProxyImpl.kt
@@ -59,19 +59,17 @@ class BlockchainGatewayProxyImpl(private val client: WebClient) : BlockchainGate
}
}
- override suspend fun getCurrencyImplementations(currency: String?): List {
- logger.info("calling bc-gateway chain details")
- return withContext(ProxyDispatchers.general) {
- client.get()
- .uri("$baseUrl/currency/chains") {
- it.queryParam("currency", currency)
- it.build()
- }.accept(MediaType.APPLICATION_JSON)
- .retrieve()
- .onStatus({ t -> t.isError }, { it.createException() })
- .bodyToFlux()
- .collectList()
- .awaitFirstOrElse { emptyList() }
- }
- }
+// override suspend fun getCurrencyImplementations(currency: String?): List {
+// logger.info("calling bc-gateway chain details")
+// return client.get()
+// .uri("$baseUrl/currency/chains") {
+// it.queryParam("currency", currency)
+// it.build()
+// }.accept(MediaType.APPLICATION_JSON)
+// .retrieve()
+// .onStatus({ t -> t.isError }, { it.createException() })
+// .bodyToFlux()
+// .collectList()
+// .awaitFirstOrElse { emptyList() }
+// }
}
\ No newline at end of file
diff --git a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/MarketUserDataProxyImpl.kt b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/MarketUserDataProxyImpl.kt
index 20abf1a99..c3b06e2ec 100644
--- a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/MarketUserDataProxyImpl.kt
+++ b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/MarketUserDataProxyImpl.kt
@@ -1,7 +1,6 @@
package co.nilin.opex.api.ports.proxy.impl
-import co.nilin.opex.api.core.inout.Order
-import co.nilin.opex.api.core.inout.Trade
+import co.nilin.opex.api.core.inout.*
import co.nilin.opex.api.core.spi.MarketUserDataProxy
import co.nilin.opex.api.ports.proxy.config.ProxyDispatchers
import co.nilin.opex.api.ports.proxy.data.AllOrderRequest
@@ -35,7 +34,7 @@ class MarketUserDataProxyImpl(private val webClient: WebClient) : MarketUserData
principal: Principal,
symbol: String,
orderId: Long?,
- origClientOrderId: String?
+ origClientOrderId: String?,
): Order? {
return withContext(ProxyDispatchers.market) {
webClient.post()
@@ -71,7 +70,7 @@ class MarketUserDataProxyImpl(private val webClient: WebClient) : MarketUserData
symbol: String?,
startTime: Date?,
endTime: Date?,
- limit: Int?
+ limit: Int?,
): List {
return withContext(ProxyDispatchers.market) {
webClient.post()
@@ -93,7 +92,7 @@ class MarketUserDataProxyImpl(private val webClient: WebClient) : MarketUserData
fromTrade: Long?,
startTime: Date?,
endTime: Date?,
- limit: Int?
+ limit: Int?,
): List {
return withContext(ProxyDispatchers.market) {
webClient.post()
@@ -109,4 +108,113 @@ class MarketUserDataProxyImpl(private val webClient: WebClient) : MarketUserData
}
}
+ override suspend fun getOrderHistory(
+ uuid: String,
+ symbol: String?,
+ startTime: Long?,
+ endTime: Long?,
+ orderType: MatchingOrderType?,
+ direction: OrderDirection?,
+ limit: Int?,
+ offset: Int?,
+ ): List {
+ return withContext(ProxyDispatchers.market) {
+ webClient.get()
+ .uri("$baseUrl/v1/user/order/history/$uuid") {
+ it.queryParam("symbol", symbol)
+ it.queryParam("startTime", startTime)
+ it.queryParam("endTime", endTime)
+ it.queryParam("orderType", orderType)
+ it.queryParam("direction", direction)
+ it.queryParam("limit", limit)
+ it.queryParam("offset", offset)
+ it.build()
+ }.accept(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToFlux()
+ .collectList()
+ .awaitFirstOrElse { emptyList() }
+ }
+ }
+
+ override suspend fun getOrderHistoryCount(
+ uuid: String,
+ symbol: String?,
+ startTime: Long?,
+ endTime: Long?,
+ orderType: MatchingOrderType?,
+ direction: OrderDirection?,
+ ): Long {
+ return withContext(ProxyDispatchers.market) {
+ webClient.get()
+ .uri("$baseUrl/v1/user/order/history/count/$uuid") {
+ it.queryParam("symbol", symbol)
+ it.queryParam("startTime", startTime)
+ it.queryParam("endTime", endTime)
+ it.queryParam("orderType", orderType)
+ it.queryParam("direction", direction)
+ it.build()
+ }.accept(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToMono()
+ .awaitFirstOrElse { 0L }
+ }
+ }
+
+ override suspend fun getTradeHistory(
+ uuid: String,
+ symbol: String?,
+ startTime: Long?,
+ endTime: Long?,
+ direction: OrderDirection?,
+ limit: Int?,
+ offset: Int?,
+ ): List {
+ return withContext(ProxyDispatchers.market) {
+ webClient.get()
+ .uri("$baseUrl/v1/user/trade/history/$uuid") {
+ it.queryParam("symbol", symbol)
+ it.queryParam("startTime", startTime)
+ it.queryParam("endTime", endTime)
+ it.queryParam("direction", direction)
+ it.queryParam("limit", limit)
+ it.queryParam("offset", offset)
+ it.build()
+ }.accept(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToFlux()
+ .collectList()
+ .awaitFirstOrElse { emptyList() }
+ }
+ }
+
+ override suspend fun getTradeHistoryCount(
+ uuid: String,
+ symbol: String?,
+ startTime: Long?,
+ endTime: Long?,
+ direction: OrderDirection?,
+ ): Long {
+ return withContext(ProxyDispatchers.market) {
+ webClient.get()
+ .uri("$baseUrl/v1/user/trade/history/count/$uuid") {
+ it.queryParam("symbol", symbol)
+ it.queryParam("startTime", startTime)
+ it.queryParam("endTime", endTime)
+ it.queryParam("direction", direction)
+ it.build()
+ }.accept(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToMono()
+ .awaitFirstOrElse { 0L }
+ }
+ }
}
\ No newline at end of file
diff --git a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/MatchingGatewayProxyImpl.kt b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/MatchingGatewayProxyImpl.kt
index 464b41ebd..92bed3034 100644
--- a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/MatchingGatewayProxyImpl.kt
+++ b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/MatchingGatewayProxyImpl.kt
@@ -1,21 +1,21 @@
package co.nilin.opex.api.ports.proxy.impl
-import co.nilin.opex.api.core.inout.MatchConstraint
-import co.nilin.opex.api.core.inout.MatchingOrderType
-import co.nilin.opex.api.core.inout.OrderDirection
-import co.nilin.opex.api.core.inout.OrderSubmitResult
+import co.nilin.opex.api.core.inout.*
import co.nilin.opex.api.core.spi.MatchingGatewayProxy
import co.nilin.opex.api.ports.proxy.config.ProxyDispatchers
import co.nilin.opex.api.ports.proxy.data.CancelOrderRequest
import co.nilin.opex.api.ports.proxy.data.CreateOrderRequest
import co.nilin.opex.common.utils.LoggerDelegate
+import kotlinx.coroutines.reactive.awaitFirstOrElse
import kotlinx.coroutines.reactor.awaitSingleOrNull
import kotlinx.coroutines.withContext
import org.springframework.beans.factory.annotation.Value
+import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.body
+import org.springframework.web.reactive.function.client.bodyToFlux
import org.springframework.web.reactive.function.client.bodyToMono
import reactor.core.publisher.Mono
import java.math.BigDecimal
@@ -38,7 +38,7 @@ class MatchingGatewayProxyImpl(private val client: WebClient) : MatchingGatewayP
matchConstraint: MatchConstraint?,
orderType: MatchingOrderType,
userLevel: String,
- token: String?
+ token: String?,
): OrderSubmitResult? {
logger.info("calling matching-gateway order create")
val body = CreateOrderRequest(uuid, pair, price, quantity, direction, matchConstraint, orderType, userLevel)
@@ -62,7 +62,7 @@ class MatchingGatewayProxyImpl(private val client: WebClient) : MatchingGatewayP
uuid: String,
orderId: Long,
symbol: String,
- token: String?
+ token: String?,
): OrderSubmitResult? {
logger.info("calling matching-gateway order cancel")
return withContext(ProxyDispatchers.general) {
@@ -78,4 +78,18 @@ class MatchingGatewayProxyImpl(private val client: WebClient) : MatchingGatewayP
.awaitSingleOrNull()
}
}
+
+ override suspend fun getPairSettings(): List {
+ return withContext(ProxyDispatchers.wallet) {
+ client.get()
+ .uri("$baseUrl/pair-setting")
+ .accept(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToFlux()
+ .collectList()
+ .awaitFirstOrElse { emptyList() }
+ }
+ }
}
\ No newline at end of file
diff --git a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/WalletProxyImpl.kt b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/WalletProxyImpl.kt
index cf38ab595..c934ad904 100644
--- a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/WalletProxyImpl.kt
+++ b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/WalletProxyImpl.kt
@@ -1,14 +1,13 @@
package co.nilin.opex.api.ports.proxy.impl
-import co.nilin.opex.api.core.inout.OwnerLimitsResponse
-import co.nilin.opex.api.core.inout.TransactionHistoryResponse
-import co.nilin.opex.api.core.inout.Wallet
-import co.nilin.opex.api.core.inout.WithdrawHistoryResponse
+import co.nilin.opex.api.core.inout.*
import co.nilin.opex.api.core.spi.WalletProxy
import co.nilin.opex.api.ports.proxy.config.ProxyDispatchers
import co.nilin.opex.api.ports.proxy.data.TransactionRequest
+import co.nilin.opex.common.OpexError
import co.nilin.opex.common.utils.LoggerDelegate
import kotlinx.coroutines.reactive.awaitFirstOrElse
+import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.withContext
import org.springframework.beans.factory.annotation.Value
@@ -74,46 +73,67 @@ class WalletProxyImpl(private val webClient: WebClient) : WalletProxy {
override suspend fun getDepositTransactions(
uuid: String,
- token: String?,
- coin: String?,
+ token: String,
+ currency: String?,
startTime: Long?,
endTime: Long?,
limit: Int,
offset: Int,
- ascendingByTime: Boolean?
- ): List {
+ ascendingByTime: Boolean?,
+ ): List {
logger.info("fetching deposit transaction history for $uuid")
return withContext(ProxyDispatchers.wallet) {
webClient.post()
- .uri("$baseUrl/transaction/deposit/$uuid")
+ .uri("$baseUrl/v1/deposit/history")
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer $token")
- .body(Mono.just(TransactionRequest(coin, startTime, endTime, limit, offset, ascendingByTime)))
+ .body(Mono.just(TransactionRequest(currency, startTime, endTime, limit, offset, ascendingByTime)))
.retrieve()
.onStatus({ t -> t.isError }, { it.createException() })
- .bodyToFlux()
+ .bodyToFlux()
.collectList()
.awaitFirstOrElse { emptyList() }
}
}
+ override suspend fun getDepositTransactionsCount(
+ uuid: String,
+ token: String,
+ currency: String?,
+ startTime: Long?,
+ endTime: Long?,
+ ): Long {
+ logger.info("fetching deposit transaction count for $uuid")
+ return withContext(ProxyDispatchers.wallet) {
+ webClient.post()
+ .uri("$baseUrl/v1/deposit/history/count")
+ .accept(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.AUTHORIZATION, "Bearer $token")
+ .body(Mono.just(TransactionRequest(currency, startTime, endTime, null, null)))
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToMono()
+ .awaitFirstOrElse { 0L }
+ }
+ }
+
override suspend fun getWithdrawTransactions(
uuid: String,
- token: String?,
- coin: String?,
+ token: String,
+ currency: String?,
startTime: Long?,
endTime: Long?,
limit: Int,
offset: Int,
- ascendingByTime: Boolean?
+ ascendingByTime: Boolean?,
): List {
logger.info("fetching withdraw transaction history for $uuid")
return withContext(ProxyDispatchers.wallet) {
webClient.post()
- .uri("$baseUrl/withdraw/history/$uuid")
+ .uri("$baseUrl/withdraw/history")
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer $token")
- .body(Mono.just(TransactionRequest(coin, startTime, endTime, limit, offset, ascendingByTime)))
+ .body(Mono.just(TransactionRequest(currency, startTime, endTime, limit, offset, ascendingByTime)))
.retrieve()
.onStatus({ t -> t.isError }, { it.createException() })
.bodyToFlux()
@@ -122,5 +142,316 @@ class WalletProxyImpl(private val webClient: WebClient) : WalletProxy {
}
}
+ override suspend fun getWithdrawTransactionsCount(
+ uuid: String,
+ token: String,
+ currency: String?,
+ startTime: Long?,
+ endTime: Long?,
+ ): Long {
+ logger.info("fetching withdraw transaction count for $uuid")
+ return withContext(ProxyDispatchers.wallet) {
+ webClient.post()
+ .uri("$baseUrl/withdraw/history/count")
+ .accept(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.AUTHORIZATION, "Bearer $token")
+ .body(Mono.just(TransactionRequest(currency, startTime, endTime, null, null)))
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToMono()
+ .awaitFirstOrElse { 0L }
+ }
+ }
+
+ override suspend fun getTransactions(
+ uuid: String,
+ token: String,
+ currency: String?,
+ category: UserTransactionCategory?,
+ startTime: Long?,
+ endTime: Long?,
+ limit: Int,
+ offset: Int,
+ ascendingByTime: Boolean?
+ ): List {
+ return webClient.post()
+ .uri("$baseUrl/v2/transaction")
+ .accept(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.AUTHORIZATION, "Bearer $token")
+ .body(
+ Mono.just(
+ UserTransactionRequest(
+ null,
+ currency,
+ null,
+ null,
+ category,
+ startTime,
+ endTime,
+ limit,
+ offset,
+ ascendingByTime == true,
+ null
+ )
+ )
+ )
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToFlux()
+ .collectList()
+ .awaitFirstOrElse { emptyList() }
+ }
+
+ override suspend fun getTransactionsCount(
+ uuid: String,
+ token: String,
+ currency: String?,
+ category: UserTransactionCategory?,
+ startTime: Long?,
+ endTime: Long?,
+ ): Long {
+ return webClient.post()
+ .uri("$baseUrl/v2/transaction/count")
+ .accept(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.AUTHORIZATION, "Bearer $token")
+ .body(
+ Mono.just(
+ UserTransactionRequest(
+ null,
+ currency,
+ null,
+ null,
+ category,
+ startTime,
+ endTime,
+ null
+ )
+ )
+ )
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToMono()
+ .awaitFirstOrElse { 0L }
+ }
+
+ override suspend fun getGateWays(
+ includeOffChainGateways: Boolean,
+ includeOnChainGateways: Boolean,
+ ): List {
+ return withContext(ProxyDispatchers.wallet) {
+ webClient.get()
+ .uri("$baseUrl/currency/gateways") {
+ it.queryParam("includeOffChainGateways", includeOffChainGateways)
+ it.queryParam("includeOnChainGateways", includeOnChainGateways)
+ it.build()
+ }.accept(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToFlux()
+ .collectList()
+ .awaitFirstOrElse { emptyList() }
+ }
+ }
+
+ override suspend fun getCurrencies(): List {
+ return withContext(ProxyDispatchers.wallet) {
+ webClient.get()
+ .uri("$baseUrl/currency/all")
+ .accept(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToFlux()
+ .collectList()
+ .awaitFirstOrElse { emptyList() }
+ }
+ }
+
+ override suspend fun getUserTradeTransactionSummary(
+ uuid: String,
+ token: String,
+ startTime: Long?,
+ endTime: Long?,
+ limit: Int?,
+ ): List {
+ return withContext(ProxyDispatchers.wallet) {
+ webClient.get()
+ .uri("$baseUrl/v2/transaction/trade/summary/$uuid") {
+ it.queryParam("startTime", startTime)
+ it.queryParam("endTime", endTime)
+ it.queryParam("limit", limit)
+ it.build()
+ }.accept(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.AUTHORIZATION, "Bearer $token")
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToFlux()
+ .collectList()
+ .awaitFirstOrElse { emptyList() }
+ }
+ }
+
+ override suspend fun getUserDepositSummary(
+ uuid: String,
+ token: String,
+ startTime: Long?,
+ endTime: Long?,
+ limit: Int?,
+ ): List {
+ return withContext(ProxyDispatchers.wallet) {
+ webClient.get()
+ .uri("$baseUrl/deposit/summary/$uuid") {
+ it.queryParam("startTime", startTime)
+ it.queryParam("endTime", endTime)
+ it.queryParam("limit", limit)
+ it.build()
+ }.accept(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.AUTHORIZATION, "Bearer $token")
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToFlux()
+ .collectList()
+ .awaitFirstOrElse { emptyList() }
+ }
+ }
+
+ override suspend fun getUserWithdrawSummary(
+ uuid: String,
+ token: String,
+ startTime: Long?,
+ endTime: Long?,
+ limit: Int?,
+ ): List {
+ return withContext(ProxyDispatchers.wallet) {
+ webClient.get()
+ .uri("$baseUrl/withdraw/summary/$uuid") {
+ it.queryParam("startTime", startTime)
+ it.queryParam("endTime", endTime)
+ it.queryParam("limit", limit)
+ it.build()
+ }.accept(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.AUTHORIZATION, "Bearer $token")
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToFlux()
+ .collectList()
+ .awaitFirstOrElse { emptyList() }
+ }
+ }
+
+ override suspend fun deposit(
+ request: RequestDepositBody
+ ): TransferResult? {
+ return withContext(ProxyDispatchers.wallet) {
+ webClient.post()
+ .uri("$baseUrl/deposit/${request.amount}_${request.chain}_${request.symbol}/${request.receiverUuid}_${request.receiverWalletType}") {
+ it.apply {
+ request.description?.let { description -> queryParam("description", description) }
+ request.transferRef?.let { transferRef -> queryParam("transferRef", transferRef) }
+ request.gatewayUuid?.let { gatewayUuid -> queryParam("gatewayUuid", gatewayUuid) }
+ }.build()
+ }.accept(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToMono()
+ .awaitFirstOrNull()
+ }
+ }
+
+ override suspend fun requestWithdraw(
+ token: String,
+ request: RequestWithdrawBody
+ ): WithdrawActionResult {
+ return webClient.post()
+ .uri("$baseUrl/withdraw")
+ .accept(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.AUTHORIZATION, "Bearer $token")
+ .body(Mono.just(request))
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToMono()
+ .awaitFirstOrElse { throw OpexError.BadRequest.exception() }
+ }
+
+ override suspend fun cancelWithdraw(token: String, withdrawId: Long): Void? {
+ return webClient.post()
+ .uri("$baseUrl/withdraw/$withdrawId/cancel")
+ .accept(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.AUTHORIZATION, "Bearer $token")
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToMono(Void::class.java)
+ .awaitFirstOrNull()
+ }
+
+ override suspend fun findWithdraw(token: String, withdrawId: Long): WithdrawResponse {
+ return webClient.get()
+ .uri("$baseUrl/withdraw/$withdrawId")
+ .accept(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.AUTHORIZATION, "Bearer $token")
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToMono()
+ .awaitFirstOrElse { throw OpexError.WithdrawNotFound.exception() }
+ }
+
+ override suspend fun submitVoucher(
+ code: String,
+ token: String
+ ): SubmitVoucherResponse {
+ return webClient.put()
+ .uri("$baseUrl/voucher/$code")
+ .accept(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.AUTHORIZATION, "Bearer $token")
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToMono()
+ .awaitFirstOrElse { throw OpexError.BadRequest.exception() }
+ }
+
+ override suspend fun getQuoteCurrencies(): List {
+ return withContext(ProxyDispatchers.wallet) {
+ webClient.get()
+ .uri("$baseUrl/currency/quotes") {
+ it.queryParam("isActive", true)
+ it.build()
+ }.accept(MediaType.APPLICATION_JSON)
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToFlux()
+ .collectList()
+ .awaitFirstOrElse { emptyList() }
+ }
+ }
+
+ override suspend fun getSwapTransactions(token: String, request: UserTransactionRequest): List {
+ return webClient.post()
+ .uri("$baseUrl/v1/swap/history")
+ .accept(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.AUTHORIZATION, "Bearer $token")
+ .body(Mono.just(request))
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToFlux()
+ .collectList()
+ .awaitFirstOrElse { emptyList() }
+ }
+
+ override suspend fun getSwapTransactionsCount(
+ token: String,
+ request: UserTransactionRequest
+ ): Long {
+ return webClient.post()
+ .uri("$baseUrl/v1/swap/history/count")
+ .accept(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.AUTHORIZATION, "Bearer $token")
+ .body(Mono.just(request))
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToMono()
+ .awaitFirstOrElse { 0L }
+ }
+}
-}
\ No newline at end of file
diff --git a/api/pom.xml b/api/pom.xml
index 509278fa0..236297638 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -82,11 +82,6 @@
interceptors
${interceptor.version}
-
- co.nilin.opex.utility
- preferences
- ${preferences.version}
-
org.springframework.cloud
spring-cloud-dependencies
diff --git a/auth-gateway/auth-gateway-app/Dockerfile b/auth-gateway/auth-gateway-app/Dockerfile
new file mode 100644
index 000000000..f519c734f
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/Dockerfile
@@ -0,0 +1,5 @@
+FROM openjdk:21
+ARG JAR_FILE=target/*.jar
+COPY ${JAR_FILE} app.jar
+ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"]
+HEALTHCHECK --interval=45s --timeout=30s --start-period=60s --retries=5 CMD curl -sf 'http://localhost:8080/actuator/health' >/dev/null || exit 1
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/pom.xml b/auth-gateway/auth-gateway-app/pom.xml
new file mode 100644
index 000000000..ad60a1764
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/pom.xml
@@ -0,0 +1,134 @@
+
+
+ 4.0.0
+
+
+ co.nilin.opex.auth
+ auth-gateway
+ 1.0.1-beta.7
+
+
+ co.nilin.opex
+ auth-gateway-app
+ user-management-app
+
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+ io.projectreactor.kotlin
+ reactor-kotlin-extensions
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-reactor
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-core
+ 1.7.3
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springframework.cloud
+ spring-cloud-starter-loadbalancer
+
+
+ org.springframework.cloud
+ spring-cloud-starter-consul-all
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
+
+
+ org.bouncycastle
+ bcprov-jdk15on
+ 1.60
+
+
+ com.fasterxml.jackson.module
+ jackson-module-kotlin
+
+
+ co.nilin.opex.utility
+ error-handler
+
+
+ com.auth0
+ java-jwt
+ 4.4.0
+
+
+ com.google.api-client
+ google-api-client
+ 2.2.0
+
+
+ com.auth0
+ jwks-rsa
+ 0.22.1
+
+
+ org.keycloak
+ keycloak-admin-client
+ 26.0.5
+
+
+ com.sun.mail
+ jakarta.mail
+ 2.0.1
+
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.12.6
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.12.6
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.12.6
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/AuthGateway.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/AuthGateway.kt
new file mode 100644
index 000000000..e5c8d7dab
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/AuthGateway.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.auth
+
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+import org.springframework.context.annotation.ComponentScan
+
+@SpringBootApplication
+@ComponentScan("co.nilin.opex")
+class AuthGateway
+
+fun main(args: Array) {
+ runApplication(*args)
+}
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/AppConfig.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/AppConfig.kt
new file mode 100644
index 000000000..d685ef2db
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/AppConfig.kt
@@ -0,0 +1,69 @@
+package co.nilin.opex.auth.config
+
+import jakarta.annotation.PostConstruct
+import org.springframework.context.annotation.Configuration
+import org.bouncycastle.util.io.pem.PemObject
+import org.bouncycastle.util.io.pem.PemWriter
+import org.springframework.context.annotation.Bean
+import java.io.File
+import java.io.FileOutputStream
+import java.io.OutputStreamWriter
+import java.io.StringWriter
+import java.security.KeyFactory
+import java.security.KeyPairGenerator
+import java.security.PrivateKey
+import java.security.PublicKey
+import java.security.interfaces.RSAPrivateCrtKey
+import java.security.spec.PKCS8EncodedKeySpec
+import java.security.spec.RSAPublicKeySpec
+import java.util.*
+
+@Configuration
+class AppConfig {
+
+ @PostConstruct
+ fun init() {
+ val pemFile = File("/app/keys/private.pem")
+ if (pemFile.exists())
+ return
+
+ val keypair = KeyPairGenerator.getInstance("RSA").apply { initialize(2048) }.generateKeyPair()
+ val privateKeyPem = convertPrivateKeyToPem(keypair.private)
+
+ File("/app/keys").apply { if (!exists()) mkdir() }
+ OutputStreamWriter(FileOutputStream("/app/keys/private.pem")).use { it.write(privateKeyPem) }
+ }
+
+ @Bean("privateKeyString")
+ fun privateKeyString(): String {
+ return File("/app/keys/private.pem").readText()
+ .replace("-----BEGIN PRIVATE KEY-----", "")
+ .replace("-----END PRIVATE KEY-----", "")
+ .replace("\n", "")
+ }
+
+ @Bean("privateKey")
+ fun privateKey(): PrivateKey {
+ val pKeyString = privateKeyString()
+ val keyBytes = Base64.getDecoder().decode(pKeyString)
+ val keySpec = PKCS8EncodedKeySpec(keyBytes)
+ val keyFactory = KeyFactory.getInstance("RSA")
+ return keyFactory.generatePrivate(keySpec)
+ }
+
+ @Bean("publicKey")
+ fun publicKey(): PublicKey {
+ val privateKey = privateKey() as RSAPrivateCrtKey
+ val publicKeySpec = RSAPublicKeySpec(privateKey.modulus, privateKey.publicExponent)
+ val keyFactory = KeyFactory.getInstance("RSA")
+ return keyFactory.generatePublic(publicKeySpec)
+ }
+
+ private fun convertPrivateKeyToPem(privateKey: PrivateKey): String {
+ val keySpec = PKCS8EncodedKeySpec(privateKey.encoded)
+ val pemObject = PemObject("PRIVATE KEY", keySpec.encoded)
+ val stringWriter = StringWriter()
+ PemWriter(stringWriter).use { it.writeObject(pemObject) }
+ return stringWriter.toString()
+ }
+}
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/CaptchaConfig.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/CaptchaConfig.kt
new file mode 100644
index 000000000..157dd37c1
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/CaptchaConfig.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.auth.config
+
+import org.springframework.boot.context.properties.ConfigurationProperties
+import org.springframework.stereotype.Component
+
+@Component
+@ConfigurationProperties(prefix = "captcha")
+class CaptchaConfig {
+ lateinit var url: String
+}
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/ErrorHandlerConfig.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/ErrorHandlerConfig.kt
new file mode 100644
index 000000000..0b59cb4ae
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/ErrorHandlerConfig.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.auth.config
+
+import co.nilin.opex.utility.error.EnableOpexErrorHandler
+import org.springframework.context.annotation.Configuration
+
+@Configuration
+@EnableOpexErrorHandler
+class ErrorHandlerConfig {
+
+}
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/KafkaProducerConfig.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/KafkaProducerConfig.kt
new file mode 100644
index 000000000..1d707f700
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/KafkaProducerConfig.kt
@@ -0,0 +1,60 @@
+package co.nilin.opex.auth.config
+
+import co.nilin.opex.auth.data.AuthEvent
+import org.apache.kafka.clients.admin.NewTopic
+import org.apache.kafka.clients.producer.ProducerConfig
+import org.apache.kafka.common.serialization.StringSerializer
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.support.GenericApplicationContext
+import org.springframework.kafka.config.TopicBuilder
+import org.springframework.kafka.core.DefaultKafkaProducerFactory
+import org.springframework.kafka.core.KafkaTemplate
+import org.springframework.kafka.core.ProducerFactory
+import org.springframework.kafka.support.serializer.JsonSerializer
+import java.util.function.Supplier
+
+object KafkaTopics {
+ const val AUTH = "auth"
+}
+
+@Configuration
+class KafkaProducerConfig(
+ @Value("\${spring.kafka.bootstrap-servers}")
+ private val bootstrapServers: String
+) {
+
+ @Bean
+ fun producerConfigs(): Map {
+ return mapOf(
+ ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers,
+ ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java,
+ ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to JsonSerializer::class.java,
+ ProducerConfig.ACKS_CONFIG to "all",
+ JsonSerializer.TYPE_MAPPINGS to "userCreatedEvent:co.nilin.opex.auth.data.UserCreatedEvent"
+ )
+ }
+
+ @Bean
+ fun producerFactory(producerConfigs: Map): ProducerFactory {
+ return DefaultKafkaProducerFactory(producerConfigs)
+ }
+
+ @Bean
+ fun kafkaTemplate(producerFactory: ProducerFactory): KafkaTemplate {
+ return KafkaTemplate(producerFactory)
+ }
+
+ @Autowired
+ fun createUserCreatedTopics(applicationContext: GenericApplicationContext) {
+ applicationContext.registerBean("topic_auth", NewTopic::class.java, Supplier {
+ TopicBuilder.name(KafkaTopics.AUTH)
+ .partitions(1)
+ .replicas(1)
+ .build()
+ })
+ }
+}
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/KeycloakAdminConfig.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/KeycloakAdminConfig.kt
new file mode 100644
index 000000000..4f87db169
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/KeycloakAdminConfig.kt
@@ -0,0 +1,27 @@
+package co.nilin.opex.auth.config
+
+import org.keycloak.admin.client.Keycloak
+import org.keycloak.admin.client.KeycloakBuilder
+import org.keycloak.admin.client.resource.RealmResource
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+
+@Configuration
+class KeycloakAdminConfig {
+
+ @Bean
+ fun keycloak(config: KeycloakConfig): Keycloak {
+ return KeycloakBuilder.builder()
+ .serverUrl(config.url)
+ .realm(config.realm)
+ .clientId(config.adminClient.id)
+ .clientSecret(config.adminClient.secret)
+ .grantType("client_credentials")
+ .build()
+ }
+
+ @Bean
+ fun opexRealm(keycloak: Keycloak, config: KeycloakConfig): RealmResource {
+ return keycloak.realm(config.realm)
+ }
+}
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/KeycloakConfig.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/KeycloakConfig.kt
new file mode 100644
index 000000000..1ed74107a
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/KeycloakConfig.kt
@@ -0,0 +1,19 @@
+package co.nilin.opex.auth.config
+
+import org.springframework.boot.context.properties.ConfigurationProperties
+import org.springframework.stereotype.Component
+
+@Component
+@ConfigurationProperties(prefix = "keycloak")
+class KeycloakConfig {
+ lateinit var url: String
+ lateinit var certUrl: String
+ lateinit var realm: String
+ lateinit var adminClient: Client
+}
+
+data class Client(
+ val id: String,
+ val secret: String,
+ val googleClientId: String?
+)
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/OTPConfig.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/OTPConfig.kt
new file mode 100644
index 000000000..d311e81ef
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/OTPConfig.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.auth.config
+
+import org.springframework.boot.context.properties.ConfigurationProperties
+import org.springframework.stereotype.Component
+
+@Component
+@ConfigurationProperties(prefix = "otp")
+class OTPConfig {
+ lateinit var url: String
+}
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/SecurityConfig.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/SecurityConfig.kt
new file mode 100644
index 000000000..1d6780e30
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/SecurityConfig.kt
@@ -0,0 +1,43 @@
+package co.nilin.opex.auth.config
+
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.config.Customizer
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder
+import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.web.reactive.function.client.WebClient
+
+@EnableWebFluxSecurity
+@Configuration
+class SecurityConfig(
+ @Qualifier("keycloakWebClient")
+ private val webClient: WebClient,
+ private val keycloakConfig: KeycloakConfig
+) {
+
+ @Bean
+ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+ return http.csrf { it.disable() }
+ .authorizeExchange {
+ it.pathMatchers("/actuator/**").permitAll()
+ .pathMatchers("/v1/oauth/protocol/openid-connect/**").permitAll()
+ .pathMatchers("/v1/oauth.***").permitAll()
+ .pathMatchers("/v1/user/public/**").permitAll()
+ .anyExchange().authenticated()
+ }
+ .oauth2ResourceServer { it.jwt(Customizer.withDefaults()) }
+ .build()
+ }
+
+ @Bean
+ @Throws(Exception::class)
+ fun reactiveJwtDecoder(): ReactiveJwtDecoder? {
+ return NimbusReactiveJwtDecoder.withJwkSetUri(keycloakConfig.certUrl)
+ .webClient(webClient)
+ .build()
+ }
+}
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/WebClientConfig.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/WebClientConfig.kt
new file mode 100644
index 000000000..88d8140e9
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/WebClientConfig.kt
@@ -0,0 +1,48 @@
+package co.nilin.opex.auth.config
+
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.cloud.client.loadbalancer.LoadBalanced
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.http.client.reactive.ReactorClientHttpConnector
+import org.springframework.web.reactive.function.client.WebClient
+import org.zalando.logbook.Logbook
+import org.zalando.logbook.netty.LogbookClientHandler
+import reactor.netty.http.client.HttpClient
+
+@Configuration
+class WebClientConfig {
+
+ @Bean("keycloakWebClient")
+ fun keycloakWebClient(keycloakConfig: KeycloakConfig, logbook: Logbook): WebClient {
+ val client = HttpClient.create().doOnConnected { it.addHandlerLast(LogbookClientHandler(logbook)) }
+ return WebClient.builder()
+ .clientConnector(ReactorClientHttpConnector(client))
+ .baseUrl(keycloakConfig.url)
+ .build()
+ }
+
+ @LoadBalanced
+ @Bean("otpWebclientBuilder")
+ fun otpWebClientBuilder(otpConfig: OTPConfig): WebClient.Builder {
+ return WebClient.builder().baseUrl(otpConfig.url)
+ }
+
+ @Bean("otpWebClient")
+ fun otpWebClient(@Qualifier("otpWebclientBuilder") builder: WebClient.Builder): WebClient {
+ return builder.build()
+ }
+
+ @LoadBalanced
+ @Bean("captchaWebclientBuilder")
+ fun captchaWebClientBuilder(captchaConfig: CaptchaConfig): WebClient.Builder {
+ return WebClient.builder().baseUrl(captchaConfig.url)
+ }
+
+
+ @Bean("captchaWebClient")
+ fun captchaWebClient(@Qualifier("captchaWebclientBuilder") builder: WebClient.Builder): WebClient {
+ return builder.build()
+ }
+
+}
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/controller/AuthController.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/controller/AuthController.kt
new file mode 100644
index 000000000..8aaa7a76e
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/controller/AuthController.kt
@@ -0,0 +1,35 @@
+package co.nilin.opex.auth.controller;
+
+import co.nilin.opex.auth.model.ExternalIdpTokenRequest
+import co.nilin.opex.auth.model.PasswordFlowTokenRequest
+import co.nilin.opex.auth.model.RefreshTokenRequest
+import co.nilin.opex.auth.model.TokenResponse
+import co.nilin.opex.auth.service.TokenService
+import org.springframework.http.ResponseEntity
+import org.springframework.web.bind.annotation.PostMapping
+import org.springframework.web.bind.annotation.RequestBody
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+@RequestMapping("/v1/oauth/protocol/openid-connect/")
+class AuthController(private val tokenService: TokenService) {
+
+ @PostMapping("/token")
+ suspend fun getToken(@RequestBody tokenRequest: PasswordFlowTokenRequest): ResponseEntity {
+ val tokenResponse = tokenService.getToken(tokenRequest)
+ return ResponseEntity.ok().body(tokenResponse)
+ }
+
+ @PostMapping("/token-external")
+ suspend fun getToken(@RequestBody tokenRequest: ExternalIdpTokenRequest): ResponseEntity {
+ val tokenResponse = tokenService.getToken(tokenRequest)
+ return ResponseEntity.ok().body(tokenResponse)
+ }
+
+ @PostMapping("/refresh")
+ suspend fun refreshToken(@RequestBody tokenRequest: RefreshTokenRequest): ResponseEntity {
+ val tokenResponse = tokenService.refreshToken(tokenRequest)
+ return ResponseEntity.ok().body(tokenResponse)
+ }
+}
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/controller/PublicUserController.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/controller/PublicUserController.kt
new file mode 100644
index 000000000..6516908fe
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/controller/PublicUserController.kt
@@ -0,0 +1,61 @@
+package co.nilin.opex.auth.controller
+
+import co.nilin.opex.auth.model.*
+import co.nilin.opex.auth.service.UserService
+import jakarta.validation.Valid
+import org.springframework.http.ResponseEntity
+import org.springframework.web.bind.annotation.PostMapping
+import org.springframework.web.bind.annotation.RequestBody
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RequestParam
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+@RequestMapping("/v1/user/public")
+class PublicUserController(private val userService: UserService) {
+
+
+ //TODO IMPORTANT: remove in production
+ @PostMapping("/register")
+ suspend fun registerUser(@Valid @RequestBody request: RegisterUserRequest): ResponseEntity {
+ val otp = userService.registerUser(request)
+ return ResponseEntity.ok().body(TempOtpResponse(otp))
+ }
+
+ @PostMapping("/register/verify")
+ suspend fun verifyRegister(@RequestBody request: VerifyOTPRequest): ResponseEntity {
+ val token = userService.verifyRegister(request)
+ return ResponseEntity.ok(OTPActionTokenResponse(token))
+ }
+
+ @PostMapping("/register/confirm")
+ suspend fun confirmRegister(@RequestBody request: ConfirmRegisterRequest): ResponseEntity {
+ val loginToken = userService.confirmRegister(request)
+ return ResponseEntity.ok(loginToken)
+ }
+
+ @PostMapping("/register-external")
+ suspend fun registerExternal(@RequestBody request: ExternalIdpUserRegisterRequest): ResponseEntity {
+ userService.registerExternalIdpUser(request)
+ return ResponseEntity.ok().build()
+ }
+
+ //TODO IMPORTANT: remove in production
+ @PostMapping("/forget")
+ suspend fun forgetPassword(@RequestBody request: ForgotPasswordRequest): ResponseEntity {
+ val code = userService.forgetPassword(request)
+ return ResponseEntity.ok().body(TempOtpResponse(code))
+ }
+
+ @PostMapping("/forget/verify")
+ suspend fun verifyForget(@RequestBody request: VerifyOTPRequest): ResponseEntity {
+ val token = userService.verifyForget(request)
+ return ResponseEntity.ok(OTPActionTokenResponse(token))
+ }
+
+ @PostMapping("/forget/confirm")
+ suspend fun forgetPassword(@RequestBody request: ConfirmForgetRequest): ResponseEntity {
+ userService.confirmForget(request)
+ return ResponseEntity.ok().build()
+ }
+}
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/controller/UserController.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/controller/UserController.kt
new file mode 100644
index 000000000..54325c1d2
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/controller/UserController.kt
@@ -0,0 +1,21 @@
+package co.nilin.opex.auth.controller
+
+import co.nilin.opex.auth.service.UserService
+import co.nilin.opex.auth.utils.jwtAuthentication
+import org.springframework.security.core.annotation.CurrentSecurityContext
+import org.springframework.security.core.context.SecurityContext
+import org.springframework.web.bind.annotation.PostMapping
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+@RequestMapping("/v1/user")
+class UserController(private val userService: UserService) {
+
+ @PostMapping("/logout")
+ suspend fun logout(@CurrentSecurityContext securityContext: SecurityContext) {
+ userService.logout(securityContext.jwtAuthentication().name)
+ }
+
+}
+
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/data/AuthEvent.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/data/AuthEvent.kt
new file mode 100644
index 000000000..3fc9f8dca
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/data/AuthEvent.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.auth.data
+
+import java.time.LocalDateTime
+
+open class AuthEvent {
+
+ val time: LocalDateTime = LocalDateTime.now()
+}
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/data/UserCreatedEvent.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/data/UserCreatedEvent.kt
new file mode 100644
index 000000000..964c6b7da
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/data/UserCreatedEvent.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.auth.data
+
+data class UserCreatedEvent(
+ val uuid: String,
+ val username: String,
+ val email: String?,
+ val mobile: String?,
+ val firstName: String?,
+ val lastName: String?
+) : AuthEvent()
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/kafka/AuthEventProducer.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/kafka/AuthEventProducer.kt
new file mode 100644
index 000000000..067322494
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/kafka/AuthEventProducer.kt
@@ -0,0 +1,33 @@
+package co.nilin.opex.auth.kafka
+
+import co.nilin.opex.auth.config.KafkaTopics
+import co.nilin.opex.auth.data.AuthEvent
+import co.nilin.opex.common.utils.LoggerDelegate
+import kotlinx.coroutines.future.asDeferred
+import org.springframework.kafka.core.KafkaTemplate
+import org.springframework.retry.support.RetryTemplate
+import org.springframework.stereotype.Component
+
+@Component
+class AuthEventProducer(private val template: KafkaTemplate) {
+
+ private val logger by LoggerDelegate()
+
+ private val retryTemplate = RetryTemplate.builder()
+ .maxAttempts(10)
+ .exponentialBackoff(1000, 1.8, 5 * 60 * 1000)
+ .retryOn(Exception::class.java)
+ .build()
+
+ fun send(event: AuthEvent) {
+ retryTemplate.execute {
+ template.send(KafkaTopics.AUTH, event).whenComplete { res, error ->
+ if (error != null) {
+ logger.error("Error sending auth event", error)
+ throw error
+ }
+ logger.info("Auth event sent")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/Attribute.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/Attribute.kt
new file mode 100644
index 000000000..32f2b9076
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/Attribute.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.auth.model
+
+data class Attribute(
+ val key: String,
+ val value: String
+)
+
+object Attributes {
+
+ const val EMAIL = "email"
+ const val MOBILE = "mobile"
+ const val OTP = "otpConfig"
+}
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/Captcha.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/Captcha.kt
new file mode 100644
index 000000000..9e564c6ec
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/Captcha.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.auth.model
+
+enum class CaptchaType {
+ INTERNAL, ARCAPTCHA, HCAPTCHA
+}
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/Error.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/Error.kt
new file mode 100644
index 000000000..e78d63016
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/Error.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.auth.model
+
+import java.time.Instant
+
+data class ErrorResponse(
+ val timestamp: Instant, // Timestamp of the error
+ val status: Int, // HTTP status code
+ val error: String, // HTTP status reason phrase (e.g., "Bad Request")
+ val message: String, // Error message
+ val path: String // API path where the error occurred
+)
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/OTP.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/OTP.kt
new file mode 100644
index 000000000..c1da668b9
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/OTP.kt
@@ -0,0 +1,37 @@
+package co.nilin.opex.auth.model
+
+import jakarta.validation.constraints.NotBlank
+
+data class OTPReceiver(
+ val receiver: String,
+ val type: OTPType,
+)
+
+data class OTPCode(
+ @field:NotBlank(message = "code is required")
+ val code: String,
+
+ @field:NotBlank(message = "otpType is required")
+ val otpType: OTPType,
+)
+
+data class OTPVerifyRequest(
+ val userId: String,
+ val otpCodes: List
+)
+
+data class OTPVerifyResponse(
+ val result: Boolean,
+ val type: OTPResultType
+)
+
+//TODO IMPORTANT: remove in production
+data class TempOtpResponse(val otp: String)
+
+enum class OTPAction {
+ REGISTER, FORGET, NONE
+}
+
+enum class OTPResultType {
+ VALID, EXPIRED, INCORRECT, INVALID
+}
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/Token.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/Token.kt
new file mode 100644
index 000000000..267c699da
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/Token.kt
@@ -0,0 +1,66 @@
+package co.nilin.opex.auth.model
+
+import com.fasterxml.jackson.annotation.JsonProperty
+
+data class PasswordFlowTokenRequest(
+ val username: String,
+ val password: String,
+ val clientId: String,
+ val clientSecret: String?,
+ val otp: String?,
+ val rememberMe: Boolean = true,
+ val captchaType: CaptchaType? = CaptchaType.INTERNAL,
+ val captchaCode: String,
+)
+
+data class RefreshTokenRequest(
+ val clientId: String,
+ val clientSecret: String?,
+ val refreshToken: String
+)
+
+data class ExternalIdpTokenRequest(
+ val idToken: String,
+ val accessToken: String,
+ val idp: String,
+ val otpVerifyRequest: OTPVerifyRequest?
+)
+
+data class Token(
+ @JsonProperty("access_token")
+ val accessToken: String, // The access token
+
+ @JsonProperty("expires_in")
+ val expiresIn: Int?, // Expiration time of the access token in seconds
+
+ @JsonProperty("refresh_expires_in")
+ val refreshExpiresIn: Int?, // Expiration time of the refresh token in seconds
+
+ @JsonProperty("refresh_token")
+ var refreshToken: String?, // The refresh token
+
+ @JsonProperty("token_type")
+ val tokenType: String?, // Type of token (usually "Bearer")
+
+ @JsonProperty("not-before-policy")
+ val notBeforePolicy: Int?, // Timestamp indicating when the token becomes valid
+
+ @JsonProperty("session_state")
+ val sessionState: String?, // Session state (optional)
+
+ @JsonProperty("scope")
+ val scope: String? // Scopes associated with the token
+
+)
+
+data class TokenResponse(
+ val token: Token?,
+ val otp: RequiredOTP?,
+ //TODO IMPORTANT: remove in production
+ val otpCode: String?,
+)
+
+data class RequiredOTP(
+ val type: OTPType,
+ val receiver: String?
+)
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/UserRegister.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/UserRegister.kt
new file mode 100644
index 000000000..c287598bc
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/UserRegister.kt
@@ -0,0 +1,63 @@
+package co.nilin.opex.auth.model
+
+data class RegisterUserRequest(
+ val username: String,
+ val firstName: String? = null,
+ val lastName: String? = null,
+ val captchaType: CaptchaType? = CaptchaType.INTERNAL,
+ val captchaCode: String,
+)
+
+data class VerifyOTPRequest(
+ val username: String,
+ val otp: String,
+)
+
+data class OTPActionTokenResponse(
+ val token: String,
+)
+
+data class ConfirmRegisterRequest(
+ val password: String,
+ val token: String,
+ val clientId: String?,
+ val clientSecret: String?,
+)
+
+data class TokenData(
+ val isValid: Boolean,
+ val userId: String,
+ val action: OTPAction,
+)
+
+data class ExternalIdpUserRegisterRequest(
+ val idToken: String,
+ val idp: String,
+ val password: String,
+ val otpVerifyRequest: OTPVerifyRequest?,
+)
+
+data class KeycloakUser(
+ val id: String,
+ val username: String,
+ val email: String?,
+ val firstName: String?,
+ val lastName: String?,
+ val emailVerified: Boolean,
+ val enabled: Boolean,
+ val attributes: Map>?,
+) {
+ val mobile: String? = attributes?.get(Attributes.MOBILE)?.get(0)
+}
+
+data class ConfirmForgetRequest(
+ val newPassword: String,
+ val newPasswordConfirmation: String,
+ val token: String,
+)
+
+data class ForgotPasswordRequest(
+ val username: String,
+ val captchaType: CaptchaType? = CaptchaType.INTERNAL,
+ val captchaCode: String,
+)
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/Username.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/Username.kt
new file mode 100644
index 000000000..6589cd493
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/Username.kt
@@ -0,0 +1,32 @@
+package co.nilin.opex.auth.model
+
+import co.nilin.opex.auth.utils.UsernameValidator
+import co.nilin.opex.common.OpexError
+
+data class Username(
+ val value: String,
+ val type: UsernameType
+) {
+
+ fun asAttribute() = Attribute(type.name.lowercase(), value)
+
+ companion object {
+ fun create(username: String): Username {
+ val type = UsernameValidator.getType(username.replace("+", ""))
+ if (type.isUnknown()) throw OpexError.InvalidUsername.exception()
+ return Username(username, type)
+ }
+ }
+}
+
+enum class UsernameType(val otpType: OTPType) {
+ MOBILE(OTPType.SMS),
+ EMAIL(OTPType.EMAIL),
+ UNKNOWN(OTPType.NONE);
+
+ fun isUnknown() = this == UNKNOWN
+}
+
+enum class OTPType {
+ EMAIL, SMS, TOTP, NONE
+}
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/CaptchaProxy.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/CaptchaProxy.kt
new file mode 100644
index 000000000..db93949cb
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/CaptchaProxy.kt
@@ -0,0 +1,37 @@
+package co.nilin.opex.auth.proxy
+
+import co.nilin.opex.auth.model.CaptchaType
+import co.nilin.opex.common.OpexError
+import kotlinx.coroutines.reactive.awaitFirstOrNull
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.http.HttpHeaders
+import org.springframework.http.HttpStatus
+import org.springframework.http.MediaType
+import org.springframework.stereotype.Component
+import org.springframework.web.reactive.function.client.WebClient
+import reactor.core.publisher.Mono
+
+@Component
+class CaptchaProxy(
+ @Value("\${captcha.enabled}") private val captchaEnabled: Boolean,
+ @Qualifier("captchaWebClient") private val webClient: WebClient,
+) {
+
+ suspend fun validateCaptcha(proof: String, type: CaptchaType) {
+ if (captchaEnabled) {
+ val statusCode = webClient.get().uri("/verify") {
+ it.queryParam("type", type)
+ it.queryParam("proof", proof)
+ it.build()
+ }.accept(MediaType.APPLICATION_JSON).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
+ .exchangeToMono { response -> Mono.just(response.statusCode()) }.awaitFirstOrNull()
+
+ when (statusCode) {
+ HttpStatus.NO_CONTENT -> return
+ HttpStatus.BAD_REQUEST -> throw OpexError.InvalidCaptcha.exception()
+ else -> throw OpexError.BadRequest.exception("Error in verify captcha")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/GoogleIdpProxy.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/GoogleIdpProxy.kt
new file mode 100644
index 000000000..72ef7457c
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/GoogleIdpProxy.kt
@@ -0,0 +1,34 @@
+package co.nilin.opex.auth.proxy
+
+import co.nilin.opex.auth.config.KeycloakConfig
+import com.auth0.jwk.JwkProvider
+import com.auth0.jwk.JwkProviderBuilder
+import com.auth0.jwt.JWT
+import com.auth0.jwt.algorithms.Algorithm
+import com.auth0.jwt.exceptions.JWTVerificationException
+import com.auth0.jwt.interfaces.DecodedJWT
+import org.springframework.stereotype.Service
+import java.net.URL
+import java.security.interfaces.RSAPublicKey
+
+@Service
+class GoogleProxy(private val keycloakConfig: KeycloakConfig) {
+
+ fun validateGoogleToken(googleToken: String): DecodedJWT {
+ // Step 1: Fetch Google's public keys
+ val jwkProvider: JwkProvider = JwkProviderBuilder(URL("https://www.googleapis.com/oauth2/v3/certs"))
+ .build()
+
+ // Step 2: Decode and verify the token
+ val algorithm = Algorithm.RSA256(jwkProvider.get(JWT.decode(googleToken).keyId).publicKey as RSAPublicKey, null)
+ val verifier = JWT.require(algorithm)
+ .withIssuer("https://accounts.google.com")
+ .build()
+
+ val decoded = verifier.verify(googleToken)
+ if ( decoded.audience.isEmpty() || !decoded.audience.contains(keycloakConfig.adminClient.googleClientId)){
+ throw JWTVerificationException("Google token's audience doesn't match")
+ }
+ return decoded
+ }
+}
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/KeycloakProxy.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/KeycloakProxy.kt
new file mode 100644
index 000000000..eb3a0cc42
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/KeycloakProxy.kt
@@ -0,0 +1,308 @@
+package co.nilin.opex.auth.proxy
+
+import co.nilin.opex.auth.config.KeycloakConfig
+import co.nilin.opex.auth.model.*
+import co.nilin.opex.common.OpexError
+import co.nilin.opex.common.utils.LoggerDelegate
+import kotlinx.coroutines.reactive.awaitFirstOrElse
+import kotlinx.coroutines.reactive.awaitSingle
+import kotlinx.coroutines.reactor.awaitSingleOrNull
+import org.keycloak.admin.client.resource.RealmResource
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.http.HttpHeaders
+import org.springframework.http.HttpStatus
+import org.springframework.http.MediaType
+import org.springframework.stereotype.Service
+import org.springframework.web.reactive.function.client.*
+
+@Service
+class KeycloakProxy(
+ @Qualifier("keycloakWebClient")
+ private val keycloakClient: WebClient,
+ private val keycloakConfig: KeycloakConfig,
+ private val opexRealm: RealmResource
+) {
+
+ private val adminClient = keycloakConfig.adminClient
+ private val logger by LoggerDelegate()
+
+ suspend fun getAdminAccessToken(): String {
+ val tokenUrl = "${keycloakConfig.url}/realms/${keycloakConfig.realm}/protocol/openid-connect/token"
+ val response = keycloakClient.post()
+ .uri(tokenUrl)
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .bodyValue("client_id=${adminClient.id}&client_secret=${adminClient.secret}&grant_type=client_credentials")
+ .retrieve()
+ .awaitBody() // Assuming the response is a JSON object
+ return response.accessToken
+ }
+
+ suspend fun getUserToken(
+ username: Username,
+ password: String?,
+ clientId: String,
+ clientSecret: String?
+ ): Token {
+ val users = findUserByAttribute(username.asAttribute())
+ if (users.isEmpty())
+ throw OpexError.UserNotFound.exception()
+
+ val userTokenUrl = "${keycloakConfig.url}/realms/${keycloakConfig.realm}/protocol/openid-connect/token"
+ return keycloakClient.post()
+ .uri(userTokenUrl)
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .bodyValue("client_id=${clientId}&client_secret=${clientSecret}&grant_type=password&username=${users[0].username}&password=${password}")
+ .retrieve()
+ .onStatus({ it == HttpStatus.valueOf(401) }) {
+ throw OpexError.InvalidUserCredentials.exception()
+ }
+ .awaitBody()
+ }
+
+ suspend fun checkUserCredentials(user: KeycloakUser, password: String) {
+ keycloakClient.post()
+ .uri("${keycloakConfig.url}/realms/${keycloakConfig.realm}/password/validate")
+ .header("Content-Type", "application/json")
+ .bodyValue(
+ object {
+ val userId = user.id
+ val password = password
+ }
+ ).retrieve()
+ .onStatus({ it == HttpStatus.valueOf(401) }) { throw OpexError.InvalidUserCredentials.exception() }
+ .awaitBodilessEntity()
+ }
+
+ suspend fun refreshUserToken(
+ refreshToken: String,
+ clientId: String,
+ clientSecret: String?
+ ): Token {
+ val userTokenUrl = "${keycloakConfig.url}/realms/${keycloakConfig.realm}/protocol/openid-connect/token"
+ return keycloakClient.post()
+ .uri(userTokenUrl)
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .bodyValue("client_id=${clientId}&client_secret=${clientSecret}&grant_type=refresh_token&refresh_token=${refreshToken}")
+ .retrieve()
+ .awaitBody()
+ }
+
+ suspend fun exchangeGoogleTokenForKeycloakToken(accessToken: String): Token {
+ val tokenUrl = "${keycloakConfig.url}/realms/${keycloakConfig.realm}/protocol/openid-connect/token"
+ val requestBody =
+ "client_id=${adminClient.id}&client_secret=${adminClient.secret}&grant_type=urn:ietf:params:oauth:grant-type:token-exchange&subject_token=$accessToken&subject_token_type=urn:ietf:params:oauth:token-type:access_token&subject_issuer=google"
+ return keycloakClient.post()
+ .uri(tokenUrl)
+ .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
+ .bodyValue(requestBody)
+ .retrieve()
+ .bodyToMono()
+ .awaitSingle()
+ }
+
+ suspend fun findUserByEmail(email: String): String {
+ // Step 1: Build the URL for the Keycloak Admin REST API
+ val userSearchUrl = "${keycloakConfig.url}/admin/realms/${keycloakConfig.realm}/users?email=${email}"
+
+ // Step 2: Make a GET request to Keycloak's Admin REST API
+ val users = keycloakClient.get()
+ .uri(userSearchUrl)
+ .header(HttpHeaders.AUTHORIZATION, "Bearer ${getAdminAccessToken()}")
+ .accept(MediaType.APPLICATION_JSON)
+ .retrieve()
+ .bodyToMono>()
+ .awaitSingle()
+
+ // Step 3: Check if a user was found
+ if (users.isEmpty()) {
+ throw IllegalArgumentException("No user found with email: $email")
+ }
+
+ // Step 4: Return the username of the first user in the list
+ return users[0].id
+ }
+
+ suspend fun findUserByUsername(username: Username): KeycloakUser? {
+ val users = findUserByAttribute(username.asAttribute())
+ return if (users.isEmpty()) null else users[0]
+ }
+
+ private suspend fun findUserByAttribute(attr: Attribute): List {
+ val uri = "${keycloakConfig.url}/admin/realms/${keycloakConfig.realm}/users?q=${attr.key}:${attr.value}"
+
+ return keycloakClient.get()
+ .uri(uri)
+ .withAdminToken()
+ .retrieve()
+ .bodyToMono>()
+ .awaitFirstOrElse { emptyList() }
+ }
+
+ suspend fun createUser(
+ username: Username,
+ firstName: String?,
+ lastName: String?,
+ enabled: Boolean
+ ) {
+ val keycloakUrl = "${keycloakConfig.url}/admin/realms/${keycloakConfig.realm}/users"
+ val token = getAdminAccessToken()
+
+ val response = keycloakClient.post()
+ .uri(keycloakUrl)
+ .header("Content-Type", "application/json")
+ .withAdminToken(token)
+ .bodyValue(
+ hashMapOf(
+ "username" to username.value,
+ "emailVerified" to enabled,
+ "firstName" to firstName,
+ "lastName" to lastName,
+ "enabled" to enabled,
+ "attributes" to hashMapOf(
+ "kycLevel" to "0"
+ ).apply {
+ if (username.type == UsernameType.MOBILE)
+ put("mobile", username.value)
+ put(Attributes.OTP, username.type.otpType.name)
+ }
+ ).apply { if (username.type == UsernameType.EMAIL) put("email", username.value) }
+ )
+ .retrieve()
+ .onStatus({ it == HttpStatus.valueOf(409) }) {
+ throw OpexError.UserAlreadyExists.exception()
+ }
+ .toBodilessEntity()
+ .awaitSingle()
+ }
+
+ suspend fun confirmCreateUser(user: KeycloakUser, password: String) {
+ val keycloakUrl = "${keycloakConfig.url}/admin/realms/${keycloakConfig.realm}/users/${user.id}"
+ val token = getAdminAccessToken()
+
+ keycloakClient.put()
+ .uri(keycloakUrl)
+ .header("Content-Type", "application/json")
+ .withAdminToken(token)
+ .bodyValue(
+ hashMapOf(
+ "emailVerified" to true,
+ "enabled" to true,
+ "credentials" to listOf(
+ mapOf(
+ "type" to "password",
+ "value" to password,
+ "temporary" to false
+ )
+ )
+ )
+ )
+ .retrieve()
+ .toBodilessEntity()
+ .awaitSingle()
+ }
+
+ suspend fun assignDefaultRoles(user: KeycloakUser) {
+ val role = opexRealm.roles().get("user-1").toRepresentation()
+ val u = opexRealm.users().get(user.id)
+ u.roles().realmLevel().add(mutableListOf(role))
+ }
+
+ suspend fun createExternalIdpUser(email: String, username: Username, password: String): String {
+ val userUrl = "${keycloakConfig.url}/admin/realms/${keycloakConfig.realm}/users"
+ val userRequest = mapOf(
+ "username" to username.value,
+ "email" to email,
+ "emailVerified" to true,
+ "enabled" to true,
+ "credentials" to listOf(
+ mapOf(
+ "type" to "password",
+ "value" to password,
+ "temporary" to false
+ )
+ ),
+ "attributes" to hashMapOf(
+ "kycLevel" to "0",
+ Attributes.OTP to username.type.otpType.name
+ )
+ )
+
+ val response = keycloakClient.post()
+ .uri(userUrl)
+ .header(HttpHeaders.AUTHORIZATION, "Bearer ${getAdminAccessToken()}")
+ .contentType(MediaType.APPLICATION_JSON)
+ .bodyValue(userRequest)
+ .retrieve()
+ .toBodilessEntity()
+ .awaitSingle()
+
+ if (response.statusCode.isError) {
+ throw RuntimeException("Failed to create user in Keycloak")
+ }
+
+ // Return the user ID (you may need to query Keycloak to get the user ID)
+ return findUserByEmail(email)
+ }
+
+ suspend fun linkGoogleIdentity(userId: String, email: String, googleUserId: String) {
+ val identityUrl =
+ "${keycloakConfig.url}/admin/realms/${keycloakConfig.realm}/users/$userId/federated-identity/google"
+ val identityRequest = mapOf(
+ "identityProvider" to "google",
+ "userId" to googleUserId, // Use the Google user ID from the token
+ "userName" to email // Use the Google email as the username
+ )
+
+ val response = keycloakClient.post()
+ .uri(identityUrl)
+ .header(HttpHeaders.AUTHORIZATION, "Bearer ${getAdminAccessToken()}")
+ .contentType(MediaType.APPLICATION_JSON)
+ .bodyValue(identityRequest)
+ .retrieve()
+ .toBodilessEntity()
+ .awaitSingle()
+
+ if (response.statusCode.isError) {
+ throw RuntimeException("Failed to link Google identity to Keycloak user")
+ }
+ }
+
+ suspend fun logout(userId: String) {
+ val url = "${keycloakConfig.url}/admin/realms/${keycloakConfig.realm}/users/${userId}/logout"
+ keycloakClient.post()
+ .uri(url)
+ .contentType(MediaType.APPLICATION_JSON)
+ .withAdminToken()
+ .retrieve()
+ .toBodilessEntity()
+ .awaitSingleOrNull()
+ }
+
+ suspend fun resetPassword(userId: String, newPassword: String) {
+ val url = "${keycloakConfig.url}/admin/realms/${keycloakConfig.realm}/users/${userId}/reset-password"
+ val request = object {
+ val type = "password"
+ val value = newPassword
+ val temporary = false
+ }
+ keycloakClient.put()
+ .uri(url)
+ .contentType(MediaType.APPLICATION_JSON)
+ .withAdminToken()
+ .bodyValue(request)
+ .retrieve()
+ .toBodilessEntity()
+ .awaitSingleOrNull()
+ }
+
+ suspend fun WebClient.RequestHeadersSpec<*>.withAdminToken(token: String? = null): WebClient.RequestHeadersSpec<*> {
+ header(HttpHeaders.AUTHORIZATION, "Bearer ${token ?: getAdminAccessToken()}")
+ return this
+ }
+
+ suspend fun WebClient.RequestBodySpec.withAdminToken(token: String? = null): WebClient.RequestBodySpec {
+ header(HttpHeaders.AUTHORIZATION, "Bearer ${token ?: getAdminAccessToken()}")
+ return this
+ }
+
+}
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/KeycloakProxyV2.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/KeycloakProxyV2.kt
new file mode 100644
index 000000000..2efdaf55f
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/KeycloakProxyV2.kt
@@ -0,0 +1,74 @@
+package co.nilin.opex.auth.proxy
+
+import co.nilin.opex.auth.config.KeycloakConfig
+import co.nilin.opex.auth.model.Attribute
+import co.nilin.opex.auth.model.Token
+import co.nilin.opex.auth.model.Username
+import co.nilin.opex.common.OpexError
+import org.keycloak.admin.client.Keycloak
+import org.keycloak.admin.client.resource.RealmResource
+import org.keycloak.representations.idm.UserRepresentation
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.http.HttpStatus
+import org.springframework.web.reactive.function.client.WebClient
+import org.springframework.web.reactive.function.client.awaitBody
+
+class KeycloakProxyV2(
+ private val keycloak: Keycloak,
+ private val opexRealm: RealmResource,
+ private val keycloakConfig: KeycloakConfig,
+ @Qualifier("keycloakWebClient")
+ private val client: WebClient
+) {
+
+ suspend fun getUserToken(username: Username, password: String, clientId: String, clientSecret: String): Token {
+ val user = findByUsername(username) ?: throw OpexError.InvalidUserCredentials.exception()
+ return getUserToken(user.username, password, clientId, clientSecret)
+ }
+
+ fun findByUsername(username: Username): UserRepresentation? {
+ val users = findUserByAttribute(username.asAttribute())
+ if (users.isEmpty())
+ return null
+ return users[0]
+ }
+
+ private suspend fun getUserToken(
+ username: String,
+ password: String?,
+ clientId: String,
+ clientSecret: String
+ ): Token {
+ val userTokenUrl = "${keycloakConfig.url}/realms/${keycloakConfig.realm}/protocol/openid-connect/token"
+ return client.post()
+ .uri(userTokenUrl)
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .bodyValue("client_id=${clientId}&client_secret=${clientSecret}&grant_type=password&username=${username}&password=${password}")
+ .retrieve()
+ .onStatus({ it == HttpStatus.valueOf(401) }) {
+ throw OpexError.InvalidUserCredentials.exception()
+ }
+ .awaitBody()
+ }
+
+ suspend fun refreshUserToken(
+ refreshToken: String,
+ clientId: String,
+ clientSecret: String
+ ): Token {
+ val userTokenUrl = "${keycloakConfig.url}/realms/${keycloakConfig.realm}/protocol/openid-connect/token"
+ return client.post()
+ .uri(userTokenUrl)
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .bodyValue("client_id=${clientId}&client_secret=${clientSecret}&grant_type=refresh_token&refresh_token=${refreshToken}")
+ .retrieve()
+ .onStatus({ it == HttpStatus.valueOf(401) }) {
+ throw OpexError.InvalidUserCredentials.exception()
+ }
+ .awaitBody()
+ }
+
+ private fun findUserByAttribute(attr: Attribute, exact: Boolean = true): List {
+ return opexRealm.users().searchByAttributes("${attr.key}:${attr.value}", exact)
+ }
+}
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/OTPProxy.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/OTPProxy.kt
new file mode 100644
index 000000000..3a81f4486
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/OTPProxy.kt
@@ -0,0 +1,50 @@
+package co.nilin.opex.auth.proxy
+
+import co.nilin.opex.auth.model.OTPReceiver
+import co.nilin.opex.auth.model.OTPVerifyRequest
+import co.nilin.opex.auth.model.OTPVerifyResponse
+import co.nilin.opex.auth.model.TempOtpResponse
+import kotlinx.coroutines.reactive.awaitSingle
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.http.MediaType
+import org.springframework.stereotype.Component
+import org.springframework.web.reactive.function.BodyInserters
+import org.springframework.web.reactive.function.client.WebClient
+import org.springframework.web.reactive.function.client.awaitBody
+import org.springframework.web.reactive.function.client.toEntity
+
+@Component
+class OTPProxy(@Qualifier("otpWebClient") private val webClient: WebClient) {
+
+ //TODO IMPORTANT: remove in production
+
+ suspend fun requestOTP(userId: String, receivers: List): TempOtpResponse {
+ val request = object {
+ val userId = userId
+ val receivers = receivers
+ }
+
+ return webClient.post().uri("/otp")
+ .contentType(MediaType.APPLICATION_JSON)
+ .body(BodyInserters.fromValue(request))
+ .retrieve()
+ .awaitBody()
+ }
+
+ suspend fun verifyOTP(verifyRequest: OTPVerifyRequest): OTPVerifyResponse {
+ val request = object {
+ val userId = verifyRequest.userId
+ val otpCodes = verifyRequest.otpCodes.map {
+ object {
+ val type = it.otpType
+ val code = it.code
+ }
+ }
+ }
+ return webClient.post().uri("/otp/verify")
+ .contentType(MediaType.APPLICATION_JSON)
+ .body(BodyInserters.fromValue(request))
+ .retrieve()
+ .awaitBody()
+ }
+}
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/TokenService.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/TokenService.kt
new file mode 100644
index 000000000..4a3fd0064
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/TokenService.kt
@@ -0,0 +1,89 @@
+package co.nilin.opex.auth.service
+
+import co.nilin.opex.auth.model.*
+import co.nilin.opex.auth.proxy.CaptchaProxy
+import co.nilin.opex.auth.proxy.GoogleProxy
+import co.nilin.opex.auth.proxy.KeycloakProxy
+import co.nilin.opex.auth.proxy.OTPProxy
+import co.nilin.opex.common.OpexError
+import org.springframework.stereotype.Service
+
+@Service
+class TokenService(
+ private val otpProxy: OTPProxy,
+ private val keycloakProxy: KeycloakProxy,
+ private val googleProxy: GoogleProxy,
+ private val captchaProxy: CaptchaProxy,
+) {
+
+ suspend fun getToken(request: PasswordFlowTokenRequest): TokenResponse {
+ captchaProxy.validateCaptcha(request.captchaCode, request.captchaType ?: CaptchaType.INTERNAL)
+ val username = Username.create(request.username)
+ val user = keycloakProxy.findUserByUsername(username) ?: throw OpexError.UserNotFound.exception()
+
+ val otpType = OTPType.valueOf(user.attributes?.get(Attributes.OTP)?.get(0) ?: OTPType.NONE.name)
+
+ if (otpType == OTPType.NONE) {
+ val token = keycloakProxy.getUserToken(
+ username,
+ request.password,
+ request.clientId,
+ request.clientSecret
+ ).apply { if (!request.rememberMe) refreshToken = null }
+ return TokenResponse(token, null, null)
+ }
+
+ if (request.otp.isNullOrBlank()) {
+ keycloakProxy.checkUserCredentials(user, request.password)
+
+ val requiredOtpTypes = listOf(OTPReceiver(username.value, otpType))
+ val res = otpProxy.requestOTP(username.value, requiredOtpTypes)
+ val receiver = when (otpType) {
+ OTPType.EMAIL -> user.email
+ OTPType.SMS -> user.mobile
+ else -> null
+ }
+ return TokenResponse(null, RequiredOTP(otpType, receiver), res.otp)
+ }
+
+ val otpRequest = OTPVerifyRequest(username.value, listOf(OTPCode(request.otp, username.type.otpType)))
+ val otpResult = otpProxy.verifyOTP(otpRequest)
+ if (!otpResult.result) {
+ when (otpResult.type) {
+ OTPResultType.EXPIRED -> throw OpexError.ExpiredOTP.exception()
+ else -> throw OpexError.InvalidOTP.exception()
+ }
+ }
+
+ val token = keycloakProxy.getUserToken(
+ username,
+ request.password,
+ request.clientId,
+ request.clientSecret
+ ).apply { if (!request.rememberMe) refreshToken = null }
+
+ return TokenResponse(token, null, null)
+ }
+
+ suspend fun getToken(tokenRequest: ExternalIdpTokenRequest): TokenResponse {
+ val idToken = tokenRequest.idToken
+ val decodedJWT = googleProxy.validateGoogleToken(idToken)
+ val email = decodedJWT.getClaim("email").asString()
+ ?: throw IllegalArgumentException("Email not found in Google token")
+ try {
+ keycloakProxy.findUserByEmail(email)
+ } catch (e: Exception) {
+ throw OpexError.UserNotFound.exception()
+ }
+ return TokenResponse(
+ keycloakProxy.exchangeGoogleTokenForKeycloakToken(
+ tokenRequest.accessToken
+ ), null, null
+ )
+ }
+
+ suspend fun refreshToken(request: RefreshTokenRequest): TokenResponse {
+ val token = keycloakProxy.refreshUserToken(request.refreshToken, request.clientId, request.clientSecret)
+ return TokenResponse(token, null, null)
+ }
+}
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/UserService.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/UserService.kt
new file mode 100644
index 000000000..7c55e5aca
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/UserService.kt
@@ -0,0 +1,180 @@
+package co.nilin.opex.auth.service
+
+import co.nilin.opex.auth.data.UserCreatedEvent
+import co.nilin.opex.auth.kafka.AuthEventProducer
+import co.nilin.opex.auth.model.*
+import co.nilin.opex.auth.proxy.CaptchaProxy
+import co.nilin.opex.auth.proxy.GoogleProxy
+import co.nilin.opex.auth.proxy.KeycloakProxy
+import co.nilin.opex.auth.proxy.OTPProxy
+import co.nilin.opex.common.OpexError
+import co.nilin.opex.common.utils.LoggerDelegate
+import io.jsonwebtoken.JwtException
+import io.jsonwebtoken.Jwts
+import org.springframework.stereotype.Service
+import java.security.PrivateKey
+import java.security.PublicKey
+import java.time.LocalDateTime
+import java.time.ZoneId
+import java.util.*
+
+@Service
+class UserService(
+ private val otpProxy: OTPProxy,
+ private val keycloakProxy: KeycloakProxy,
+ private val googleProxy: GoogleProxy,
+ private val privateKey: PrivateKey,
+ private val publicKey: PublicKey,
+ private val captchaProxy: CaptchaProxy,
+ private val authProducer: AuthEventProducer
+) {
+
+ private val logger by LoggerDelegate()
+
+ //TODO IMPORTANT: remove in production
+ suspend fun registerUser(request: RegisterUserRequest): String {
+ captchaProxy.validateCaptcha(request.captchaCode, request.captchaType ?: CaptchaType.INTERNAL)
+ val username = Username.create(request.username)
+ val userStatus = isUserDuplicate(username)
+
+ val otpType = username.type.otpType
+ val res = otpProxy.requestOTP(request.username, listOf(OTPReceiver(request.username, otpType)))
+
+ if (!userStatus)
+ keycloakProxy.createUser(
+ username,
+ request.firstName,
+ request.lastName,
+ false
+ )
+ return res.otp
+ }
+
+ suspend fun verifyRegister(request: VerifyOTPRequest): String {
+ val username = Username.create(request.username)
+ val otpRequest = OTPVerifyRequest(username.value, listOf(OTPCode(request.otp, username.type.otpType)))
+ val otpResult = otpProxy.verifyOTP(otpRequest)
+ if (!otpResult.result) {
+ when (otpResult.type) {
+ OTPResultType.EXPIRED -> throw OpexError.ExpiredOTP.exception()
+ else -> throw OpexError.InvalidOTP.exception()
+ }
+ }
+ return generateToken(username.value, OTPAction.REGISTER)
+ }
+
+ suspend fun confirmRegister(request: ConfirmRegisterRequest): Token? {
+ val data = verifyToken(request.token)
+ if (!data.isValid || data.action != OTPAction.REGISTER)
+ throw OpexError.InvalidRegisterToken.exception()
+
+ val username = Username.create(data.userId)
+ val user = keycloakProxy.findUserByUsername(username)
+ if (user == null || user.enabled)
+ throw OpexError.BadRequest.exception()
+
+ keycloakProxy.confirmCreateUser(user, request.password)
+ keycloakProxy.assignDefaultRoles(user)
+
+ // Send event to let other services know a user just registered
+ val event = UserCreatedEvent(user.id, user.username, user.email, user.mobile, user.firstName, user.lastName)
+ authProducer.send(event)
+
+ return if (request.clientId.isNullOrBlank() || request.clientSecret.isNullOrBlank())
+ null
+ else
+ keycloakProxy.getUserToken(username, request.password, request.clientId, request.clientSecret)
+ }
+
+ suspend fun registerExternalIdpUser(externalIdpUserRegisterRequest: ExternalIdpUserRegisterRequest) {
+ val decodedJWT = googleProxy.validateGoogleToken(externalIdpUserRegisterRequest.idToken)
+ val email = decodedJWT.getClaim("email").asString()
+ ?: throw OpexError.GmailNotFoundInToken.exception()
+ val googleUserId = decodedJWT.getClaim("sub").asString()
+ ?: throw OpexError.UserIDNotFoundInToken.exception()
+
+ val username = Username.create(email) // Use email as the username
+ isUserDuplicate(username)
+
+ val userId = keycloakProxy.createExternalIdpUser(email, username, externalIdpUserRegisterRequest.password)
+ keycloakProxy.linkGoogleIdentity(userId, email, googleUserId)
+ }
+
+ suspend fun logout(userId: String) {
+ keycloakProxy.logout(userId)
+ }
+
+ suspend fun forgetPassword(request: ForgotPasswordRequest): String {
+ captchaProxy.validateCaptcha(request.captchaCode, request.captchaType ?: CaptchaType.INTERNAL)
+ val uName = Username.create(request.username)
+ val user = keycloakProxy.findUserByUsername(uName) ?: return null ?: ""
+ //TODO IMPORTANT: remove in production
+ return otpProxy.requestOTP(uName.value, listOf(OTPReceiver(uName.value, uName.type.otpType))).otp
+ }
+
+ suspend fun verifyForget(request: VerifyOTPRequest): String {
+ val username = Username.create(request.username)
+ val otpRequest = OTPVerifyRequest(username.value, listOf(OTPCode(request.otp, username.type.otpType)))
+ val otpResult = otpProxy.verifyOTP(otpRequest)
+ if (!otpResult.result) {
+ when (otpResult.type) {
+ OTPResultType.EXPIRED -> throw OpexError.ExpiredOTP.exception()
+ else -> throw OpexError.InvalidOTP.exception()
+ }
+ }
+ return generateToken(username.value, OTPAction.FORGET)
+ }
+
+ suspend fun confirmForget(request: ConfirmForgetRequest) {
+ if (request.newPassword != request.newPasswordConfirmation)
+ throw OpexError.InvalidPassword.exception()
+
+ val data = verifyToken(request.token)
+ if (!data.isValid || data.action != OTPAction.FORGET)
+ throw OpexError.InvalidRegisterToken.exception()
+
+ val username = Username.create(data.userId)
+ val user = keycloakProxy.findUserByUsername(username) ?: return
+
+ keycloakProxy.resetPassword(user.id, request.newPassword)
+ }
+
+ private suspend fun isUserDuplicate(username: Username): Boolean {
+ val user = keycloakProxy.findUserByUsername(username)
+ return if (user == null)
+ false
+ else if (!user.enabled)
+ return true
+ else
+ throw OpexError.UserAlreadyExists.exception()
+ }
+
+ private fun generateToken(userId: String, action: OTPAction): String {
+ val issuedAt = Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant())
+ val exp = Date.from(LocalDateTime.now().plusMinutes(2).atZone(ZoneId.systemDefault()).toInstant())
+ return Jwts.builder()
+ .issuer("opex-auth")
+ .claim("userId", userId)
+ .claim("action", action)
+ .issuedAt(issuedAt)
+ .expiration(exp)
+ .signWith(privateKey)
+ .compact()
+ }
+
+ private fun verifyToken(token: String): TokenData {
+ try {
+ val claims = Jwts.parser()
+ .verifyWith(publicKey)
+ .build()
+ .parseSignedClaims(token)
+ .payload
+ return TokenData(true, claims["userId"] as String, OTPAction.valueOf(claims["action"] as String))
+ } catch (e: JwtException) {
+ logger.error("Could not verify token", e)
+ return TokenData(false, "", OTPAction.REGISTER)
+ }
+ }
+
+}
+
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/utils/SecurityExtension.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/utils/SecurityExtension.kt
new file mode 100644
index 000000000..d97a1ceea
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/utils/SecurityExtension.kt
@@ -0,0 +1,12 @@
+package co.nilin.opex.auth.utils
+
+import org.springframework.security.core.context.SecurityContext
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
+
+fun SecurityContext.jwtAuthentication(): JwtAuthenticationToken {
+ return authentication as JwtAuthenticationToken
+}
+
+fun JwtAuthenticationToken.tokenValue(): String {
+ return this.token.tokenValue
+}
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/utils/UsernameValidator.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/utils/UsernameValidator.kt
new file mode 100644
index 000000000..724dc2a00
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/utils/UsernameValidator.kt
@@ -0,0 +1,33 @@
+package co.nilin.opex.auth.utils
+
+import co.nilin.opex.auth.model.UsernameType
+import jakarta.mail.internet.InternetAddress
+import java.util.regex.Pattern
+
+object UsernameValidator {
+
+ private val mobileRegex = Pattern.compile("^\\d{10,15}$")
+
+ fun getType(username: String): UsernameType {
+ if (isValidEmail(username))
+ return UsernameType.EMAIL
+
+ if (isValidMobile(username))
+ return UsernameType.MOBILE
+
+ return UsernameType.UNKNOWN
+ }
+
+ fun isValidMobile(input: String): Boolean {
+ return mobileRegex.matcher(input).matches()
+ }
+
+ fun isValidEmail(input: String): Boolean {
+ return try {
+ InternetAddress(input).validate()
+ true
+ } catch (e: Exception) {
+ false
+ }
+ }
+}
\ No newline at end of file
diff --git a/auth-gateway/auth-gateway-app/src/main/resources/application.yml b/auth-gateway/auth-gateway-app/src/main/resources/application.yml
new file mode 100644
index 000000000..4525eb1d5
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/resources/application.yml
@@ -0,0 +1,72 @@
+server:
+ port: 8080
+
+spring:
+ application:
+ name: opex-auth-gateway
+ kafka:
+ bootstrap-servers: ${KAFKA_IP_PORT:localhost:9092}
+ consumer:
+ group-id: auth
+ cloud:
+ bootstrap:
+ enabled: true
+ consul:
+ host: ${CONSUL_HOST:localhost}
+ port: 8500
+ discovery:
+ #healthCheckPath: ${management.context-path}/health
+ instance-id: ${spring.application.name}:${server.port}
+ healthCheckInterval: 20s
+ prefer-ip-address: true
+management:
+ endpoints:
+ web:
+ base-path: /actuator
+ exposure:
+ include: [ "health", "prometheus", "metrics", "loggers" ]
+logbook:
+ secure-filter:
+ enabled: true
+ format:
+ style: http
+ filter:
+ enabled: true
+ form-request-mode: BODY
+ attribute-extractors:
+ - type: JwtFirstMatchingClaimExtractor
+ claim-names: [ "sub", "subject" ]
+ obfuscate:
+ headers:
+ - Authorization
+ parameters:
+ - password
+ json-body-fields:
+ - password
+ replacement: "***"
+ write:
+ max-body-size: 500 #kb
+ predicate:
+ exclude:
+ - path: /auth**
+ - path: /actuator/**
+ - path: /swagger**
+ - path: /config/**
+logging:
+ level:
+ co.nilin: INFO
+ org.zalando.logbook: TRACE
+keycloak:
+ url: http://keycloak:8080
+ cert-url: http://keycloak:8080/realms/opex/protocol/openid-connect/certs
+ realm: opex
+ admin-client:
+ id: "opex-admin"
+ secret: ${ADMIN_CLIENT_SECRET}
+ google-client-id: ${GOOGLE_CLIENT_ID}
+
+otp:
+ url: http://opex-otp/v1
+captcha:
+ url: http://opex-captcha
+ enabled: false
\ No newline at end of file
diff --git a/auth-gateway/docker-compose.yml b/auth-gateway/docker-compose.yml
new file mode 100644
index 000000000..5246b65ed
--- /dev/null
+++ b/auth-gateway/docker-compose.yml
@@ -0,0 +1,50 @@
+version: '3.8'
+
+services:
+ keycloak:
+ image: quay.io/keycloak/keycloak:26.1
+ container_name: keycloak
+ environment:
+ KEYCLOAK_ADMIN: admin
+ KEYCLOAK_ADMIN_PASSWORD: admin
+ KC_DB: postgres
+ KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
+ KC_DB_USERNAME: keycloak
+ KC_DB_PASSWORD: keycloak
+ KC_HEALTH_ENABLED: true
+ KC_METRICS_ENABLED: true
+ GOOGLE_CLIENT_ID: 625813606110-er3v3sol4v206kdg40gf0ltqv08scgs2.apps.googleusercontent.com
+ GOOGLE_CLIENT_SECRET: "*************"
+ command:
+ - start-dev
+ - --import-realm
+ - --features=admin-fine-grained-authz,token-exchange
+ volumes:
+ - ./keycloak-setup/realms:/opt/keycloak/data/import
+ ports:
+ - "8080:8080"
+ depends_on:
+ - postgres
+ networks:
+ - keycloak-network
+
+ postgres:
+ image: postgres:15
+ container_name: postgres
+ environment:
+ POSTGRES_DB: keycloak
+ POSTGRES_USER: keycloak
+ POSTGRES_PASSWORD: keycloak
+ ports:
+ - "5432:5432"
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ networks:
+ - keycloak-network
+
+volumes:
+ postgres_data:
+
+networks:
+ keycloak-network:
+ driver: bridge
\ No newline at end of file
diff --git a/auth-gateway/keycloak-setup/Dockerfile b/auth-gateway/keycloak-setup/Dockerfile
new file mode 100644
index 000000000..8ecca1eb9
--- /dev/null
+++ b/auth-gateway/keycloak-setup/Dockerfile
@@ -0,0 +1,10 @@
+FROM maven:3.9.6-eclipse-temurin-21 AS builder
+WORKDIR /build/spi
+COPY spi/pom.xml .
+RUN mvn dependency:go-offline
+COPY spi/src ./src
+RUN mvn clean install
+
+FROM quay.io/keycloak/keycloak:26.1
+COPY --from=builder /build/spi/target/*.jar /opt/keycloak/providers/
+COPY realms/ /opt/keycloak/data/import/
\ No newline at end of file
diff --git a/auth-gateway/keycloak-setup/config/keycloak-server.json b/auth-gateway/keycloak-setup/config/keycloak-server.json
new file mode 100644
index 000000000..6c6b39cc1
--- /dev/null
+++ b/auth-gateway/keycloak-setup/config/keycloak-server.json
@@ -0,0 +1,215 @@
+{
+ "hostname": {
+ "provider": "${keycloak.hostname.provider:default}",
+ "fixed": {
+ "hostname": "${keycloak.hostname.fixed.hostname:localhost}",
+ "httpPort": "${keycloak.hostname.fixed.httpPort:-1}",
+ "httpsPort": "${keycloak.hostname.fixed.httpsPort:-1}",
+ "alwaysHttps": "${keycloak.hostname.fixed.alwaysHttps:false}"
+ },
+ "default": {
+ "frontendUrl": "${keycloak.frontendUrl:}",
+ "adminUrl": "${keycloak.adminUrl:}",
+ "forceBackendUrlToFrontendUrl": "${keycloak.hostname.default.forceBackendUrlToFrontendUrl:false}"
+ }
+ },
+ "admin": {
+ "realm": "master"
+ },
+ "eventsStore": {
+ "provider": "${keycloak.eventsStore.provider:jpa}",
+ "jpa": {
+ "max-detail-length": "${keycloak.eventsStore.maxDetailLength:1000}"
+ }
+ },
+ "eventsListener": {
+ "jboss-logging": {
+ "success-level": "debug",
+ "error-level": "warn"
+ },
+ "event-queue": {}
+ },
+ "realm": {
+ "provider": "${keycloak.realm.provider:jpa}"
+ },
+ "user": {
+ "provider": "${keycloak.user.provider:jpa}"
+ },
+ "client": {
+ "provider": "${keycloak.client.provider:jpa}"
+ },
+ "clientScope": {
+ "provider": "${keycloak.clientScope.provider:jpa}"
+ },
+ "group": {
+ "provider": "${keycloak.group.provider:jpa}"
+ },
+ "role": {
+ "provider": "${keycloak.role.provider:jpa}"
+ },
+ "authenticationSessions": {
+ "provider": "${keycloak.authSession.provider:infinispan}"
+ },
+ "mapStorage": {
+ "provider": "${keycloak.mapStorage.provider:concurrenthashmap}",
+ "concurrenthashmap": {
+ "dir": "${project.build.directory:target}"
+ }
+ },
+ "userFederatedStorage": {
+ "provider": "${keycloak.userFederatedStorage.provider:jpa}"
+ },
+ "userSessionPersister": {
+ "provider": "${keycloak.userSessionPersister.provider:jpa}"
+ },
+ "authorizationPersister": {
+ "provider": "${keycloak.authorization.provider:jpa}"
+ },
+ "userCache": {
+ "provider": "${keycloak.user.cache.provider:default}",
+ "default": {
+ "enabled": true
+ },
+ "mem": {
+ "maxSize": 20000
+ }
+ },
+ "userSessions": {
+ "provider": "${keycloak.userSessions.provider:infinispan}"
+ },
+ "timer": {
+ "provider": "basic"
+ },
+ "theme": {
+ "staticMaxAge": "${keycloak.theme.staticMaxAge:2592000}",
+ "cacheTemplates": "${keycloak.theme.cacheTemplates:true}",
+ "cacheThemes": "${keycloak.theme.cacheThemes:true}",
+ "folder": {
+ "dir": "${keycloak.theme.dir}"
+ }
+ },
+ "login": {
+ "provider": "freemarker"
+ },
+ "account": {
+ "provider": "freemarker"
+ },
+ "email": {
+ "provider": "freemarker"
+ },
+ "scheduled": {
+ "interval": 900
+ },
+ "connectionsHttpClient": {
+ "default": {
+ "reuse-connections": false
+ }
+ },
+ "connectionsJpa": {
+ "default": {
+ "url": "${spring.datasource.url}",
+ "driver": "${spring.datasource.driver-class-name}",
+ "driverDialect": "${spring.jpa.properties.hibernate.dialect}",
+ "user": "${spring.datasource.username}",
+ "password": "${spring.datasource.password}",
+ "initializeEmpty": true,
+ "migrationStrategy": "update",
+ "showSql": "true",
+ "formatSql": "true",
+ "globalStatsInterval": "-1"
+ }
+ },
+ "realmCache": {
+ "provider": "${keycloak.realm.cache.provider:default}",
+ "default": {
+ "enabled": true
+ }
+ },
+ "connectionsInfinispan": {
+ "default": {
+ "jgroupsUdpMcastAddr": "${keycloak.connectionsInfinispan.jgroupsUdpMcastAddr:234.56.78.90}",
+ "nodeName": "${keycloak.connectionsInfinispan.nodeName,jboss.node.name:}",
+ "siteName": "${keycloak.connectionsInfinispan.siteName,jboss.site.name:}",
+ "clustered": "${keycloak.connectionsInfinispan.clustered:false}",
+ "async": "${keycloak.connectionsInfinispan.async:false}",
+ "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:1}",
+ "l1Lifespan": "${keycloak.connectionsInfinispan.l1Lifespan:600000}",
+ "remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:false}",
+ "remoteStoreHost": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}",
+ "remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}",
+ "hotrodProtocolVersion": "${keycloak.connectionsInfinispan.hotrodProtocolVersion}",
+ "embedded": "${keycloak.connectionsInfinispan.embedded:true}"
+ }
+ },
+ "truststore": {
+ "file": {
+ "disabled": "${keycloak.truststore.disabled:true}"
+ }
+ },
+ "jta-lookup": {
+ "provider": "${keycloak.jta.lookup.provider:jboss}",
+ "jboss": {
+ "enabled": true
+ }
+ },
+ "login-protocol": {
+ "saml": {
+ "knownProtocols": [
+ "http=${auth.server.http.port}",
+ "https=${auth.server.https.port}"
+ ]
+ }
+ },
+ "userProfile": {
+ "legacy-user-profile": {
+ "read-only-attributes": [
+ "deniedFoo",
+ "deniedBar*",
+ "deniedSome/thing",
+ "deniedsome*thing"
+ ],
+ "admin-read-only-attributes": [
+ "deniedSomeAdmin"
+ ]
+ }
+ },
+ "x509cert-lookup": {
+ "provider": "${keycloak.x509cert.lookup.provider:default}",
+ "default": {
+ "enabled": true
+ },
+ "haproxy": {
+ "enabled": true,
+ "sslClientCert": "x-ssl-client-cert",
+ "sslCertChainPrefix": "x-ssl-client-cert-chain",
+ "certificateChainLength": 1
+ },
+ "apache": {
+ "enabled": true,
+ "sslClientCert": "x-ssl-client-cert",
+ "sslCertChainPrefix": "x-ssl-client-cert-chain",
+ "certificateChainLength": 1
+ },
+ "nginx": {
+ "enabled": true,
+ "sslClientCert": "x-ssl-client-cert",
+ "sslCertChainPrefix": "x-ssl-client-cert-chain",
+ "certificateChainLength": 1
+ }
+ },
+ "vault": {
+ "provider": "hachicorp-vault",
+ "default": {
+ "enabled": true
+ },
+ "hachicorp-vault": {
+ "url": "${keycloak.hashicorp.url}",
+ "appId": "${spring.application.name}",
+ "engine-name": "secret",
+ "enabled": "${keycloak.vault.files-plaintext.provider.enabled:true}"
+ }
+ },
+ "saml-artifact-resolver": {
+ "provider": "${keycloak.saml-artifact-resolver.provider:default}"
+ }
+}
diff --git a/auth-gateway/keycloak-setup/realms/opex-master-realm.json b/auth-gateway/keycloak-setup/realms/opex-master-realm.json
new file mode 100644
index 000000000..146a5976d
--- /dev/null
+++ b/auth-gateway/keycloak-setup/realms/opex-master-realm.json
@@ -0,0 +1,47 @@
+{
+ "id": "master",
+ "realm": "master",
+ "notBefore": 0,
+ "revokeRefreshToken": false,
+ "refreshTokenMaxReuse": 0,
+ "accessTokenLifespan": 300,
+ "accessTokenLifespanForImplicitFlow": 900,
+ "ssoSessionIdleTimeout": 1800,
+ "ssoSessionMaxLifespan": 36000,
+ "ssoSessionIdleTimeoutRememberMe": 0,
+ "ssoSessionMaxLifespanRememberMe": 0,
+ "offlineSessionIdleTimeout": 2592000,
+ "offlineSessionMaxLifespanEnabled": false,
+ "offlineSessionMaxLifespan": 5184000,
+ "accessCodeLifespan": 60,
+ "accessCodeLifespanUserAction": 300,
+ "accessCodeLifespanLogin": 1800,
+ "actionTokenGeneratedByAdminLifespan": 43200,
+ "actionTokenGeneratedByUserLifespan": 300,
+ "enabled": true,
+ "sslRequired": "none",
+ "registrationAllowed": true,
+ "registrationEmailAsUsername": false,
+ "rememberMe": false,
+ "verifyEmail": false,
+ "loginWithEmailAllowed": true,
+ "duplicateEmailsAllowed": false,
+ "resetPasswordAllowed": true,
+ "editUsernameAllowed": false,
+ "bruteForceProtected": true,
+ "permanentLockout": false,
+ "maxFailureWaitSeconds": 900,
+ "minimumQuickLoginWaitSeconds": 60,
+ "waitIncrementSeconds": 60,
+ "quickLoginCheckMilliSeconds": 1000,
+ "maxDeltaTimeSeconds": 43200,
+ "failureFactor": 30,
+ "smtpServer": {
+ "host": "smtp.elasticemail.com",
+ "port": 2525,
+ "from": "for.demo.purpose.only@opex.dev",
+ "auth": true,
+ "user": "for.demo.purpose.only@opex.dev",
+ "password": "${vault.smtppass}"
+ }
+}
\ No newline at end of file
diff --git a/auth-gateway/keycloak-setup/realms/opex-realm.json b/auth-gateway/keycloak-setup/realms/opex-realm.json
new file mode 100644
index 000000000..c26448246
--- /dev/null
+++ b/auth-gateway/keycloak-setup/realms/opex-realm.json
@@ -0,0 +1,4118 @@
+{
+ "id": "opex",
+ "realm": "opex",
+ "notBefore": 0,
+ "defaultSignatureAlgorithm": "RS256",
+ "revokeRefreshToken": false,
+ "refreshTokenMaxReuse": 0,
+ "accessTokenLifespan": 1800,
+ "accessTokenLifespanForImplicitFlow": 900,
+ "ssoSessionIdleTimeout": 1209600,
+ "ssoSessionMaxLifespan": 1209600,
+ "ssoSessionIdleTimeoutRememberMe": 0,
+ "ssoSessionMaxLifespanRememberMe": 0,
+ "offlineSessionIdleTimeout": 2592000,
+ "offlineSessionMaxLifespanEnabled": false,
+ "offlineSessionMaxLifespan": 5184000,
+ "clientSessionIdleTimeout": 0,
+ "clientSessionMaxLifespan": 0,
+ "clientOfflineSessionIdleTimeout": 0,
+ "clientOfflineSessionMaxLifespan": 0,
+ "accessCodeLifespan": 60,
+ "accessCodeLifespanUserAction": 300,
+ "accessCodeLifespanLogin": 1800,
+ "actionTokenGeneratedByAdminLifespan": 43200,
+ "actionTokenGeneratedByUserLifespan": 300,
+ "oauth2DeviceCodeLifespan": 600,
+ "oauth2DevicePollingInterval": 5,
+ "enabled": true,
+ "sslRequired": "none",
+ "registrationAllowed": true,
+ "registrationEmailAsUsername": false,
+ "rememberMe": false,
+ "verifyEmail": false,
+ "loginWithEmailAllowed": true,
+ "duplicateEmailsAllowed": false,
+ "resetPasswordAllowed": true,
+ "editUsernameAllowed": false,
+ "bruteForceProtected": true,
+ "permanentLockout": false,
+ "maxTemporaryLockouts": 0,
+ "bruteForceStrategy": "MULTIPLE",
+ "maxFailureWaitSeconds": 900,
+ "minimumQuickLoginWaitSeconds": 60,
+ "waitIncrementSeconds": 60,
+ "quickLoginCheckMilliSeconds": 1000,
+ "maxDeltaTimeSeconds": 43200,
+ "failureFactor": 30,
+ "roles": {
+ "realm": [
+ {
+ "id": "01633d43-b1b2-4c6d-a987-e8a88664ed5c",
+ "name": "super-admin",
+ "description": "",
+ "composite": true,
+ "composites": {
+ "realm": [
+ "admin"
+ ]
+ },
+ "clientRole": false,
+ "containerId": "opex",
+ "attributes": {}
+ },
+ {
+ "id": "77c38736-27cc-4787-b83c-6b30872df6f7",
+ "name": "user-1",
+ "description": "Base user role",
+ "composite": true,
+ "composites": {
+ "realm": [
+ "user"
+ ]
+ },
+ "clientRole": false,
+ "containerId": "opex",
+ "attributes": {
+ "permissions": [
+ "deposit:write"
+ ]
+ }
+ },
+ {
+ "id": "7a788c40-fb3f-47e6-bdbe-2004b999afb6",
+ "name": "user",
+ "description": "Default role for all users",
+ "composite": false,
+ "clientRole": false,
+ "containerId": "opex",
+ "attributes": {
+ "permissions": [
+ "order:write,address:assign"
+ ]
+ }
+ },
+ {
+ "id": "1135b8ef-3838-4397-961e-79a77845fac2",
+ "name": "impersonation",
+ "composite": false,
+ "clientRole": false,
+ "containerId": "opex",
+ "attributes": {}
+ },
+ {
+ "id": "67844e9a-9943-4e18-b05a-775943347188",
+ "name": "default-roles-opex",
+ "description": "${role_default-roles}",
+ "composite": true,
+ "composites": {
+ "client": {
+ "account": [
+ "manage-account",
+ "view-profile"
+ ]
+ }
+ },
+ "clientRole": false,
+ "containerId": "opex",
+ "attributes": {}
+ },
+ {
+ "id": "3b6109f5-6e5a-4578-83c3-791ec3e2bf9e",
+ "name": "offline_access",
+ "description": "${role_offline-access}",
+ "composite": false,
+ "clientRole": false,
+ "containerId": "opex",
+ "attributes": {}
+ },
+ {
+ "id": "0dd6a8c7-d669-4941-9ea1-521980e9c53f",
+ "name": "uma_authorization",
+ "description": "${role_uma_authorization}",
+ "composite": false,
+ "clientRole": false,
+ "containerId": "opex",
+ "attributes": {}
+ },
+ {
+ "id": "904fce7c-bea5-43ed-8526-580370b1827e",
+ "name": "user-2",
+ "description": "",
+ "composite": true,
+ "composites": {
+ "realm": [
+ "user"
+ ]
+ },
+ "clientRole": false,
+ "containerId": "opex",
+ "attributes": {
+ "permissions": [
+ "withdraw:write,voucher:submit"
+ ]
+ }
+ },
+ {
+ "id": "99bad44d-9a1b-403f-9b90-2fc81646adf6",
+ "name": "admin",
+ "description": "Base role for all admin users",
+ "composite": false,
+ "clientRole": false,
+ "containerId": "opex",
+ "attributes": {}
+ }
+ ],
+ "client": {
+ "web-app": [],
+ "realm-management": [
+ {
+ "id": "5d00243f-ceec-4b0c-995e-d86d5b8a0ae6",
+ "name": "view-clients",
+ "description": "${role_view-clients}",
+ "composite": true,
+ "composites": {
+ "client": {
+ "realm-management": [
+ "query-clients"
+ ]
+ }
+ },
+ "clientRole": true,
+ "containerId": "6a4bfbd0-576d-4778-af56-56f876647355",
+ "attributes": {}
+ },
+ {
+ "id": "7df58488-6470-4f4e-962d-2900019e9906",
+ "name": "uma_protection",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "6a4bfbd0-576d-4778-af56-56f876647355",
+ "attributes": {}
+ },
+ {
+ "id": "941612de-bd85-47a5-8dfa-37c270dde28c",
+ "name": "view-authorization",
+ "description": "${role_view-authorization}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "6a4bfbd0-576d-4778-af56-56f876647355",
+ "attributes": {}
+ },
+ {
+ "id": "5ea9810d-63cc-4277-9b32-ba8a3d3c6091",
+ "name": "manage-realm",
+ "description": "${role_manage-realm}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "6a4bfbd0-576d-4778-af56-56f876647355",
+ "attributes": {}
+ },
+ {
+ "id": "8b7b0dd8-350b-473e-b8cd-8acad34f1358",
+ "name": "query-clients",
+ "description": "${role_query-clients}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "6a4bfbd0-576d-4778-af56-56f876647355",
+ "attributes": {}
+ },
+ {
+ "id": "0f8e5ee8-b014-4b7c-9b69-50f46abcba5f",
+ "name": "query-groups",
+ "description": "${role_query-groups}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "6a4bfbd0-576d-4778-af56-56f876647355",
+ "attributes": {}
+ },
+ {
+ "id": "911b1489-9383-4734-b134-bf49bf992ce9",
+ "name": "manage-clients",
+ "description": "${role_manage-clients}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "6a4bfbd0-576d-4778-af56-56f876647355",
+ "attributes": {}
+ },
+ {
+ "id": "5d48274c-bd6b-4c26-ad54-f1a2254beac0",
+ "name": "view-realm",
+ "description": "${role_view-realm}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "6a4bfbd0-576d-4778-af56-56f876647355",
+ "attributes": {}
+ },
+ {
+ "id": "3ea43b64-316f-4693-8346-9ee78b24adaf",
+ "name": "manage-identity-providers",
+ "description": "${role_manage-identity-providers}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "6a4bfbd0-576d-4778-af56-56f876647355",
+ "attributes": {}
+ },
+ {
+ "id": "49735614-96ec-49b2-98fe-3af9bcd1a33a",
+ "name": "create-client",
+ "description": "${role_create-client}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "6a4bfbd0-576d-4778-af56-56f876647355",
+ "attributes": {}
+ },
+ {
+ "id": "e8f8c3cc-0ff1-4f72-a271-db6821a3cdb6",
+ "name": "manage-users",
+ "description": "${role_manage-users}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "6a4bfbd0-576d-4778-af56-56f876647355",
+ "attributes": {}
+ },
+ {
+ "id": "387418b1-4f80-4b00-b9dd-805ca041f805",
+ "name": "view-identity-providers",
+ "description": "${role_view-identity-providers}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "6a4bfbd0-576d-4778-af56-56f876647355",
+ "attributes": {}
+ },
+ {
+ "id": "427c27d4-521a-464b-a0df-16d7f537e8d5",
+ "name": "realm-admin",
+ "description": "${role_realm-admin}",
+ "composite": true,
+ "composites": {
+ "client": {
+ "realm-management": [
+ "view-clients",
+ "view-authorization",
+ "manage-realm",
+ "query-clients",
+ "query-groups",
+ "manage-clients",
+ "view-realm",
+ "manage-identity-providers",
+ "create-client",
+ "manage-users",
+ "view-identity-providers",
+ "query-users",
+ "query-realms",
+ "view-users",
+ "impersonation",
+ "manage-authorization",
+ "manage-events",
+ "view-events"
+ ]
+ }
+ },
+ "clientRole": true,
+ "containerId": "6a4bfbd0-576d-4778-af56-56f876647355",
+ "attributes": {}
+ },
+ {
+ "id": "a574cf01-03e4-4573-ab9e-276d13a1ce8d",
+ "name": "query-users",
+ "description": "${role_query-users}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "6a4bfbd0-576d-4778-af56-56f876647355",
+ "attributes": {}
+ },
+ {
+ "id": "c3a253a8-a1b6-4d38-9677-f728f32482ad",
+ "name": "query-realms",
+ "description": "${role_query-realms}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "6a4bfbd0-576d-4778-af56-56f876647355",
+ "attributes": {}
+ },
+ {
+ "id": "f3cb93da-273e-419a-b2f4-93f09896abcf",
+ "name": "view-users",
+ "description": "${role_view-users}",
+ "composite": true,
+ "composites": {
+ "client": {
+ "realm-management": [
+ "query-users",
+ "query-groups"
+ ]
+ }
+ },
+ "clientRole": true,
+ "containerId": "6a4bfbd0-576d-4778-af56-56f876647355",
+ "attributes": {}
+ },
+ {
+ "id": "0332e99b-3dfc-4193-9e13-5728f8f3e6d6",
+ "name": "impersonation",
+ "description": "${role_impersonation}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "6a4bfbd0-576d-4778-af56-56f876647355",
+ "attributes": {}
+ },
+ {
+ "id": "6eedf2b7-50ef-4495-a89b-54aef751b7fa",
+ "name": "manage-authorization",
+ "description": "${role_manage-authorization}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "6a4bfbd0-576d-4778-af56-56f876647355",
+ "attributes": {}
+ },
+ {
+ "id": "aac3def5-f193-4a6c-9065-1667a0746a8a",
+ "name": "manage-events",
+ "description": "${role_manage-events}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "6a4bfbd0-576d-4778-af56-56f876647355",
+ "attributes": {}
+ },
+ {
+ "id": "b690cb9c-0f4a-4be5-ade0-b40443d8149d",
+ "name": "view-events",
+ "description": "${role_view-events}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "6a4bfbd0-576d-4778-af56-56f876647355",
+ "attributes": {}
+ }
+ ],
+ "ios-app": [],
+ "opex-api-key": [
+ {
+ "id": "95d01e3b-1442-415c-9f86-4d86187558ca",
+ "name": "uma_protection",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "d2f0f1b6-46b7-4678-842a-8c67524ea2da",
+ "attributes": {}
+ }
+ ],
+ "security-admin-console": [],
+ "admin-cli": [],
+ "account-console": [],
+ "android-app": [],
+ "broker": [
+ {
+ "id": "397b5703-4c81-48fd-a24c-a7e8177ef657",
+ "name": "read-token",
+ "description": "${role_read-token}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "4b9609f0-48d1-4e71-9381-2ecec08616f9",
+ "attributes": {}
+ }
+ ],
+ "opex-admin": [
+ {
+ "id": "cd0dca5f-aa89-4a97-8cb7-6525f919c3b6",
+ "name": "uma_protection",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "fb5f91c4-42fa-4769-b45d-febef22b4976",
+ "attributes": {}
+ }
+ ],
+ "account": [
+ {
+ "id": "8daa8096-d14e-4d1c-ad1f-83f822016aa1",
+ "name": "manage-account",
+ "description": "${role_manage-account}",
+ "composite": true,
+ "composites": {
+ "client": {
+ "account": [
+ "manage-account-links"
+ ]
+ }
+ },
+ "clientRole": true,
+ "containerId": "12eebf0b-a3eb-49f8-9ecf-173cf8a00145",
+ "attributes": {}
+ },
+ {
+ "id": "33f3dfbc-323f-4a01-9539-bcba0d06f3bc",
+ "name": "view-groups",
+ "description": "${role_view-groups}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "12eebf0b-a3eb-49f8-9ecf-173cf8a00145",
+ "attributes": {}
+ },
+ {
+ "id": "cc86369d-55fc-47ed-9592-9e89116032c0",
+ "name": "view-consent",
+ "description": "${role_view-consent}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "12eebf0b-a3eb-49f8-9ecf-173cf8a00145",
+ "attributes": {}
+ },
+ {
+ "id": "ca726012-91f9-4c58-bc7e-e435c9c244ab",
+ "name": "manage-consent",
+ "description": "${role_manage-consent}",
+ "composite": true,
+ "composites": {
+ "client": {
+ "account": [
+ "view-consent"
+ ]
+ }
+ },
+ "clientRole": true,
+ "containerId": "12eebf0b-a3eb-49f8-9ecf-173cf8a00145",
+ "attributes": {}
+ },
+ {
+ "id": "f6839de1-e7fc-42bb-be60-578ea3d9361b",
+ "name": "view-applications",
+ "description": "${role_view-applications}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "12eebf0b-a3eb-49f8-9ecf-173cf8a00145",
+ "attributes": {}
+ },
+ {
+ "id": "948269c7-a69c-4c82-a7f3-88868713dfd9",
+ "name": "manage-account-links",
+ "description": "${role_manage-account-links}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "12eebf0b-a3eb-49f8-9ecf-173cf8a00145",
+ "attributes": {}
+ },
+ {
+ "id": "ee9cf17f-b9b1-4f4a-b521-83b5d1d7388b",
+ "name": "delete-account",
+ "description": "${role_delete-account}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "12eebf0b-a3eb-49f8-9ecf-173cf8a00145",
+ "attributes": {}
+ },
+ {
+ "id": "aed18201-2433-4998-8fa3-0979b0b31c10",
+ "name": "view-profile",
+ "description": "${role_view-profile}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "12eebf0b-a3eb-49f8-9ecf-173cf8a00145",
+ "attributes": {}
+ }
+ ]
+ }
+ },
+ "groups": [
+ {
+ "id": "700a0042-6146-42fc-a97a-f0f63f913301",
+ "name": "admins",
+ "path": "/admins",
+ "subGroups": [],
+ "attributes": {},
+ "realmRoles": [],
+ "clientRoles": {}
+ }
+ ],
+ "defaultRole": {
+ "id": "67844e9a-9943-4e18-b05a-775943347188",
+ "name": "default-roles-opex",
+ "description": "${role_default-roles}",
+ "composite": true,
+ "clientRole": false,
+ "containerId": "opex"
+ },
+ "requiredCredentials": [
+ "password"
+ ],
+ "passwordPolicy": "length(8)",
+ "otpPolicyType": "totp",
+ "otpPolicyAlgorithm": "HmacSHA1",
+ "otpPolicyInitialCounter": 0,
+ "otpPolicyDigits": 6,
+ "otpPolicyLookAheadWindow": 5,
+ "otpPolicyPeriod": 30,
+ "otpPolicyCodeReusable": false,
+ "otpSupportedApplications": [
+ "totpAppFreeOTPName",
+ "totpAppGoogleName",
+ "totpAppMicrosoftAuthenticatorName"
+ ],
+ "localizationTexts": {},
+ "webAuthnPolicyRpEntityName": "keycloak",
+ "webAuthnPolicySignatureAlgorithms": [
+ "ES256"
+ ],
+ "webAuthnPolicyRpId": "",
+ "webAuthnPolicyAttestationConveyancePreference": "not specified",
+ "webAuthnPolicyAuthenticatorAttachment": "not specified",
+ "webAuthnPolicyRequireResidentKey": "not specified",
+ "webAuthnPolicyUserVerificationRequirement": "not specified",
+ "webAuthnPolicyCreateTimeout": 0,
+ "webAuthnPolicyAvoidSameAuthenticatorRegister": false,
+ "webAuthnPolicyAcceptableAaguids": [],
+ "webAuthnPolicyExtraOrigins": [],
+ "webAuthnPolicyPasswordlessRpEntityName": "keycloak",
+ "webAuthnPolicyPasswordlessSignatureAlgorithms": [
+ "ES256"
+ ],
+ "webAuthnPolicyPasswordlessRpId": "",
+ "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified",
+ "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified",
+ "webAuthnPolicyPasswordlessRequireResidentKey": "not specified",
+ "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified",
+ "webAuthnPolicyPasswordlessCreateTimeout": 0,
+ "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false,
+ "webAuthnPolicyPasswordlessAcceptableAaguids": [],
+ "webAuthnPolicyPasswordlessExtraOrigins": [],
+ "users": [
+ {
+ "id": "cb6af759-5e4f-42b3-86ea-b3754ce4d422",
+ "username": "service-account-account-console",
+ "emailVerified": false,
+ "createdTimestamp": 1624136397065,
+ "enabled": true,
+ "totp": false,
+ "serviceAccountClientId": "account-console",
+ "disableableCredentialTypes": [],
+ "requiredActions": [],
+ "realmRoles": [
+ "offline_access",
+ "uma_authorization"
+ ],
+ "clientRoles": {
+ "realm-management": [
+ "manage-users"
+ ],
+ "account": [
+ "manage-account",
+ "view-profile"
+ ]
+ },
+ "notBefore": 0,
+ "groups": []
+ },
+ {
+ "id": "2cda7f75-c5d9-4b64-b90e-58b381689a9d",
+ "username": "service-account-opex-admin",
+ "emailVerified": false,
+ "createdTimestamp": 1643624421752,
+ "enabled": true,
+ "totp": false,
+ "serviceAccountClientId": "opex-admin",
+ "disableableCredentialTypes": [],
+ "requiredActions": [],
+ "realmRoles": [
+ "offline_access",
+ "uma_authorization"
+ ],
+ "clientRoles": {
+ "realm-management": [
+ "realm-admin",
+ "manage-users"
+ ],
+ "opex-admin": [
+ "uma_protection"
+ ],
+ "account": [
+ "manage-account",
+ "view-profile"
+ ]
+ },
+ "notBefore": 0,
+ "groups": []
+ },
+ {
+ "id": "4bb55f60-faa0-464a-b0a3-04a18f421dff",
+ "username": "service-account-opex-api-key",
+ "emailVerified": false,
+ "createdTimestamp": 1749460715817,
+ "enabled": true,
+ "totp": false,
+ "serviceAccountClientId": "opex-api-key",
+ "disableableCredentialTypes": [],
+ "requiredActions": [],
+ "realmRoles": [
+ "default-roles-opex"
+ ],
+ "clientRoles": {
+ "opex-api-key": [
+ "uma_protection"
+ ]
+ },
+ "notBefore": 0,
+ "groups": []
+ },
+ {
+ "id": "854a5e2b-1e45-4ff7-bd1c-d9764d41a5bd",
+ "username": "service-account-realm-management",
+ "emailVerified": false,
+ "createdTimestamp": 1634844207750,
+ "enabled": true,
+ "totp": false,
+ "serviceAccountClientId": "realm-management",
+ "disableableCredentialTypes": [],
+ "requiredActions": [],
+ "realmRoles": [
+ "offline_access",
+ "uma_authorization"
+ ],
+ "clientRoles": {
+ "realm-management": [
+ "uma_protection"
+ ],
+ "account": [
+ "manage-account",
+ "view-profile"
+ ]
+ },
+ "notBefore": 0,
+ "groups": []
+ }
+ ],
+ "scopeMappings": [
+ {
+ "client": "account-console",
+ "roles": [
+ "offline_access",
+ "uma_authorization"
+ ]
+ },
+ {
+ "client": "opex-admin",
+ "roles": [
+ "impersonation",
+ "offline_access",
+ "uma_authorization"
+ ]
+ },
+ {
+ "clientScope": "offline_access",
+ "roles": [
+ "offline_access"
+ ]
+ }
+ ],
+ "clientScopeMappings": {
+ "realm-management": [
+ {
+ "client": "account-console",
+ "roles": [
+ "impersonation",
+ "realm-admin",
+ "manage-users"
+ ]
+ },
+ {
+ "client": "admin-cli",
+ "roles": [
+ "impersonation"
+ ]
+ },
+ {
+ "client": "opex-admin",
+ "roles": [
+ "realm-admin"
+ ]
+ }
+ ],
+ "account": [
+ {
+ "client": "account-console",
+ "roles": [
+ "manage-account",
+ "view-groups"
+ ]
+ },
+ {
+ "client": "opex-admin",
+ "roles": [
+ "manage-account"
+ ]
+ }
+ ]
+ },
+ "clients": [
+ {
+ "id": "12eebf0b-a3eb-49f8-9ecf-173cf8a00145",
+ "clientId": "account",
+ "name": "${client_account}",
+ "rootUrl": "${authBaseUrl}",
+ "baseUrl": "/realms/opex/account/",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "secret": "**********",
+ "redirectUris": [
+ "/realms/opex/account/*",
+ "http://localhost:3000/*"
+ ],
+ "webOrigins": [
+ "*"
+ ],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": false,
+ "serviceAccountsEnabled": false,
+ "publicClient": false,
+ "frontchannelLogout": false,
+ "protocol": "openid-connect",
+ "attributes": {
+ "saml.assertion.signature": "false",
+ "saml.multivalued.roles": "false",
+ "saml.force.post.binding": "false",
+ "saml.encrypt": "false",
+ "post.logout.redirect.uris": "+",
+ "saml.server.signature": "false",
+ "backchannel.logout.revoke.offline.tokens": "false",
+ "saml.server.signature.keyinfo.ext": "false",
+ "exclude.session.state.from.auth.response": "false",
+ "realm_client": "false",
+ "backchannel.logout.session.required": "false",
+ "client_credentials.use_refresh_token": "false",
+ "saml_force_name_id_format": "false",
+ "saml.client.signature": "false",
+ "tls.client.certificate.bound.access.tokens": "false",
+ "saml.authnstatement": "false",
+ "display.on.consent.screen": "false",
+ "saml.onetimeuse.condition": "false"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": false,
+ "nodeReRegistrationTimeout": 0,
+ "defaultClientScopes": [
+ "trust",
+ "web-origins",
+ "acr",
+ "profile",
+ "basic",
+ "email"
+ ],
+ "optionalClientScopes": [
+ "address",
+ "offline_access",
+ "microprofile-jwt"
+ ]
+ },
+ {
+ "id": "ceabb7ca-b063-4755-90fb-8de2cc7e5e00",
+ "clientId": "account-console",
+ "name": "${client_account-console}",
+ "rootUrl": "${authBaseUrl}",
+ "baseUrl": "/realms/opex/account/",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "secret": "**********",
+ "redirectUris": [
+ "/realms/opex/account/*",
+ "http://localhost:3000/*",
+ "https://opex.dev/*"
+ ],
+ "webOrigins": [
+ "+"
+ ],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": true,
+ "serviceAccountsEnabled": true,
+ "publicClient": false,
+ "frontchannelLogout": false,
+ "protocol": "openid-connect",
+ "attributes": {
+ "saml.assertion.signature": "false",
+ "saml.multivalued.roles": "false",
+ "saml.force.post.binding": "false",
+ "saml.encrypt": "false",
+ "post.logout.redirect.uris": "+",
+ "saml.server.signature": "false",
+ "backchannel.logout.revoke.offline.tokens": "false",
+ "saml.server.signature.keyinfo.ext": "false",
+ "exclude.session.state.from.auth.response": "false",
+ "realm_client": "false",
+ "backchannel.logout.session.required": "false",
+ "client_credentials.use_refresh_token": "false",
+ "saml_force_name_id_format": "false",
+ "saml.client.signature": "false",
+ "tls.client.certificate.bound.access.tokens": "false",
+ "saml.authnstatement": "false",
+ "display.on.consent.screen": "false",
+ "pkce.code.challenge.method": "S256",
+ "saml.onetimeuse.condition": "false"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": false,
+ "nodeReRegistrationTimeout": 0,
+ "protocolMappers": [
+ {
+ "id": "b83a852e-e3e3-46dc-9e76-3d175fc4a5d4",
+ "name": "Client Host",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usersessionmodel-note-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.session.note": "clientHost",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "clientHost",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "6b335962-bc9b-4095-ad36-48163e443a6f",
+ "name": "Client ID",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usersessionmodel-note-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.session.note": "clientId",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "clientId",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "4c6842a7-9120-49b4-92eb-6cd4a3ff742d",
+ "name": "Client IP Address",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usersessionmodel-note-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.session.note": "clientAddress",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "clientAddress",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "6551bb7e-54af-455e-8de6-0e5acb1b3527",
+ "name": "audience resolve",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-audience-resolve-mapper",
+ "consentRequired": false,
+ "config": {}
+ }
+ ],
+ "defaultClientScopes": [
+ "trust",
+ "acr",
+ "profile",
+ "basic",
+ "email"
+ ],
+ "optionalClientScopes": [
+ "address",
+ "offline_access",
+ "microprofile-jwt"
+ ]
+ },
+ {
+ "id": "13d76feb-d762-4409-bb84-7a75bc395a61",
+ "clientId": "admin-cli",
+ "name": "${client_admin-cli}",
+ "description": "",
+ "rootUrl": "",
+ "adminUrl": "",
+ "baseUrl": "",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "redirectUris": [],
+ "webOrigins": [
+ "*"
+ ],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": false,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": true,
+ "serviceAccountsEnabled": false,
+ "publicClient": true,
+ "frontchannelLogout": false,
+ "protocol": "openid-connect",
+ "attributes": {
+ "request.object.signature.alg": "any",
+ "saml.multivalued.roles": "false",
+ "saml.force.post.binding": "false",
+ "post.logout.redirect.uris": "+",
+ "oauth2.device.authorization.grant.enabled": "false",
+ "backchannel.logout.revoke.offline.tokens": "false",
+ "saml.server.signature.keyinfo.ext": "false",
+ "use.refresh.tokens": "true",
+ "realm_client": "false",
+ "oidc.ciba.grant.enabled": "false",
+ "backchannel.logout.session.required": "false",
+ "client_credentials.use_refresh_token": "false",
+ "saml.client.signature": "false",
+ "require.pushed.authorization.requests": "false",
+ "request.object.encryption.enc": "any",
+ "saml.assertion.signature": "false",
+ "client.secret.creation.time": "1749563219",
+ "request.object.encryption.alg": "any",
+ "client.introspection.response.allow.jwt.claim.enabled": "false",
+ "saml.encrypt": "false",
+ "saml.server.signature": "false",
+ "exclude.session.state.from.auth.response": "false",
+ "client.use.lightweight.access.token.enabled": "true",
+ "request.object.required": "not required",
+ "saml_force_name_id_format": "false",
+ "tls.client.certificate.bound.access.tokens": "false",
+ "acr.loa.map": "{}",
+ "saml.authnstatement": "false",
+ "display.on.consent.screen": "false",
+ "token.response.type.bearer.lower-case": "false",
+ "saml.onetimeuse.condition": "false"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": true,
+ "nodeReRegistrationTimeout": 0,
+ "protocolMappers": [
+ {
+ "id": "c652a15d-6b9d-4f11-af40-ed451f30589c",
+ "name": "groups",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-realm-role-mapper",
+ "consentRequired": false,
+ "config": {
+ "multivalued": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "foo",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "groups",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "0cede09c-fee1-4e60-85fa-7067180bbae5",
+ "name": "Group Mapper",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-group-membership-mapper",
+ "consentRequired": false,
+ "config": {
+ "full.path": "false",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "groups",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "5111c406-34fd-499f-b8b4-9603bc1e686c",
+ "name": "realm roles",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-realm-role-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "foo",
+ "access.token.claim": "true",
+ "claim.name": "realm_access.roles",
+ "jsonType.label": "String",
+ "multivalued": "true"
+ }
+ },
+ {
+ "id": "f53d8370-145f-4123-a680-abe4e7ef5d39",
+ "name": "client roles",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-client-role-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "foo",
+ "access.token.claim": "true",
+ "claim.name": "resource_access.${client_id}.roles",
+ "jsonType.label": "String",
+ "multivalued": "true"
+ }
+ },
+ {
+ "id": "a74e3676-91f1-4897-830c-14a2fad94c73",
+ "name": "UserLeveL",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "level",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "level",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ }
+ ],
+ "defaultClientScopes": [
+ "trust",
+ "web-origins",
+ "acr",
+ "profile",
+ "basic",
+ "email"
+ ],
+ "optionalClientScopes": [
+ "address",
+ "offline_access",
+ "microprofile-jwt"
+ ]
+ },
+ {
+ "id": "30c68e47-1e37-4ce4-a323-efec3dcec65c",
+ "clientId": "android-app",
+ "name": "Android app",
+ "description": "Client for opex android app",
+ "rootUrl": "",
+ "adminUrl": "",
+ "baseUrl": "",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "secret": "**********",
+ "redirectUris": [
+ "/*"
+ ],
+ "webOrigins": [
+ "/*"
+ ],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": true,
+ "serviceAccountsEnabled": false,
+ "publicClient": false,
+ "frontchannelLogout": true,
+ "protocol": "openid-connect",
+ "attributes": {
+ "access.token.lifespan": "600",
+ "request.object.signature.alg": "any",
+ "frontchannel.logout.session.required": "true",
+ "oauth2.device.authorization.grant.enabled": "false",
+ "use.jwks.url": "false",
+ "backchannel.logout.revoke.offline.tokens": "false",
+ "use.refresh.tokens": "true",
+ "realm_client": "false",
+ "oidc.ciba.grant.enabled": "false",
+ "exclude.issuer.from.auth.response": "false",
+ "backchannel.logout.session.required": "true",
+ "client_credentials.use_refresh_token": "false",
+ "require.pushed.authorization.requests": "false",
+ "request.object.encryption.enc": "any",
+ "client.session.max.lifespan": "604800",
+ "client.secret.creation.time": "1747492073",
+ "request.object.encryption.alg": "any",
+ "client.introspection.response.allow.jwt.claim.enabled": "false",
+ "exclude.session.state.from.auth.response": "false",
+ "client.use.lightweight.access.token.enabled": "false",
+ "request.object.required": "not required",
+ "tls.client.certificate.bound.access.tokens": "false",
+ "acr.loa.map": "{}",
+ "display.on.consent.screen": "false",
+ "token.response.type.bearer.lower-case": "false"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": true,
+ "nodeReRegistrationTimeout": -1,
+ "defaultClientScopes": [
+ "trust",
+ "basic",
+ "role_permission_attribute"
+ ],
+ "optionalClientScopes": []
+ },
+ {
+ "id": "4b9609f0-48d1-4e71-9381-2ecec08616f9",
+ "clientId": "broker",
+ "name": "${client_broker}",
+ "rootUrl": "",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "secret": "**********",
+ "redirectUris": [
+ "http://localhost:3000/*",
+ "https://opex.dev/*"
+ ],
+ "webOrigins": [
+ "*"
+ ],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": false,
+ "serviceAccountsEnabled": false,
+ "publicClient": false,
+ "frontchannelLogout": false,
+ "protocol": "openid-connect",
+ "attributes": {
+ "saml.assertion.signature": "false",
+ "saml.multivalued.roles": "false",
+ "saml.force.post.binding": "false",
+ "saml.encrypt": "false",
+ "post.logout.redirect.uris": "+",
+ "saml.server.signature": "false",
+ "backchannel.logout.revoke.offline.tokens": "false",
+ "saml.server.signature.keyinfo.ext": "false",
+ "exclude.session.state.from.auth.response": "false",
+ "realm_client": "true",
+ "backchannel.logout.session.required": "false",
+ "client_credentials.use_refresh_token": "false",
+ "saml_force_name_id_format": "false",
+ "saml.client.signature": "false",
+ "tls.client.certificate.bound.access.tokens": "false",
+ "saml.authnstatement": "false",
+ "display.on.consent.screen": "false",
+ "saml.onetimeuse.condition": "false"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": false,
+ "nodeReRegistrationTimeout": 0,
+ "defaultClientScopes": [
+ "trust",
+ "web-origins",
+ "acr",
+ "profile",
+ "basic",
+ "email"
+ ],
+ "optionalClientScopes": [
+ "address",
+ "offline_access",
+ "microprofile-jwt"
+ ]
+ },
+ {
+ "id": "01ca7824-0953-4d5f-acf5-99fbf0ef2eb6",
+ "clientId": "ios-app",
+ "name": "iOS app",
+ "description": "Client for opex iOS app",
+ "rootUrl": "",
+ "adminUrl": "",
+ "baseUrl": "",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "secret": "**********",
+ "redirectUris": [
+ "/*"
+ ],
+ "webOrigins": [
+ "/*"
+ ],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": true,
+ "serviceAccountsEnabled": false,
+ "publicClient": false,
+ "frontchannelLogout": true,
+ "protocol": "openid-connect",
+ "attributes": {
+ "access.token.lifespan": "600",
+ "request.object.signature.alg": "any",
+ "frontchannel.logout.session.required": "true",
+ "oauth2.device.authorization.grant.enabled": "false",
+ "use.jwks.url": "false",
+ "backchannel.logout.revoke.offline.tokens": "false",
+ "use.refresh.tokens": "true",
+ "realm_client": "false",
+ "oidc.ciba.grant.enabled": "false",
+ "exclude.issuer.from.auth.response": "false",
+ "backchannel.logout.session.required": "true",
+ "client_credentials.use_refresh_token": "false",
+ "require.pushed.authorization.requests": "false",
+ "request.object.encryption.enc": "any",
+ "client.session.max.lifespan": "604800",
+ "client.secret.creation.time": "1747492982",
+ "request.object.encryption.alg": "any",
+ "client.introspection.response.allow.jwt.claim.enabled": "false",
+ "exclude.session.state.from.auth.response": "false",
+ "client.use.lightweight.access.token.enabled": "false",
+ "request.object.required": "not required",
+ "tls.client.certificate.bound.access.tokens": "false",
+ "acr.loa.map": "{}",
+ "display.on.consent.screen": "false",
+ "token.response.type.bearer.lower-case": "false"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": true,
+ "nodeReRegistrationTimeout": -1,
+ "defaultClientScopes": [
+ "trust",
+ "basic",
+ "role_permission_attribute"
+ ],
+ "optionalClientScopes": []
+ },
+ {
+ "id": "fb5f91c4-42fa-4769-b45d-febef22b4976",
+ "clientId": "opex-admin",
+ "name": "${client_opex-admin}",
+ "description": "",
+ "rootUrl": "${authBaseUrl}",
+ "adminUrl": "",
+ "baseUrl": "",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "secret": "**********",
+ "redirectUris": [
+ "*"
+ ],
+ "webOrigins": [],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": true,
+ "serviceAccountsEnabled": true,
+ "authorizationServicesEnabled": true,
+ "publicClient": false,
+ "frontchannelLogout": false,
+ "protocol": "openid-connect",
+ "attributes": {
+ "saml.assertion.signature": "false",
+ "access.token.lifespan": "86400",
+ "client.secret.creation.time": "1745840695",
+ "saml.multivalued.roles": "false",
+ "saml.force.post.binding": "false",
+ "saml.encrypt": "false",
+ "post.logout.redirect.uris": "+",
+ "oauth2.device.authorization.grant.enabled": "false",
+ "backchannel.logout.revoke.offline.tokens": "false",
+ "saml.server.signature": "false",
+ "saml.server.signature.keyinfo.ext": "false",
+ "exclude.session.state.from.auth.response": "false",
+ "realm_client": "false",
+ "oidc.ciba.grant.enabled": "false",
+ "backchannel.logout.session.required": "false",
+ "client_credentials.use_refresh_token": "false",
+ "saml_force_name_id_format": "false",
+ "saml.client.signature": "false",
+ "tls.client.certificate.bound.access.tokens": "false",
+ "saml.authnstatement": "false",
+ "display.on.consent.screen": "false",
+ "saml.onetimeuse.condition": "false"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": false,
+ "nodeReRegistrationTimeout": -1,
+ "protocolMappers": [
+ {
+ "id": "1543d277-d4f0-4498-89a3-8fe488eb8d87",
+ "name": "audience resolve",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-audience-resolve-mapper",
+ "consentRequired": false,
+ "config": {}
+ },
+ {
+ "id": "4ca1806c-d230-40e4-8aeb-7be48ec9a1ef",
+ "name": "Client ID",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usersessionmodel-note-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.session.note": "clientId",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "clientId",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "ec94a143-2a78-4275-a4fc-aa246c1c6628",
+ "name": "Client Host",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usersessionmodel-note-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.session.note": "clientHost",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "clientHost",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "f7258787-d1d7-4a41-82c6-8e9e00008b27",
+ "name": "Client IP Address",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usersessionmodel-note-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.session.note": "clientAddress",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "clientAddress",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ }
+ ],
+ "defaultClientScopes": [
+ "service_account",
+ "trust",
+ "web-origins",
+ "acr",
+ "profile",
+ "basic",
+ "email"
+ ],
+ "optionalClientScopes": [
+ "address",
+ "offline_access",
+ "microprofile-jwt"
+ ],
+ "authorizationSettings": {
+ "allowRemoteResourceManagement": true,
+ "policyEnforcementMode": "ENFORCING",
+ "resources": [
+ {
+ "name": "Default Resource",
+ "type": "urn:opex-admin:resources:default",
+ "ownerManagedAccess": false,
+ "attributes": {},
+ "uris": [
+ "/*"
+ ]
+ }
+ ],
+ "policies": [
+ {
+ "name": "Default Policy",
+ "description": "A policy that grants access only for users within this realm",
+ "type": "js",
+ "logic": "POSITIVE",
+ "decisionStrategy": "AFFIRMATIVE",
+ "config": {
+ "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n"
+ }
+ },
+ {
+ "name": "Default Permission",
+ "description": "A permission that applies to the default resource type",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "defaultResourceType": "urn:opex-admin:resources:default",
+ "applyPolicies": "[\"Default Policy\"]"
+ }
+ }
+ ],
+ "scopes": [],
+ "decisionStrategy": "UNANIMOUS"
+ }
+ },
+ {
+ "id": "d2f0f1b6-46b7-4678-842a-8c67524ea2da",
+ "clientId": "opex-api-key",
+ "name": "",
+ "description": "",
+ "rootUrl": "",
+ "adminUrl": "",
+ "baseUrl": "",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "secret": "**********",
+ "redirectUris": [
+ "*"
+ ],
+ "webOrigins": [],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": true,
+ "serviceAccountsEnabled": true,
+ "authorizationServicesEnabled": true,
+ "publicClient": false,
+ "frontchannelLogout": false,
+ "protocol": "openid-connect",
+ "attributes": {
+ "saml.assertion.signature": "false",
+ "access.token.lifespan": "43200",
+ "client.secret.creation.time": "1749460384",
+ "saml.multivalued.roles": "false",
+ "saml.force.post.binding": "false",
+ "saml.encrypt": "false",
+ "post.logout.redirect.uris": "+",
+ "oauth2.device.authorization.grant.enabled": "false",
+ "backchannel.logout.revoke.offline.tokens": "false",
+ "saml.server.signature": "false",
+ "saml.server.signature.keyinfo.ext": "false",
+ "exclude.session.state.from.auth.response": "false",
+ "realm_client": "false",
+ "oidc.ciba.grant.enabled": "false",
+ "backchannel.logout.session.required": "true",
+ "client_credentials.use_refresh_token": "false",
+ "saml_force_name_id_format": "false",
+ "saml.client.signature": "false",
+ "tls.client.certificate.bound.access.tokens": "false",
+ "saml.authnstatement": "false",
+ "display.on.consent.screen": "false",
+ "saml.onetimeuse.condition": "false"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": false,
+ "nodeReRegistrationTimeout": -1,
+ "defaultClientScopes": [
+ "trust",
+ "Forced_Roles",
+ "basic",
+ "role_permission_attribute"
+ ],
+ "optionalClientScopes": [
+ "offline_access"
+ ],
+ "authorizationSettings": {
+ "allowRemoteResourceManagement": true,
+ "policyEnforcementMode": "ENFORCING",
+ "resources": [
+ {
+ "name": "Default Resource",
+ "type": "urn:opex-api-key:resources:default",
+ "ownerManagedAccess": false,
+ "attributes": {},
+ "uris": [
+ "/*"
+ ]
+ }
+ ],
+ "policies": [
+ {
+ "name": "Default Policy",
+ "description": "A policy that grants access only for users within this realm",
+ "type": "js",
+ "logic": "POSITIVE",
+ "decisionStrategy": "AFFIRMATIVE",
+ "config": {
+ "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n"
+ }
+ },
+ {
+ "name": "Default Permission",
+ "description": "A permission that applies to the default resource type",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "defaultResourceType": "urn:opex-api-key:resources:default",
+ "applyPolicies": "[\"Default Policy\"]"
+ }
+ }
+ ],
+ "scopes": [],
+ "decisionStrategy": "UNANIMOUS"
+ }
+ },
+ {
+ "id": "6a4bfbd0-576d-4778-af56-56f876647355",
+ "clientId": "realm-management",
+ "name": "${client_realm-management}",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "secret": "**********",
+ "redirectUris": [],
+ "webOrigins": [],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": false,
+ "serviceAccountsEnabled": true,
+ "authorizationServicesEnabled": true,
+ "publicClient": false,
+ "frontchannelLogout": false,
+ "protocol": "openid-connect",
+ "attributes": {
+ "realm_client": "true",
+ "post.logout.redirect.uris": "+"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": false,
+ "nodeReRegistrationTimeout": 0,
+ "protocolMappers": [
+ {
+ "id": "14ccedf9-e008-4fe5-901a-98663b937712",
+ "name": "Client Host",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usersessionmodel-note-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.session.note": "clientHost",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "clientHost",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "f82db182-9fba-4ffe-8138-65060d603dba",
+ "name": "Client ID",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usersessionmodel-note-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.session.note": "clientId",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "clientId",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "0f723243-8327-4640-861a-ee940cd18de9",
+ "name": "Client IP Address",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usersessionmodel-note-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.session.note": "clientAddress",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "clientAddress",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ }
+ ],
+ "defaultClientScopes": [
+ "web-origins",
+ "acr",
+ "profile",
+ "basic",
+ "email"
+ ],
+ "optionalClientScopes": [
+ "address",
+ "offline_access",
+ "microprofile-jwt"
+ ],
+ "authorizationSettings": {
+ "allowRemoteResourceManagement": false,
+ "policyEnforcementMode": "ENFORCING",
+ "resources": [
+ {
+ "name": "client.resource.13d76feb-d762-4409-bb84-7a75bc395a61",
+ "type": "Client",
+ "ownerManagedAccess": false,
+ "attributes": {},
+ "uris": [],
+ "scopes": [
+ {
+ "name": "view"
+ },
+ {
+ "name": "map-roles-client-scope"
+ },
+ {
+ "name": "configure"
+ },
+ {
+ "name": "map-roles"
+ },
+ {
+ "name": "manage"
+ },
+ {
+ "name": "token-exchange"
+ },
+ {
+ "name": "map-roles-composite"
+ }
+ ]
+ },
+ {
+ "name": "client.resource.ceabb7ca-b063-4755-90fb-8de2cc7e5e00",
+ "type": "Client",
+ "ownerManagedAccess": false,
+ "attributes": {},
+ "uris": [],
+ "scopes": [
+ {
+ "name": "view"
+ },
+ {
+ "name": "map-roles-client-scope"
+ },
+ {
+ "name": "configure"
+ },
+ {
+ "name": "map-roles"
+ },
+ {
+ "name": "manage"
+ },
+ {
+ "name": "token-exchange"
+ },
+ {
+ "name": "map-roles-composite"
+ }
+ ]
+ },
+ {
+ "name": "Users",
+ "ownerManagedAccess": false,
+ "attributes": {},
+ "uris": [],
+ "scopes": [
+ {
+ "name": "user-impersonated"
+ },
+ {
+ "name": "manage-group-membership"
+ },
+ {
+ "name": "view"
+ },
+ {
+ "name": "impersonate"
+ },
+ {
+ "name": "map-roles"
+ },
+ {
+ "name": "manage"
+ }
+ ]
+ },
+ {
+ "name": "client.resource.fb5f91c4-42fa-4769-b45d-febef22b4976",
+ "type": "Client",
+ "ownerManagedAccess": false,
+ "attributes": {},
+ "uris": [],
+ "scopes": [
+ {
+ "name": "view"
+ },
+ {
+ "name": "map-roles-client-scope"
+ },
+ {
+ "name": "configure"
+ },
+ {
+ "name": "map-roles"
+ },
+ {
+ "name": "manage"
+ },
+ {
+ "name": "token-exchange"
+ },
+ {
+ "name": "map-roles-composite"
+ }
+ ]
+ },
+ {
+ "name": "client.resource.d2f0f1b6-46b7-4678-842a-8c67524ea2da",
+ "type": "Client",
+ "ownerManagedAccess": false,
+ "attributes": {},
+ "uris": [],
+ "scopes": [
+ {
+ "name": "view"
+ },
+ {
+ "name": "map-roles-client-scope"
+ },
+ {
+ "name": "configure"
+ },
+ {
+ "name": "map-roles"
+ },
+ {
+ "name": "manage"
+ },
+ {
+ "name": "token-exchange"
+ },
+ {
+ "name": "map-roles-composite"
+ }
+ ]
+ },
+ {
+ "name": "idp.resource.6456448e-2415-49ad-bf95-1b5176557862",
+ "type": "IdentityProvider",
+ "ownerManagedAccess": false,
+ "attributes": {},
+ "uris": [],
+ "scopes": [
+ {
+ "name": "token-exchange"
+ }
+ ]
+ }
+ ],
+ "policies": [
+ {
+ "name": "account-console-client-impersonate",
+ "type": "client",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "clients": "[\"opex-admin\",\"account-console\"]"
+ }
+ },
+ {
+ "name": "opex-api-exchange",
+ "type": "client",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "clients": "[\"opex-admin\"]"
+ }
+ },
+ {
+ "name": "manage.permission.client.13d76feb-d762-4409-bb84-7a75bc395a61",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.13d76feb-d762-4409-bb84-7a75bc395a61\"]",
+ "scopes": "[\"manage\"]"
+ }
+ },
+ {
+ "name": "configure.permission.client.13d76feb-d762-4409-bb84-7a75bc395a61",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.13d76feb-d762-4409-bb84-7a75bc395a61\"]",
+ "scopes": "[\"configure\"]"
+ }
+ },
+ {
+ "name": "view.permission.client.13d76feb-d762-4409-bb84-7a75bc395a61",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.13d76feb-d762-4409-bb84-7a75bc395a61\"]",
+ "scopes": "[\"view\"]"
+ }
+ },
+ {
+ "name": "map-roles.permission.client.13d76feb-d762-4409-bb84-7a75bc395a61",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.13d76feb-d762-4409-bb84-7a75bc395a61\"]",
+ "scopes": "[\"map-roles\"]"
+ }
+ },
+ {
+ "name": "map-roles-client-scope.permission.client.13d76feb-d762-4409-bb84-7a75bc395a61",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.13d76feb-d762-4409-bb84-7a75bc395a61\"]",
+ "scopes": "[\"map-roles-client-scope\"]"
+ }
+ },
+ {
+ "name": "map-roles-composite.permission.client.13d76feb-d762-4409-bb84-7a75bc395a61",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.13d76feb-d762-4409-bb84-7a75bc395a61\"]",
+ "scopes": "[\"map-roles-composite\"]"
+ }
+ },
+ {
+ "name": "token-exchange.permission.client.13d76feb-d762-4409-bb84-7a75bc395a61",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.13d76feb-d762-4409-bb84-7a75bc395a61\"]",
+ "scopes": "[\"token-exchange\"]"
+ }
+ },
+ {
+ "name": "token-exchange.permission.idp.6456448e-2415-49ad-bf95-1b5176557862",
+ "description": "",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"idp.resource.6456448e-2415-49ad-bf95-1b5176557862\"]",
+ "scopes": "[\"token-exchange\"]",
+ "applyPolicies": "[\"opex-api-exchange\"]"
+ }
+ },
+ {
+ "name": "manage.permission.client.ceabb7ca-b063-4755-90fb-8de2cc7e5e00",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.ceabb7ca-b063-4755-90fb-8de2cc7e5e00\"]",
+ "scopes": "[\"manage\"]"
+ }
+ },
+ {
+ "name": "configure.permission.client.ceabb7ca-b063-4755-90fb-8de2cc7e5e00",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.ceabb7ca-b063-4755-90fb-8de2cc7e5e00\"]",
+ "scopes": "[\"configure\"]"
+ }
+ },
+ {
+ "name": "view.permission.client.ceabb7ca-b063-4755-90fb-8de2cc7e5e00",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.ceabb7ca-b063-4755-90fb-8de2cc7e5e00\"]",
+ "scopes": "[\"view\"]"
+ }
+ },
+ {
+ "name": "map-roles.permission.client.ceabb7ca-b063-4755-90fb-8de2cc7e5e00",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.ceabb7ca-b063-4755-90fb-8de2cc7e5e00\"]",
+ "scopes": "[\"map-roles\"]"
+ }
+ },
+ {
+ "name": "map-roles-client-scope.permission.client.ceabb7ca-b063-4755-90fb-8de2cc7e5e00",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.ceabb7ca-b063-4755-90fb-8de2cc7e5e00\"]",
+ "scopes": "[\"map-roles-client-scope\"]"
+ }
+ },
+ {
+ "name": "map-roles-composite.permission.client.ceabb7ca-b063-4755-90fb-8de2cc7e5e00",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.ceabb7ca-b063-4755-90fb-8de2cc7e5e00\"]",
+ "scopes": "[\"map-roles-composite\"]"
+ }
+ },
+ {
+ "name": "token-exchange.permission.client.ceabb7ca-b063-4755-90fb-8de2cc7e5e00",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.ceabb7ca-b063-4755-90fb-8de2cc7e5e00\"]",
+ "scopes": "[\"token-exchange\"]"
+ }
+ },
+ {
+ "name": "manage.permission.users",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Users\"]",
+ "scopes": "[\"manage\"]"
+ }
+ },
+ {
+ "name": "view.permission.users",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Users\"]",
+ "scopes": "[\"view\"]"
+ }
+ },
+ {
+ "name": "map-roles.permission.users",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Users\"]",
+ "scopes": "[\"map-roles\"]"
+ }
+ },
+ {
+ "name": "manage-group-membership.permission.users",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Users\"]",
+ "scopes": "[\"manage-group-membership\"]"
+ }
+ },
+ {
+ "name": "admin-impersonating.permission.users",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Users\"]",
+ "scopes": "[\"impersonate\"]",
+ "applyPolicies": "[\"account-console-client-impersonate\"]"
+ }
+ },
+ {
+ "name": "user-impersonated.permission.users",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Users\"]",
+ "scopes": "[\"user-impersonated\"]"
+ }
+ },
+ {
+ "name": "view.permission.client.fb5f91c4-42fa-4769-b45d-febef22b4976",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.fb5f91c4-42fa-4769-b45d-febef22b4976\"]",
+ "scopes": "[\"view\"]"
+ }
+ },
+ {
+ "name": "manage.permission.client.fb5f91c4-42fa-4769-b45d-febef22b4976",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.fb5f91c4-42fa-4769-b45d-febef22b4976\"]",
+ "scopes": "[\"manage\"]"
+ }
+ },
+ {
+ "name": "configure.permission.client.fb5f91c4-42fa-4769-b45d-febef22b4976",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.fb5f91c4-42fa-4769-b45d-febef22b4976\"]",
+ "scopes": "[\"configure\"]"
+ }
+ },
+ {
+ "name": "map-roles.permission.client.fb5f91c4-42fa-4769-b45d-febef22b4976",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.fb5f91c4-42fa-4769-b45d-febef22b4976\"]",
+ "scopes": "[\"map-roles\"]"
+ }
+ },
+ {
+ "name": "map-roles-client-scope.permission.client.fb5f91c4-42fa-4769-b45d-febef22b4976",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.fb5f91c4-42fa-4769-b45d-febef22b4976\"]",
+ "scopes": "[\"map-roles-client-scope\"]"
+ }
+ },
+ {
+ "name": "map-roles-composite.permission.client.fb5f91c4-42fa-4769-b45d-febef22b4976",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.fb5f91c4-42fa-4769-b45d-febef22b4976\"]",
+ "scopes": "[\"map-roles-composite\"]"
+ }
+ },
+ {
+ "name": "token-exchange.permission.client.fb5f91c4-42fa-4769-b45d-febef22b4976",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.fb5f91c4-42fa-4769-b45d-febef22b4976\"]",
+ "scopes": "[\"token-exchange\"]"
+ }
+ },
+ {
+ "name": "manage.permission.client.d2f0f1b6-46b7-4678-842a-8c67524ea2da",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.d2f0f1b6-46b7-4678-842a-8c67524ea2da\"]",
+ "scopes": "[\"manage\"]"
+ }
+ },
+ {
+ "name": "configure.permission.client.d2f0f1b6-46b7-4678-842a-8c67524ea2da",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.d2f0f1b6-46b7-4678-842a-8c67524ea2da\"]",
+ "scopes": "[\"configure\"]"
+ }
+ },
+ {
+ "name": "view.permission.client.d2f0f1b6-46b7-4678-842a-8c67524ea2da",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.d2f0f1b6-46b7-4678-842a-8c67524ea2da\"]",
+ "scopes": "[\"view\"]"
+ }
+ },
+ {
+ "name": "map-roles.permission.client.d2f0f1b6-46b7-4678-842a-8c67524ea2da",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.d2f0f1b6-46b7-4678-842a-8c67524ea2da\"]",
+ "scopes": "[\"map-roles\"]"
+ }
+ },
+ {
+ "name": "map-roles-client-scope.permission.client.d2f0f1b6-46b7-4678-842a-8c67524ea2da",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.d2f0f1b6-46b7-4678-842a-8c67524ea2da\"]",
+ "scopes": "[\"map-roles-client-scope\"]"
+ }
+ },
+ {
+ "name": "map-roles-composite.permission.client.d2f0f1b6-46b7-4678-842a-8c67524ea2da",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.d2f0f1b6-46b7-4678-842a-8c67524ea2da\"]",
+ "scopes": "[\"map-roles-composite\"]"
+ }
+ },
+ {
+ "name": "token-exchange.permission.client.d2f0f1b6-46b7-4678-842a-8c67524ea2da",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"client.resource.d2f0f1b6-46b7-4678-842a-8c67524ea2da\"]",
+ "scopes": "[\"token-exchange\"]",
+ "applyPolicies": "[\"opex-api-exchange\"]"
+ }
+ }
+ ],
+ "scopes": [
+ {
+ "name": "manage"
+ },
+ {
+ "name": "view"
+ },
+ {
+ "name": "map-roles"
+ },
+ {
+ "name": "map-roles-client-scope"
+ },
+ {
+ "name": "map-roles-composite"
+ },
+ {
+ "name": "configure"
+ },
+ {
+ "name": "token-exchange"
+ },
+ {
+ "name": "impersonate"
+ },
+ {
+ "name": "user-impersonated"
+ },
+ {
+ "name": "manage-group-membership"
+ },
+ {
+ "name": "map-role"
+ },
+ {
+ "name": "map-role-client-scope"
+ },
+ {
+ "name": "map-role-composite"
+ }
+ ],
+ "decisionStrategy": "UNANIMOUS"
+ }
+ },
+ {
+ "id": "8e358d2f-b085-4243-8e6e-c175431e5eeb",
+ "clientId": "security-admin-console",
+ "name": "${client_security-admin-console}",
+ "rootUrl": "${authAdminUrl}",
+ "baseUrl": "/admin/opex/console/",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "redirectUris": [
+ "/admin/opex/console/*"
+ ],
+ "webOrigins": [
+ "+"
+ ],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": false,
+ "serviceAccountsEnabled": false,
+ "publicClient": true,
+ "frontchannelLogout": false,
+ "protocol": "openid-connect",
+ "attributes": {
+ "saml.assertion.signature": "false",
+ "saml.multivalued.roles": "false",
+ "saml.force.post.binding": "false",
+ "saml.encrypt": "false",
+ "post.logout.redirect.uris": "+",
+ "saml.server.signature": "false",
+ "backchannel.logout.revoke.offline.tokens": "false",
+ "saml.server.signature.keyinfo.ext": "false",
+ "exclude.session.state.from.auth.response": "false",
+ "realm_client": "false",
+ "client.use.lightweight.access.token.enabled": "true",
+ "backchannel.logout.session.required": "false",
+ "client_credentials.use_refresh_token": "false",
+ "saml_force_name_id_format": "false",
+ "saml.client.signature": "false",
+ "tls.client.certificate.bound.access.tokens": "false",
+ "saml.authnstatement": "false",
+ "display.on.consent.screen": "false",
+ "pkce.code.challenge.method": "S256",
+ "saml.onetimeuse.condition": "false"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": true,
+ "nodeReRegistrationTimeout": 0,
+ "protocolMappers": [
+ {
+ "id": "9cfca9ee-493d-4b5e-8170-2d364149de59",
+ "name": "locale",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "locale",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "locale",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ }
+ ],
+ "defaultClientScopes": [
+ "web-origins",
+ "acr",
+ "profile",
+ "basic",
+ "email"
+ ],
+ "optionalClientScopes": [
+ "address",
+ "offline_access",
+ "microprofile-jwt"
+ ]
+ },
+ {
+ "id": "fd116873-8b00-4851-a88d-1a72575783ba",
+ "clientId": "web-app",
+ "name": "Web app",
+ "description": "Client for opex web app",
+ "rootUrl": "",
+ "adminUrl": "",
+ "baseUrl": "",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "redirectUris": [
+ "/*"
+ ],
+ "webOrigins": [
+ "/*"
+ ],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": true,
+ "serviceAccountsEnabled": false,
+ "publicClient": true,
+ "frontchannelLogout": true,
+ "protocol": "openid-connect",
+ "attributes": {
+ "access.token.lifespan": "3600",
+ "request.object.signature.alg": "any",
+ "frontchannel.logout.session.required": "true",
+ "oauth2.device.authorization.grant.enabled": "false",
+ "use.jwks.url": "false",
+ "backchannel.logout.revoke.offline.tokens": "false",
+ "use.refresh.tokens": "true",
+ "realm_client": "false",
+ "oidc.ciba.grant.enabled": "false",
+ "exclude.issuer.from.auth.response": "false",
+ "backchannel.logout.session.required": "true",
+ "client_credentials.use_refresh_token": "false",
+ "require.pushed.authorization.requests": "false",
+ "request.object.encryption.enc": "any",
+ "client.session.max.lifespan": "604800",
+ "client.secret.creation.time": "1747492073",
+ "request.object.encryption.alg": "any",
+ "client.introspection.response.allow.jwt.claim.enabled": "false",
+ "standard.token.exchange.enabled": "false",
+ "exclude.session.state.from.auth.response": "false",
+ "client.use.lightweight.access.token.enabled": "false",
+ "request.object.required": "not required",
+ "tls.client.certificate.bound.access.tokens": "false",
+ "acr.loa.map": "{}",
+ "display.on.consent.screen": "false",
+ "token.response.type.bearer.lower-case": "false"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": true,
+ "nodeReRegistrationTimeout": -1,
+ "defaultClientScopes": [
+ "trust",
+ "audience-opex-api-key",
+ "basic",
+ "role_permission_attribute"
+ ],
+ "optionalClientScopes": []
+ }
+ ],
+ "clientScopes": [
+ {
+ "id": "0cbd4466-de57-4fc9-81a7-f34f3cd2262a",
+ "name": "service_account",
+ "description": "Specific scope for a client enabled for service accounts",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "false",
+ "display.on.consent.screen": "false"
+ },
+ "protocolMappers": [
+ {
+ "id": "4fbc5bd0-1bd0-4fda-89b7-b83492a8ac42",
+ "name": "Client ID",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usersessionmodel-note-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.session.note": "client_id",
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "client_id",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "3bea689a-7d18-47df-a545-499ef0548698",
+ "name": "Client Host",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usersessionmodel-note-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.session.note": "clientHost",
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "clientHost",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "d509437d-eb13-4322-9f76-d5d79a65ff20",
+ "name": "Client IP Address",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usersessionmodel-note-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.session.note": "clientAddress",
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "clientAddress",
+ "jsonType.label": "String"
+ }
+ }
+ ]
+ },
+ {
+ "id": "05ec9110-1046-4784-ae97-d8bf86bcfc62",
+ "name": "acr",
+ "description": "OpenID Connect scope for add acr (authentication context class reference) to the token",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "false",
+ "display.on.consent.screen": "false"
+ },
+ "protocolMappers": [
+ {
+ "id": "dd201a3e-1243-474d-af83-312a0130f2f3",
+ "name": "acr loa level",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-acr-mapper",
+ "consentRequired": false,
+ "config": {
+ "id.token.claim": "true",
+ "introspection.token.claim": "true",
+ "access.token.claim": "true",
+ "userinfo.token.claim": "true"
+ }
+ }
+ ]
+ },
+ {
+ "id": "ba8c9950-fd0b-4434-8be6-b58456d7b6d4",
+ "name": "profile",
+ "description": "OpenID Connect built-in scope: profile",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "true",
+ "consent.screen.text": "${profileScopeConsentText}",
+ "display.on.consent.screen": "true"
+ },
+ "protocolMappers": [
+ {
+ "id": "0a9ddd71-309c-40f0-8ea6-a0791070c6ed",
+ "name": "profile",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "profile",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "profile",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "fbf53bbd-1ad0-4bf8-8030-50f81696d8ee",
+ "name": "nickname",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "nickname",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "nickname",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "423be2cd-42c0-462e-9030-18f9b28ff2d3",
+ "name": "gender",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "gender",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "gender",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "53eb9006-4b81-474a-8b60-80f775d54b63",
+ "name": "picture",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "picture",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "picture",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "4d8bc82a-eaeb-499e-8eb2-0f1dcbe91699",
+ "name": "locale",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "locale",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "locale",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "d3b25485-4042-419d-afff-cfd63a76e229",
+ "name": "full name",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-full-name-mapper",
+ "consentRequired": false,
+ "config": {
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "422cfa5a-f2f4-4f36-82df-91b47ae1ea50",
+ "name": "given name",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-property-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "firstName",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "given_name",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "3f2863c1-d98d-45b5-b08f-af9c4d9c10f8",
+ "name": "website",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "website",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "website",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "c98c063d-eee4-41a0-9130-595afd709d1f",
+ "name": "middle name",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "middleName",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "middle_name",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "8dbed80a-d672-4185-8dda-4bba2a56ec83",
+ "name": "family name",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-property-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "lastName",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "family_name",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "5e5c690c-93cf-489d-a054-b109eab8911b",
+ "name": "updated at",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "updatedAt",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "updated_at",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "3b985202-af8a-42f1-ac5f-0966a404f5d7",
+ "name": "birthdate",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "birthdate",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "birthdate",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "6eafd1b3-7121-4919-ad1e-039fa58acc32",
+ "name": "username",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-property-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "username",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "preferred_username",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "73cba925-8c31-443f-9601-b1514e6396c1",
+ "name": "zoneinfo",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "zoneinfo",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "zoneinfo",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ }
+ ]
+ },
+ {
+ "id": "d1debc5e-632b-4c2e-863b-2f5b2b1572d5",
+ "name": "user-roles",
+ "description": "",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "true",
+ "display.on.consent.screen": "true",
+ "gui.order": "",
+ "consent.screen.text": ""
+ },
+ "protocolMappers": [
+ {
+ "id": "d8aa4645-5f5b-41e7-b7e6-b12739ca0cca",
+ "name": "realm roles",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-realm-role-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "foo",
+ "introspection.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "realm_access.roles",
+ "jsonType.label": "String",
+ "multivalued": "true"
+ }
+ }
+ ]
+ },
+ {
+ "id": "dbb7860d-fba0-4c05-8b99-f2f9cdd3ffb4",
+ "name": "audience-opex-api-key",
+ "description": "",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "false",
+ "display.on.consent.screen": "true",
+ "gui.order": "",
+ "consent.screen.text": ""
+ },
+ "protocolMappers": [
+ {
+ "id": "627885b9-1bb8-465a-9125-e9603ac1dcd5",
+ "name": "audience-opex-api-key",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-audience-mapper",
+ "consentRequired": false,
+ "config": {
+ "included.client.audience": "opex-api-key",
+ "id.token.claim": "false",
+ "lightweight.claim": "false",
+ "access.token.claim": "true",
+ "introspection.token.claim": "true"
+ }
+ }
+ ]
+ },
+ {
+ "id": "51d49314-b511-43e0-9258-bfb873758a78",
+ "name": "web-origins",
+ "description": "OpenID Connect scope for add allowed web origins to the access token",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "false",
+ "consent.screen.text": "",
+ "display.on.consent.screen": "false"
+ },
+ "protocolMappers": [
+ {
+ "id": "2b384cd0-9e85-4a87-8eeb-2b480b0587b7",
+ "name": "allowed web origins",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-allowed-origins-mapper",
+ "consentRequired": false,
+ "config": {}
+ }
+ ]
+ },
+ {
+ "id": "e39a0ba3-b2df-4378-9628-4205d41a8ead",
+ "name": "basic",
+ "description": "OpenID Connect scope for add all basic claims to the token",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "false",
+ "display.on.consent.screen": "false"
+ },
+ "protocolMappers": [
+ {
+ "id": "3fe8e716-16f7-48b1-beeb-e9c2807a9376",
+ "name": "sub",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-sub-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "access.token.claim": "true"
+ }
+ },
+ {
+ "id": "48c68e2a-5035-4ba8-8d17-d2b4922a1613",
+ "name": "auth_time",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usersessionmodel-note-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.session.note": "AUTH_TIME",
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "auth_time",
+ "jsonType.label": "long"
+ }
+ },
+ {
+ "id": "f4b67796-04fa-49b5-846f-a5ab81587c55",
+ "name": "audience resolve",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-audience-resolve-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "access.token.claim": "true"
+ }
+ }
+ ]
+ },
+ {
+ "id": "18e141bf-dabe-4858-879c-dbc439cdead4",
+ "name": "role_list",
+ "description": "SAML role list",
+ "protocol": "saml",
+ "attributes": {
+ "include.in.token.scope": "false",
+ "display.on.consent.screen": "true",
+ "gui.order": "",
+ "consent.screen.text": "${samlRoleListScopeConsentText}"
+ },
+ "protocolMappers": [
+ {
+ "id": "10cbe37f-0198-4d65-bc8a-bfe5ad8145d1",
+ "name": "role list",
+ "protocol": "saml",
+ "protocolMapper": "saml-role-list-mapper",
+ "consentRequired": false,
+ "config": {
+ "single": "false",
+ "attribute.nameformat": "Basic",
+ "attribute.name": "Role"
+ }
+ }
+ ]
+ },
+ {
+ "id": "94624568-d736-46dc-83af-d1f29f25a25e",
+ "name": "realm-roles",
+ "description": "Shows user's assigned realm roles",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "false",
+ "display.on.consent.screen": "false",
+ "gui.order": "",
+ "consent.screen.text": ""
+ },
+ "protocolMappers": [
+ {
+ "id": "02f5bef3-673f-4242-a6de-12eaaffe5d58",
+ "name": "realm roles",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-realm-role-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "multivalued": "true",
+ "userinfo.token.claim": "false",
+ "user.attribute": "foo",
+ "id.token.claim": "true",
+ "lightweight.claim": "false",
+ "access.token.claim": "true",
+ "claim.name": "roles",
+ "jsonType.label": "String"
+ }
+ }
+ ]
+ },
+ {
+ "id": "c658ae14-e96a-4745-b21b-2ed5c4c63f5f",
+ "name": "microprofile-jwt",
+ "description": "Microprofile - JWT built-in scope",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "true",
+ "display.on.consent.screen": "false"
+ },
+ "protocolMappers": [
+ {
+ "id": "959521bc-5ffd-465b-95f2-5b0c20d1909c",
+ "name": "upn",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-property-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "username",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "upn",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "07b8550c-b298-4cce-9ffb-900182575b76",
+ "name": "groups",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-realm-role-mapper",
+ "consentRequired": false,
+ "config": {
+ "multivalued": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "foo",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "groups",
+ "jsonType.label": "String"
+ }
+ }
+ ]
+ },
+ {
+ "id": "569b3d44-4ecd-4768-a58c-70ff38f4b4fe",
+ "name": "offline_access",
+ "description": "OpenID Connect built-in scope: offline_access",
+ "protocol": "openid-connect",
+ "attributes": {
+ "consent.screen.text": "${offlineAccessScopeConsentText}",
+ "display.on.consent.screen": "true"
+ }
+ },
+ {
+ "id": "d4e253fb-7361-47cf-9d4a-86245686fdf2",
+ "name": "trust",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "true",
+ "display.on.consent.screen": "true"
+ },
+ "protocolMappers": [
+ {
+ "id": "2bafcd16-ff19-4f72-adb4-c1735793842d",
+ "name": "User roles",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-realm-role-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "multivalued": "true",
+ "id.token.claim": "true",
+ "lightweight.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "roles",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "aeca072b-0153-4ffc-b3de-bea60f4a7fd7",
+ "name": "Mobile",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "aggregate.attrs": "false",
+ "introspection.token.claim": "true",
+ "multivalued": "false",
+ "userinfo.token.claim": "true",
+ "user.attribute": "mobile",
+ "id.token.claim": "true",
+ "lightweight.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "mobile",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "b37f97ee-3a0a-498f-9535-f2afe2a881b9",
+ "name": "Email",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "aggregate.attrs": "false",
+ "introspection.token.claim": "true",
+ "multivalued": "false",
+ "userinfo.token.claim": "true",
+ "user.attribute": "email",
+ "id.token.claim": "true",
+ "lightweight.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "email",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "a163e6a9-3fbe-481d-b783-779acdf41436",
+ "name": "First name",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "aggregate.attrs": "false",
+ "introspection.token.claim": "true",
+ "multivalued": "false",
+ "userinfo.token.claim": "true",
+ "user.attribute": "firstName",
+ "id.token.claim": "true",
+ "lightweight.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "firstName",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "ba98367f-171f-490d-8fdf-65ab4cdf46f4",
+ "name": "Last name",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "aggregate.attrs": "false",
+ "introspection.token.claim": "true",
+ "multivalued": "false",
+ "userinfo.token.claim": "true",
+ "user.attribute": "lastName",
+ "id.token.claim": "true",
+ "lightweight.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "lastName",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "270e9cfc-cdbf-4fa1-8358-47e381abce45",
+ "name": "Username",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "aggregate.attrs": "false",
+ "introspection.token.claim": "true",
+ "multivalued": "false",
+ "userinfo.token.claim": "true",
+ "user.attribute": "username",
+ "id.token.claim": "true",
+ "lightweight.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "username",
+ "jsonType.label": "String"
+ }
+ }
+ ]
+ },
+ {
+ "id": "77c7e29d-1a22-4419-bbfb-4a62bb033449",
+ "name": "address",
+ "description": "OpenID Connect built-in scope: address",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "true",
+ "consent.screen.text": "${addressScopeConsentText}",
+ "display.on.consent.screen": "true"
+ },
+ "protocolMappers": [
+ {
+ "id": "94e1879d-b49e-4178-96e0-bf8d7f32c160",
+ "name": "address",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-address-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute.formatted": "formatted",
+ "user.attribute.country": "country",
+ "user.attribute.postal_code": "postal_code",
+ "userinfo.token.claim": "true",
+ "user.attribute.street": "street",
+ "id.token.claim": "true",
+ "user.attribute.region": "region",
+ "access.token.claim": "true",
+ "user.attribute.locality": "locality"
+ }
+ }
+ ]
+ },
+ {
+ "id": "b3526ac1-10e2-4344-8621-9c5a0853e97a",
+ "name": "email",
+ "description": "OpenID Connect built-in scope: email",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "true",
+ "consent.screen.text": "${emailScopeConsentText}",
+ "display.on.consent.screen": "true"
+ },
+ "protocolMappers": [
+ {
+ "id": "d30270dc-baa6-455a-8ff6-ddccf8a78d86",
+ "name": "email verified",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-property-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "emailVerified",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "email_verified",
+ "jsonType.label": "boolean",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "f5b1684d-e479-4134-8578-457fa64717da",
+ "name": "email",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-property-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "email",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "email",
+ "jsonType.label": "String",
+ "userinfo.token.claim": "true"
+ }
+ }
+ ]
+ },
+ {
+ "id": "b5f64b6e-cf08-47d8-be89-67bb0789d93f",
+ "name": "role_permission_attribute",
+ "description": "",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "false",
+ "display.on.consent.screen": "false",
+ "gui.order": "",
+ "consent.screen.text": ""
+ },
+ "protocolMappers": [
+ {
+ "id": "f9a8a8ff-93b8-4299-8fa0-7db51cc715bb",
+ "name": "Role permission attribute",
+ "protocol": "openid-connect",
+ "protocolMapper": "role-attributes-mapper",
+ "consentRequired": false,
+ "config": {
+ "claim.name": "permissions",
+ "jsonType.label": "String",
+ "attribute.name": "permissions"
+ }
+ }
+ ]
+ },
+ {
+ "id": "60575b8d-b5ab-47c3-a2fa-b14e77d4f392",
+ "name": "Forced_Roles",
+ "description": "",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "false",
+ "display.on.consent.screen": "true",
+ "gui.order": "",
+ "consent.screen.text": ""
+ },
+ "protocolMappers": [
+ {
+ "id": "a2ffe329-0ed7-4861-a6aa-e72a25b98770",
+ "name": "roles",
+ "protocol": "openid-connect",
+ "protocolMapper": "forced-role-mapper",
+ "consentRequired": false,
+ "config": {
+ "claim.name": "roles",
+ "jsonType.label": "String",
+ "key.name": "roles"
+ }
+ }
+ ]
+ }
+ ],
+ "defaultDefaultClientScopes": [
+ "email",
+ "trust",
+ "acr",
+ "basic",
+ "microprofile-jwt",
+ "web-origins",
+ "role_permission_attribute",
+ "role_list",
+ "realm-roles",
+ "audience-opex-api-key",
+ "user-roles",
+ "Forced_Roles"
+ ],
+ "defaultOptionalClientScopes": [
+ "offline_access",
+ "address"
+ ],
+ "browserSecurityHeaders": {
+ "contentSecurityPolicyReportOnly": "",
+ "xContentTypeOptions": "nosniff",
+ "referrerPolicy": "no-referrer",
+ "xRobotsTag": "none",
+ "xFrameOptions": "SAMEORIGIN",
+ "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';",
+ "xXSSProtection": "1; mode=block",
+ "strictTransportSecurity": "max-age=31536000; includeSubDomains"
+ },
+ "smtpServer": {
+ "host": "smtp.elasticemail.com",
+ "password": "${vault.smtppass}",
+ "from": "for.demo.purpose.only@opex.dev",
+ "auth": "true",
+ "port": "2525",
+ "user": "for.demo.purpose.only@opex.dev"
+ },
+ "eventsEnabled": false,
+ "eventsListeners": [
+ "jboss-logging"
+ ],
+ "enabledEventTypes": [],
+ "adminEventsEnabled": false,
+ "adminEventsDetailsEnabled": false,
+ "identityProviders": [
+ {
+ "alias": "google",
+ "internalId": "6456448e-2415-49ad-bf95-1b5176557862",
+ "providerId": "google",
+ "enabled": true,
+ "updateProfileFirstLoginMode": "on",
+ "trustEmail": false,
+ "storeToken": false,
+ "addReadTokenRoleOnCreate": false,
+ "authenticateByDefault": false,
+ "linkOnly": false,
+ "hideOnLogin": false,
+ "config": {
+ "syncMode": "LEGACY",
+ "clientSecret": "**********",
+ "clientId": "625813606110-er3v3sol4v206kdg40gf0ltqv08scgs2.apps.googleusercontent.com",
+ "autoLink": "true",
+ "defaultScope": "openid profile email"
+ }
+ }
+ ],
+ "identityProviderMappers": [],
+ "components": {
+ "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [
+ {
+ "id": "365b2899-befe-4417-b89b-562650ec4446",
+ "name": "Full Scope Disabled",
+ "providerId": "scope",
+ "subType": "anonymous",
+ "subComponents": {},
+ "config": {}
+ },
+ {
+ "id": "76075388-2782-4656-a986-313493239a9f",
+ "name": "Consent Required",
+ "providerId": "consent-required",
+ "subType": "anonymous",
+ "subComponents": {},
+ "config": {}
+ },
+ {
+ "id": "3caaf57a-9cd7-48c1-b709-b40b887414f7",
+ "name": "Allowed Protocol Mapper Types",
+ "providerId": "allowed-protocol-mappers",
+ "subType": "anonymous",
+ "subComponents": {},
+ "config": {
+ "allowed-protocol-mapper-types": [
+ "oidc-sha256-pairwise-sub-mapper",
+ "oidc-usermodel-property-mapper",
+ "oidc-usermodel-attribute-mapper",
+ "oidc-address-mapper",
+ "oidc-full-name-mapper",
+ "saml-user-property-mapper",
+ "saml-user-attribute-mapper",
+ "saml-role-list-mapper"
+ ]
+ }
+ },
+ {
+ "id": "84305f42-4b6d-4b0a-ac7c-53e406e3ac63",
+ "name": "Allowed Client Scopes",
+ "providerId": "allowed-client-templates",
+ "subType": "authenticated",
+ "subComponents": {},
+ "config": {
+ "allow-default-scopes": [
+ "true"
+ ]
+ }
+ },
+ {
+ "id": "c7c38a95-744f-4558-a403-9cf692fe1944",
+ "name": "Allowed Client Scopes",
+ "providerId": "allowed-client-templates",
+ "subType": "anonymous",
+ "subComponents": {},
+ "config": {
+ "allow-default-scopes": [
+ "true"
+ ]
+ }
+ },
+ {
+ "id": "81c32244-7921-43e9-9356-a3469259b78c",
+ "name": "Trusted Hosts",
+ "providerId": "trusted-hosts",
+ "subType": "anonymous",
+ "subComponents": {},
+ "config": {
+ "host-sending-registration-request-must-match": [
+ "true"
+ ],
+ "trusted-hosts": [
+ "https://opex.dev/bdemo/login"
+ ],
+ "client-uris-must-match": [
+ "true"
+ ]
+ }
+ },
+ {
+ "id": "d09b2147-afea-4f7f-a49c-0aec7eee10de",
+ "name": "Max Clients Limit",
+ "providerId": "max-clients",
+ "subType": "anonymous",
+ "subComponents": {},
+ "config": {
+ "max-clients": [
+ "200"
+ ]
+ }
+ },
+ {
+ "id": "41ffde1b-72a2-416f-87a7-94989e940dc0",
+ "name": "Allowed Protocol Mapper Types",
+ "providerId": "allowed-protocol-mappers",
+ "subType": "authenticated",
+ "subComponents": {},
+ "config": {
+ "allowed-protocol-mapper-types": [
+ "saml-user-property-mapper",
+ "oidc-sha256-pairwise-sub-mapper",
+ "oidc-address-mapper",
+ "oidc-usermodel-attribute-mapper",
+ "saml-user-attribute-mapper",
+ "saml-role-list-mapper",
+ "oidc-full-name-mapper",
+ "oidc-usermodel-property-mapper"
+ ]
+ }
+ }
+ ],
+ "org.keycloak.userprofile.UserProfileProvider": [
+ {
+ "id": "9975d471-20ec-4099-8f08-52db9aaed738",
+ "providerId": "declarative-user-profile",
+ "subComponents": {},
+ "config": {
+ "kc.user.profile.config": [
+ "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"mobile\",\"displayName\":\"Mobile\",\"validations\":{\"length\":{\"min\":\"11\",\"max\":\"15\"}},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"otpConfig\",\"displayName\":\"OTP Config\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}]}"
+ ]
+ }
+ }
+ ],
+ "org.keycloak.keys.KeyProvider": [
+ {
+ "id": "d67a940a-52e4-44a5-9f69-6ffdd67a188f",
+ "name": "rsa-generated",
+ "providerId": "rsa-generated",
+ "subComponents": {},
+ "config": {
+ "priority": [
+ "100"
+ ]
+ }
+ },
+ {
+ "id": "7fe566f1-60d8-49b7-89cc-30c31e00b86c",
+ "name": "hmac-generated-hs512",
+ "providerId": "hmac-generated",
+ "subComponents": {},
+ "config": {
+ "priority": [
+ "100"
+ ],
+ "algorithm": [
+ "HS512"
+ ]
+ }
+ },
+ {
+ "id": "48d40de3-6234-42e8-9449-f68f56abb54b",
+ "name": "aes-generated",
+ "providerId": "aes-generated",
+ "subComponents": {},
+ "config": {
+ "priority": [
+ "100"
+ ]
+ }
+ },
+ {
+ "id": "52ea1c5d-2a30-459f-b66a-249f298b32f8",
+ "name": "hmac-generated",
+ "providerId": "hmac-generated",
+ "subComponents": {},
+ "config": {
+ "priority": [
+ "100"
+ ],
+ "algorithm": [
+ "HS256"
+ ]
+ }
+ }
+ ]
+ },
+ "internationalizationEnabled": false,
+ "supportedLocales": [],
+ "authenticationFlows": [
+ {
+ "id": "a5158583-26f5-45da-8c7d-edfed5c20889",
+ "alias": "Handle Existing Account",
+ "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "idp-confirm-link",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": true,
+ "flowAlias": "Handle Existing Account - Alternatives - 0",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "580af056-728f-4784-95a4-6b4b5cbbfda3",
+ "alias": "Handle Existing Account - Alternatives - 0",
+ "description": "Subflow of Handle Existing Account with alternative executions",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "idp-email-verification",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "ALTERNATIVE",
+ "priority": 20,
+ "autheticatorFlow": true,
+ "flowAlias": "Verify Existing Account by Re-authentication",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "cb9a1006-177c-4ae9-8a13-a392cac637f9",
+ "alias": "Opex Direct Grant",
+ "description": "OpenID Connect Resource Owner Grant",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": false,
+ "authenticationExecutions": [
+ {
+ "authenticator": "direct-grant-validate-username",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "direct-grant-validate-password",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 30,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "15bc451f-81d6-4b45-abd3-0a742f8b87ca",
+ "alias": "Opex Registration",
+ "description": "registration flow",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": false,
+ "authenticationExecutions": [
+ {
+ "authenticator": "registration-page-form",
+ "authenticatorFlow": true,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": true,
+ "flowAlias": "Opex Registration registration form",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "52d99dce-a3d9-4551-a230-424223ab4a61",
+ "alias": "Opex Registration registration form",
+ "description": "registration form",
+ "providerId": "form-flow",
+ "topLevel": false,
+ "builtIn": false,
+ "authenticationExecutions": [
+ {
+ "authenticator": "registration-opex-captcha-action",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "registration-user-creation",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 40,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "registration-password-action",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 51,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "960bd7aa-8c01-4334-9460-238edb6c716a",
+ "alias": "Verify Existing Account by Re-authentication",
+ "description": "Reauthentication of existing account",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "idp-username-password-form",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "CONDITIONAL",
+ "priority": 20,
+ "autheticatorFlow": true,
+ "flowAlias": "Verify Existing Account by Re-authentication - auth-otp-form - Conditional",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "8ff718aa-4e39-48d2-aacf-fdd456c75a53",
+ "alias": "Verify Existing Account by Re-authentication - auth-otp-form - Conditional",
+ "description": "Flow to determine if the auth-otp-form authenticator should be used or not.",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "conditional-user-configured",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "auth-otp-form",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "011b0d5c-1da6-4519-b3bd-7779de83021b",
+ "alias": "browser",
+ "description": "browser based authentication",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "auth-cookie",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "auth-spnego",
+ "authenticatorFlow": false,
+ "requirement": "DISABLED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "identity-provider-redirector",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 25,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "ALTERNATIVE",
+ "priority": 30,
+ "autheticatorFlow": true,
+ "flowAlias": "forms",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "084faf8d-bfa0-4f0d-b93b-027b54132537",
+ "alias": "clients",
+ "description": "Base authentication for clients",
+ "providerId": "client-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "client-secret",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "client-jwt",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "client-secret-jwt",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 30,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "client-x509",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 40,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "2b945774-136a-45c8-b2dd-f023f441edc4",
+ "alias": "direct grant",
+ "description": "OpenID Connect Resource Owner Grant",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "direct-grant-validate-username",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "direct-grant-validate-password",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "CONDITIONAL",
+ "priority": 30,
+ "autheticatorFlow": true,
+ "flowAlias": "direct grant - direct-grant-validate-otp - Conditional",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "caa8b7d7-0bf8-4f8c-ae53-a25bbb52ad0a",
+ "alias": "direct grant - direct-grant-validate-otp - Conditional",
+ "description": "Flow to determine if the direct-grant-validate-otp authenticator should be used or not.",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "conditional-user-configured",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "direct-grant-validate-otp",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "1ab8d8fd-db75-4cf6-b105-813cce023ece",
+ "alias": "docker auth",
+ "description": "Used by Docker clients to authenticate against the IDP",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "docker-http-basic-authenticator",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "934881e3-32ff-4050-933e-d281935e4b8f",
+ "alias": "first broker login",
+ "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticatorConfig": "review profile config",
+ "authenticator": "idp-review-profile",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": true,
+ "flowAlias": "first broker login - Alternatives - 0",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "d8feb86c-0c21-4570-9b7b-ad42223ceec5",
+ "alias": "first broker login - Alternatives - 0",
+ "description": "Subflow of first broker login with alternative executions",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticatorConfig": "create unique user config",
+ "authenticator": "idp-create-user-if-unique",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "ALTERNATIVE",
+ "priority": 20,
+ "autheticatorFlow": true,
+ "flowAlias": "Handle Existing Account",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "3a39fbcd-8450-428c-b4cf-e9e1fa46c395",
+ "alias": "forms",
+ "description": "Username, password, otp and other auth forms.",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "auth-username-password-form",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "CONDITIONAL",
+ "priority": 20,
+ "autheticatorFlow": true,
+ "flowAlias": "forms - auth-otp-form - Conditional",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "c5ee3815-9c15-4ee6-a266-c693f1e2e655",
+ "alias": "forms - auth-otp-form - Conditional",
+ "description": "Flow to determine if the auth-otp-form authenticator should be used or not.",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "conditional-user-configured",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "auth-otp-form",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "d3363796-a3f2-458a-9510-cd0daaedbb18",
+ "alias": "registration",
+ "description": "registration flow",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "registration-page-form",
+ "authenticatorFlow": true,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": true,
+ "flowAlias": "registration form",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "619ca7f4-f21d-450c-ad5a-043866073779",
+ "alias": "registration form",
+ "description": "registration form",
+ "providerId": "form-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "registration-user-creation",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "registration-password-action",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 50,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "registration-recaptcha-action",
+ "authenticatorFlow": false,
+ "requirement": "DISABLED",
+ "priority": 60,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "71edeb90-7547-41e3-a395-547bfbc3ae51",
+ "alias": "reset credentials",
+ "description": "Reset credentials for a user if they forgot their password or something",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "reset-credentials-choose-user",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "reset-credential-email",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "reset-password",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 30,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "CONDITIONAL",
+ "priority": 40,
+ "autheticatorFlow": true,
+ "flowAlias": "reset credentials - reset-otp - Conditional",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "2e577b18-4986-4556-8396-737eff6dcf40",
+ "alias": "reset credentials - reset-otp - Conditional",
+ "description": "Flow to determine if the reset-otp authenticator should be used or not.",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "conditional-user-configured",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "reset-otp",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "631176c8-0237-4488-9d3d-1662da821b3b",
+ "alias": "saml ecp",
+ "description": "SAML ECP Profile Authentication Flow",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "http-basic-authenticator",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ }
+ ],
+ "authenticatorConfig": [
+ {
+ "id": "a4f4b792-ec6b-4fb1-89ec-8b6ae589fff6",
+ "alias": "create unique user config",
+ "config": {
+ "require.password.update.after.registration": "false"
+ }
+ },
+ {
+ "id": "9b1f556c-8d73-4bdf-8341-7cf7cf858ad7",
+ "alias": "review profile config",
+ "config": {
+ "update.profile.on.first.login": "missing"
+ }
+ }
+ ],
+ "requiredActions": [
+ {
+ "alias": "CONFIGURE_TOTP",
+ "name": "Configure OTP",
+ "providerId": "CONFIGURE_TOTP",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 10,
+ "config": {}
+ },
+ {
+ "alias": "TERMS_AND_CONDITIONS",
+ "name": "Terms and Conditions",
+ "providerId": "TERMS_AND_CONDITIONS",
+ "enabled": false,
+ "defaultAction": false,
+ "priority": 20,
+ "config": {}
+ },
+ {
+ "alias": "UPDATE_PASSWORD",
+ "name": "Update Password",
+ "providerId": "UPDATE_PASSWORD",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 30,
+ "config": {}
+ },
+ {
+ "alias": "UPDATE_PROFILE",
+ "name": "Update Profile",
+ "providerId": "UPDATE_PROFILE",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 40,
+ "config": {}
+ },
+ {
+ "alias": "VERIFY_EMAIL",
+ "name": "Verify Email",
+ "providerId": "VERIFY_EMAIL",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 50,
+ "config": {}
+ },
+ {
+ "alias": "delete_account",
+ "name": "Delete Account",
+ "providerId": "delete_account",
+ "enabled": false,
+ "defaultAction": false,
+ "priority": 60,
+ "config": {}
+ },
+ {
+ "alias": "delete_credential",
+ "name": "Delete Credential",
+ "providerId": "delete_credential",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 100,
+ "config": {}
+ },
+ {
+ "alias": "update_user_locale",
+ "name": "Update User Locale",
+ "providerId": "update_user_locale",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 1000,
+ "config": {}
+ }
+ ],
+ "browserFlow": "browser",
+ "registrationFlow": "Opex Registration",
+ "directGrantFlow": "Opex Direct Grant",
+ "resetCredentialsFlow": "reset credentials",
+ "clientAuthenticationFlow": "clients",
+ "dockerAuthenticationFlow": "docker auth",
+ "firstBrokerLoginFlow": "first broker login",
+ "attributes": {
+ "cibaBackchannelTokenDeliveryMode": "poll",
+ "cibaExpiresIn": "120",
+ "cibaAuthRequestedUserHint": "login_hint",
+ "oauth2DeviceCodeLifespan": "600",
+ "clientOfflineSessionMaxLifespan": "0",
+ "oauth2DevicePollingInterval": "5",
+ "clientSessionIdleTimeout": "0",
+ "parRequestUriLifespan": "60",
+ "clientSessionMaxLifespan": "0",
+ "clientOfflineSessionIdleTimeout": "0",
+ "cibaInterval": "5",
+ "realmReusableOtpCode": "false"
+ },
+ "keycloakVersion": "26.1.5",
+ "userManagedAccessAllowed": false,
+ "organizationsEnabled": false,
+ "verifiableCredentialsEnabled": false,
+ "adminPermissionsEnabled": false,
+ "clientProfiles": {
+ "profiles": []
+ },
+ "clientPolicies": {
+ "policies": []
+ }
+}
\ No newline at end of file
diff --git a/auth-gateway/keycloak-setup/spi/.gitignore b/auth-gateway/keycloak-setup/spi/.gitignore
new file mode 100644
index 000000000..5ff6309b7
--- /dev/null
+++ b/auth-gateway/keycloak-setup/spi/.gitignore
@@ -0,0 +1,38 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### IntelliJ IDEA ###
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
\ No newline at end of file
diff --git a/auth-gateway/keycloak-setup/spi/pom.xml b/auth-gateway/keycloak-setup/spi/pom.xml
new file mode 100644
index 000000000..426d13566
--- /dev/null
+++ b/auth-gateway/keycloak-setup/spi/pom.xml
@@ -0,0 +1,40 @@
+
+
+ 4.0.0
+
+ co.nilin.opex.keycloak
+ spi
+ 1.0
+
+
+ 21
+ 21
+ UTF-8
+
+
+
+
+ org.keycloak
+ keycloak-core
+ 26.0.0
+
+
+ org.keycloak
+ keycloak-server-spi
+ 26.0.0
+
+
+ org.keycloak
+ keycloak-server-spi-private
+ 26.0.0
+
+
+ org.keycloak
+ keycloak-services
+ 26.0.0
+
+
+
+
\ No newline at end of file
diff --git a/auth-gateway/keycloak-setup/spi/src/main/java/co/nilin/opex/keycloak/spi/ForcedRoleProtocolMapper.java b/auth-gateway/keycloak-setup/spi/src/main/java/co/nilin/opex/keycloak/spi/ForcedRoleProtocolMapper.java
new file mode 100644
index 000000000..f858bd900
--- /dev/null
+++ b/auth-gateway/keycloak-setup/spi/src/main/java/co/nilin/opex/keycloak/spi/ForcedRoleProtocolMapper.java
@@ -0,0 +1,73 @@
+package co.nilin.opex.keycloak.spi;
+
+import org.keycloak.models.*;
+import org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper;
+import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;
+import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.representations.AccessToken;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+public class ForcedRoleProtocolMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper {
+
+ private static final String PROVIDER_ID = "forced-role-mapper";
+ private static final String ROLE_ATTRIBUTES_CLAIM = "forced_role";
+
+ public static final String KEY_NAME = "key.name";
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public String getDisplayCategory() {
+ return "Forced Role";
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Forced Role Mapper";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Forces the addition of roles to the token.";
+ }
+
+ @Override
+ public List getConfigProperties() {
+ List configProperties = new ArrayList<>();
+ OIDCAttributeMapperHelper.addTokenClaimNameConfig(configProperties);
+ OIDCAttributeMapperHelper.addJsonTypeConfig(configProperties);
+
+ ProviderConfigProperty attributeName = new ProviderConfigProperty();
+ attributeName.setName(KEY_NAME);
+ attributeName.setLabel("Key Name");
+ attributeName.setType(ProviderConfigProperty.STRING_TYPE);
+ attributeName.setHelpText("The name of the role to include in the token.");
+ configProperties.add(attributeName);
+ return configProperties;
+ }
+
+ @Override
+ public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel user, ClientSessionContext clientSessionCtx) {
+ String claimName = mappingModel.getConfig().get(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME);
+ var finalList = new HashSet<>();
+
+ user.getUser().getRealmRoleMappingsStream().forEach(role -> {
+ finalList.add(role.getName());
+ role.getCompositesStream().forEach(r -> finalList.add(r.getName()));
+ });
+
+ if (!finalList.isEmpty()) {
+ token.getOtherClaims().put(claimName != null && !claimName.isEmpty() ? claimName : ROLE_ATTRIBUTES_CLAIM, finalList);
+ }
+
+ return token;
+ }
+
+}
diff --git a/auth-gateway/keycloak-setup/spi/src/main/java/co/nilin/opex/keycloak/spi/RoleAttributesProtocolMapper.java b/auth-gateway/keycloak-setup/spi/src/main/java/co/nilin/opex/keycloak/spi/RoleAttributesProtocolMapper.java
new file mode 100644
index 000000000..392f8df98
--- /dev/null
+++ b/auth-gateway/keycloak-setup/spi/src/main/java/co/nilin/opex/keycloak/spi/RoleAttributesProtocolMapper.java
@@ -0,0 +1,91 @@
+package co.nilin.opex.keycloak.spi;
+
+import org.keycloak.models.*;
+import org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper;
+import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;
+import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.representations.AccessToken;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.stream.Collectors;
+
+public class RoleAttributesProtocolMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper {
+
+ private static final String PROVIDER_ID = "role-attributes-mapper";
+ private static final String ROLE_ATTRIBUTES_CLAIM = "role_attributes";
+
+ public static final String ATTRIBUTE_NAME = "attribute.name";
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public String getDisplayCategory() {
+ return "Role Attributes";
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Role Attributes Mapper";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Adds attributes of user's roles to the token.";
+ }
+
+ @Override
+ public List getConfigProperties() {
+ List configProperties = new ArrayList<>();
+ OIDCAttributeMapperHelper.addTokenClaimNameConfig(configProperties);
+ OIDCAttributeMapperHelper.addJsonTypeConfig(configProperties);
+
+ ProviderConfigProperty attributeName = new ProviderConfigProperty();
+ attributeName.setName(ATTRIBUTE_NAME);
+ attributeName.setLabel("Attribute Name");
+ attributeName.setType(ProviderConfigProperty.STRING_TYPE);
+ attributeName.setHelpText("The name of the role attribute to include in the token.");
+ configProperties.add(attributeName);
+ return configProperties;
+ }
+
+ @Override
+ public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel user, ClientSessionContext clientSessionCtx) {
+ String attributeNameToInclude = mappingModel.getConfig().get(ATTRIBUTE_NAME);
+ String claimName = mappingModel.getConfig().get(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME);
+ var finalList = new ArrayList<>();
+
+ List userRoles = user.getUser().getRealmRoleMappingsStream().collect(Collectors.toList());
+ for (RoleModel role : userRoles) {
+ extract(role, attributeNameToInclude, finalList);
+ var compositeRoles = role.getCompositesStream().collect(Collectors.toList());
+ compositeRoles.forEach(r -> extract(r, attributeNameToInclude, finalList));
+ }
+
+ if (!finalList.isEmpty()) {
+ token.getOtherClaims().put(claimName != null && !claimName.isEmpty() ? claimName : ROLE_ATTRIBUTES_CLAIM, finalList);
+ }
+ return token;
+ }
+
+ private void extract(RoleModel role, String attributeNameToInclude, ArrayList
+
+ co.nilin.opex.bcgateway.ports.omniwallet
+ bc-gateway-omniwallet-proxy
+
co.nilin.opex.bcgateway.ports.authproxy
bc-gateway-auth-proxy
@@ -120,10 +124,6 @@
co.nilin.opex.bcgateway.ports.kafka.listener
bc-gateway-eventlistener-kafka
-
- co.nilin.opex.utility
- preferences
-
io.springfox
springfox-boot-starter
diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/AppConfig.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/AppConfig.kt
index 67e51c50a..7c2b0e6fb 100644
--- a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/AppConfig.kt
+++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/AppConfig.kt
@@ -6,24 +6,33 @@ import co.nilin.opex.bcgateway.core.api.InfoService
import co.nilin.opex.bcgateway.core.service.AssignAddressServiceImpl
import co.nilin.opex.bcgateway.core.service.InfoServiceImpl
import co.nilin.opex.bcgateway.core.spi.AssignedAddressHandler
-import co.nilin.opex.bcgateway.core.spi.CurrencyHandler
+import co.nilin.opex.bcgateway.core.spi.ChainLoader
import co.nilin.opex.bcgateway.core.spi.ReservedAddressHandler
import co.nilin.opex.bcgateway.ports.kafka.listener.consumer.AdminEventKafkaListener
import co.nilin.opex.bcgateway.ports.kafka.listener.spi.AdminEventListener
+import co.nilin.opex.bcgateway.ports.postgres.impl.CurrencyHandlerImplV2
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
+import org.springframework.core.io.ResourceLoader
+import java.security.KeyFactory
+import java.security.PublicKey
+import java.security.spec.PKCS8EncodedKeySpec
+import java.security.spec.X509EncodedKeySpec
+import java.util.*
+
@Configuration
-class AppConfig {
+class AppConfig(private val resourceLoader: ResourceLoader) {
@Bean
fun assignAddressService(
- currencyHandler: CurrencyHandler,
+ currencyHandler: CurrencyHandlerImplV2,
assignedAddressHandler: AssignedAddressHandler,
- reservedAddressHandler: ReservedAddressHandler
+ reservedAddressHandler: ReservedAddressHandler,
+ chainLoader: ChainLoader
): AssignAddressService {
- return AssignAddressServiceImpl(currencyHandler, assignedAddressHandler, reservedAddressHandler)
+ return AssignAddressServiceImpl(currencyHandler, assignedAddressHandler, reservedAddressHandler, chainLoader)
}
@Bean
@@ -38,4 +47,21 @@ class AppConfig {
) {
adminKafkaEventListener.addEventListener(adminEventListener)
}
+
+ @Bean("webhookPublicKey")
+ fun webhookPublicKey(): PublicKey {
+ val publicKeyString = resourceLoader.getResource("classpath:scanner-public.pem").inputStream
+ .readAllBytes()
+ .toString(Charsets.UTF_8)
+ .replace("-----BEGIN PUBLIC KEY-----", "")
+ .replace("-----END PUBLIC KEY-----", "")
+ .replace("\n", "")
+ .replace("\r", "")
+
+ val keyBytes = Base64.getDecoder().decode(publicKeyString)
+ val keySpec = X509EncodedKeySpec(keyBytes)
+ val keyFactory = KeyFactory.getInstance("RSA")
+ return keyFactory.generatePublic(keySpec)
+ }
+
}
diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/ErrorConfig.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/ErrorConfig.kt
new file mode 100644
index 000000000..b05337f4a
--- /dev/null
+++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/ErrorConfig.kt
@@ -0,0 +1,30 @@
+package co.nilin.opex.bcgateway.app.config
+
+import co.nilin.opex.common.utils.CustomErrorTranslator
+import co.nilin.opex.utility.error.spi.ErrorTranslator
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
+import org.springframework.context.MessageSource
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Profile
+import org.springframework.context.support.ReloadableResourceBundleMessageSource
+
+@Configuration
+@Profile("otc")
+class ErrorConfig {
+
+ @Bean
+ fun messageSource(): MessageSource {
+ val messageSource = ReloadableResourceBundleMessageSource()
+ messageSource.setBasename("classpath:messages")
+ messageSource.setCacheSeconds(10) //reload messages every 10 seconds
+ messageSource.setDefaultEncoding("UTF-8")
+ return messageSource
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ fun translator(): ErrorTranslator {
+ return CustomErrorTranslator(messageSource = messageSource())
+ }
+}
\ No newline at end of file
diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/InitializeService.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/InitializeService.kt
index 58879f017..acb9d51dd 100644
--- a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/InitializeService.kt
+++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/InitializeService.kt
@@ -1,79 +1,20 @@
package co.nilin.opex.bcgateway.app.config
-import co.nilin.opex.bcgateway.ports.postgres.dao.*
-import co.nilin.opex.bcgateway.ports.postgres.model.AddressTypeModel
-import co.nilin.opex.bcgateway.ports.postgres.model.ChainAddressTypeModel
-import co.nilin.opex.bcgateway.ports.postgres.model.CurrencyImplementationModel
-import co.nilin.opex.utility.preferences.AddressType
-import co.nilin.opex.utility.preferences.Chain
-import co.nilin.opex.utility.preferences.Currency
-import co.nilin.opex.utility.preferences.Preferences
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.reactor.awaitSingle
-import kotlinx.coroutines.reactor.awaitSingleOrNull
import kotlinx.coroutines.runBlocking
-import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.DependsOn
+import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Component
import javax.annotation.PostConstruct
@Component
@DependsOn("postgresConfig")
-class InitializeService(
- private val addressTypeRepository: AddressTypeRepository,
- private val chainRepository: ChainRepository,
- private val chainAddressTypeRepository: ChainAddressTypeRepository,
- private val currencyRepository: CurrencyRepository,
- private val currencyImplementationRepository: CurrencyImplementationRepository,
-) {
- @Autowired
- private lateinit var preferences: Preferences
+@Profile("!otc")
+class InitializeService {
@PostConstruct
fun init() = runBlocking {
- addAddressTypes(preferences.addressTypes)
- addChains(preferences.chains)
- addCurrencies(preferences.currencies)
- }
-
- private suspend fun addAddressTypes(data: List) = coroutineScope {
- val items = data.mapIndexed { i, it ->
- if (addressTypeRepository.existsById(i + 1L).awaitSingle()) null
- else AddressTypeModel(null, it.addressType, it.addressRegex, null)
- }.filterNotNull()
- runCatching { addressTypeRepository.saveAll(items).collectList().awaitSingleOrNull() }
- }
-
- private suspend fun addChains(data: List) = coroutineScope {
- data.map { chainRepository.insert(it.name).awaitSingleOrNull() }
- val items1 = data.map {
- val addressTypeId = addressTypeRepository.findByType(it.addressType).awaitSingle().id!!
- ChainAddressTypeModel(null, it.name, addressTypeId)
- }
- runCatching { chainAddressTypeRepository.saveAll(items1).collectList().awaitSingleOrNull() }
- }
-
- private suspend fun addCurrencies(data: List) = coroutineScope {
- coroutineScope {
- data.forEach {
- currencyRepository.insert(it.name, it.symbol).awaitSingleOrNull()
- }
- }
- val items = data.flatMap { it.implementations.map { impl -> it to impl } }.map { (currency, impl) ->
- CurrencyImplementationModel(
- null,
- currency.symbol,
- impl.symbol.takeUnless { it.isEmpty() } ?: currency.symbol,
- impl.chain,
- impl.token,
- impl.tokenAddress,
- impl.tokenName,
- impl.withdrawEnabled,
- impl.withdrawFee,
- impl.withdrawMin,
- impl.decimal
- )
- }
- runCatching { currencyImplementationRepository.saveAll(items).collectList().awaitSingleOrNull() }
+ // addAddressTypes()
+ // addChains()
+ // addCurrencies()
}
}
diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/SecurityConfig.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/SecurityConfig.kt
index ca05ba715..78e17725a 100644
--- a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/SecurityConfig.kt
+++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/SecurityConfig.kt
@@ -1,9 +1,7 @@
package co.nilin.opex.bcgateway.app.config
-import co.nilin.opex.bcgateway.app.utils.hasRole
-import co.nilin.opex.bcgateway.app.utils.hasRole
import co.nilin.opex.bcgateway.app.utils.hasRoleAndLevel
-import org.springframework.beans.factory.annotation.Qualifier
+import co.nilin.opex.common.security.ReactiveCustomJwtConverter
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Profile
@@ -25,21 +23,34 @@ class SecurityConfig(private val webClient: WebClient) {
@Profile("!otc")
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain? {
http.csrf().disable()
- .authorizeExchange()
- .pathMatchers("/actuator/**").permitAll()
- .pathMatchers("/swagger-ui/**").permitAll()
- .pathMatchers("/swagger-resources/**").permitAll()
- .pathMatchers("/wallet-sync/**").permitAll()
- .pathMatchers("/currency/**").permitAll()
- .pathMatchers("/filter/**").hasAuthority("SCOPE_trust")
- .pathMatchers("/admin/**").hasRole("SCOPE_trust", "admin_system")
- .pathMatchers("/v1/address/**").permitAll()
- .pathMatchers("/deposit/**").permitAll()
- .pathMatchers("/addresses/**").hasRole("SCOPE_trust", "admin_system")
- .anyExchange().authenticated()
- .and()
- .oauth2ResourceServer()
- .jwt()
+ .authorizeExchange()
+ .pathMatchers("/actuator/**").permitAll()
+ .pathMatchers("/swagger-ui/**").permitAll()
+ .pathMatchers("/swagger-resources/**").permitAll()
+ .pathMatchers("/wallet-sync/**").permitAll()
+ .pathMatchers("/currency/**").permitAll()
+ .pathMatchers("/filter/**").authenticated()
+ .pathMatchers("/admin/**").hasAuthority("ROLE_admin")
+ .pathMatchers("/v1/address/assign").hasAuthority("PREM_address:assign")
+ .pathMatchers(HttpMethod.PUT, "/v1/address").hasAuthority("ROLE_admin")
+ .pathMatchers("/deposit/**").permitAll()
+ .pathMatchers("/addresses/**").hasAuthority("ROLE_admin")
+ .pathMatchers("/scanner/**").permitAll()
+ .pathMatchers("/crypto-currency/**").permitAll()
+ .pathMatchers("/currency/**").permitAll()
+ //otc
+ .pathMatchers(HttpMethod.PUT, "/v1/address").hasAuthority("ROLE_admin")
+ .pathMatchers("/admin/**").hasAuthority("ROLE_admin")
+ .pathMatchers("/wallet-sync/**").hasAuthority("ROLE_system")
+ .pathMatchers("/crypto-currency/chain").hasAuthority("ROLE_admin")
+ .pathMatchers("/crypto-currency/**").hasAuthority("ROLE_system")
+ .pathMatchers("/omni-balance/bc/**").hasAuthority("ROLE_admin")
+ .pathMatchers("/actuator/**").permitAll()
+ .pathMatchers("/scanner/**").permitAll()
+ .anyExchange().authenticated()
+ .and()
+ .oauth2ResourceServer()
+ .jwt { it.jwtAuthenticationConverter(ReactiveCustomJwtConverter()) }
return http.build()
}
@@ -48,19 +59,22 @@ class SecurityConfig(private val webClient: WebClient) {
@Profile("otc")
fun otcSpringSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain? {
http.csrf().disable()
- .authorizeExchange()
- .pathMatchers("/actuator/**").permitAll()
- .pathMatchers("/swagger-ui/**").permitAll()
- .pathMatchers(HttpMethod.PUT,"/v1/address").hasRoleAndLevel("Admin")
- .pathMatchers("/swagger-resources/**").permitAll()
- .pathMatchers("/admin/**").hasRoleAndLevel("Admin")
- .pathMatchers("/wallet-sync/**").hasRoleAndLevel("System")
- .pathMatchers("/currency/chains").hasRoleAndLevel("user")
- .pathMatchers("/currency/**").hasRoleAndLevel("System")
- .anyExchange().authenticated()
- .and()
- .oauth2ResourceServer()
- .jwt()
+ .authorizeExchange()
+ .pathMatchers("/actuator/**").permitAll()
+ .pathMatchers("/swagger-ui/**").permitAll()
+ .pathMatchers(HttpMethod.PUT, "/v1/address").hasRoleAndLevel("Admin")
+ .pathMatchers("/swagger-resources/**").permitAll()
+ .pathMatchers("/admin/**").hasRoleAndLevel("Admin")
+ .pathMatchers("/wallet-sync/**").hasRoleAndLevel("System")
+ .pathMatchers("/crypto-currency/chain").hasRoleAndLevel("user")
+ .pathMatchers("/crypto-currency/**").hasRoleAndLevel("System")
+ .pathMatchers("/omni-balance/bc/**").hasRoleAndLevel("Admin")
+ .pathMatchers("/actuator/**").permitAll()
+ .pathMatchers("/scanner/**").permitAll()
+ .anyExchange().authenticated()
+ .and()
+ .oauth2ResourceServer()
+ .jwt()
return http.build()
}
@@ -68,8 +82,8 @@ class SecurityConfig(private val webClient: WebClient) {
@Throws(Exception::class)
fun reactiveJwtDecoder(): ReactiveJwtDecoder? {
return NimbusReactiveJwtDecoder.withJwkSetUri(jwkUrl)
- .webClient(webClient)
- .build()
+ .webClient(WebClient.create())
+ .build()
}
diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/WebClientConfig.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/WebClientConfig.kt
index 8e4775d8b..4330d39fa 100644
--- a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/WebClientConfig.kt
+++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/WebClientConfig.kt
@@ -6,7 +6,6 @@ import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalance
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
-import org.springframework.http.client.reactive.ReactorClientHttpConnector
import org.springframework.web.reactive.function.client.ExchangeStrategies
import org.springframework.web.reactive.function.client.WebClient
import org.zalando.logbook.Logbook
@@ -18,7 +17,10 @@ class WebClientConfig {
@Bean
@Profile("!otc")
- fun loadBalancedWebClient(loadBalancerFactory: ReactiveLoadBalancer.Factory, logbook: Logbook): WebClient {
+ fun loadBalancedWebClient(
+ loadBalancerFactory: ReactiveLoadBalancer.Factory,
+ logbook: Logbook
+ ): WebClient {
val client = HttpClient.create().doOnConnected { it.addHandlerLast(LogbookClientHandler(logbook)) }
return WebClient.builder()
//.clientConnector(ReactorClientHttpConnector(client))
diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/AddressController.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/AddressController.kt
index e5510a1dc..4757b2737 100644
--- a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/AddressController.kt
+++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/AddressController.kt
@@ -2,12 +2,10 @@ package co.nilin.opex.bcgateway.app.controller
import co.nilin.opex.bcgateway.core.api.AssignAddressService
import co.nilin.opex.bcgateway.core.model.AssignedAddress
-import co.nilin.opex.bcgateway.core.model.Currency
import co.nilin.opex.bcgateway.core.model.ReservedAddress
import co.nilin.opex.bcgateway.core.spi.AddressTypeHandler
import co.nilin.opex.bcgateway.core.spi.ReservedAddressHandler
import co.nilin.opex.common.OpexError
-import co.nilin.opex.utility.error.data.OpexException
import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.springframework.http.codec.multipart.FilePart
@@ -17,10 +15,6 @@ import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Mono
import java.io.File
import java.nio.charset.StandardCharsets
-import java.time.ZoneId
-import java.util.Collections
-import java.util.stream.Collector
-import java.util.stream.Collectors
@RestController
@RequestMapping("/v1/address")
@@ -41,7 +35,7 @@ class AddressController(
throw OpexError.Forbidden.exception()
val assignedAddress = assignAddressService.assignAddress(
assignAddressRequest.uuid,
- Currency(assignAddressRequest.currency, assignAddressRequest.currency),
+ assignAddressRequest.currency,
assignAddressRequest.chain
)
diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/AdminController.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/AdminController.kt
index cf4b720d0..19491fb7d 100644
--- a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/AdminController.kt
+++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/AdminController.kt
@@ -5,18 +5,17 @@ import co.nilin.opex.bcgateway.app.service.AdminService
import co.nilin.opex.bcgateway.core.model.AddressType
import co.nilin.opex.bcgateway.core.spi.AddressTypeHandler
import co.nilin.opex.bcgateway.core.spi.ChainLoader
-import co.nilin.opex.bcgateway.core.spi.CurrencyHandler
+import co.nilin.opex.bcgateway.ports.postgres.impl.CurrencyHandlerImplV2
import co.nilin.opex.common.OpexError
import org.springframework.web.bind.annotation.*
-import java.math.BigDecimal
@RestController
@RequestMapping("/admin")
class AdminController(
private val service: AdminService,
private val chainLoader: ChainLoader,
- private val currencyHandler: CurrencyHandler,
- private val addressTypeHandler: AddressTypeHandler
+ private val currencyHandler: CurrencyHandlerImplV2,
+ private val addressTypeHandler: AddressTypeHandler,
) {
@GetMapping("/chain")
@@ -43,55 +42,65 @@ class AdminController(
service.addAddressType(body.name, body.addressRegex, body.memoRegex)
}
- @GetMapping("/token")
- suspend fun getCurrencyImplementation(): List {
- return currencyHandler.fetchAllImplementations()
- .map {
- TokenResponse(
- it.currency.symbol,
- it.chain.name,
- it.token,
- it.tokenAddress,
- it.tokenName,
- it.withdrawEnabled,
- it.withdrawFee,
- it.withdrawMin,
- it.decimal
- )
- }
+ @PostMapping("/addresses")
+ suspend fun addAddresses(@RequestBody body: AddAddressesRequest) {
+ service.addAddresses(body.addresses, body.memos, body.addressType)
}
- @PostMapping("/token")
- suspend fun addCurrencyImplementation(@RequestBody body: TokenRequest): TokenResponse {
- val ex = OpexError.InvalidRequestBody.exception()
- with(body) {
- if (currencySymbol.isNullOrEmpty() || chain.isNullOrEmpty()) throw ex
- if (isToken && (tokenName.isNullOrEmpty() || tokenAddress.isNullOrEmpty())) throw ex
- if (withdrawFee < BigDecimal.ZERO || minimumWithdraw < BigDecimal.ZERO || decimal < 0) throw ex
- }
+ // shifted to crypto currency class!
- return with(service.addToken(body)) {
- TokenResponse(
- currency.symbol,
- chain.name,
- token,
- tokenAddress,
- tokenName,
- withdrawEnabled,
- withdrawFee,
- withdrawMin,
- decimal
- )
- }
- }
+// //todo filter tokens?????
+// @GetMapping("/token")
+// suspend fun getCurrencyImplementation(): List? {
+// return currencyHandler.fetchCurrencyImpls()?.imps
+// ?.map {
+// TokenResponse(
+// it.currencySymbol,
+// it.chain,
+// it.isToken!!,
+// it.tokenAddress,
+// it.tokenName,
+// it.withdrawAllowed!!,
+// it.withdrawFee!!,
+// it.withdrawMin!!,
+// it.decimal,
+// it.isActive!!
+// )
+// }
+// }
- @PutMapping("/token/{symbol}_{chain}/withdraw")
- suspend fun changeWithdrawStatus(
- @PathVariable symbol: String,
- @PathVariable chain: String,
- @RequestParam("enabled") status: Boolean
- ) {
- service.changeTokenWithdrawStatus(symbol, chain, status)
- }
+// @PostMapping("/token")
+// suspend fun addCurrencyImplementation(@RequestBody body: TokenRequest): TokenResponse {
+// val ex = OpexError.InvalidRequestBody.exception()
+// with(body) {
+// if (currencySymbol.isNullOrEmpty() || chain.isNullOrEmpty()) throw ex
+// if (isToken && (tokenName.isNullOrEmpty() || tokenAddress.isNullOrEmpty())) throw ex
+// if (withdrawFee < BigDecimal.ZERO || minimumWithdraw < BigDecimal.ZERO || decimal < 0) throw ex
+// }
+//
+// return with(service.addToken(body)) {
+// TokenResponse(
+// currency,
+// chain,
+// token,
+// tokenAddress,
+// tokenName,
+// withdrawEnabled,
+// withdrawFee,
+// withdrawMin,
+// decimal,
+// isActive
+// )
+// }
+// }
+
+// @PutMapping("/token/{symbol}_{chain}/withdraw")
+// suspend fun changeWithdrawStatus(
+// @PathVariable symbol: String,
+// @PathVariable chain: String,
+// @RequestParam("enabled") status: Boolean
+// ) {
+// service.changeTokenWithdrawStatus(symbol, chain, status)
+// }
}
diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/CryptoCurrencyController.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/CryptoCurrencyController.kt
new file mode 100644
index 000000000..de882f7a3
--- /dev/null
+++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/CryptoCurrencyController.kt
@@ -0,0 +1,79 @@
+package co.nilin.opex.bcgateway.app.controller
+
+import co.nilin.opex.bcgateway.app.dto.ChainResponse
+import co.nilin.opex.bcgateway.core.model.*
+import co.nilin.opex.bcgateway.core.spi.ChainLoader
+import co.nilin.opex.bcgateway.core.spi.CryptoCurrencyHandlerV2
+import org.springframework.web.bind.annotation.*
+
+@RestController
+@RequestMapping("/crypto-currency")
+class CryptoCurrencyController(
+ val cryptoCurrencyHandler: CryptoCurrencyHandlerV2,
+ private val chainLoader: ChainLoader
+) {
+
+ @PostMapping("/{currencySymbol}/gateway")
+ suspend fun addNewCurrencyGateway(
+ @PathVariable("currencySymbol") currencySymbol: String,
+ @RequestBody request: CryptoCurrencyCommand
+ ): CryptoCurrencyCommand? {
+ return cryptoCurrencyHandler.createOnChainGateway(request.apply {
+ this.currencySymbol = currencySymbol
+ })
+ }
+
+
+ @PutMapping("/{currency}/gateway/{gatewayUuid}")
+ suspend fun updateCurrencyGateway(
+ @PathVariable("currency") currencySymbol: String,
+ @PathVariable("gatewayUuid") gatewayUuid: String,
+ @RequestBody request: CryptoCurrencyCommand
+ ): CryptoCurrencyCommand? {
+ return cryptoCurrencyHandler.updateOnChainGateway(request.apply {
+ this.currencySymbol = currencySymbol
+ this.gatewayUuid = gatewayUuid
+ })
+ }
+
+
+ @DeleteMapping("/{currency}/gateway/{gatewayUuid}")
+ suspend fun deleteCurrencyGateway(
+ @PathVariable("currency") currencySymbol: String,
+ @PathVariable("gatewayUuid") gatewayUuid: String,
+ ): Void? {
+ return cryptoCurrencyHandler.deleteOnChainGateway(
+ gatewayUuid, currencySymbol
+ )
+ }
+
+ @GetMapping("/gateways")
+ suspend fun fetchGateways(@RequestParam("currency") currencySymbol: String? = null): List? {
+ return cryptoCurrencyHandler.fetchCurrencyOnChainGateways(FetchGateways(currencySymbol = currencySymbol))
+ }
+
+ @GetMapping("/{currency}/gateways")
+ suspend fun fetchCurrencyGateways(@PathVariable("currency") currencySymbol: String): List?? {
+ return cryptoCurrencyHandler.fetchCurrencyOnChainGateways(FetchGateways(currencySymbol = currencySymbol))
+ }
+
+ @GetMapping("/{currency}/gateway/{gatewayUuid}")
+ suspend fun fetchSpecificGateway(
+ @PathVariable("gatewayUuid") gatewayUuid: String,
+ @PathVariable("currency") currencySymbol: String
+ ): CryptoCurrencyCommand? {
+ return cryptoCurrencyHandler.fetchOnChainGateway(gatewayUuid, currencySymbol)
+ }
+
+ @GetMapping("/chain")
+ suspend fun getChains(): List {
+ return chainLoader.fetchAllChains().map { c -> ChainResponse(c.name, c.addressTypes.map { it.type }) }
+ }
+
+
+ @GetMapping("/{currency}/network/{network}/withdrawData")
+ suspend fun getFeeForCurrency(@PathVariable currency: String, @PathVariable network: String): WithdrawData {
+ return cryptoCurrencyHandler.getWithdrawData(currency, network)
+ }
+
+}
diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/CurrencyController.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/CurrencyController.kt
deleted file mode 100644
index 213f273a4..000000000
--- a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/CurrencyController.kt
+++ /dev/null
@@ -1,86 +0,0 @@
-package co.nilin.opex.bcgateway.app.controller
-
-import co.nilin.opex.bcgateway.app.dto.AddCurrencyRequest
-import co.nilin.opex.bcgateway.core.model.CurrencyImplementation
-import co.nilin.opex.bcgateway.core.model.CurrencyInfo
-import co.nilin.opex.bcgateway.core.model.WithdrawData
-import co.nilin.opex.bcgateway.core.spi.CurrencyHandler
-import org.springframework.web.bind.annotation.GetMapping
-import org.springframework.web.bind.annotation.PathVariable
-import org.springframework.web.bind.annotation.PostMapping
-import org.springframework.web.bind.annotation.PutMapping
-import org.springframework.web.bind.annotation.RequestBody
-import org.springframework.web.bind.annotation.RequestMapping
-import org.springframework.web.bind.annotation.RequestParam
-import org.springframework.web.bind.annotation.RestController
-import java.math.BigDecimal
-
-@RestController
-@RequestMapping("/currency")
-class CurrencyController(val currencyHandler: CurrencyHandler) {
-
- @GetMapping("/{currency}")
- suspend fun fetchCurrencyInfo(@PathVariable("currency") currency: String): CurrencyInfo {
- return currencyHandler.fetchCurrencyInfo(currency)
- }
-
- @PostMapping("/{currency}")
- suspend fun addCurrencyInfo(
- @PathVariable("currency") currencySymbol: String,
- @RequestBody addCurrencyRequest: AddCurrencyRequest
- ): CurrencyImplementation? {
- addCurrencyRequest.currencySymbol = currencySymbol
- with(addCurrencyRequest) {
- return currencyHandler.addCurrencyImplementationV2(
- this.currencySymbol,
- implementationSymbol,
- currencyName,
- chain,
- tokenName,
- tokenAddress,
- isToken!!,
- withdrawFee,
- minimumWithdraw,
- isWithdrawEnabled!!,
- decimal
- )
- }
- }
-
- @PutMapping("/{currency}")
- suspend fun updateCurrencyInfo(
- @PathVariable("currency") currencySymbol: String,
- @RequestBody addCurrencyRequest: AddCurrencyRequest
- ): CurrencyImplementation? {
- addCurrencyRequest.currencySymbol = currencySymbol
- with(addCurrencyRequest) {
- return currencyHandler.updateCurrencyImplementation(
- this.currencySymbol,
- implementationSymbol,
- currencyName,
- newChain,
- tokenName,
- tokenAddress,
- isToken!!,
- withdrawFee,
- minimumWithdraw,
- isWithdrawEnabled!!,
- decimal,
- chain
- )
- }
- }
-
- @GetMapping("/chains")
- suspend fun getNetworks(@RequestParam(required = false) currency: String?): List {
- return if (currency != null)
- currencyHandler.findImplementationsByCurrency(currency)
- else
- currencyHandler.fetchAllImplementations()
- }
-
- @GetMapping("/{currency}/network/{network}/withdrawData")
- suspend fun getFeeForCurrency(@PathVariable currency: String, @PathVariable network: String): WithdrawData {
- return currencyHandler.getWithdrawData(currency, network)
- }
-}
diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/OmniBCWalletController.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/OmniBCWalletController.kt
new file mode 100644
index 000000000..17bba3a55
--- /dev/null
+++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/OmniBCWalletController.kt
@@ -0,0 +1,23 @@
+package co.nilin.opex.bcgateway.app.controller
+
+import co.nilin.opex.bcgateway.app.service.OmniBalanceService
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.PathVariable
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+@RequestMapping("/omni-balance/bc")
+class OmniBCWalletController(private val omniBalanceService: OmniBalanceService) {
+
+ @GetMapping("")
+ suspend fun getOmniBalance(): List? {
+ return omniBalanceService.fetchSystemBalance()
+ }
+
+ @GetMapping("/{currency}")
+ suspend fun getOmniBalanceOfCurrency(@PathVariable currency: String): OmniBalanceService.OmniBalanceForCurrency {
+ return omniBalanceService.fetchSystemBalance(currency)
+ }
+
+}
\ No newline at end of file
diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/ScannerController.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/ScannerController.kt
new file mode 100644
index 000000000..fd89f0b83
--- /dev/null
+++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/ScannerController.kt
@@ -0,0 +1,71 @@
+package co.nilin.opex.bcgateway.app.controller
+
+import co.nilin.opex.bcgateway.core.api.WalletSyncService
+import co.nilin.opex.bcgateway.core.model.Transfer
+import co.nilin.opex.bcgateway.core.model.Wallet
+import co.nilin.opex.common.OpexError
+import co.nilin.opex.utility.error.data.OpexException
+import com.fasterxml.jackson.databind.ObjectMapper
+import org.slf4j.LoggerFactory
+import org.springframework.core.io.ResourceLoader
+import org.springframework.web.bind.annotation.PostMapping
+import org.springframework.web.bind.annotation.RequestBody
+import org.springframework.web.bind.annotation.RequestHeader
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+import java.math.BigDecimal
+import java.security.PublicKey
+import java.security.Signature
+import java.util.*
+
+data class WebhookBody(
+ val txId: String,
+ val address: String,
+ val chain: String,
+ val amount: BigDecimal,
+ val memo: String?,
+ val isToken: Boolean,
+ val tokenAddress: String?,
+ val id: String?,
+ val date: Long
+)
+
+@RestController
+@RequestMapping("/scanner")
+class ScannerController(
+ private val publicKey: PublicKey,
+ private val mapper: ObjectMapper,
+ private val service: WalletSyncService
+) {
+
+ private val logger = LoggerFactory.getLogger(ScannerController::class.java)
+
+ @PostMapping("/webhook")
+ suspend fun webhook(@RequestHeader("X-Signature") sign: String, @RequestBody body: WebhookBody) {
+ verifySignature(sign, body)
+ logger.info("Webhook received for address ${body.address}, amount ${body.amount}")
+ service.sendTransfer(with(body) { Transfer(txId, Wallet(address, memo), isToken, amount, chain, tokenAddress) })
+ }
+
+ private fun verifySignature(sign: String, request: WebhookBody) {
+ try {
+ logger.info("Verifying signature for address ${request.address}")
+ val reqStr = mapper.writeValueAsString(request)
+ val decodedSign = Base64.getDecoder().decode(sign)
+ val verifier = Signature.getInstance("SHA256withRSA").apply {
+ initVerify(publicKey)
+ update(reqStr.toByteArray())
+ }
+
+ if (!verifier.verify(decodedSign)) {
+ logger.warn("Signature is not valid!")
+ throw OpexError.Forbidden.exception()
+ }
+ } catch (e: OpexException) {
+ throw e
+ } catch (e: Exception) {
+ logger.error("Unable to verify signature", e)
+ throw OpexError.InternalServerError.exception()
+ }
+ }
+}
\ No newline at end of file
diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/WalletSyncController.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/WalletSyncController.kt
index 56904c44c..2c7d235ec 100644
--- a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/WalletSyncController.kt
+++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/WalletSyncController.kt
@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
+@Deprecated("ScannerController will be used")
@RestController
class WalletSyncController(private val chainHandler: ChainHandler, private val walletSyncService: WalletSyncService) {
diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/dto/AddAddressesRequest.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/dto/AddAddressesRequest.kt
new file mode 100644
index 000000000..90441cad8
--- /dev/null
+++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/dto/AddAddressesRequest.kt
@@ -0,0 +1,7 @@
+package co.nilin.opex.bcgateway.app.dto
+
+data class AddAddressesRequest(
+ val addresses: List,
+ val memos: List?,
+ val addressType: String,
+)
\ No newline at end of file
diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/dto/AddCurrencyRequest.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/dto/AddCurrencyRequest.kt
index 26682e900..23293ef52 100644
--- a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/dto/AddCurrencyRequest.kt
+++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/dto/AddCurrencyRequest.kt
@@ -3,16 +3,16 @@ package co.nilin.opex.bcgateway.app.dto
import java.math.BigDecimal
data class AddCurrencyRequest(
- var currencySymbol: String,
- var implementationSymbol: String,
- var currencyName:String,
- var newChain: String?=null,
- var tokenName: String?,
- var tokenAddress: String?,
- var isToken: Boolean? = false,
- var withdrawFee: BigDecimal,
- var minimumWithdraw: BigDecimal,
- var isWithdrawEnabled: Boolean? = true,
- var decimal: Int,
- var chain: String
+ var currencySymbol: String,
+ var implementationSymbol: String,
+ var currencyName: String,
+ var newChain: String? = null,
+ var tokenName: String?,
+ var tokenAddress: String?,
+ var isToken: Boolean? = false,
+ var withdrawFee: BigDecimal,
+ var minimumWithdraw: BigDecimal,
+ var isWithdrawEnabled: Boolean? = true,
+ var decimal: Int,
+ var chain: String
)
\ No newline at end of file
diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/dto/TokenResponse.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/dto/TokenResponse.kt
index d6cbbc9f5..d5bee76b6 100644
--- a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/dto/TokenResponse.kt
+++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/dto/TokenResponse.kt
@@ -11,5 +11,6 @@ data class TokenResponse(
val isWithdrawEnabled: Boolean,
val withdrawFee: BigDecimal,
val withdrawMin: BigDecimal,
- val decimal: Int
+ val decimal: Int,
+ val isActive: Boolean
)
\ No newline at end of file
diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/listener/AdminEventListenerImpl.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/listener/AdminEventListenerImpl.kt
index 1bcf766c7..7787bbe31 100644
--- a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/listener/AdminEventListenerImpl.kt
+++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/listener/AdminEventListenerImpl.kt
@@ -1,10 +1,7 @@
package co.nilin.opex.bcgateway.app.listener
import co.nilin.opex.bcgateway.app.service.AdminService
-import co.nilin.opex.bcgateway.ports.kafka.listener.model.AddCurrencyEvent
import co.nilin.opex.bcgateway.ports.kafka.listener.model.AdminEvent
-import co.nilin.opex.bcgateway.ports.kafka.listener.model.DeleteCurrencyEvent
-import co.nilin.opex.bcgateway.ports.kafka.listener.model.EditCurrencyEvent
import co.nilin.opex.bcgateway.ports.kafka.listener.spi.AdminEventListener
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
@@ -18,12 +15,14 @@ class AdminEventListenerImpl(private val adminService: AdminService) : AdminEven
override fun id() = "AdminEventListener"
override fun onEvent(event: AdminEvent, partition: Int, offset: Long, timestamp: Long): Unit = runBlocking {
+ //todo check with peyman
logger.info("Incoming admin event $event")
when (event) {
- is AddCurrencyEvent -> adminService.addCurrency(event.name, event.symbol)
- is EditCurrencyEvent -> adminService.editCurrency(event.name, event.symbol)
- is DeleteCurrencyEvent -> adminService.deleteCurrency(event.name)
+// is AddCurrencyEvent -> adminService.addCurrency(event.name, event.symbol)
+// is EditCurrencyEvent -> adminService.editCurrency(event.name, event.symbol)
+// is DeleteCurrencyEvent -> adminService.deleteCurrency(event.name)
else -> {}
}
}
+
}
\ No newline at end of file
diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/service/AddressAllocatorJob.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/service/AddressAllocatorJob.kt
index 4fda7a2c5..94ca46580 100644
--- a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/service/AddressAllocatorJob.kt
+++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/service/AddressAllocatorJob.kt
@@ -7,18 +7,17 @@ import org.slf4j.Logger
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Configuration
import org.springframework.scheduling.annotation.Scheduled
-import java.util.concurrent.TimeUnit
@Configuration
class AddressAllocatorJob(private val addressManager: AddressManager) {
private val logger: Logger by LoggerDelegate()
- @Value("\${app.address.life-time.value}")
- private var lifeTime: Long? = null
+ @Value("\${app.address.life-time}")
+ private var addressLifeTime: Long? = null
- @Scheduled(fixedDelayString = "\${app.address.life-time.value:0}000")
+ @Scheduled(fixedDelayString = "60000")
fun revokeExpiredAddress() {
- if (lifeTime != null) {
+ if (addressLifeTime != null) {
logger.info("going to lookup assigned address .....")
runBlocking { addressManager.revokeExpiredAddress() }
}
diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/service/AdminService.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/service/AdminService.kt
index aa3b9de76..f9ad972c0 100644
--- a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/service/AdminService.kt
+++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/service/AdminService.kt
@@ -1,32 +1,32 @@
package co.nilin.opex.bcgateway.app.service
import co.nilin.opex.bcgateway.app.dto.AddChainRequest
-import co.nilin.opex.bcgateway.app.dto.TokenRequest
-import co.nilin.opex.bcgateway.core.model.CurrencyImplementation
+import co.nilin.opex.bcgateway.core.model.ReservedAddress
import co.nilin.opex.bcgateway.core.spi.AddressTypeHandler
import co.nilin.opex.bcgateway.core.spi.ChainLoader
-import co.nilin.opex.bcgateway.core.spi.CurrencyHandler
+import co.nilin.opex.bcgateway.core.spi.ReservedAddressHandler
+import co.nilin.opex.common.OpexError
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
class AdminService(
private val chainLoader: ChainLoader,
- private val currencyHandler: CurrencyHandler,
- private val addressTypeHandler: AddressTypeHandler
+ private val addressTypeHandler: AddressTypeHandler,
+ private val reservedAddressHandler: ReservedAddressHandler,
) {
- suspend fun addCurrency(name: String, symbol: String) {
- currencyHandler.addCurrency(name, symbol)
- }
-
- suspend fun editCurrency(name: String, symbol: String) {
- currencyHandler.editCurrency(name, symbol)
- }
-
- suspend fun deleteCurrency(name: String) {
- currencyHandler.deleteCurrency(name)
- }
+// suspend fun addCurrency(name: String, symbol: String) {
+// currencyHandler.addCurrency(name, symbol)
+// }
+//
+// suspend fun editCurrency(name: String, symbol: String) {
+// currencyHandler.editCurrency(name, symbol)
+// }
+//
+// suspend fun deleteCurrency(name: String) {
+// currencyHandler.deleteCurrency(name)
+// }
@Transactional
suspend fun addChain(body: AddChainRequest) {
@@ -37,25 +37,38 @@ class AdminService(
addressTypeHandler.addAddressType(name, addressRegex, memoRegex)
}
- suspend fun addToken(body: TokenRequest): CurrencyImplementation {
- return with(body) {
- currencyHandler.addCurrencyImplementation(
- currencySymbol!!,
- implementationSymbol ?: currencySymbol,
- chain!!,
- tokenName,
- tokenAddress,
- isToken,
- withdrawFee,
- minimumWithdraw,
- isWithdrawEnabled,
- decimal
+ suspend fun addAddresses(addresses: List, memos: List?, addressType: String) {
+ var addressTypeObj =
+ addressTypeHandler.fetchAddressType(addressType) ?: throw OpexError.InvalidAddressType.exception()
+ val reservedAddresses = addresses.mapIndexed { index, address ->
+ ReservedAddress(
+ address = address,
+ memo = memos?.getOrNull(index).orEmpty(),
+ type = addressTypeObj
)
}
+ reservedAddressHandler.addReservedAddress(reservedAddresses)
}
- suspend fun changeTokenWithdrawStatus(symbol: String, chain: String, status: Boolean) {
- currencyHandler.changeWithdrawStatus(symbol, chain, status)
- }
+// suspend fun addToken(body: TokenRequest): CryptoCurrencyCommand {
+// return with(body) {
+// currencyHandler.addCurrencyImplementation(
+// currencySymbol!!,
+// implementationSymbol ?: currencySymbol,
+// chain!!,
+// tokenName,
+// tokenAddress,
+// isToken,
+// withdrawFee,
+// minimumWithdraw,
+// isWithdrawEnabled,
+// decimal
+// )
+// }
+// }
+
+// suspend fun changeTokenWithdrawStatus(symbol: String, chain: String, status: Boolean) {
+// currencyHandler.changeWithdrawStatus(symbol, chain, status)
+// }
}
diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/service/OmniBalanceService.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/service/OmniBalanceService.kt
new file mode 100644
index 000000000..93318c40a
--- /dev/null
+++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/service/OmniBalanceService.kt
@@ -0,0 +1,58 @@
+package co.nilin.opex.bcgateway.app.service
+
+import co.nilin.opex.bcgateway.core.model.FetchGateways
+import co.nilin.opex.bcgateway.core.spi.CryptoCurrencyHandlerV2
+import co.nilin.opex.bcgateway.core.spi.OmniWalletManager
+import co.nilin.opex.common.OpexError
+import org.slf4j.LoggerFactory
+import org.springframework.stereotype.Service
+import java.math.BigDecimal
+
+@Service
+class OmniBalanceService(
+ private val cryptoCurrencyHandlerV2: CryptoCurrencyHandlerV2,
+ private val omniWalletManager: OmniWalletManager
+) {
+
+ data class OmniBalanceForCurrency(val currency: String, val balance: BigDecimal? = BigDecimal.ZERO)
+ data class OmniBalance(val data: ArrayList? = ArrayList())
+
+ private val logger = LoggerFactory.getLogger(OmniBalanceService::class.java)
+
+ suspend fun fetchSystemBalance(currency: String): OmniBalanceForCurrency {
+ val currencyImpls =
+ cryptoCurrencyHandlerV2.fetchCurrencyOnChainGateways(FetchGateways(currencySymbol = currency))
+ ?: throw OpexError.CurrencyNotFound.exception()
+ val totalBalance: BigDecimal = currencyImpls?.map {
+ when (it.isToken) {
+ true -> it.tokenAddress?.let { ta -> omniWalletManager.getTokenBalance(it).balance }
+ ?: BigDecimal.ZERO
+
+ false -> omniWalletManager.getAssetBalance(it).balance ?: BigDecimal.ZERO
+ else -> BigDecimal.ZERO
+ }
+ }.reduce { a, b -> a + b }
+ return OmniBalanceForCurrency(currency = currency, balance = totalBalance)
+ }
+
+ suspend fun fetchSystemBalance(): List? {
+ val currencyImpls = cryptoCurrencyHandlerV2.fetchCurrencyOnChainGateways(FetchGateways())
+ ?: throw OpexError.CurrencyNotFound.exception()
+ val implsGroupedByCurrency = currencyImpls?.groupBy { it.currencySymbol }
+ val result = ArrayList()
+ for (currency in implsGroupedByCurrency.keys) {
+ val balance = implsGroupedByCurrency[currency]?.map {
+ when (it.isToken) {
+ true -> it.tokenAddress?.let { ta -> omniWalletManager.getTokenBalance(it).balance }
+ ?: BigDecimal.ZERO
+
+ false -> omniWalletManager.getAssetBalance(it).balance ?: BigDecimal.ZERO
+ else -> BigDecimal.ZERO
+ }
+ }?.reduce { a, b -> a + b }
+ result.add(OmniBalanceForCurrency(currency, balance))
+ }
+ return result.toList()
+ }
+
+}
\ No newline at end of file
diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/utils/Extensions.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/utils/Extensions.kt
index f7306eea4..121c5fc88 100644
--- a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/utils/Extensions.kt
+++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/utils/Extensions.kt
@@ -6,8 +6,8 @@ import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.oauth2.jwt.Jwt
fun ServerHttpSecurity.AuthorizeExchangeSpec.Access.hasRole(
- authority: String,
- role: String
+ authority: String,
+ role: String
): ServerHttpSecurity.AuthorizeExchangeSpec = access { mono, _ ->
mono.map { auth ->
val hasAuthority = auth.authorities.any { it.authority == authority }
@@ -17,12 +17,12 @@ fun ServerHttpSecurity.AuthorizeExchangeSpec.Access.hasRole(
}
fun ServerHttpSecurity.AuthorizeExchangeSpec.Access.hasRoleAndLevel(
- role: String,
- level: String?=null
+ role: String,
+ level: String? = null
): ServerHttpSecurity.AuthorizeExchangeSpec = access { mono, _ ->
mono.map { auth ->
val hasLevel = level?.let { ((auth.principal as Jwt).claims["level"] as String?)?.equals(level) == true }
- ?: true
+ ?: true
val hasRole = ((auth.principal as Jwt).claims["roles"] as JSONArray?)?.contains(role) == true
AuthorizationDecision(hasLevel && hasRole)
}
diff --git a/bc-gateway/bc-gateway-app/src/main/resources/application-otc.yml b/bc-gateway/bc-gateway-app/src/main/resources/application-otc.yml
index 2661b7831..3d5598e06 100644
--- a/bc-gateway/bc-gateway-app/src/main/resources/application-otc.yml
+++ b/bc-gateway/bc-gateway-app/src/main/resources/application-otc.yml
@@ -66,6 +66,8 @@ logging:
swagger:
authUrl: ${SWAGGER_AUTH_URL:https://api.opex.dev/auth}/realms/opex/protocol/openid-connect/token
app:
+ omni-wallet:
+ url: http://app:8080 #todo ->env
auth:
url: ${auth_url}
cert-url: ${auth_jwk_endpoint}
@@ -74,5 +76,4 @@ app:
wallet:
url: http://wallet:8080
address:
- life-time:
- value: ${ADDRESS_EXP_TIME} # second
+ life-time: ${ADDRESS_EXP_TIME} # second
diff --git a/bc-gateway/bc-gateway-app/src/main/resources/application.yml b/bc-gateway/bc-gateway-app/src/main/resources/application.yml
index 09aafe8ff..ed44e9400 100644
--- a/bc-gateway/bc-gateway-app/src/main/resources/application.yml
+++ b/bc-gateway/bc-gateway-app/src/main/resources/application.yml
@@ -48,7 +48,7 @@ management:
web:
base-path: /actuator
exposure:
- include: ["health", "prometheus", "metrics"]
+ include: [ "health", "prometheus", "metrics" ]
endpoint:
health:
show-details: when_authorized
@@ -99,16 +99,17 @@ logging:
co.nilin: INFO
org.zalando.logbook: TRACE
app:
+ omni-wallet:
+ url: localhost:8080 #todo -> env
auth:
url: lb://opex-auth
- cert-url: lb://opex-auth/auth/realms/opex/protocol/openid-connect/certs
+ cert-url: http://keycloak:8080/realms/opex/protocol/openid-connect/certs
client-id: none
client-secret: none
wallet:
url: lb://opex-wallet
address:
- life-time:
- value: ${ADDRESS_EXP_TIME} # second
+ life-time: 167234670
swagger:
authUrl: ${SWAGGER_AUTH_URL:https://api.opex.dev/auth}/realms/opex/protocol/openid-connect/token}
diff --git a/bc-gateway/bc-gateway-app/src/main/resources/scanner-public.pem b/bc-gateway/bc-gateway-app/src/main/resources/scanner-public.pem
new file mode 100644
index 000000000..8180c26d9
--- /dev/null
+++ b/bc-gateway/bc-gateway-app/src/main/resources/scanner-public.pem
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqn6tRj45adbvNt6TzxYg
+zo4WaREorv91NM5vhQ+wXeY787EmPsQ/mZqyX6yyo5+RnBy9M4mU7ADrS3jQmi+4
+jMCncGrylgYTGAtsY9O6x0sVM/aG7Na3jlXqL/0ZeLyMv0uo0IWhzcapSSTnozOz
+oGAyp/VLmQ5Jtk9wgxQlz67sqFMHXRzF4p3/I15eZu7td1oEViHaY1rArXYOsPMD
+M4/avAr50kYtP995hApGEdxw1mlBrXyB1Wy7cHuuRDJ7BY3jY8gLE/JJddmKH1/i
+vFvKM+K5If2d50qhkI6SQ0aTZ0lEnQsVcdt4o2HT1hxsA8TIz6N1+xVoFjJ27Ry6
+yQIDAQAB
+-----END PUBLIC KEY-----
diff --git a/bc-gateway/bc-gateway-core/pom.xml b/bc-gateway/bc-gateway-core/pom.xml
index 0dce4d6ce..cbd3c236a 100644
--- a/bc-gateway/bc-gateway-core/pom.xml
+++ b/bc-gateway/bc-gateway-core/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/api/AssignAddressService.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/api/AssignAddressService.kt
index 069db26cc..a154cfc0b 100644
--- a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/api/AssignAddressService.kt
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/api/AssignAddressService.kt
@@ -1,8 +1,9 @@
package co.nilin.opex.bcgateway.core.api
import co.nilin.opex.bcgateway.core.model.AssignedAddress
-import co.nilin.opex.bcgateway.core.model.Currency
+
+//import co.nilin.opex.bcgateway.core.model.Currency
interface AssignAddressService {
- suspend fun assignAddress(user: String, currency: Currency, chain: String): List
+ suspend fun assignAddress(user: String, currency: String, chain: String): List
}
\ No newline at end of file
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/api/WalletSyncService.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/api/WalletSyncService.kt
index 68a2032c9..584801888 100644
--- a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/api/WalletSyncService.kt
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/api/WalletSyncService.kt
@@ -3,5 +3,9 @@ package co.nilin.opex.bcgateway.core.api
import co.nilin.opex.bcgateway.core.model.Transfer
interface WalletSyncService {
+
+ suspend fun sendTransfer(transfer: Transfer)
+
+ @Deprecated("Use above function instead")
suspend fun syncTransfers(transfers: List)
}
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/AssignedAddress.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/AssignedAddress.kt
index 47bdd13e6..9dfe6e5db 100644
--- a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/AssignedAddress.kt
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/AssignedAddress.kt
@@ -12,7 +12,7 @@ data class AssignedAddress(
var assignedDate: LocalDateTime? = null,
var revokedDate: LocalDateTime? = null,
var status: AddressStatus? = AddressStatus.Reserved,
- var id: Long?=null
+ var id: Long? = null
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/AssignedAddressV2.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/AssignedAddressV2.kt
new file mode 100644
index 000000000..918423b3c
--- /dev/null
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/AssignedAddressV2.kt
@@ -0,0 +1,32 @@
+package co.nilin.opex.bcgateway.core.model
+
+import java.time.LocalDateTime
+
+data class AssignedAddressV2(
+ val typeId: Long,
+ val address: String,
+ val memo: String?,
+ var expTime: LocalDateTime? = null,
+ var assignedDate: LocalDateTime? = null,
+ var revokedDate: LocalDateTime? = null,
+ var status: AddressStatus? = AddressStatus.Reserved,
+ var id: Long? = null
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as AssignedAddressV2
+
+ if (address != other.address) return false
+ if (memo != other.memo) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = address.hashCode()
+ result = 31 * result + (memo?.hashCode() ?: 0)
+ return result
+ }
+}
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/CryptoCurrencyCommand.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/CryptoCurrencyCommand.kt
new file mode 100644
index 000000000..dde6a200b
--- /dev/null
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/CryptoCurrencyCommand.kt
@@ -0,0 +1,37 @@
+package co.nilin.opex.bcgateway.core.model
+
+import java.math.BigDecimal
+
+data class CryptoCurrencyCommand(
+ var currencySymbol: String,
+ var gatewayUuid: String?,
+ var implementationSymbol: String? = currencySymbol,
+ var isActive: Boolean? = true,
+ var isToken: Boolean? = false,
+ var tokenName: String? = null,
+ var tokenAddress: String? = null,
+ var withdrawFee: BigDecimal?,
+ var withdrawAllowed: Boolean? = true,
+ var depositAllowed: Boolean? = true,
+ val withdrawMin: BigDecimal? = BigDecimal.ZERO,
+ var withdrawMax: BigDecimal? = BigDecimal.ZERO,
+ var depositMin: BigDecimal? = BigDecimal.ZERO,
+ var depositMax: BigDecimal? = BigDecimal.ZERO,
+ var decimal: Int,
+ var chain: String,
+ var type: String = "OnChain"
+
+// var chainDetail: Chain? = null
+
+
+) {
+ fun updateTo(newData: CryptoCurrencyCommand): CryptoCurrencyCommand {
+ return newData.apply {
+ this.currencySymbol = currencySymbol
+ this.gatewayUuid = gatewayUuid
+ }
+ }
+
+}
+
+
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/Currency.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/Currency.kt
index a66ef7a84..580f36d28 100644
--- a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/Currency.kt
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/Currency.kt
@@ -1,3 +1,3 @@
-package co.nilin.opex.bcgateway.core.model
-
-data class Currency(val symbol: String, val name: String)
+//package co.nilin.opex.bcgateway.core.model
+//
+//data class Currency(val symbol: String, val name: String)
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/CurrencyImplementation.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/CurrencyImplementation.kt
index 7eb58a866..9e2f544c8 100644
--- a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/CurrencyImplementation.kt
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/CurrencyImplementation.kt
@@ -3,14 +3,16 @@ package co.nilin.opex.bcgateway.core.model
import java.math.BigDecimal
data class CurrencyImplementation(
- val currency: Currency,
- val implCurrency: Currency,
- val chain: Chain,
+ val currency: String,
+ val implCurrency: String,
+ val chain: String,
val token: Boolean,
val tokenAddress: String?,
val tokenName: String?,
val withdrawEnabled: Boolean,
val withdrawFee: BigDecimal,
val withdrawMin: BigDecimal,
- val decimal: Int
+ val decimal: Int,
+// val chainDetail:Chain?=null,
+
)
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/CurrencyInfo.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/CurrencyInfo.kt
index fdb9ad665..2a350b440 100644
--- a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/CurrencyInfo.kt
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/CurrencyInfo.kt
@@ -1,3 +1,3 @@
package co.nilin.opex.bcgateway.core.model
-data class CurrencyInfo(val currency: Currency, val implementations: List)
+data class CurrencyInfo(val currency: String, val implementations: List)
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/Deposit.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/Deposit.kt
index 01b728da9..ac44765b4 100644
--- a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/Deposit.kt
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/Deposit.kt
@@ -1,16 +1,15 @@
package co.nilin.opex.bcgateway.core.model
import java.math.BigDecimal
-import java.time.LocalDateTime
data class Deposit(
- val id: Long?,
- val hash: String,
- val depositor: String,
- val depositorMemo: String?,
- val amount: BigDecimal,
- val chain: String,
- val token: Boolean,
- val tokenAddress: String?,
+ val id: Long?,
+ val hash: String,
+ val depositor: String,
+ val depositorMemo: String?,
+ val amount: BigDecimal,
+ val chain: String,
+ val token: Boolean,
+ val tokenAddress: String?,
-)
\ No newline at end of file
+ )
\ No newline at end of file
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/FetchGateways.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/FetchGateways.kt
new file mode 100644
index 000000000..3af7b2ad0
--- /dev/null
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/FetchGateways.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.bcgateway.core.model
+
+data class FetchGateways(
+ val currencySymbol: String? = null,
+ var gatewayUuid: String? = null,
+ var chain: String? = null,
+ var currencyImplementationName: String? = null
+)
\ No newline at end of file
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/OmniBalance.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/OmniBalance.kt
new file mode 100644
index 000000000..76d6bb134
--- /dev/null
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/OmniBalance.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.bcgateway.core.model
+
+import java.math.BigDecimal
+
+data class OmniBalance(val currency: String, val network: String, val balance: BigDecimal? = BigDecimal.ZERO)
+
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/Transfer.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/Transfer.kt
index e01b440c5..e33ffbd99 100644
--- a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/Transfer.kt
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/Transfer.kt
@@ -5,7 +5,6 @@ import java.math.BigInteger
data class Transfer(
val txHash: String,
- val blockNumber: BigInteger,
val receiver: Wallet,
val isTokenTransfer: Boolean,
val amount: BigDecimal,
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/otc/LoginRequest.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/otc/LoginRequest.kt
index 7d9dc1c70..eac149b2c 100644
--- a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/otc/LoginRequest.kt
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/otc/LoginRequest.kt
@@ -1,3 +1,3 @@
package co.nilin.opex.bcgateway.core.model.otc
-data class LoginRequest(var clientId:String, var clientSecret:String)
+data class LoginRequest(var clientId: String, var clientSecret: String)
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/otc/LoginResponse.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/otc/LoginResponse.kt
index 8a03bffdc..d8c03116c 100644
--- a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/otc/LoginResponse.kt
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/model/otc/LoginResponse.kt
@@ -4,7 +4,9 @@ import com.fasterxml.jackson.annotation.JsonProperty
data class LoginResponse(var data: Token)
-data class Token(@JsonProperty("access_token")
- val accessToken: String,
- @JsonProperty("expires_in")
- val expireIn: Long)
+data class Token(
+ @JsonProperty("access_token")
+ val accessToken: String,
+ @JsonProperty("expires_in")
+ val expireIn: Long
+)
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/service/AssignAddressServiceImpl.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/service/AssignAddressServiceImpl.kt
index 37516d23b..da6bc54b2 100644
--- a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/service/AssignAddressServiceImpl.kt
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/service/AssignAddressServiceImpl.kt
@@ -3,44 +3,48 @@ package co.nilin.opex.bcgateway.core.service
import co.nilin.opex.bcgateway.core.api.AssignAddressService
import co.nilin.opex.bcgateway.core.model.*
import co.nilin.opex.bcgateway.core.spi.AssignedAddressHandler
-import co.nilin.opex.bcgateway.core.spi.CurrencyHandler
+import co.nilin.opex.bcgateway.core.spi.ChainLoader
+import co.nilin.opex.bcgateway.core.spi.CryptoCurrencyHandlerV2
import co.nilin.opex.bcgateway.core.spi.ReservedAddressHandler
import co.nilin.opex.bcgateway.core.utils.LoggerDelegate
import co.nilin.opex.common.OpexError
-import co.nilin.opex.utility.error.data.OpexException
import org.slf4j.Logger
import org.springframework.beans.factory.annotation.Value
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
-import java.time.ZoneId
open class AssignAddressServiceImpl(
- private val currencyHandler: CurrencyHandler,
- private val assignedAddressHandler: AssignedAddressHandler,
- private val reservedAddressHandler: ReservedAddressHandler
+ private val currencyHandler: CryptoCurrencyHandlerV2,
+ private val assignedAddressHandler: AssignedAddressHandler,
+ private val reservedAddressHandler: ReservedAddressHandler,
+ private val chainLoader: ChainLoader
+
) : AssignAddressService {
- @Value("\${app.address.life-time.value}")
- private var lifeTime: Long? = null
+ @Value("\${app.address.life-time}")
+ private var addressLifeTime: Long? = null
private val logger: Logger by LoggerDelegate()
@Transactional
- override suspend fun assignAddress(user: String, currency: Currency, chain: String): List {
- logger.info(ZoneId.systemDefault().toString())
- val currencyInfo = currencyHandler.fetchCurrencyInfo(currency.symbol)
- val chains = currencyInfo.implementations
- .map { imp -> imp.chain }
- .filter { it.name.equals(chain, true) }
+ override suspend fun assignAddress(user: String, currency: String, chain: String): List {
+ logger.info("address life time: " + addressLifeTime.toString())
+ addressLifeTime = 7200
+ val currencyInfo = currencyHandler.fetchCurrencyOnChainGateways(FetchGateways(currencySymbol = currency))
+ ?: throw OpexError.CurrencyNotFound.exception()
+ val chains = currencyInfo
+ ?.map { imp -> chainLoader.fetchChainInfo(imp.chain) }
+ ?.filter { it?.name.equals(chain, true) }
val addressTypes = chains
- .flatMap { chain -> chain.addressTypes }
- .distinct()
+ ?.flatMap { chain -> chain?.addressTypes!! }
+ ?.distinct()
val chainAddressTypeMap = HashMap>()
- chains.forEach { chain ->
- chain.addressTypes.forEach { addressType ->
+ chains?.forEach { chain ->
+ chain?.addressTypes?.forEach { addressType ->
chainAddressTypeMap.putIfAbsent(addressType, mutableListOf())
- chainAddressTypeMap.getValue(addressType).add(chain)
+ chainAddressTypeMap.getValue(addressType).add(chain!!)
}
}
- val userAssignedAddresses = (assignedAddressHandler.fetchAssignedAddresses(user, addressTypes)).toMutableList()
+ val userAssignedAddresses =
+ (assignedAddressHandler.fetchAssignedAddresses(user, addressTypes!!)).toMutableList()
val result = mutableSetOf()
addressTypes.forEach { addressType ->
val assigned = userAssignedAddresses.firstOrNull { assignAddress -> assignAddress.type == addressType }
@@ -55,17 +59,17 @@ open class AssignAddressServiceImpl(
val reservedAddress = reservedAddressHandler.peekReservedAddress(addressType)
if (reservedAddress != null) {
val newAssigned = AssignedAddress(
- user,
- reservedAddress.address,
- reservedAddress.memo,
- addressType,
- chainAddressTypeMap[addressType]!!,
- lifeTime?.let { LocalDateTime.now().plusSeconds(lifeTime!!) }
- ?: null,
- LocalDateTime.now(),
- null,
- AddressStatus.Assigned,
- null
+ user,
+ reservedAddress.address,
+ reservedAddress.memo,
+ addressType,
+ chainAddressTypeMap[addressType]!!,
+ addressLifeTime?.let { LocalDateTime.now().plusSeconds(addressLifeTime!!) }
+ ?: null,
+ LocalDateTime.now(),
+ null,
+ AddressStatus.Assigned,
+ null
)
reservedAddressHandler.remove(reservedAddress)
result.add(newAssigned)
@@ -78,7 +82,7 @@ open class AssignAddressServiceImpl(
}
result.forEach { address ->
assignedAddressHandler.persist(address)
- address.apply { id=null }
+ address.apply { id = null }
}
return result.toMutableList()
}
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/service/AssignAddressServiceImplV2.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/service/AssignAddressServiceImplV2.kt
new file mode 100644
index 000000000..755a84199
--- /dev/null
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/service/AssignAddressServiceImplV2.kt
@@ -0,0 +1,69 @@
+//package co.nilin.opex.bcgateway.core.service
+//
+//import co.nilin.opex.bcgateway.core.api.AssignAddressService
+//import co.nilin.opex.bcgateway.core.model.*
+//import co.nilin.opex.bcgateway.core.spi.*
+//import co.nilin.opex.bcgateway.core.utils.LoggerDelegate
+//import co.nilin.opex.common.OpexError
+//import org.slf4j.Logger
+//import org.springframework.beans.factory.annotation.Value
+//import org.springframework.transaction.annotation.Transactional
+//import java.time.LocalDateTime
+//import java.time.ZoneId
+//
+//open class AssignAddressServiceImplV2(
+// private val currencyHandler: CryptoCurrencyHandlerV2,
+// private val assignedAddressHandler: AssignedAddressHandler,
+// private val reservedAddressHandler: ReservedAddressHandler,
+// private val chainLoader: ChainLoader
+//) : AssignAddressService {
+// @Value("\${app.address.life-time}")
+// private var lifeTime: Long? = null
+// private val logger: Logger by LoggerDelegate()
+//
+// override suspend fun assignAddress(user: String, currencyImplUuid: String): List {
+// logger.info(ZoneId.systemDefault().toString())
+// val result = mutableSetOf()
+// currencyHandler.fetchCurrencyImpls(FetchImpls(currencyImplUuid))
+// ?.imps?.firstOrNull()?.let { it ->
+// //for requested chain check all available address types and for each of them do :
+// chainLoader.fetchChainInfo(it.chain!!).addressTypes.map { it -> it.id }.distinct()
+// .forEach { addressType ->
+// //check: Is there any assigned address(user, specific address type on requested chain)
+// assignedAddressHandler.fetchAssignedAddresses(user, addressType)?.let {
+// result.add(it)
+// } ?: run {
+// // there is no assigned address(user,specific address type on requested chain)
+// //then assign new address (ip applicable)
+// assignNewAddress(user, addressType)?.let { ra ->
+// result.add(ra)
+// }
+// }
+//
+//
+// }
+// if (result.size == 0)
+// throw OpexError.ReservedAddressNotAvailable.exception()
+// return result.toMutableList()
+// } ?: throw OpexError.BadRequest.exception()
+//
+// }
+//
+// @Transactional
+// open suspend fun assignNewAddress(user: String, addressTypeId: Long): AssignedAddressV2? {
+// reservedAddressHandler.peekReservedAddress(addressTypeId)?.let {//
+// reservedAddressHandler.remove(it)
+// return assignedAddressHandler.persist(user,
+// AssignedAddressV2(addressTypeId, it.address, it.memo,
+// lifeTime?.let { LocalDateTime.now().plusSeconds(lifeTime!!) } ?: null,
+// LocalDateTime.now(),
+// null,
+// AddressStatus.Assigned,
+// null))
+// }
+//
+// ?: run { return null }
+//
+// }
+//
+//}
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/service/WalletSyncServiceImpl.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/service/WalletSyncServiceImpl.kt
index 997811691..8f0938eea 100644
--- a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/service/WalletSyncServiceImpl.kt
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/service/WalletSyncServiceImpl.kt
@@ -1,15 +1,15 @@
package co.nilin.opex.bcgateway.core.service
import co.nilin.opex.bcgateway.core.api.WalletSyncService
-import co.nilin.opex.bcgateway.core.model.CurrencyImplementation
+import co.nilin.opex.bcgateway.core.model.CryptoCurrencyCommand
import co.nilin.opex.bcgateway.core.model.Deposit
import co.nilin.opex.bcgateway.core.model.Transfer
import co.nilin.opex.bcgateway.core.spi.AssignedAddressHandler
-import co.nilin.opex.bcgateway.core.spi.CurrencyHandler
+import co.nilin.opex.bcgateway.core.spi.CryptoCurrencyHandlerV2
import co.nilin.opex.bcgateway.core.spi.DepositHandler
import co.nilin.opex.bcgateway.core.spi.WalletProxy
import co.nilin.opex.bcgateway.core.utils.LoggerDelegate
-import kotlinx.coroutines.async
+import co.nilin.opex.common.OpexError
import kotlinx.coroutines.coroutineScope
import org.slf4j.Logger
import org.springframework.stereotype.Service
@@ -20,23 +20,55 @@ import java.math.BigDecimal
class WalletSyncServiceImpl(
private val walletProxy: WalletProxy,
private val assignedAddressHandler: AssignedAddressHandler,
- private val currencyHandler: CurrencyHandler,
+ private val currencyHandler: CryptoCurrencyHandlerV2,
private val depositHandler: DepositHandler
) : WalletSyncService {
private val logger: Logger by LoggerDelegate()
+ @Transactional
+ override suspend fun sendTransfer(transfer: Transfer) {
+ val uuid = assignedAddressHandler.findUuid(transfer.receiver.address, transfer.receiver.memo) ?: return
+ val currencyGateway =
+ currencyHandler.fetchGatewayWithoutSymbol(transfer.chain, transfer.isTokenTransfer, transfer.tokenAddress)
+ ?: throw OpexError.CurrencyNotFound.exception()
+
+ depositHandler.save(
+ with(transfer) {
+ Deposit(
+ null,
+ txHash,
+ receiver.address,
+ receiver.memo,
+ amount,
+ chain,
+ isTokenTransfer,
+ tokenAddress
+ )
+ }
+ )
+
+ sendDeposit(uuid, currencyGateway, transfer)
+ }
+
@Transactional
override suspend fun syncTransfers(transfers: List) = coroutineScope {
- val groupedByChain = currencyHandler.fetchAllImplementations().groupBy { it.chain.name }
+ val groupedByChain = currencyHandler.fetchCurrencyOnChainGateways()?.groupBy { it.chain }
+ ?: throw OpexError.CurrencyNotFound.exception()
val deposits = transfers.mapNotNull {
coroutineScope {
+
val currencyImpl = groupedByChain[it.chain]?.find { c -> c.tokenAddress == it.tokenAddress }
- ?: throw IllegalStateException("Currency implementation not found")
- assignedAddressHandler.findUuid(it.receiver.address, it.receiver.memo)?.let { it to currencyImpl }
+ ?: run {
+ logger.info("Currency implementation not found")
+ return@coroutineScope null
+ }
+ assignedAddressHandler.findUuid(it.receiver.address, it.receiver.memo)?.let {
+ it to currencyImpl
+ }
}?.let { (uuid, currencyImpl) ->
sendDeposit(uuid, currencyImpl, it)
- logger.info("Deposit synced for $uuid on ${currencyImpl.currency.symbol} - to ${it.receiver.address}")
+ logger.info("Deposit synced for $uuid on ${currencyImpl.currencySymbol} - to ${it.receiver.address}")
it
}
}.map {
@@ -54,10 +86,10 @@ class WalletSyncServiceImpl(
depositHandler.saveAll(deposits)
}
- private suspend fun sendDeposit(uuid: String, currencyImpl: CurrencyImplementation, transfer: Transfer) {
+ private suspend fun sendDeposit(uuid: String, currencyImpl: CryptoCurrencyCommand, transfer: Transfer) {
val amount = transfer.amount.divide(BigDecimal.TEN.pow(currencyImpl.decimal))
- val symbol = currencyImpl.currency.symbol
+ val symbol = currencyImpl.currencySymbol
logger.info("Sending deposit to $uuid - $amount $symbol")
- walletProxy.transfer(uuid, symbol, amount, transfer.txHash,transfer.chain)
+ walletProxy.transfer(uuid, symbol, amount, transfer.txHash, transfer.chain, currencyImpl.gatewayUuid)
}
}
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/AddressTypeHandler.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/AddressTypeHandler.kt
index 6a60f06aa..282dfe489 100644
--- a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/AddressTypeHandler.kt
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/AddressTypeHandler.kt
@@ -8,4 +8,5 @@ interface AddressTypeHandler {
suspend fun addAddressType(name: String, addressRegex: String, memoRegex: String?)
+ suspend fun fetchAddressType(name: String): AddressType?
}
\ No newline at end of file
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/AssignedAddressHandler.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/AssignedAddressHandler.kt
index e6a4d12fe..9af419764 100644
--- a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/AssignedAddressHandler.kt
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/AssignedAddressHandler.kt
@@ -2,7 +2,6 @@ package co.nilin.opex.bcgateway.core.spi
import co.nilin.opex.bcgateway.core.model.AddressType
import co.nilin.opex.bcgateway.core.model.AssignedAddress
-import java.time.LocalDateTime
interface AssignedAddressHandler {
suspend fun fetchAssignedAddresses(user: String, addressTypes: List): List
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/AuthProxy.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/AuthProxy.kt
index 8cbf15f68..ac9f21f6f 100644
--- a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/AuthProxy.kt
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/AuthProxy.kt
@@ -1,8 +1,9 @@
package co.nilin.opex.bcgateway.core.spi
-import co.nilin.opex.bcgateway.core.model.otc.*
+import co.nilin.opex.bcgateway.core.model.otc.LoginRequest
+import co.nilin.opex.bcgateway.core.model.otc.LoginResponse
interface AuthProxy {
- suspend fun getToken(loginRequest: LoginRequest):LoginResponse
+ suspend fun getToken(loginRequest: LoginRequest): LoginResponse
}
\ No newline at end of file
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/ChainLoader.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/ChainLoader.kt
index ac274c2e0..6c3f6bd4c 100644
--- a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/ChainLoader.kt
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/ChainLoader.kt
@@ -4,9 +4,9 @@ import co.nilin.opex.bcgateway.core.model.Chain
interface ChainLoader {
- suspend fun addChain(name: String, addressType:String):Chain
+ suspend fun addChain(name: String, addressType: String): Chain
- suspend fun fetchAllChains():List
+ suspend fun fetchAllChains(): List
suspend fun fetchChainInfo(chain: String): Chain
}
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/CryptoCurrencyHandlerV2.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/CryptoCurrencyHandlerV2.kt
new file mode 100644
index 000000000..11b4eb755
--- /dev/null
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/CryptoCurrencyHandlerV2.kt
@@ -0,0 +1,29 @@
+package co.nilin.opex.bcgateway.core.spi
+
+import co.nilin.opex.bcgateway.core.model.CryptoCurrencyCommand
+import co.nilin.opex.bcgateway.core.model.FetchGateways
+import co.nilin.opex.bcgateway.core.model.WithdrawData
+
+interface CryptoCurrencyHandlerV2 {
+
+ suspend fun createOnChainGateway(request: CryptoCurrencyCommand): CryptoCurrencyCommand?
+
+ suspend fun updateOnChainGateway(request: CryptoCurrencyCommand): CryptoCurrencyCommand?
+
+ suspend fun deleteOnChainGateway(gatewayUuid: String, currency: String): Void?
+
+ suspend fun fetchCurrencyOnChainGateways(data: FetchGateways? = null): List?
+
+ suspend fun fetchOnChainGateway(gatewayUuid: String, currency: String): CryptoCurrencyCommand?
+
+ suspend fun changeWithdrawStatus(symbol: String, chain: String, status: Boolean)
+
+ suspend fun getWithdrawData(symbol: String, network: String): WithdrawData
+
+ suspend fun fetchGatewayWithoutSymbol(
+ chain: String,
+ isToken: Boolean,
+ tokenAddress: String?
+ ): CryptoCurrencyCommand?
+
+}
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/CurrencyHandler.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/CurrencyHandler.kt
index 768141df4..e69de29bb 100644
--- a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/CurrencyHandler.kt
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/CurrencyHandler.kt
@@ -1,74 +0,0 @@
-package co.nilin.opex.bcgateway.core.spi
-
-import co.nilin.opex.bcgateway.core.model.CurrencyImplementation
-import co.nilin.opex.bcgateway.core.model.CurrencyInfo
-import co.nilin.opex.bcgateway.core.model.WithdrawData
-import java.math.BigDecimal
-
-interface CurrencyHandler {
-
- suspend fun addCurrency(name: String, symbol: String)
-
- suspend fun addCurrencyImplementationV2(
- currencySymbol: String,
- implementationSymbol: String,
- currencyName: String,
- chain: String,
- tokenName: String?,
- tokenAddress: String?,
- isToken: Boolean,
- withdrawFee: BigDecimal,
- minimumWithdraw: BigDecimal,
- isWithdrawEnabled: Boolean,
- decimal: Int
- ): CurrencyImplementation?
-
-
- suspend fun updateCurrencyImplementation(
- currencySymbol: String,
- implementationSymbol: String,
- currencyName: String,
- newChain: String? = null,
- tokenName: String?,
- tokenAddress: String?,
- isToken: Boolean,
- withdrawFee: BigDecimal,
- minimumWithdraw: BigDecimal,
- isWithdrawEnabled: Boolean,
- decimal: Int,
- chain: String
- ): CurrencyImplementation?
-
-
- suspend fun editCurrency(name: String, symbol: String)
-
- suspend fun deleteCurrency(name: String)
-
- suspend fun addCurrencyImplementation(
- currencySymbol: String,
- implementationSymbol: String,
- chain: String,
- tokenName: String?,
- tokenAddress: String?,
- isToken: Boolean,
- withdrawFee: BigDecimal,
- minimumWithdraw: BigDecimal,
- isWithdrawEnabled: Boolean,
- decimal: Int
- ): CurrencyImplementation
-
- suspend fun fetchAllImplementations(): List
-
- suspend fun fetchCurrencyInfo(symbol: String): CurrencyInfo
-
- suspend fun findByChainAndTokenAddress(chain: String, address: String?): CurrencyImplementation?
-
- suspend fun findImplementationsWithTokenOnChain(chain: String): List
-
- suspend fun findImplementationsByCurrency(currency: String): List
-
- suspend fun changeWithdrawStatus(symbol: String, chain: String, status: Boolean)
-
- suspend fun getWithdrawData(symbol: String, network: String): WithdrawData
-
-}
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/DepositHandler.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/DepositHandler.kt
index a8373e322..8451688db 100644
--- a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/DepositHandler.kt
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/DepositHandler.kt
@@ -3,8 +3,10 @@ package co.nilin.opex.bcgateway.core.spi
import co.nilin.opex.bcgateway.core.model.Deposit
interface DepositHandler {
+
suspend fun findDepositsByHash(hash: List): List
- suspend fun saveAll(deposits: List)
+ suspend fun saveAll(deposits: List)
+ suspend fun save(deposit: Deposit)
}
\ No newline at end of file
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/OmniWalletManager.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/OmniWalletManager.kt
new file mode 100644
index 000000000..85901e004
--- /dev/null
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/OmniWalletManager.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.bcgateway.core.spi
+
+import co.nilin.opex.bcgateway.core.model.CryptoCurrencyCommand
+import co.nilin.opex.bcgateway.core.model.OmniBalance
+
+interface OmniWalletManager {
+
+ suspend fun getTokenBalance(currencyImpl: CryptoCurrencyCommand): OmniBalance
+ suspend fun getAssetBalance(cryptoCurrencyCommand: CryptoCurrencyCommand): OmniBalance
+
+}
\ No newline at end of file
diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/WalletProxy.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/WalletProxy.kt
index 1c932e776..be1ee0c49 100644
--- a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/WalletProxy.kt
+++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/spi/WalletProxy.kt
@@ -3,5 +3,12 @@ package co.nilin.opex.bcgateway.core.spi
import java.math.BigDecimal
interface WalletProxy {
- suspend fun transfer(uuid: String, symbol: String, amount: BigDecimal, hash: String, chain: String)
+ suspend fun transfer(
+ uuid: String,
+ symbol: String,
+ amount: BigDecimal,
+ hash: String,
+ chain: String,
+ gatewayUuid: String?
+ )
}
diff --git a/bc-gateway/bc-gateway-core/src/test/kotlin/co/nilin/opex/bcgateway/core/service/AssignAddressServiceImplUnitTest.kt b/bc-gateway/bc-gateway-core/src/test/kotlin/co/nilin/opex/bcgateway/core/service/AssignAddressServiceImplUnitTest.kt
index 8aa515ff6..c74996234 100644
--- a/bc-gateway/bc-gateway-core/src/test/kotlin/co/nilin/opex/bcgateway/core/service/AssignAddressServiceImplUnitTest.kt
+++ b/bc-gateway/bc-gateway-core/src/test/kotlin/co/nilin/opex/bcgateway/core/service/AssignAddressServiceImplUnitTest.kt
@@ -1,29 +1,33 @@
package co.nilin.opex.bcgateway.core.service
+//import co.nilin.opex.bcgateway.core.spi.CurrencyHandler
import co.nilin.opex.bcgateway.core.model.*
-import co.nilin.opex.bcgateway.core.model.Currency
import co.nilin.opex.bcgateway.core.spi.AssignedAddressHandler
-import co.nilin.opex.bcgateway.core.spi.CurrencyHandler
+import co.nilin.opex.bcgateway.core.spi.ChainLoader
+import co.nilin.opex.bcgateway.core.spi.CryptoCurrencyHandlerV2
import co.nilin.opex.bcgateway.core.spi.ReservedAddressHandler
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
-import org.junit.jupiter.api.Test
import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
import java.math.BigDecimal
import java.util.*
class AssignAddressServiceImplUnitTest {
- private val currencyHandler = mockk()
+ private val currencyHandler = mockk()
private val assignedAddressHandler = mockk()
private val reservedAddressHandler = mockk()
+ private val chainLoader = mockk()
+
private val assignAddressServiceImpl =
- AssignAddressServiceImpl(currencyHandler, assignedAddressHandler, reservedAddressHandler)
+ AssignAddressServiceImpl(currencyHandler, assignedAddressHandler, reservedAddressHandler, chainLoader)
- private val currency = Currency("ETH", "Ethereum")
+ // private val currency = Currency("ETH", "Ethereum")
private val chain = "ETH_MAINNET"
+ private val currency = "ETH"
private val ethAddressType = AddressType(1, "ETH", "+*", ".*")
private val ethMemoAddressType = AddressType(2, "ETH", "+*", "+*")
private val ethChain = Chain("ETH_MAINNET", arrayListOf(ethAddressType))
@@ -31,37 +35,56 @@ class AssignAddressServiceImplUnitTest {
init {
- val eth = CurrencyImplementation(
+ val eth = CryptoCurrencyCommand(
currency,
+ UUID.randomUUID().toString(),
currency,
- ethChain,
+ true,
false,
null,
null,
+ BigDecimal.ZERO,
true,
- BigDecimal.ONE,
- BigDecimal.TEN,
- 18
+ true,
+ BigDecimal.ZERO,
+ BigDecimal.ZERO,
+ BigDecimal.ZERO,
+ BigDecimal.ZERO,
+ 18,
+ ethChain.name,
+// ethChain
)
- val wrappedEth = CurrencyImplementation(
+
+ val wrappedEth = CryptoCurrencyCommand(
currency,
+ UUID.randomUUID().toString(),
currency,
- bscChain,
+ true,
false,
- "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"WETH",
+ "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ BigDecimal.ZERO,
true,
- BigDecimal.ONE,
- BigDecimal.ONE,
- 18
- )
+ true,
+ BigDecimal.ZERO,
+ BigDecimal.ZERO,
+ BigDecimal.ZERO,
+ BigDecimal.ZERO,
+ 18,
+ bscChain.name,
+// bscChain
- coEvery { currencyHandler.fetchCurrencyInfo(currency.symbol) } returns CurrencyInfo(
- currency,
- listOf(eth, wrappedEth)
)
+ coEvery { currencyHandler.fetchCurrencyOnChainGateways(FetchGateways(currencySymbol = currency)) } returns
+ listOf(eth, wrappedEth)
+
+ coEvery { chainLoader.fetchChainInfo(chain = ethChain.name) } returns ethChain
+
+ coEvery { chainLoader.fetchChainInfo(chain = bscChain.name) } returns bscChain
+
+
coEvery { assignedAddressHandler.persist(any()) } returns Unit
coEvery { reservedAddressHandler.remove(any()) } returns Unit
}
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-auth-proxy/pom.xml b/bc-gateway/bc-gateway-ports/bc-gateway-auth-proxy/pom.xml
index 359b1ce63..73b162182 100644
--- a/bc-gateway/bc-gateway-ports/bc-gateway-auth-proxy/pom.xml
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-auth-proxy/pom.xml
@@ -1,6 +1,6 @@
-
4.0.0
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-auth-proxy/src/main/kotlin/co/nilin/opex/bcgateway/ports/authproxy/impl/AuthProxyImpl.kt b/bc-gateway/bc-gateway-ports/bc-gateway-auth-proxy/src/main/kotlin/co/nilin/opex/bcgateway/ports/authproxy/impl/AuthProxyImpl.kt
index 553b1cd3c..5937ccb2f 100644
--- a/bc-gateway/bc-gateway-ports/bc-gateway-auth-proxy/src/main/kotlin/co/nilin/opex/bcgateway/ports/authproxy/impl/AuthProxyImpl.kt
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-auth-proxy/src/main/kotlin/co/nilin/opex/bcgateway/ports/authproxy/impl/AuthProxyImpl.kt
@@ -5,6 +5,7 @@ import co.nilin.opex.bcgateway.core.model.otc.LoginResponse
import co.nilin.opex.bcgateway.core.spi.AuthProxy
import kotlinx.coroutines.reactive.awaitFirst
import org.springframework.beans.factory.annotation.Value
+import org.springframework.context.annotation.Profile
import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
@@ -24,14 +25,16 @@ class AuthProxyImpl(private val webClient: WebClient) : AuthProxy {
override suspend fun getToken(loginRequest: LoginRequest): LoginResponse {
return webClient.post()
- .uri(URI.create("${baseUrl}/api/v1/login"))
- .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
- .body(BodyInserters.fromFormData("mobile", loginRequest.clientId)
- .with("password", loginRequest.clientSecret))
- .retrieve()
- .onStatus({ t -> t.isError }, { it.createException() })
- .bodyToMono(typeRef())
- .awaitFirst()
+ .uri(URI.create("${baseUrl}/api/v1/login"))
+ .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
+ .body(
+ BodyInserters.fromFormData("mobile", loginRequest.clientId)
+ .with("password", loginRequest.clientSecret)
+ )
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToMono(typeRef())
+ .awaitFirst()
}
}
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-eventlistener-kafka/pom.xml b/bc-gateway/bc-gateway-ports/bc-gateway-eventlistener-kafka/pom.xml
index b9b9baa18..c60167193 100644
--- a/bc-gateway/bc-gateway-ports/bc-gateway-eventlistener-kafka/pom.xml
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-eventlistener-kafka/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-omniwallet-proxy/pom.xml b/bc-gateway/bc-gateway-ports/bc-gateway-omniwallet-proxy/pom.xml
new file mode 100644
index 000000000..6ac89442c
--- /dev/null
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-omniwallet-proxy/pom.xml
@@ -0,0 +1,48 @@
+
+
+ 4.0.0
+
+
+ co.nilin.opex.bcgateway
+ bc-gateway
+ 1.0.1-beta.7
+ ../../pom.xml
+
+
+ co.nilin.opex.bcgateway.ports.omniwallet
+ bc-gateway-omniwallet-proxy
+ bc-gateway-omniwallet-proxy
+
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ io.projectreactor.kotlin
+ reactor-kotlin-extensions
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-reactor
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-core
+
+
+ co.nilin.opex.bcgateway.core
+ bc-gateway-core
+
+
+ org.springframework
+ spring-webflux
+
+
+
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-omniwallet-proxy/src/main/kotlin/co/nilin/opex/bcgateway/omniwallet/impl/OmniWalletManagerImpl.kt b/bc-gateway/bc-gateway-ports/bc-gateway-omniwallet-proxy/src/main/kotlin/co/nilin/opex/bcgateway/omniwallet/impl/OmniWalletManagerImpl.kt
new file mode 100644
index 000000000..f134aef2d
--- /dev/null
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-omniwallet-proxy/src/main/kotlin/co/nilin/opex/bcgateway/omniwallet/impl/OmniWalletManagerImpl.kt
@@ -0,0 +1,31 @@
+package co.nilin.opex.bcgateway.omniwallet.impl
+
+import co.nilin.opex.bcgateway.core.model.CryptoCurrencyCommand
+import co.nilin.opex.bcgateway.core.model.OmniBalance
+import co.nilin.opex.bcgateway.core.spi.OmniWalletManager
+import co.nilin.opex.bcgateway.omniwallet.model.AddressBalanceWithUsd
+import co.nilin.opex.bcgateway.omniwallet.proxy.OmniWalletProxy
+import org.slf4j.LoggerFactory
+import org.springframework.stereotype.Component
+import java.math.BigDecimal
+
+@Component
+class OmniWalletManagerImpl(private val omniWalletProxy: OmniWalletProxy) : OmniWalletManager {
+ private val logger = LoggerFactory.getLogger(omniWalletProxy::class.java)
+
+ override suspend fun getTokenBalance(cryptoCurrencyCommand: CryptoCurrencyCommand): OmniBalance {
+ return OmniBalance(currency = cryptoCurrencyCommand.currencySymbol,
+ network = cryptoCurrencyCommand.chain,
+ balance = omniWalletProxy.getTokenBalance(cryptoCurrencyCommand.tokenAddress!!, cryptoCurrencyCommand.chain)
+ ?.stream()?.map(AddressBalanceWithUsd::balance)?.reduce { a, b -> a + b }?.orElse(BigDecimal.ZERO)
+ )
+ }
+
+ override suspend fun getAssetBalance(cryptoCurrencyCommand: CryptoCurrencyCommand): OmniBalance {
+ return OmniBalance(
+ cryptoCurrencyCommand.currencySymbol,
+ cryptoCurrencyCommand.chain,
+ omniWalletProxy.getAssetBalance(cryptoCurrencyCommand.chain)?.balance ?: BigDecimal.ZERO
+ )
+ }
+}
\ No newline at end of file
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-omniwallet-proxy/src/main/kotlin/co/nilin/opex/bcgateway/omniwallet/model/AddressBalanceWithUsd.kt b/bc-gateway/bc-gateway-ports/bc-gateway-omniwallet-proxy/src/main/kotlin/co/nilin/opex/bcgateway/omniwallet/model/AddressBalanceWithUsd.kt
new file mode 100644
index 000000000..29f74ea17
--- /dev/null
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-omniwallet-proxy/src/main/kotlin/co/nilin/opex/bcgateway/omniwallet/model/AddressBalanceWithUsd.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.bcgateway.omniwallet.model
+
+import java.math.BigDecimal
+
+data class AddressBalanceWithUsd(val address: String, val balance: BigDecimal, val balanceUsd: BigDecimal)
+
+
+data class ChainBalanceResponse(val data: List)
\ No newline at end of file
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-omniwallet-proxy/src/main/kotlin/co/nilin/opex/bcgateway/omniwallet/proxy/OmniWalletProxy.kt b/bc-gateway/bc-gateway-ports/bc-gateway-omniwallet-proxy/src/main/kotlin/co/nilin/opex/bcgateway/omniwallet/proxy/OmniWalletProxy.kt
new file mode 100644
index 000000000..fe5fd9fef
--- /dev/null
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-omniwallet-proxy/src/main/kotlin/co/nilin/opex/bcgateway/omniwallet/proxy/OmniWalletProxy.kt
@@ -0,0 +1,69 @@
+package co.nilin.opex.bcgateway.omniwallet.proxy
+
+import co.nilin.opex.bcgateway.omniwallet.model.AddressBalanceWithUsd
+import kotlinx.coroutines.reactive.awaitFirst
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.core.ParameterizedTypeReference
+import org.springframework.http.HttpHeaders
+import org.springframework.http.MediaType
+import org.springframework.stereotype.Component
+import org.springframework.web.reactive.function.client.WebClient
+import java.math.BigDecimal
+
+inline fun typeRef(): ParameterizedTypeReference = object : ParameterizedTypeReference() {}
+data class TotalAssetByChainWithUsd(
+ val balance: BigDecimal,
+ val chain: String? = null,
+ val symbol: String? = null,
+ val balanceUsd: BigDecimal? = null
+)
+
+@Component
+class OmniWalletProxy(private val webClient: WebClient) {
+
+
+ @Value("\${app.omni-wallet.url}")
+ private lateinit var baseUrl: String
+
+ private val logger: Logger = LoggerFactory.getLogger(OmniWalletProxy::class.java)
+
+ suspend fun getAssetBalance(network: String): TotalAssetByChainWithUsd? {
+// return TotalAssetByChainWithUsd(BigDecimal(15),network,"", BigDecimal(65))
+//
+ logger.info("----&&&&&&&&&&&----")
+
+ return webClient.get()
+ .uri("${baseUrl}/v1/balance/chain/${network}/total")
+ {
+ it.queryParam("excludeZero", false)
+ it.build()
+ }
+ .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
+ .retrieve()
+ .bodyToMono(typeRef())
+ .doOnError { e -> logger.info("An error happened during get balance of chain $network : ${e.message}") }
+ .onErrorReturn(TotalAssetByChainWithUsd(balance = BigDecimal.ZERO))
+ .log()
+ .awaitFirst()
+ }
+
+ suspend fun getTokenBalance(tokenAddress: String, network: String): List? {
+// return listOf( AddressBalanceWithUsd("", BigDecimal.TEN, BigDecimal.TEN))
+ return webClient.get()
+ .uri("${baseUrl}/v1/balance/token/address/${tokenAddress}")
+ {
+ it.queryParam("excludeZero", false)
+ it.build()
+ }
+ .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
+ .retrieve()
+ .bodyToMono(typeRef?>())
+ .doOnError { e -> logger.info("An error happened during get balance of token $tokenAddress : ${e.message}") }
+ .onErrorReturn(listOf(AddressBalanceWithUsd(tokenAddress, BigDecimal.ZERO, BigDecimal.ZERO)))
+ .log()
+ .awaitFirst()
+
+ }
+}
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/pom.xml b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/pom.xml
index ba0b4b5af..3f3cea67f 100644
--- a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/pom.xml
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
@@ -55,6 +55,11 @@
reactor-test
test
+
+ org.modelmapper
+ modelmapper
+ 3.2.0
+
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/dao/AssignedAddressRepository.kt b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/dao/AssignedAddressRepository.kt
index 42854193a..de431c1d4 100644
--- a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/dao/AssignedAddressRepository.kt
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/dao/AssignedAddressRepository.kt
@@ -17,7 +17,7 @@ interface AssignedAddressRepository : ReactiveCrudRepository,
- @Param("status") status:AddressStatus?=null
+ @Param("status") status: AddressStatus? = null
): Flow
@Query("select * from assigned_addresses where address = :address and (memo is null or memo = '' or memo = :memo)")
@@ -27,18 +27,17 @@ interface AssignedAddressRepository : ReactiveCrudRepository
-
@Query("select * from assigned_addresses where address = :address and (memo is null or memo = '' or memo = :memo) and (:status is null or status =:status)")
fun findByAddressAndMemoAndStatus(
- @Param("address") address: String,
- @Param("memo") memo: String?,
- @Param("status") status:AddressStatus?=null
+ @Param("address") address: String,
+ @Param("memo") memo: String?,
+ @Param("status") status: AddressStatus? = null
): Mono
@Query("select * from assigned_addresses where (:windowPoint is null or assigned_date > :windowPoint ) and (:now is null or exp_time< :now ) and (:status is null or status =:status) ")
fun findPotentialExpAddress(
- @Param("windowPoint") windowPont: LocalDateTime?,
- @Param("now") now: LocalDateTime?,
- @Param("status") status:AddressStatus?=null
+ @Param("windowPoint") windowPont: LocalDateTime?,
+ @Param("now") now: LocalDateTime?,
+ @Param("status") status: AddressStatus? = null
): Flow?
}
\ No newline at end of file
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/dao/CurrencyImplementationRepository.kt b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/dao/CurrencyImplementationRepository.kt
index e480905c5..249d62a7c 100644
--- a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/dao/CurrencyImplementationRepository.kt
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/dao/CurrencyImplementationRepository.kt
@@ -1,29 +1,44 @@
package co.nilin.opex.bcgateway.ports.postgres.dao
import co.nilin.opex.bcgateway.core.model.WithdrawData
-import co.nilin.opex.bcgateway.ports.postgres.model.CurrencyImplementationModel
-import kotlinx.coroutines.flow.Flow
+import co.nilin.opex.bcgateway.ports.postgres.model.CurrencyOnChainGatewayModel
import org.springframework.data.r2dbc.repository.Query
import org.springframework.data.repository.reactive.ReactiveCrudRepository
import org.springframework.stereotype.Repository
+import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
-import java.math.BigDecimal
@Repository
-interface CurrencyImplementationRepository : ReactiveCrudRepository {
+interface CurrencyImplementationRepository : ReactiveCrudRepository {
- fun findByCurrencySymbol(currencySymbol: String): Flow
+ fun findByGatewayUuid(uuid: String): Mono?
- fun findByChain(chain: String): Flow
+ @Query("select * from currency_on_chain_gateway where (:gatewayUuid is null or gateway_uuid=:gatewayUuid) and (:currencySymbol is null or currency_symbol=:currencySymbol ) and (:implementationSymbol is null or implementation_symbol=:implementationSymbol ) and (:chain is null or chain=:chain ) ")
+ fun findGateways(
+ currencySymbol: String? = null,
+ gatewayUuid: String? = null,
+ chain: String? = null,
+ implementationSymbol: String? = null
+ ): Flux?
- fun findByCurrencySymbolAndChain(currencySymbol: String, chain: String): Mono
+ fun deleteByGatewayUuid(uuid: String): Mono
- fun findByChainAndTokenAddress(chain: String, tokenAddress: String?): Mono
-
- @Query("""
+ @Query(
+ """
select withdraw_enabled as is_enabled, withdraw_fee as fee, withdraw_min as minimum
- from currency_implementations
+ from currency_on_chain_gateway
where implementation_symbol = :symbol and chain = :chain
- """)
+ """
+ )
fun findWithdrawDataBySymbolAndChain(symbol: String, chain: String): Mono
-}
+
+ fun findByCurrencySymbolAndChain(symbol: String, chain: String): Mono
+
+ fun findByGatewayUuidAndCurrencySymbol(gatewayUuid: String?, symbol: String?): Mono?
+
+ @Query("select * from currency_on_chain_gateway where chain = :chain and is_token is false")
+ fun findMainAssetGateway(chain: String): Mono
+
+ @Query("select * from currency_on_chain_gateway where chain = :chain and is_token is true and token_address = :tokenAddress")
+ fun findTokenGateway(chain: String, tokenAddress: String): Mono
+}
\ No newline at end of file
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/dao/CurrencyRepository.kt b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/dao/CurrencyRepository.kt
index 2dd1e4517..41a6fccd1 100644
--- a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/dao/CurrencyRepository.kt
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/dao/CurrencyRepository.kt
@@ -1,19 +1,13 @@
package co.nilin.opex.bcgateway.ports.postgres.dao
-import co.nilin.opex.bcgateway.ports.postgres.model.CurrencyModel
-import org.springframework.data.r2dbc.repository.Query
-import org.springframework.data.repository.reactive.ReactiveCrudRepository
-import org.springframework.stereotype.Repository
-import reactor.core.publisher.Mono
-
-@Repository
-interface CurrencyRepository : ReactiveCrudRepository {
-
- fun findBySymbol(symbol: String): Mono
-
- @Query("insert into currency values (:symbol, :name) on conflict do nothing")
- fun insert(name: String, symbol: String): Mono
-
- @Query("delete from currency where name = :name")
- fun deleteByName(name: String): Mono
-}
+//@Repository
+//interface CurrencyRepository : ReactiveCrudRepository {
+//
+//// fun findBySymbol(symbol: String): Mono
+////
+//// @Query("insert into currency values (:symbol, :name) on conflict do nothing")
+//// fun insert(name: String, symbol: String): Mono
+////
+//// @Query("delete from currency where name = :name")
+//// fun deleteByName(name: String): Mono
+//}
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/AddressManagerImpl.kt b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/AddressManagerImpl.kt
index b68f382b4..f12c3c08d 100644
--- a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/AddressManagerImpl.kt
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/AddressManagerImpl.kt
@@ -3,22 +3,23 @@ package co.nilin.opex.bcgateway.ports.postgres.impl
import co.nilin.opex.bcgateway.core.model.AddressStatus
import co.nilin.opex.bcgateway.core.model.ReservedAddress
import co.nilin.opex.bcgateway.core.spi.AddressManager
-import co.nilin.opex.bcgateway.core.spi.CurrencyHandler
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import java.time.LocalDateTime
@Component
-class AddressManagerImpl(private val addressHandlerImpl: AssignedAddressHandlerImpl,
- private val reservedAddressHandlerImpl: ReservedAddressHandlerImpl) : AddressManager {
+class AddressManagerImpl(
+ private val addressHandlerImpl: AssignedAddressHandlerImpl,
+ private val reservedAddressHandlerImpl: ReservedAddressHandlerImpl
+) : AddressManager {
private val logger = LoggerFactory.getLogger(AddressManagerImpl::class.java)
override suspend fun revokeExpiredAddress() {
addressHandlerImpl.fetchExpiredAssignedAddresses()?.map {
addressHandlerImpl.revoke(it.apply {
- id=it.id
+ id = it.id
status = AddressStatus.Revoked
- revokedDate= LocalDateTime.now()
+ revokedDate = LocalDateTime.now()
})
reservedAddressHandlerImpl.addReservedAddress(listOf(ReservedAddress(it.address, it.memo, it.type)))
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/AddressTypeHandlerImpl.kt b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/AddressTypeHandlerImpl.kt
index 4fa98fc3a..833ceb32e 100644
--- a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/AddressTypeHandlerImpl.kt
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/AddressTypeHandlerImpl.kt
@@ -23,4 +23,9 @@ class AddressTypeHandlerImpl(private val repository: AddressTypeRepository) : Ad
repository.save(AddressTypeModel(null, name, addressRegex, memoRegex)).awaitFirstOrNull()
}
}
+
+ override suspend fun fetchAddressType(name: String): AddressType? {
+ return repository.findByType(name)
+ .map { AddressType(it.id!!, it.type, it.addressRegex, it.memoRegex) }.awaitFirstOrNull()
+ }
}
\ No newline at end of file
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/AssignedAddressHandlerImpl.kt b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/AssignedAddressHandlerImpl.kt
index 3974b6c04..8c5220a3d 100644
--- a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/AssignedAddressHandlerImpl.kt
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/AssignedAddressHandlerImpl.kt
@@ -19,27 +19,27 @@ import org.slf4j.Logger
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import java.time.LocalDateTime
-import java.time.ZoneId
@Service
class AssignedAddressHandlerImpl(
- val assignedAddressRepository: AssignedAddressRepository,
- val addressTypeRepository: AddressTypeRepository,
- val assignedAddressChainRepository: AssignedAddressChainRepository,
- val chainLoader: ChainLoader
+ val assignedAddressRepository: AssignedAddressRepository,
+ val addressTypeRepository: AddressTypeRepository,
+ val assignedAddressChainRepository: AssignedAddressChainRepository,
+ val chainLoader: ChainLoader
) : AssignedAddressHandler {
- @Value("\${app.address.life-time.value}")
- private var lifeTime: Long? = null
+ @Value("\${app.address.life-time}")
+ private var addressLifeTime: Long? = null
private val logger: Logger by LoggerDelegate()
override suspend fun fetchAssignedAddresses(user: String, addressTypes: List): List {
+ addressLifeTime = 7200
if (addressTypes.isEmpty()) return emptyList()
val addressTypeMap = addressTypeRepository.findAll().map { aam ->
AddressType(aam.id!!, aam.type, aam.addressRegex, aam.memoRegex)
}.collectMap { it.id }.awaitFirst()
return assignedAddressRepository.findByUuidAndAddressTypeAndStatus(
- user, addressTypes.map(AddressType::id), AddressStatus.Assigned
+ user, addressTypes.map(AddressType::id), AddressStatus.Assigned
).map { model ->
model.toDto(addressTypeMap).apply { id = model.id }
}.filter { it.expTime?.let { it > LocalDateTime.now() } ?: true }.toList()
@@ -49,19 +49,19 @@ class AssignedAddressHandlerImpl(
logger.info("going to save new address .............")
assignedAddressRepository.save(
- AssignedAddressModel(
- assignedAddress.id ?: null,
- assignedAddress.uuid,
- assignedAddress.address,
- assignedAddress.memo,
- assignedAddress.type.id,
- assignedAddress.id?.let { assignedAddress.expTime }
- ?: (lifeTime?.let { (LocalDateTime.now().plusSeconds(lifeTime!!)) }
- ?: null),
- assignedAddress.id?.let { assignedAddress.assignedDate } ?: LocalDateTime.now(),
- null,
- assignedAddress.status
- )
+ AssignedAddressModel(
+ assignedAddress.id ?: null,
+ assignedAddress.uuid,
+ assignedAddress.address,
+ assignedAddress.memo,
+ assignedAddress.type.id,
+ assignedAddress.id?.let { assignedAddress.expTime }
+ ?: (addressLifeTime?.let { (LocalDateTime.now().plusSeconds(addressLifeTime!!)) }
+ ?: null),
+ assignedAddress.id?.let { assignedAddress.assignedDate } ?: LocalDateTime.now(),
+ null,
+ assignedAddress.status
+ )
).awaitFirstOrNull()
}
@@ -69,23 +69,24 @@ class AssignedAddressHandlerImpl(
override suspend fun revoke(assignedAddress: AssignedAddress) {
assignedAddressRepository.save(
- AssignedAddressModel(
- assignedAddress.id,
- assignedAddress.uuid,
- assignedAddress.address,
- assignedAddress.memo,
- assignedAddress.type.id,
- assignedAddress.expTime,
- assignedAddress.assignedDate,
- assignedAddress.revokedDate,
- assignedAddress.status
- )
+ AssignedAddressModel(
+ assignedAddress.id,
+ assignedAddress.uuid,
+ assignedAddress.address,
+ assignedAddress.memo,
+ assignedAddress.type.id,
+ assignedAddress.expTime,
+ assignedAddress.assignedDate,
+ assignedAddress.revokedDate,
+ assignedAddress.status
+ )
).awaitFirst()
}
override suspend fun findUuid(address: String, memo: String?): String? {
- return assignedAddressRepository.findByAddressAndMemoAndStatus(address, memo, AddressStatus.Assigned).awaitFirstOrNull()?.uuid
+ return assignedAddressRepository.findByAddressAndMemoAndStatus(address, memo, AddressStatus.Assigned)
+ .awaitFirstOrNull()?.uuid
}
override suspend fun fetchExpiredAssignedAddresses(): List? {
@@ -95,9 +96,9 @@ class AssignedAddressHandlerImpl(
}.collectMap { it.id }.awaitFirst()
//for having significant margin : (minus(5 mints)
return assignedAddressRepository.findPotentialExpAddress(
- (now.minusSeconds(lifeTime!!)).minusMinutes(5),
- now,
- AddressStatus.Assigned
+ (now.minusSeconds(addressLifeTime!!)).minusMinutes(5),
+ now,
+ AddressStatus.Assigned
)?.filter {
it.expTime != null
}?.map {
@@ -107,18 +108,18 @@ class AssignedAddressHandlerImpl(
private suspend fun AssignedAddressModel.toDto(addressTypeMap: MutableMap): AssignedAddress {
return AssignedAddress(
- this.uuid,
- this.address,
- this.memo,
- addressTypeMap.getValue(this.addressTypeId),
- assignedAddressChainRepository.findByAssignedAddress(this.id!!).map { cm ->
- chainLoader.fetchChainInfo(cm.chain)
- }.toList().toMutableList(),
- this.expTime,
- this.assignedDate,
- this.revokedDate,
- this.status,
- null
+ this.uuid,
+ this.address,
+ this.memo,
+ addressTypeMap.getValue(this.addressTypeId),
+ assignedAddressChainRepository.findByAssignedAddress(this.id!!).map { cm ->
+ chainLoader.fetchChainInfo(cm.chain)
+ }.toList().toMutableList(),
+ this.expTime,
+ this.assignedDate,
+ this.revokedDate,
+ this.status,
+ null
)
}
}
\ No newline at end of file
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/ChainHandler.kt b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/ChainHandler.kt
index 2bde53614..4114bdb01 100644
--- a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/ChainHandler.kt
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/ChainHandler.kt
@@ -7,6 +7,7 @@ import co.nilin.opex.bcgateway.ports.postgres.dao.AddressTypeRepository
import co.nilin.opex.bcgateway.ports.postgres.dao.ChainAddressTypeRepository
import co.nilin.opex.bcgateway.ports.postgres.dao.ChainRepository
import co.nilin.opex.bcgateway.ports.postgres.model.ChainAddressTypeModel
+import co.nilin.opex.common.OpexError
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.reactive.awaitFirst
@@ -14,7 +15,6 @@ import kotlinx.coroutines.reactive.awaitFirstOrElse
import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.reactive.awaitSingle
import org.springframework.stereotype.Component
-import co.nilin.opex.common.OpexError
@Component
class ChainHandler(
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/CurrencyHandlerImpl.kt b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/CurrencyHandlerImpl.kt
index f19a84256..e69de29bb 100644
--- a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/CurrencyHandlerImpl.kt
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/CurrencyHandlerImpl.kt
@@ -1,246 +0,0 @@
-package co.nilin.opex.bcgateway.ports.postgres.impl
-
-import co.nilin.opex.bcgateway.core.model.*
-import co.nilin.opex.bcgateway.core.spi.CurrencyHandler
-import co.nilin.opex.bcgateway.ports.postgres.dao.ChainRepository
-import co.nilin.opex.bcgateway.ports.postgres.dao.CurrencyImplementationRepository
-import co.nilin.opex.bcgateway.ports.postgres.dao.CurrencyRepository
-import co.nilin.opex.bcgateway.ports.postgres.model.CurrencyImplementationModel
-import co.nilin.opex.bcgateway.ports.postgres.model.CurrencyModel
-import co.nilin.opex.common.OpexError
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.toList
-import kotlinx.coroutines.reactive.awaitFirst
-import kotlinx.coroutines.reactive.awaitFirstOrElse
-import kotlinx.coroutines.reactive.awaitFirstOrNull
-import kotlinx.coroutines.reactive.awaitSingle
-import kotlinx.coroutines.reactor.awaitSingleOrNull
-import org.slf4j.LoggerFactory
-import org.springframework.stereotype.Component
-import java.math.BigDecimal
-
-@Component
-class CurrencyHandlerImpl(
- private val chainRepository: ChainRepository,
- private val currencyRepository: CurrencyRepository,
- private val currencyImplementationRepository: CurrencyImplementationRepository
-) : CurrencyHandler {
-
- private val logger = LoggerFactory.getLogger(CurrencyHandler::class.java)
-
- override suspend fun addCurrency(name: String, symbol: String) {
- try {
- currencyRepository.insert(name, symbol.uppercase()).awaitSingleOrNull()
- } catch (e: Exception) {
- logger.error("Could not insert new currency $name", e)
- }
- }
-
- override suspend fun addCurrencyImplementationV2(
- currencySymbol: String,
- implementationSymbol: String,
- currencyName: String,
- chain: String,
- tokenName: String?,
- tokenAddress: String?,
- isToken: Boolean,
- withdrawFee: BigDecimal,
- minimumWithdraw: BigDecimal,
- isWithdrawEnabled: Boolean,
- decimal: Int
- ): CurrencyImplementation {
- currencyRepository.findBySymbol(currencySymbol).awaitFirstOrNull()?.let {
- throw OpexError.CurrencyIsExist.exception()
- } ?: run {
- addCurrency(currencyName, currencySymbol)
- return addCurrencyImplementation(
- currencySymbol,
- implementationSymbol,
- chain,
- tokenName,
- tokenAddress,
- isToken,
- withdrawFee,
- minimumWithdraw,
- isWithdrawEnabled,
- decimal
- )
- }
- }
-
- override suspend fun updateCurrencyImplementation(
- currencySymbol: String,
- implementationSymbol: String,
- currencyName: String,
- newChain: String?,
- tokenName: String?,
- tokenAddress: String?,
- isToken: Boolean,
- withdrawFee: BigDecimal,
- minimumWithdraw: BigDecimal,
- isWithdrawEnabled: Boolean,
- decimal: Int,
- oldChain: String
- ): CurrencyImplementation? {
- currencyRepository.findBySymbol(currencySymbol).awaitFirstOrNull()?.let { cm ->
- currencyRepository.save(CurrencyModel(currencySymbol, currencyName)).awaitSingleOrNull()
- return currencyImplementationRepository.findByCurrencySymbolAndChain(currencySymbol, oldChain)
- ?.awaitSingleOrNull()
- ?.let {
- it.apply {
- this.implementationSymbol = implementationSymbol
- this.chain = newChain ?: oldChain
- this.decimal = decimal
- this.token = isToken
- this.tokenAddress = tokenAddress
- this.tokenName = tokenName
- this.withdrawEnabled = isWithdrawEnabled
- this.withdrawFee = withdrawFee
- this.withdrawMin = minimumWithdraw
- }
- currencyImplementationRepository.save(it).awaitSingleOrNull()
- ?.let { icm -> projectCurrencyImplementation(icm, cm) }
- }
-
- } ?: throw OpexError.CurrencyNotFound.exception()
- }
-
- override suspend fun editCurrency(name: String, symbol: String) {
- val currency = currencyRepository.findBySymbol(symbol).awaitFirstOrNull()
- if (currency != null) {
- currency.name = name
- currencyRepository.save(currency).awaitFirst()
- }
- }
-
- override suspend fun deleteCurrency(name: String) {
- try {
- currencyRepository.deleteByName(name).awaitFirstOrNull()
- } catch (e: Exception) {
- logger.error("Could not delete currency $name", e)
- }
- }
-
- override suspend fun addCurrencyImplementation(
- currencySymbol: String,
- implementationSymbol: String,
- chain: String,
- tokenName: String?,
- tokenAddress: String?,
- isToken: Boolean,
- withdrawFee: BigDecimal,
- minimumWithdraw: BigDecimal,
- isWithdrawEnabled: Boolean,
- decimal: Int
- ): CurrencyImplementation {
- val chainModel = chainRepository.findByName(chain).awaitFirstOrNull()
- ?: throw OpexError.ChainNotFound.exception()
-
- currencyImplementationRepository.findByCurrencySymbolAndChain(currencySymbol.uppercase(), chain)
- .awaitFirstOrNull()
- ?.let { throw OpexError.DuplicateToken.exception() }
-
- val currency = currencyRepository.findBySymbol(currencySymbol.uppercase()).awaitFirstOrNull()
- ?: throw OpexError.CurrencyNotFoundBC.exception()
-
- val model = currencyImplementationRepository.save(
- CurrencyImplementationModel(
- null,
- currencySymbol.uppercase(),
- implementationSymbol,
- chainModel.name,
- isToken,
- tokenAddress,
- tokenName,
- isWithdrawEnabled,
- withdrawFee,
- minimumWithdraw,
- decimal
- )
- ).awaitFirst()
-
- logger.info("Add currency implementation: ${model.currencySymbol} - ${model.chain}")
-
- return projectCurrencyImplementation(model, currency)
- }
-
- override suspend fun fetchAllImplementations(): List {
- return currencyImplementationRepository.findAll()
- .collectList()
- .awaitFirstOrElse { emptyList() }
- .map {
- val currency = currencyRepository.findBySymbol(it.currencySymbol).awaitFirstOrNull()
- projectCurrencyImplementation(it, currency)
- }
- }
-
- override suspend fun fetchCurrencyInfo(symbol: String): CurrencyInfo {
- val symbolUpperCase = symbol.uppercase()
- val currencyModel = currencyRepository.findBySymbol(symbolUpperCase).awaitSingleOrNull()
- if (currencyModel === null) {
- return CurrencyInfo(Currency("", symbolUpperCase), emptyList())
- }
- val currencyImplModel = currencyImplementationRepository.findByCurrencySymbol(symbolUpperCase)
- val currency = Currency(currencyModel.symbol, currencyModel.name)
- val implementations = currencyImplModel.map { projectCurrencyImplementation(it, currencyModel) }
- return CurrencyInfo(currency, implementations.toList())
- }
-
- override suspend fun findByChainAndTokenAddress(chain: String, address: String?): CurrencyImplementation? {
- val impl = currencyImplementationRepository.findByChainAndTokenAddress(chain, address)
- .awaitFirstOrNull()
-
- return if (impl != null)
- projectCurrencyImplementation(impl)
- else
- null
- }
-
- override suspend fun findImplementationsWithTokenOnChain(chain: String): List {
- return currencyImplementationRepository.findByChain(chain).map { projectCurrencyImplementation(it) }.toList()
- }
-
- override suspend fun findImplementationsByCurrency(currency: String): List {
- return currencyImplementationRepository.findByCurrencySymbol(currency)
- .map { projectCurrencyImplementation(it) }
- .toList()
- }
-
- override suspend fun changeWithdrawStatus(symbol: String, chain: String, status: Boolean) {
- val impl = currencyImplementationRepository.findByCurrencySymbolAndChain(symbol, chain).awaitSingleOrNull()
- ?: throw OpexError.TokenNotFound.exception()
-
- impl.apply {
- withdrawEnabled = status
- currencyImplementationRepository.save(impl).awaitFirstOrNull()
- }
- }
-
- override suspend fun getWithdrawData(symbol: String, network: String): WithdrawData {
- return currencyImplementationRepository.findWithdrawDataBySymbolAndChain(symbol, network)
- .awaitSingleOrNull() ?: throw OpexError.CurrencyNotFound.exception()
- }
-
- private suspend fun projectCurrencyImplementation(
- currencyImplementationModel: CurrencyImplementationModel,
- currencyModel: CurrencyModel? = null
- ): CurrencyImplementation {
- val addressTypesModel = chainRepository.findAddressTypesByName(currencyImplementationModel.chain)
- val addressTypes =
- addressTypesModel.map { AddressType(it.id!!, it.type, it.addressRegex, it.memoRegex) }.toList()
- val currencyModelVal =
- currencyModel ?: currencyRepository.findBySymbol(currencyImplementationModel.currencySymbol).awaitSingle()
- return CurrencyImplementation(
- Currency(currencyModelVal.symbol, currencyModelVal.name),
- Currency(currencyImplementationModel.implementationSymbol, currencyModelVal.name),
- Chain(currencyImplementationModel.chain, addressTypes),
- currencyImplementationModel.token,
- currencyImplementationModel.tokenAddress,
- currencyImplementationModel.tokenName,
- currencyImplementationModel.withdrawEnabled,
- currencyImplementationModel.withdrawFee,
- currencyImplementationModel.withdrawMin,
- currencyImplementationModel.decimal
- )
- }
-}
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/CurrencyHandlerImplV2.kt b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/CurrencyHandlerImplV2.kt
new file mode 100644
index 000000000..1745cfd42
--- /dev/null
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/CurrencyHandlerImplV2.kt
@@ -0,0 +1,121 @@
+package co.nilin.opex.bcgateway.ports.postgres.impl
+
+import co.nilin.opex.bcgateway.core.model.CryptoCurrencyCommand
+import co.nilin.opex.bcgateway.core.model.FetchGateways
+import co.nilin.opex.bcgateway.core.model.WithdrawData
+import co.nilin.opex.bcgateway.core.spi.CryptoCurrencyHandlerV2
+import co.nilin.opex.bcgateway.ports.postgres.dao.ChainRepository
+import co.nilin.opex.bcgateway.ports.postgres.dao.CurrencyImplementationRepository
+import co.nilin.opex.bcgateway.ports.postgres.model.CurrencyOnChainGatewayModel
+import co.nilin.opex.bcgateway.ports.postgres.util.toDto
+import co.nilin.opex.bcgateway.ports.postgres.util.toModel
+import co.nilin.opex.common.OpexError
+import kotlinx.coroutines.reactive.awaitFirstOrElse
+import kotlinx.coroutines.reactive.awaitFirstOrNull
+import kotlinx.coroutines.reactor.awaitSingleOrNull
+import org.slf4j.LoggerFactory
+import org.springframework.stereotype.Component
+import reactor.core.publisher.Flux
+import reactor.core.publisher.Mono
+import java.util.stream.Collectors
+
+@Component
+class CurrencyHandlerImplV2(
+ private val chainRepository: ChainRepository,
+ private val currencyImplementationRepository: CurrencyImplementationRepository
+) : CryptoCurrencyHandlerV2 {
+
+ private val logger = LoggerFactory.getLogger(CurrencyHandlerImplV2::class.java)
+
+ override suspend fun createOnChainGateway(request: CryptoCurrencyCommand): CryptoCurrencyCommand? {
+ chainRepository.findByName(request.chain)
+ ?.awaitFirstOrElse { throw OpexError.ChainNotFound.exception() }
+ currencyImplementationRepository.findGateways(
+ currencySymbol = request.currencySymbol,
+ chain = request.chain,
+ implementationSymbol = request.implementationSymbol
+ )
+ ?.awaitFirstOrNull()?.let { throw OpexError.GatewayIsExist.exception() }
+ return doSave(request.toModel())?.toDto();
+ }
+
+ override suspend fun updateOnChainGateway(request: CryptoCurrencyCommand): CryptoCurrencyCommand? {
+ return loadImpls(FetchGateways(gatewayUuid = request.gatewayUuid, currencySymbol = request.currencySymbol))
+ ?.awaitFirstOrElse { throw OpexError.GatewayNotFount.exception() }?.let { oldGateway ->
+ doSave(oldGateway.toDto().updateTo(request).toModel().apply { id = oldGateway.id })?.toDto()
+ }
+ }
+
+ override suspend fun deleteOnChainGateway(gatewayUuid: String, currency: String): Void? {
+
+ loadImpls(FetchGateways(gatewayUuid = gatewayUuid, currencySymbol = currency))
+ ?.awaitFirstOrElse { throw OpexError.GatewayNotFount.exception() }?.let {
+ try {
+ return currencyImplementationRepository.deleteByGatewayUuid(gatewayUuid)?.awaitFirstOrNull()
+ } catch (e: Exception) {
+ throw OpexError.BadRequest.exception()
+
+ }
+ }
+ return null
+ }
+
+ override suspend fun fetchCurrencyOnChainGateways(data: FetchGateways?): List? {
+ logger.info("going to fetch impls of ${data?.currencySymbol ?: "all currencies"}")
+ return loadImpls(data)?.map { it.toDto() }
+ ?.collect(Collectors.toList())?.awaitFirstOrNull()
+ }
+
+ override suspend fun fetchOnChainGateway(gatewayUuid: String, symbol: String): CryptoCurrencyCommand? {
+ return loadImpl(gatewayUuid, symbol)?.awaitFirstOrNull()?.toDto()
+ }
+
+ private suspend fun loadImpls(request: FetchGateways?): Flux? {
+ var resp = currencyImplementationRepository.findGateways(
+ request?.currencySymbol,
+ request?.gatewayUuid,
+ request?.chain,
+ request?.currencyImplementationName
+ )
+ return resp
+ ?: throw OpexError.ImplNotFound.exception()
+ }
+
+ private suspend fun loadImpl(gateway: String, symbol: String): Mono? {
+ return currencyImplementationRepository.findByGatewayUuidAndCurrencySymbol(gateway, symbol)
+ ?: throw OpexError.ImplNotFound.exception()
+ }
+
+ private suspend fun doSave(request: CurrencyOnChainGatewayModel): CurrencyOnChainGatewayModel? {
+ return currencyImplementationRepository.save(request).awaitSingleOrNull()
+ }
+
+ override suspend fun changeWithdrawStatus(symbol: String, chain: String, status: Boolean) {
+ val onChainGateway =
+ currencyImplementationRepository.findByCurrencySymbolAndChain(symbol, chain).awaitSingleOrNull()
+ ?: throw OpexError.TokenNotFound.exception()
+
+ onChainGateway.apply {
+ withdrawAllowed = status
+ currencyImplementationRepository.save(onChainGateway).awaitFirstOrNull()
+ }
+ }
+
+ override suspend fun getWithdrawData(symbol: String, network: String): WithdrawData {
+ return currencyImplementationRepository.findWithdrawDataBySymbolAndChain(symbol, network)
+ .awaitSingleOrNull() ?: throw OpexError.CurrencyNotFound.exception()
+ }
+
+ override suspend fun fetchGatewayWithoutSymbol(
+ chain: String,
+ isToken: Boolean,
+ tokenAddress: String?
+ ): CryptoCurrencyCommand? {
+ chainRepository.findByName(chain).awaitFirstOrElse { throw OpexError.ChainNotFound.exception() }
+
+ return if (isToken)
+ currencyImplementationRepository.findTokenGateway(chain, tokenAddress!!).awaitSingleOrNull()?.toDto()
+ else
+ currencyImplementationRepository.findMainAssetGateway(chain).awaitSingleOrNull()?.toDto()
+ }
+}
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/DepositHandlerImpl.kt b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/DepositHandlerImpl.kt
index 9e475a2f3..839f2ecba 100644
--- a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/DepositHandlerImpl.kt
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/impl/DepositHandlerImpl.kt
@@ -7,15 +7,15 @@ import co.nilin.opex.bcgateway.ports.postgres.model.DepositModel
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.reactor.awaitSingle
-import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.springframework.stereotype.Component
@Component
class DepositHandlerImpl(private val depositRepository: DepositRepository) : DepositHandler {
+
override suspend fun findDepositsByHash(hash: List): List {
return depositRepository.findAllByHash(hash).map {
Deposit(
- it.id, it.hash, it.depositor, it.depositorMemo, it.amount, it.chain, it.token, it.tokenAddress
+ it.id, it.hash, it.depositor, it.depositorMemo, it.amount, it.chain, it.token, it.tokenAddress
)
}.toList()
}
@@ -23,11 +23,24 @@ class DepositHandlerImpl(private val depositRepository: DepositRepository) : Dep
override suspend fun saveAll(deposits: List) {
depositRepository.saveAll(deposits.map {
DepositModel(
- null, it.hash, it.depositor, it.depositorMemo, it.amount, it.chain, it.token, it.tokenAddress
+ null, it.hash, it.depositor, it.depositorMemo, it.amount, it.chain, it.token, it.tokenAddress
)
}).collectList().awaitSingle()
}
-
+ override suspend fun save(deposit: Deposit) {
+ depositRepository.save(
+ DepositModel(
+ null,
+ deposit.hash,
+ deposit.depositor,
+ deposit.depositorMemo,
+ deposit.amount,
+ deposit.chain,
+ deposit.token,
+ deposit.tokenAddress
+ )
+ ).awaitSingle()
+ }
}
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/model/AssignedAddressModel.kt b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/model/AssignedAddressModel.kt
index 3494e4c79..f35480806 100644
--- a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/model/AssignedAddressModel.kt
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/model/AssignedAddressModel.kt
@@ -8,16 +8,16 @@ import java.time.LocalDateTime
@Table("assigned_addresses")
data class AssignedAddressModel(
- @Id val id: Long?,
- val uuid: String,
- val address: String,
- val memo: String?,
- @Column("addr_type_id") val addressTypeId: Long,
- @Column("exp_time") val expTime: LocalDateTime?=null,
- @Column("assigned_Date") val assignedDate: LocalDateTime?=null,
- @Column("revoked_Date") val revokedDate: LocalDateTime?=null,
- val status: AddressStatus?=null,
+ @Id val id: Long?,
+ val uuid: String,
+ val address: String,
+ val memo: String?,
+ @Column("addr_type_id") val addressTypeId: Long,
+ @Column("exp_time") val expTime: LocalDateTime? = null,
+ @Column("assigned_Date") val assignedDate: LocalDateTime? = null,
+ @Column("revoked_Date") val revokedDate: LocalDateTime? = null,
+ val status: AddressStatus? = null,
-)
+ )
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/model/CurrencyImplementationModel.kt b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/model/CurrencyImplementationModel.kt
deleted file mode 100644
index 4a17aa2b6..000000000
--- a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/model/CurrencyImplementationModel.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package co.nilin.opex.bcgateway.ports.postgres.model
-
-
-import org.springframework.data.annotation.Id
-import org.springframework.data.relational.core.mapping.Column
-import org.springframework.data.relational.core.mapping.Table
-import java.math.BigDecimal
-
-@Table("currency_implementations")
-class CurrencyImplementationModel(
- @Id var id: Long?,
- @Column("currency_symbol") val currencySymbol: String,
- @Column("implementation_symbol") var implementationSymbol: String,
- @Column("chain") var chain: String,
- @Column("token") var token: Boolean,
- @Column("token_address") var tokenAddress: String?,
- @Column("token_name") var tokenName: String?,
- @Column("withdraw_enabled") var withdrawEnabled: Boolean,
- @Column("withdraw_fee") var withdrawFee: BigDecimal,
- @Column("withdraw_min") var withdrawMin: BigDecimal,
- @Column("decimal") var decimal: Int
-)
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/model/CurrencyOnChainGatewayModel.kt b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/model/CurrencyOnChainGatewayModel.kt
new file mode 100644
index 000000000..c954a3540
--- /dev/null
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/model/CurrencyOnChainGatewayModel.kt
@@ -0,0 +1,33 @@
+package co.nilin.opex.bcgateway.ports.postgres.model
+
+
+import org.springframework.data.annotation.Id
+import org.springframework.data.relational.core.mapping.Column
+import org.springframework.data.relational.core.mapping.Table
+import java.math.BigDecimal
+
+@Table("currency_on_chain_gateway")
+class CurrencyOnChainGatewayModel(
+ @Id var id: Long?,
+ @Column("gateway_uuid") val gatewayUuid: String,
+ @Column("currency_symbol") val currencySymbol: String,
+ @Column("implementation_symbol") var implementationSymbol: String? = currencySymbol,
+ @Column("chain") var chain: String,
+ @Column("is_token") var isToken: Boolean? = false,
+ @Column("token_address") var tokenAddress: String? = null,
+ @Column("token_name") var tokenName: String? = null,
+ @Column("withdraw_allowed") var withdrawAllowed: Boolean,
+ @Column("deposit_allowed") var depositAllowed: Boolean,
+ @Column("withdraw_fee") var withdrawFee: BigDecimal,
+ @Column("withdraw_min") var withdrawMin: BigDecimal? = BigDecimal.ZERO,
+ @Column("withdraw_max") var withdrawMax: BigDecimal? = BigDecimal.ZERO,
+ @Column("deposit_min") var depositMin: BigDecimal? = BigDecimal.ZERO,
+ @Column("deposit_max") var depositMax: BigDecimal? = BigDecimal.ZERO, @Column("decimal") var decimal: Int,
+ @Column("is_active") var isActive: Boolean? = true,
+
+
+ )
+
+
+
+
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/model/DepositModel.kt b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/model/DepositModel.kt
index 6b6c411a2..070f9b746 100644
--- a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/model/DepositModel.kt
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/model/DepositModel.kt
@@ -4,7 +4,6 @@ import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.math.BigDecimal
-import java.time.LocalDateTime
@Table("deposits")
data class DepositModel(
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/model/NewCurrencyImplementationModel.kt b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/model/NewCurrencyImplementationModel.kt
new file mode 100644
index 000000000..2cd08b2ce
--- /dev/null
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/model/NewCurrencyImplementationModel.kt
@@ -0,0 +1,28 @@
+//package co.nilin.opex.bcgateway.ports.postgres.model
+//
+//
+//import org.springframework.data.annotation.Id
+//import org.springframework.data.relational.core.mapping.Column
+//import org.springframework.data.relational.core.mapping.Table
+//import java.math.BigDecimal
+//import java.util.*
+//
+//@Table("new_currency_implementations")
+//class NewCurrencyImplementationModel(
+// @Id var id: Long?,
+// @Column("currency_uuid") val currencyUuid: String,
+// //todo unique
+// @Column("uuid") val currencyImplUuid: String,
+// @Column("implementation_symbol") var implementationSymbol: String,
+// @Column("chain") var chain: String,
+// @Column("is_token") var isToken: Boolean?=false,
+// @Column("is_active") var isActive: Boolean?=true,
+// @Column("token_address") var tokenAddress: String?,
+// @Column("token_name") var tokenName: String?,
+// @Column("withdraw_is_enable") var withdrawIsEnable: Boolean?=true,
+// @Column("withdraw_fee") var withdrawFee: BigDecimal,
+// @Column("withdraw_min") var withdrawMin: BigDecimal,
+// @Column("decimal") var decimal: Int,
+// @Column("deposit_is_enable") var depositIsEnable: Boolean? = true
+//
+//)
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/util/convertor.kt b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/util/convertor.kt
new file mode 100644
index 000000000..3123e9d74
--- /dev/null
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/util/convertor.kt
@@ -0,0 +1,50 @@
+package co.nilin.opex.bcgateway.ports.postgres.util
+
+import co.nilin.opex.bcgateway.core.model.CryptoCurrencyCommand
+import co.nilin.opex.bcgateway.ports.postgres.model.CurrencyOnChainGatewayModel
+
+
+fun CryptoCurrencyCommand.toModel(): CurrencyOnChainGatewayModel {
+ return CurrencyOnChainGatewayModel(
+ null, gatewayUuid!!,
+ currencySymbol,
+ implementationSymbol,
+ chain,
+ isToken,
+ tokenAddress,
+ tokenName,
+ withdrawAllowed!!,
+ depositAllowed!!,
+ withdrawFee!!,
+ withdrawMin,
+ withdrawMax,
+ depositMin,
+ depositMax,
+ decimal,
+ isActive
+ )
+}
+
+fun CurrencyOnChainGatewayModel.toDto(): CryptoCurrencyCommand {
+
+ return CryptoCurrencyCommand(
+ currencySymbol,
+ gatewayUuid!!,
+ implementationSymbol,
+ isActive,
+ isToken,
+ tokenName,
+ tokenAddress,
+ withdrawFee,
+ withdrawAllowed,
+ depositAllowed,
+ withdrawMin,
+ withdrawMax,
+ depositMin,
+ depositMax,
+ decimal,
+ chain
+ )
+
+}
+
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/resources/schema.sql b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/resources/schema.sql
index 2bbbb39af..1fb51670b 100644
--- a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/resources/schema.sql
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/resources/schema.sql
@@ -1,3 +1,5 @@
+CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+
CREATE TABLE IF NOT EXISTS address_types
(
id SERIAL PRIMARY KEY,
@@ -8,24 +10,18 @@ CREATE TABLE IF NOT EXISTS address_types
CREATE TABLE IF NOT EXISTS assigned_addresses
(
- id SERIAL PRIMARY KEY,
- uuid VARCHAR(72) NOT NULL,
- address VARCHAR(72) NOT NULL,
- memo VARCHAR(72) NOT NULL,
- addr_type_id INTEGER NOT NULL REFERENCES address_types (id),
+ id SERIAL PRIMARY KEY,
+ uuid VARCHAR(72) NOT NULL,
+ address VARCHAR(72) NOT NULL,
+ memo VARCHAR(72) NOT NULL,
+ addr_type_id INTEGER NOT NULL REFERENCES address_types (id),
assigned_date TIMESTAMP,
- revoked_date TIMESTAMP,
- status VARCHAR(25),
- exp_time TIMESTAMP,
+ revoked_date TIMESTAMP,
+ status VARCHAR(25),
+ exp_time TIMESTAMP,
UNIQUE (address, memo, exp_time)
);
-ALTER TABLE assigned_addresses ADD COLUMN IF NOT EXISTS assigned_date TIMESTAMP;
-ALTER TABLE assigned_addresses ADD COLUMN IF NOT EXISTS revoked_date TIMESTAMP;
-ALTER TABLE assigned_addresses ADD COLUMN IF NOT EXISTS exp_time TIMESTAMP;
-ALTER TABLE assigned_addresses ADD COLUMN IF NOT EXISTS status VARCHAR(25);
-
-
CREATE TABLE IF NOT EXISTS reserved_addresses
@@ -44,51 +40,59 @@ CREATE TABLE IF NOT EXISTS chains
CREATE TABLE IF NOT EXISTS assigned_address_chains
(
- id SERIAL PRIMARY KEY,
+ id SERIAL PRIMARY KEY,
assigned_address_id INTEGER NOT NULL REFERENCES assigned_addresses (id),
chain VARCHAR(72) NOT NULL REFERENCES chains (name)
);
CREATE TABLE IF NOT EXISTS chain_address_types
(
- id SERIAL PRIMARY KEY,
+ id SERIAL PRIMARY KEY,
chain_name VARCHAR(72) NOT NULL REFERENCES chains (name),
addr_type_id INTEGER NOT NULL REFERENCES address_types (id),
UNIQUE (chain_name, addr_type_id)
);
-CREATE TABLE IF NOT EXISTS currency
-(
- symbol VARCHAR(72) PRIMARY KEY,
- name VARCHAR(72) NOT NULL
-);
+-- CREATE TABLE IF NOT EXISTS currency
+-- (
+-- symbol VARCHAR(72) PRIMARY KEY,
+-- name VARCHAR(72) NOT NULL
+-- );
-CREATE TABLE IF NOT EXISTS currency_implementations
+CREATE TABLE IF NOT EXISTS currency_on_chain_gateway
(
id SERIAL PRIMARY KEY,
- currency_symbol VARCHAR(72) NOT NULL REFERENCES currency (symbol),
- implementation_symbol VARCHAR(72) NOT NULL,
- chain VARCHAR(72) NOT NULL REFERENCES chains (name),
- token BOOLEAN NOT NULL,
+ currency_symbol VARCHAR(72) NOT NULL,
+ implementation_symbol VARCHAR(72) NOT NULL,
+ gateway_uuid VARCHAR(256) NOT NULL UNIQUE DEFAULT uuid_generate_v4(),
+ chain VARCHAR(72) NOT NULL REFERENCES chains (name),
+ is_token BOOLEAN NOT NULL,
token_address VARCHAR(72),
token_name VARCHAR(72),
- withdraw_enabled BOOLEAN NOT NULL,
- withdraw_fee DECIMAL NOT NULL,
- withdraw_min DECIMAL NOT NULL,
- decimal INTEGER NOT NULL,
+ withdraw_allowed BOOLEAN NOT NULL,
+ deposit_allowed BOOLEAN NOT NULL,
+ withdraw_fee DECIMAL NOT NULL,
+ withdraw_min DECIMAL NOT NULL,
+ withdraw_max DECIMAL NOT NULL,
+ deposit_min DECIMAL NOT NULL,
+ deposit_max DECIMAL NOT NULL,
+ decimal INTEGER NOT NULL,
+ is_active BOOLEAN NOT NULL DEFAULT TRUE,
UNIQUE (currency_symbol, chain, implementation_symbol)
);
+
+
CREATE TABLE IF NOT EXISTS deposits
(
- id SERIAL PRIMARY KEY,
- hash VARCHAR(100) UNIQUE NOT NULL,
- chain VARCHAR(72) NOT NULL REFERENCES chains (name),
- token BOOLEAN NOT NULL,
- token_address VARCHAR(72),
- amount DECIMAL NOT NULL,
- depositor VARCHAR(72) NOT NULL,
- depositor_memo VARCHAR(72)
+ id SERIAL PRIMARY KEY,
+ hash VARCHAR(100) UNIQUE NOT NULL,
+ chain VARCHAR(72) NOT NULL REFERENCES chains (name),
+ token BOOLEAN NOT NULL,
+ token_address VARCHAR(72),
+ amount DECIMAL NOT NULL,
+ depositor VARCHAR(72) NOT NULL,
+ depositor_memo VARCHAR(72)
);
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-wallet-proxy/pom.xml b/bc-gateway/bc-gateway-ports/bc-gateway-wallet-proxy/pom.xml
index 3bc027eee..401349381 100644
--- a/bc-gateway/bc-gateway-ports/bc-gateway-wallet-proxy/pom.xml
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-wallet-proxy/pom.xml
@@ -1,6 +1,6 @@
-
4.0.0
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-wallet-proxy/src/main/kotlin/co/nilin/opex/bcgateway/ports/walletproxy/impl/ExtractBackgroundAuth.kt b/bc-gateway/bc-gateway-ports/bc-gateway-wallet-proxy/src/main/kotlin/co/nilin/opex/bcgateway/ports/walletproxy/impl/ExtractBackgroundAuth.kt
index 048392ca6..77308d779 100644
--- a/bc-gateway/bc-gateway-ports/bc-gateway-wallet-proxy/src/main/kotlin/co/nilin/opex/bcgateway/ports/walletproxy/impl/ExtractBackgroundAuth.kt
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-wallet-proxy/src/main/kotlin/co/nilin/opex/bcgateway/ports/walletproxy/impl/ExtractBackgroundAuth.kt
@@ -4,13 +4,10 @@ package co.nilin.opex.bcgateway.ports.walletproxy.impl
import co.nilin.opex.bcgateway.core.model.otc.LoginRequest
import co.nilin.opex.bcgateway.core.spi.AuthProxy
import org.springframework.beans.factory.annotation.Value
-import org.springframework.context.annotation.Profile
import org.springframework.core.env.Environment
-
import org.springframework.stereotype.Component
-
@Component
class ExtractBackgroundAuth(private val authProxy: AuthProxy, private val environment: Environment) {
@@ -29,7 +26,6 @@ class ExtractBackgroundAuth(private val authProxy: AuthProxy, private val enviro
}
-
//save for config Reactive Security context instead of using api
diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-wallet-proxy/src/main/kotlin/co/nilin/opex/bcgateway/ports/walletproxy/impl/WalletProxyImpl.kt b/bc-gateway/bc-gateway-ports/bc-gateway-wallet-proxy/src/main/kotlin/co/nilin/opex/bcgateway/ports/walletproxy/impl/WalletProxyImpl.kt
index 1f824d9a5..7e46a51ab 100644
--- a/bc-gateway/bc-gateway-ports/bc-gateway-wallet-proxy/src/main/kotlin/co/nilin/opex/bcgateway/ports/walletproxy/impl/WalletProxyImpl.kt
+++ b/bc-gateway/bc-gateway-ports/bc-gateway-wallet-proxy/src/main/kotlin/co/nilin/opex/bcgateway/ports/walletproxy/impl/WalletProxyImpl.kt
@@ -3,7 +3,6 @@ package co.nilin.opex.bcgateway.ports.walletproxy.impl
import co.nilin.opex.bcgateway.core.spi.WalletProxy
import co.nilin.opex.bcgateway.ports.walletproxy.model.TransferResult
import kotlinx.coroutines.reactive.awaitFirst
-import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.beans.factory.annotation.Value
import org.springframework.core.ParameterizedTypeReference
import org.springframework.stereotype.Component
@@ -14,38 +13,36 @@ import java.net.URI
inline fun typeRef(): ParameterizedTypeReference = object : ParameterizedTypeReference() {}
@Component
-class WalletProxyImpl(private val webClient: WebClient,
- private val extractBackgroundAuth: ExtractBackgroundAuth) : WalletProxy {
+class WalletProxyImpl(
+ private val webClient: WebClient,
+ private val extractBackgroundAuth: ExtractBackgroundAuth
+) : WalletProxy {
@Value("\${app.wallet.url}")
private lateinit var baseUrl: String
-// override suspend fun transfer(uuid: String, symbol: String, amount: BigDecimal, hash: String) {
-// webClient.post()
-// .uri(URI.create("$baseUrl/deposit/${amount}_${symbol}/${uuid}_main?transferRef=$hash"))
-// .header("Content-Type", "application/json")
-// .header("Authorization", "Bearer ${extractBackgroundAuth.extractToken()}")
-// .retrieve()
-// .onStatus({ t -> t.isError }, { it.createException() })
-// .bodyToMono(typeRef())
-// .awaitFirst()
-// }
-
- override suspend fun transfer(uuid: String, symbol: String, amount: BigDecimal, hash: String, chain: String) {
+ override suspend fun transfer(
+ uuid: String,
+ symbol: String,
+ amount: BigDecimal,
+ hash: String,
+ chain: String,
+ gatewayUuid: String?
+ ) {
val token = extractBackgroundAuth.extractToken()
webClient.post()
- .uri(URI.create("$baseUrl/deposit/${amount}_${chain}_${symbol}/${uuid}_MAIN?transferRef=$hash"))
- .headers { httpHeaders ->
- run {
- httpHeaders.add("Content-Type", "application/json");
- token?.let { httpHeaders.add("Authorization", "Bearer $it") }
- }
+ .uri(URI.create("$baseUrl/deposit/${amount}_${chain}_${symbol}/${uuid}_MAIN?transferRef=$hash&gatewayUuid=$gatewayUuid"))
+ .headers { httpHeaders ->
+ run {
+ httpHeaders.add("Content-Type", "application/json");
+ token?.let { httpHeaders.add("Authorization", "Bearer $it") }
}
- .retrieve()
- .onStatus({ t -> t.isError }, { it.createException() })
- .bodyToMono(typeRef())
- .awaitFirst()
+ }
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToMono(typeRef())
+ .awaitFirst()
}
}
diff --git a/bc-gateway/pom.xml b/bc-gateway/pom.xml
index cc2ddb897..ee39af0a8 100644
--- a/bc-gateway/pom.xml
+++ b/bc-gateway/pom.xml
@@ -1,5 +1,5 @@
-
core
@@ -20,6 +20,7 @@
bc-gateway-ports/bc-gateway-persister-postgres
bc-gateway-ports/bc-gateway-wallet-proxy
bc-gateway-ports/bc-gateway-auth-proxy
+ bc-gateway-ports/bc-gateway-omniwallet-proxy
bc-gateway-ports/bc-gateway-eventlistener-kafka
@@ -56,6 +57,11 @@
bc-gateway-auth-proxy
${project.version}
+
+ co.nilin.opex.bcgateway.ports.omniwallet
+ bc-gateway-omniwallet-proxy
+ ${project.version}
+
co.nilin.opex.bcgateway.ports.kafka.listener
bc-gateway-eventlistener-kafka
@@ -71,12 +77,6 @@
interceptors
${interceptor.version}
-
- co.nilin.opex.utility
- preferences
- ${preferences.version}
-
-
org.springframework.batch
spring-batch-core
diff --git a/common/pom.xml b/common/pom.xml
index ce854d4a3..98592f44e 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -1,6 +1,6 @@
-
4.0.0
@@ -28,6 +28,10 @@
co.nilin.opex.utility
error-handler
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
+
diff --git a/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt b/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt
index 5e3b1d774..33b7e7711 100644
--- a/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt
+++ b/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt
@@ -28,6 +28,8 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus
// code 4000: matching-gateway
SubmitOrderForbiddenByAccountant(4001, null, HttpStatus.BAD_REQUEST),
+ InvalidOrderType(4002, "Invalid order type", HttpStatus.BAD_REQUEST),
+ InvalidQuantity(4003, "Invalid quantity", HttpStatus.BAD_REQUEST),
// code 5000: user-management
EmailAlreadyVerified(5001, "Email is already verified", HttpStatus.BAD_REQUEST),
@@ -39,9 +41,16 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus
AlreadyInKYC(5007, "KYC flow for this user has executed", HttpStatus.BAD_REQUEST),
UserKYCBlocked(5008, "User is blocked from KYC", HttpStatus.BAD_REQUEST),
InvalidPassword(5009, "Password is not valid", HttpStatus.BAD_REQUEST),
- UserAlreadyExists(5009, "User with email is already registered", HttpStatus.BAD_REQUEST),
+ UserAlreadyExists(5009, "User is already registered", HttpStatus.BAD_REQUEST),
LoginIsLimited(5010, "Your email is not in whitelist", HttpStatus.BAD_REQUEST),
RegisterIsLimited(5011, "Your email is not in whitelist", HttpStatus.BAD_REQUEST),
+ GmailNotFoundInToken(5012, "Email not found in Google token", HttpStatus.NOT_FOUND),
+ UserIDNotFoundInToken(5013, "Google user ID (sub) not found in token", HttpStatus.NOT_FOUND),
+ InvalidUsername(5014, "Invalid username", HttpStatus.BAD_REQUEST),
+ InvalidUserCredentials(5015, "Invalid user credentials", HttpStatus.BAD_REQUEST),
+ InvalidRegisterToken(5016, "Invalid register token", HttpStatus.BAD_REQUEST),
+ ExpiredOTP(5017, "OTP is expired", HttpStatus.BAD_REQUEST),
+
// code 6000: wallet
WalletOwnerNotFound(6001, null, HttpStatus.NOT_FOUND),
@@ -71,6 +80,27 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus
WithdrawAmountLessThanMinimum(6025, "Withdraw amount is less than minimum", HttpStatus.BAD_REQUEST),
WithdrawCannotBeCanceled(6026, "Withdraw cannot be canceled", HttpStatus.BAD_REQUEST),
WithdrawCannotBeRejected(6027, "Withdraw cannot be rejected", HttpStatus.BAD_REQUEST),
+ WithdrawAmountGreaterThanMaximum(6028, "Withdraw amount is more than maximum", HttpStatus.BAD_REQUEST),
+ ImplNotFound(6029, null, HttpStatus.NOT_FOUND),
+ InvalidWithdrawStatus(6030, "Withdraw status is invalid", HttpStatus.NOT_FOUND),
+ GatewayNotFount(6031, null, HttpStatus.NOT_FOUND),
+ GatewayIsExist(6032, null, HttpStatus.NOT_FOUND),
+ InvalidDeposit(6033, "Invalid deposit", HttpStatus.BAD_REQUEST),
+ TerminalIsExist(6034, "This identifier is exist", HttpStatus.BAD_REQUEST),
+ TerminalNotFound(6035, "Object not found", HttpStatus.BAD_REQUEST),
+ VoucherNotFound(6036, "Voucher not found", HttpStatus.NOT_FOUND),
+ InvalidVoucher(6037, "Invalid Voucher", HttpStatus.BAD_REQUEST),
+ PairIsNotAvailable(6038, "Pair is not available", HttpStatus.BAD_REQUEST),
+ VoucherGroupNotFound(6039, "Voucher Group not found", HttpStatus.NOT_FOUND),
+ VoucherGroupIsInactive(6040, "Voucher Group is inactive", HttpStatus.BAD_REQUEST),
+ VoucherAlreadyUsed(6041, "Voucher has already been used", HttpStatus.BAD_REQUEST),
+ VoucherExpired(6042, "Voucher has expired", HttpStatus.BAD_REQUEST),
+ VoucherSaleDataNotFound(6043, "Voucher sale data not found", HttpStatus.NOT_FOUND),
+ VoucherNotForSale(6044, "Voucher not for sale", HttpStatus.BAD_REQUEST),
+ VoucherUsageLimitExceeded(6045, "Voucher usage limit exceeded", HttpStatus.BAD_REQUEST),
+ InvalidMaximumAmount(6046, "Invalid maximum amount", HttpStatus.BAD_REQUEST),
+ InvalidMinimumAmount(6047, "Invalid minimum amount", HttpStatus.BAD_REQUEST),
+
// code 7000: api
OrderNotFound(7001, "No order found", HttpStatus.NOT_FOUND),
@@ -95,6 +125,26 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus
// code 11000: market
+ // code 12000: otp
+ OTPConfigNotFound(12001, "Config for otp type not found", HttpStatus.NOT_FOUND),
+ UnableToSendOTP(12002, "Unable to send OTP code to the receiver", HttpStatus.INTERNAL_SERVER_ERROR),
+ OTPAlreadyRequested(12003, "OTP code is already requested for the receiver and OTP type", HttpStatus.BAD_REQUEST),
+ TOTPNotFound(12004, "TOTP for the requested user not found", HttpStatus.NOT_FOUND),
+ InvalidTOTPCode(12005, "TOTP code is invalid", HttpStatus.BAD_REQUEST),
+ TOTPSetupIncomplete(12006, "TOTP setup is incomplete", HttpStatus.BAD_REQUEST),
+ TOTPAlreadyRegistered(12007, "User already registered for TOTP", HttpStatus.BAD_REQUEST),
+ OTPDisabled(12008, "OTP for this receiver type is disabled", HttpStatus.INTERNAL_SERVER_ERROR),
+
+
+ //code 12000 profile
+ UserIdAlreadyExists(130001, "User with this id or email is already registered", HttpStatus.BAD_REQUEST),
+ InvalidLinkedAccount(130002, "Irrelevant account", HttpStatus.BAD_REQUEST),
+ AccountNotFound(130003, " Account not found", HttpStatus.BAD_REQUEST),
+ DuplicateAccount(130004, " Duplicate account", HttpStatus.BAD_REQUEST),
+ InvalidIban(130005, " Invalid iban number", HttpStatus.BAD_REQUEST),
+ InvalidCard(130006, " Invalid card number", HttpStatus.BAD_REQUEST),
+ VerificationFailed(130007, "Verification Failed", HttpStatus.BAD_REQUEST),
+ ProfileApprovalRequestAlreadyExists(130008, "Request Already Exists", HttpStatus.BAD_REQUEST),
;
override fun code() = this.code
diff --git a/common/src/main/kotlin/co/nilin/opex/common/security/CustomJwtAuthConverter.kt b/common/src/main/kotlin/co/nilin/opex/common/security/CustomJwtAuthConverter.kt
new file mode 100644
index 000000000..a08de7b5e
--- /dev/null
+++ b/common/src/main/kotlin/co/nilin/opex/common/security/CustomJwtAuthConverter.kt
@@ -0,0 +1,25 @@
+package co.nilin.opex.common.security
+
+import org.springframework.core.convert.converter.Converter
+import org.springframework.security.authentication.AbstractAuthenticationToken
+import org.springframework.security.core.GrantedAuthority
+import org.springframework.security.core.authority.SimpleGrantedAuthority
+import org.springframework.security.oauth2.jwt.Jwt
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
+import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter
+import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtGrantedAuthoritiesConverterAdapter
+import reactor.core.publisher.Mono
+
+class ReactiveCustomJwtConverter : Converter> {
+
+ override fun convert(source: Jwt): Mono {
+ val permissions = source.getClaimAsStringList("permissions")
+ ?.map { SimpleGrantedAuthority("PERM_${it}") }
+ ?.toList() ?: emptyList()
+ val roles = source.getClaimAsStringList("roles")
+ ?.map { SimpleGrantedAuthority("ROLE_${it}") }
+ ?.toList() ?: emptyList()
+ return Mono.just(JwtAuthenticationToken(source, roles + permissions))
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/co/nilin/opex/common/utils/CustomErrorTranslator.kt b/common/src/main/kotlin/co/nilin/opex/common/utils/CustomErrorTranslator.kt
new file mode 100644
index 000000000..d7d6bab05
--- /dev/null
+++ b/common/src/main/kotlin/co/nilin/opex/common/utils/CustomErrorTranslator.kt
@@ -0,0 +1,22 @@
+package co.nilin.opex.common.utils
+
+import co.nilin.opex.utility.error.data.DefaultExceptionResponse
+import co.nilin.opex.utility.error.data.OpexException
+import co.nilin.opex.utility.error.spi.ErrorTranslator
+import co.nilin.opex.utility.error.spi.ExceptionResponse
+import org.springframework.context.MessageSource
+import java.util.*
+
+
+class CustomErrorTranslator(private val messageSource: MessageSource) : ErrorTranslator {
+ override fun translate(ex: OpexException): ExceptionResponse {
+ return DefaultExceptionResponse(
+ ex.error.errorName(),
+ ex.error.code(),
+ messageSource.getMessage(ex.error.errorName().toString(), null, "", Locale("fa")),
+ ex.status ?: ex.error.status(),
+ ex.data,
+ ex.crimeScene
+ )
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/resources/messages_en.properties b/common/src/main/resources/messages_en.properties
new file mode 100644
index 000000000..0837188fd
--- /dev/null
+++ b/common/src/main/resources/messages_en.properties
@@ -0,0 +1,76 @@
+# Generic errors
+Error=Generic error
+InternalServerError=Internal server error
+BadRequest=Bad request
+UnAuthorized=Unauthorized
+Forbidden=Forbidden
+NotFound=Not found
+ServiceUnavailable=Service unavailable
+# Parameter errors
+InvalidRequestParam=Parameter '%s' is either missing or invalid
+InvalidRequestBody=Request body is invalid
+NoRecordFound=No record found for this service
+# Accountant errors
+InvalidPair=%s is not available
+InvalidPairFee=%s fee is not available
+PairFeeNotFound=No fee for requested pair found
+# Matching-engine errors (code block omitted)
+# Matching-gateway errors
+SubmitOrderForbiddenByAccountant=Submitting order is forbidden by accountant
+# User-management errors
+EmailAlreadyVerified=Email is already verified
+GroupNotFound=Group not found
+OTPAlreadyEnabled=2FA/OTP already configured
+UserNotFound=User not found
+InvalidOTP=Invalid OTP
+OTPRequired=OTP Required
+AlreadyInKYC=KYC flow for this user has executed
+UserKYCBlocked=User is blocked from KYC
+InvalidPassword=Password is not valid
+UserAlreadyExists=User with email is already registered
+LoginIsLimited=Your email is not in whitelist
+RegisterIsLimited=Your email is not in whitelist
+# Wallet errors
+WalletOwnerNotFound=Wallet owner not found
+WalletNotFound=Wallet not found
+CurrencyNotFound=Currency not found
+InvalidCashOutUsage=Use withdraw services
+WithdrawNotFound=Withdraw not found
+NOT_EXCHANGEABLE_CURRENCIES=These two currencies can't be exchanged
+CurrencyIsExist=Currency already exists
+PairIsExist=Pair already exists
+ForbiddenPair=Forbidden pair
+InvalidRate=Invalid rate
+PairNotFound=Pair not found
+SourceIsEqualDest=Source and destination currency are the same
+AtLeastNeedOneTransitiveSymbol=At least one transitive symbol is needed
+CurrencyIsDisable=Currency is disabled
+CurrencyIsTransitiveAndDisablingIsImpossible=Disabling transitive currency is impossible
+InvalidReserveNumber=Invalid reserve number
+CurrentSystemAssetsAreNotEnough=Current system assets are not enough
+NotEnoughBalance=Not enough balance
+WithdrawNotAllowed=Withdraw is not allowed
+# Deposit errors
+DepositLimitExceeded=Deposit limit exceeded
+InvalidAmount=Invalid amount
+# Implementation errors
+ImplNotFound=Implementation not found
+InvalidWithdrawStatus=Withdraw status is invalid
+# API errors
+OrderNotFound=No order found
+SymbolNotFound=No symbol found
+InvalidLimitForOrderBook=Valid limits: [5, 10, 20, 50, 100, 500, 1000, 5000]
+InvalidLimitForRecentTrades=Valid limits: 1 min - 1000 max
+InvalidPriceChangeDuration=Valid durations: [24h, 7d, 1m]
+CancelOrderNotAllowed=Canceling this order is not allowed
+InvalidInterval=Invalid interval
+APIKeyLimitReached=Reached API key limit. Maximum number of API key is 10
+# Blockchain-gateway errors
+ReservedAddressNotAvailable=No reserved address available
+DuplicateToken=Asset already exists
+ChainNotFound=Chain not found
+CurrencyNotFoundBC=Currency not found
+TokenNotFound=Coin/Token not found
+InvalidAddressType=Address type is invalid
+# Captcha errors
+InvalidCaptcha=Captcha is not valid
diff --git a/common/src/main/resources/messages_fa.properties b/common/src/main/resources/messages_fa.properties
new file mode 100644
index 000000000..e54bb81ca
--- /dev/null
+++ b/common/src/main/resources/messages_fa.properties
@@ -0,0 +1,78 @@
+# Generic errors
+Error=خطای عمومی
+InternalServerError=خطای سرور داخلی
+BadRequest=درخواست نامعتبر
+UnAuthorized=مجوز لازم نیست
+Forbidden=دسترسی ممنوع
+NotFound=یافت نشد
+ServiceUnavailable=سرویس در دسترس نیست
+# Parameter errors
+InvalidRequestParam=پارامتر '%s' یا وجود ندارد یا نامعتبر است
+InvalidRequestBody=بدنه درخواست نامعتبر است
+NoRecordFound=رکوردی برای این سرویس یافت نشد
+# Accountant errors
+InvalidPair=%s در دسترس نیست
+InvalidPairFee=کارمزد %s در دسترس نیست
+PairFeeNotFound=کارمزد برای جفت درخواست شده یافت نشد
+# Matching-engine errors (code block omitted)
+# Matching-gateway errors
+SubmitOrderForbiddenByAccountant=ثبت سفارش توسط حسابدار ممنوع است
+# User-management errors
+EmailAlreadyVerified=ایمیل قبلاً تأیید شده است
+GroupNotFound=گروه یافت نشد
+OTPAlreadyEnabled=تأیید دو مرحله ای (OTP) قبلاً پیکربندی شده است
+UserNotFound=کاربر یافت نشد
+InvalidOTP=کد تأیید نامعتبر است
+OTPRequired=کد تأیید لازم است
+AlreadyInKYC=فرایند احراز هویت برای این کاربر اجرا شده است
+UserKYCBlocked=کاربر از احراز هویت مسدود شده است
+InvalidPassword=رمز عبور نامعتبر است
+UserAlreadyExists=کاربری با این ایمیل قبلاً ثبت نام کرده است
+LoginIsLimited=ایمیل شما در لیست سفید نیست
+RegisterIsLimited=ایمیل شما در لیست سفید نیست
+# Wallet errors
+WalletOwnerNotFound=مالک کیف پول یافت نشد
+WalletNotFound=کیف پول یافت نشد
+CurrencyNotFound=ارز یافت نشد
+InvalidCashOutUsage=از خدمات برداشت استفاده کنید
+WithdrawNotFound=برداشت یافت نشد
+NOT_EXCHANGEABLE_CURRENCIES=این دو ارز قابل تبادل نیستند
+CurrencyIsExist=ارز وجود دارد
+PairIsExist=جفت وجود دارد
+ForbiddenPair=جفت ممنوع
+InvalidRate=نرخ نامعتبر است
+PairNotFound=جفت یافت نشد
+SourceIsEqualDest=ارز مبدا و مقصد یکسان هستند
+AtLeastNeedOneTransitiveSymbol=حداقل به یک نماد انتقالی نیاز است
+CurrencyIsDisable=ارز غیرفعال است
+CurrencyIsTransitiveAndDisablingIsImpossible=غیرفعال کردن ارز انتقالی امکان پذیر نیست
+InvalidReserveNumber=تعداد رزرو نامعتبر است
+CurrentSystemAssetsAreNotEnough=دارایی های فعلی سیستم کافی نیست
+NotEnoughBalance=موجودی کافی نیست
+WithdrawNotAllowed=برداشت مجاز نیست
+# Deposit errors
+DepositLimitExceeded=حداقل سپرده فراتر رفته است
+InvalidAmount=مبلغ نامعتبر است
+# Implementation errors
+ImplNotFound=پیاده سازی یافت نشد
+InvalidWithdrawStatus=وضعیت برداشت نامعتبر است
+GatewayIsExist=درگاه مورد نظر پیش از این در سییستم تعریف شده است
+GatewayNotFount=درگاه مورد نظر یافت نشد
+# API errors
+OrderNotFound=سفارش یافت نشد
+SymbolNotFound=نماد یافت نشد
+InvalidLimitForOrderBook=محدوده های معتبر: [5, 10, 20, 50, 100, 500, 1000, 5000]
+InvalidLimitForRecentTrades=محدوده های معتبر: 1 دقیقه - 1000 حداکثر
+InvalidPriceChangeDuration=مدت های معتبر: [24 ساعت، 7 روز، 1 ماه]
+CancelOrderNotAllowed=لغو این سفارش مجاز نیست
+InvalidInterval=فاصله زمانی نامعتبر است
+APIKeyLimitReached=به محدودیت کلید API رسیدید. حداکثر تعداد کلید API 10 است
+# Blockchain-gateway errors
+ReservedAddressNotAvailable=آدرس رزرو شده در دسترس نیست
+DuplicateToken=دارایی قبلاً وجود دارد
+ChainNotFound=زنجیره یافت نشد
+CurrencyNotFoundBC=ارز یافت نشد
+TokenNotFound=سکه/توکن یافت نشد
+InvalidAddressType=نوع آدرس نامعتبر است
+# Captcha errors
+InvalidCaptcha=کد امنیتی نامعتبر است
\ No newline at end of file
diff --git a/docker-compose-otc.override.yml b/docker-compose-otc.override.yml
index 8d620a5f6..fe920a84e 100644
--- a/docker-compose-otc.override.yml
+++ b/docker-compose-otc.override.yml
@@ -8,13 +8,9 @@ services:
wallet:
build: wallet/wallet-app
volumes:
- - "./preferences-dev.yml:/preferences.yml"
- "./drive-key.json:/drive-key.json"
vault:
build: docker-images/vault
bc-gateway:
build: bc-gateway/bc-gateway-app
- volumes:
- - "./preferences-dev.yml:/preferences.yml"
-
diff --git a/docker-compose-otc.yml b/docker-compose-otc.yml
index 59bbbec0b..950c01a66 100644
--- a/docker-compose-otc.yml
+++ b/docker-compose-otc.yml
@@ -29,14 +29,13 @@ services:
- BACKEND_USER=${BACKEND_USER}
- VAULT_HOST=vault
- SWAGGER_AUTH_URL=$KEYCLOAK_FRONTEND_URL
- - PREFERENCES=$PREFERENCES
- DRIVE_FOLDER_ID=$DRIVE_FOLDER_ID
- BACKUP_ENABLED=$WALLET_BACKUP_ENABLED
- SPRING_PROFILES_ACTIVE=otc
- - auth_url=${AUTH_URL}
- - auth_jwk_endpoint=${JWK_ENDPOINT}
- configs:
- - preferences.yml
+ - AUTH_URL=${AUTH_URL}
+ - AUTH_JWK_ENDPOINT=${JWK_ENDPOINT}
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
depends_on:
- postgres-wallet
- vault
@@ -51,6 +50,8 @@ services:
deploy:
restart_policy:
condition: on-failure
+ volumes:
+ - documents:/Documents
vault:
image: ghcr.io/opexdev/vault-opex:${TAG}
volumes:
@@ -88,15 +89,13 @@ services:
- BACKEND_USER=${BACKEND_USER}
- VAULT_HOST=vault
- SWAGGER_AUTH_URL=$KEYCLOAK_FRONTEND_URL
- - PREFERENCES=$PREFERENCES
- - ADDRESS_EXP_TIME=120
+ - ADDRESS_EXP_TIME=7200
- SPRING_PROFILES_ACTIVE=otc
- - auth_url=${AUTH_URL}
- - auth_jwk_endpoint=${JWK_ENDPOINT}
- dns:
- - "8.8.8.8"
- configs:
- - preferences.yml
+ - AUTH_URL=${AUTH_URL}
+ - AUTH_JWK_ENDPOINT=${JWK_ENDPOINT}
+ - OMNI_URL=${OMNI_URL}
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
depends_on:
- vault
- postgres-bc-gateway
@@ -115,11 +114,11 @@ services:
volumes:
- bc-gateway-data:/var/lib/postgresql/data/
-
volumes:
wallet-data:
vault-data:
bc-gateway-data:
+ documents:
networks:
default:
@@ -131,6 +130,3 @@ secrets:
file: opex.dev.crt
private_pem:
file: private.pem
-configs:
- preferences.yml:
- file: preferences.yml
diff --git a/docker-compose.build.yml b/docker-compose.build.yml
index 00ed11152..dabbc4894 100644
--- a/docker-compose.build.yml
+++ b/docker-compose.build.yml
@@ -9,6 +9,9 @@ services:
kafka-3:
image: ghcr.io/opexdev/kafka:$TAG
build: docker-images/kafka
+ keycloak:
+ image: ghcr.io/opexdev/keycloak:$TAG
+ build: auth-gateway/keycloak-setup
vault:
image: ghcr.io/opexdev/vault-opex:$TAG
build: docker-images/vault
@@ -33,6 +36,9 @@ services:
auth:
image: ghcr.io/opexdev/auth:$TAG
build: user-management/keycloak-gateway
+ auth-gateway:
+ image: ghcr.io/opexdev/auth-gateway:$TAG
+ build: auth-gateway/auth-gateway-app
wallet:
image: ghcr.io/opexdev/wallet:$TAG
build: wallet/wallet-app
@@ -45,3 +51,9 @@ services:
bc-gateway:
image: ghcr.io/opexdev/bc-gateway:$TAG
build: bc-gateway/bc-gateway-app
+ otp:
+ image: ghcr.io/opexdev/otp:$TAG
+ build: otp/otp-app
+ profile:
+ image: ghcr.io/opexdev/profile:$TAG
+ build: profile/profile-app
\ No newline at end of file
diff --git a/docker-compose.local.yml b/docker-compose.local.yml
index e9c9f0431..177eebbce 100644
--- a/docker-compose.local.yml
+++ b/docker-compose.local.yml
@@ -1,7 +1,5 @@
version: '3.8'
services:
- zookeeper:
- user: "root"
kafka-1:
user: "root"
kafka-2:
@@ -22,9 +20,22 @@ services:
redis-cache:
ports:
- "6380:6379"
+ keycloak:
+ ports:
+ - "8193:8080"
akhq:
ports:
- "127.0.0.1:10100:8080"
+ auth-gateway:
+ ports:
+ - "8184:8080"
+ - "127.0.0.1:1055:5005"
+ postgres-keycloak:
+ ports:
+ - "127.0.0.1:5461:5432"
+ postgres-otp:
+ ports:
+ - "127.0.0.1:5462:5432"
accountant:
ports:
- "127.0.0.1:8089:8080"
@@ -60,3 +71,7 @@ services:
ports:
- "0.0.0.0:8095:8080"
- "127.0.0.1:1052:5005"
+ otp:
+ ports:
+ - "0.0.0.0:8097:8080"
+ - "127.0.0.1:1053:5005"
diff --git a/docker-compose.override.yml b/docker-compose.override.yml
index 78bcbb7ca..095270fe6 100644
--- a/docker-compose.override.yml
+++ b/docker-compose.override.yml
@@ -2,38 +2,25 @@ version: '3.8'
services:
accountant:
build: accountant/accountant-app
- volumes:
- - "./preferences-dev.yml:/preferences.yml"
eventlog:
build: eventlog/eventlog-app
matching-engine:
build: matching-engine/matching-engine-app
- volumes:
- - "./preferences-dev.yml:/preferences.yml"
matching-engine-duo:
build: matching-engine/matching-engine-app
- volumes:
- - "./preferences-dev.yml:/preferences.yml"
matching-gateway:
build: matching-gateway/matching-gateway-app
auth:
build: user-management/keycloak-gateway
- volumes:
- - "./preferences-dev.yml:/preferences.yml"
wallet:
build: wallet/wallet-app
volumes:
- - "./preferences-dev.yml:/preferences.yml"
- "./drive-key.json:/drive-key.json"
market:
build: market/market-app
- volumes:
- - "./preferences-dev.yml:/preferences.yml"
api:
build: api/api-app
- volumes:
- - "./preferences-dev.yml:/preferences.yml"
bc-gateway:
build: bc-gateway/bc-gateway-app
- volumes:
- - "./preferences-dev.yml:/preferences.yml"
+ profile:
+ build: profile/profile-app
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 5352c13a4..10bd08219 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -14,36 +14,25 @@ x-postgres-db: &postgres-db
version: '3.8'
services:
- zookeeper:
- image: confluentinc/cp-zookeeper:7.1.1
- hostname: zookeeper
- volumes:
- - zookeeper-data:/var/lib/zookeeper/data
- - zookeeper-log:/var/lib/zookeeper/log
- environment:
- - ALLOW_ANONYMOUS_LOGIN=yes
- - ZOOKEEPER_CLIENT_PORT=2181
- networks:
- - default
- deploy:
- restart_policy:
- condition: on-failure
kafka-1:
image: ghcr.io/opexdev/kafka
hostname: kafka-1
volumes:
- kafka-1:/var/lib/kafka/data
environment:
- - KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181
+ - KAFKA_NODE_ID=1
+ - KAFKA_PROCESS_ROLES=broker,controller
+ - CLUSTER_ID=${KAFKA_CLUSTER_ID}
- ALLOW_PLAINTEXT_LISTENER=yes
- - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=CLIENT:PLAINTEXT,EXTERNAL:PLAINTEXT
- - KAFKA_LISTENERS=CLIENT://kafka-1:29092,EXTERNAL://kafka-1:9092
- KAFKA_ADVERTISED_LISTENERS=CLIENT://kafka-1:29092,EXTERNAL://kafka-1:9092
- KAFKA_INTER_BROKER_LISTENER_NAME=CLIENT
- KAFKA_UNCLEAN_LEADER_ELECTION_ENABLE=false
- KAFKA_OPTS=-javaagent:/opt/prometheus/jmx-exporter.jar=1234:/opt/prometheus/kafka-jmx-exporter.yml
- depends_on:
- - zookeeper
+ - KAFKA_CONTROLLER_QUORUM_VOTERS=1@kafka-1:29093,2@kafka-2:29093,3@kafka-3:29093
+ - KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER
+ - KAFKA_LISTENERS=CLIENT://kafka-1:29092,EXTERNAL://kafka-1:9092,CONTROLLER://kafka-1:29093
+ - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=CLIENT:PLAINTEXT,EXTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT
+
networks:
- default
deploy:
@@ -55,16 +44,19 @@ services:
volumes:
- kafka-2:/var/lib/kafka/data
environment:
- - KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181
+ - KAFKA_NODE_ID=2
+ - KAFKA_PROCESS_ROLES=broker,controller
+ - CLUSTER_ID=${KAFKA_CLUSTER_ID}
- ALLOW_PLAINTEXT_LISTENER=yes
- - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=CLIENT:PLAINTEXT,EXTERNAL:PLAINTEXT
- - KAFKA_LISTENERS=CLIENT://kafka-2:29092,EXTERNAL://kafka-2:9092
- KAFKA_ADVERTISED_LISTENERS=CLIENT://kafka-2:29092,EXTERNAL://kafka-2:9092
- KAFKA_INTER_BROKER_LISTENER_NAME=CLIENT
- KAFKA_UNCLEAN_LEADER_ELECTION_ENABLE=false
- KAFKA_OPTS=-javaagent:/opt/prometheus/jmx-exporter.jar=1234:/opt/prometheus/kafka-jmx-exporter.yml
- depends_on:
- - zookeeper
+ - KAFKA_CONTROLLER_QUORUM_VOTERS=1@kafka-1:29093,2@kafka-2:29093,3@kafka-3:29093
+ - KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER
+ - KAFKA_LISTENERS=CLIENT://kafka-2:29092,EXTERNAL://kafka-2:9092,CONTROLLER://kafka-2:29093
+ - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=CLIENT:PLAINTEXT,EXTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT
+
networks:
- default
deploy:
@@ -76,16 +68,19 @@ services:
volumes:
- kafka-3:/var/lib/kafka/data
environment:
- - KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181
+ - KAFKA_NODE_ID=3
+ - KAFKA_PROCESS_ROLES=broker,controller
+ - CLUSTER_ID=${KAFKA_CLUSTER_ID}
- ALLOW_PLAINTEXT_LISTENER=yes
- - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=CLIENT:PLAINTEXT,EXTERNAL:PLAINTEXT
- - KAFKA_LISTENERS=CLIENT://kafka-3:29092,EXTERNAL://kafka-3:9092
- KAFKA_ADVERTISED_LISTENERS=CLIENT://kafka-3:29092,EXTERNAL://kafka-3:9092
- KAFKA_INTER_BROKER_LISTENER_NAME=CLIENT
- KAFKA_UNCLEAN_LEADER_ELECTION_ENABLE=false
- KAFKA_OPTS=-javaagent:/opt/prometheus/jmx-exporter.jar=1234:/opt/prometheus/kafka-jmx-exporter.yml
- depends_on:
- - zookeeper
+ - KAFKA_CONTROLLER_QUORUM_VOTERS=1@kafka-1:29093,2@kafka-2:29093,3@kafka-3:29093
+ - KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER
+ - KAFKA_LISTENERS=CLIENT://kafka-3:29092,EXTERNAL://kafka-3:9092,CONTROLLER://kafka-3:29093
+ - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=CLIENT:PLAINTEXT,EXTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT
+
networks:
- default
deploy:
@@ -109,6 +104,32 @@ services:
deploy:
restart_policy:
condition: on-failure
+ keycloak:
+ image: ghcr.io/opexdev/keycloak
+ container_name: keycloak
+ environment:
+ KEYCLOAK_ADMIN: ${KC_PANEL_USERNAME}
+ KEYCLOAK_ADMIN_PASSWORD: ${KC_PANEL_PASSWORD}
+ KC_DB: postgres
+ KC_DB_URL: jdbc:postgresql://postgres-keycloak:5432/opex
+ KC_DB_USERNAME: ${DB_USER}
+ KC_DB_PASSWORD: ${DB_PASS}
+ KC_HEALTH_ENABLED: true
+ KC_METRICS_ENABLED: true
+ GOOGLE_CLIENT_ID: ${KC_GOOGLE_CLIENT_ID}
+ GOOGLE_CLIENT_SECRET: ${KC_GOOGLE_CLIENT_SECRET}
+ command:
+ - start-dev
+ - --import-realm
+ - --features=admin-fine-grained-authz,token-exchange,scripts,token-exchange
+ - --log-level=INFO
+ depends_on:
+ - postgres-keycloak
+ networks:
+ - default
+ deploy:
+ restart_policy:
+ condition: on-failure
vault:
image: ghcr.io/opexdev/vault-opex
volumes:
@@ -189,6 +210,10 @@ services:
<<: *postgres-db
volumes:
- auth-data:/var/lib/postgresql/data/
+ postgres-keycloak:
+ <<: *postgres-db
+ volumes:
+ - keycloak-data:/var/lib/postgresql/data/
postgres-wallet:
<<: *postgres-db
volumes:
@@ -205,6 +230,18 @@ services:
<<: *postgres-db
volumes:
- bc-gateway-data:/var/lib/postgresql/data/
+ postgres-matching-gateway:
+ <<: *postgres-db
+ volumes:
+ - matching-gateway-data:/var/lib/postgresql/data/
+ postgres-otp:
+ <<: *postgres-db
+ volumes:
+ - otp-data:/var/lib/postgresql/data/
+ postgres-profile:
+ <<: *postgres-db
+ volumes:
+ - profile-data:/var/lib/postgresql/data/
accountant:
image: ghcr.io/opexdev/accountant
environment:
@@ -216,16 +253,12 @@ services:
- BACKEND_USER=${BACKEND_USER}
- VAULT_HOST=vault
- SWAGGER_AUTH_URL=$KEYCLOAK_FRONTEND_URL
- - PREFERENCES=$PREFERENCES
- configs:
- - preferences.yml
networks:
- default
depends_on:
- kafka-1
- kafka-2
- kafka-3
- - wallet
- consul
- vault
- postgres-accountant
@@ -264,9 +297,6 @@ services:
- KAFKA_IP_PORT=kafka-1:29092,kafka-2:29092,kafka-3:29092
- REDIS_HOST=redis
- SYMBOLS=BTC_USDT,ETH_USDT,BTC_IRT,ETH_IRT,USDT_IRT,ETH_BUSD,BTC_BUSD,BNB_BUSD
- - PREFERENCES=$PREFERENCES
- configs:
- - preferences.yml
networks:
- default
depends_on:
@@ -286,9 +316,6 @@ services:
- KAFKA_IP_PORT=kafka-1:29092,kafka-2:29092,kafka-3:29092
- REDIS_HOST=redis-duo
- SYMBOLS=SOL_USDT,DOGE_USDT,TON_USDT
- - PREFERENCES=$PREFERENCES
- configs:
- - preferences.yml
networks:
- default
depends_on:
@@ -307,15 +334,18 @@ services:
- JAVA_OPTS=-Xmx256m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
- KAFKA_IP_PORT=kafka-1:29092,kafka-2:29092,kafka-3:29092
- CONSUL_HOST=consul
+ - DB_IP_PORT=postgres-matching-gateway
+ - BACKEND_USER=${BACKEND_USER}
+ - VAULT_HOST=vault
+ - SYMBOLS=BTC_USDT,ETH_USDT,BTC_IRT,ETH_IRT,USDT_IRT,ETH_BUSD,BTC_BUSD,BNB_BUSD
networks:
- default
depends_on:
- kafka-1
- kafka-2
- kafka-3
- - auth
- consul
- - matching-engine
+ - postgres-matching-gateway
labels:
collect_logs: "true"
deploy:
@@ -335,7 +365,6 @@ services:
- FORGOT_REDIRECT_URL=$KEYCLOAK_FORGOT_REDIRECT_URL
- VAULT_URL=http://vault:8200
- VAULT_HOST=vault
- - PREFERENCES=$PREFERENCES
- APP_NAME=$APP_NAME
- APP_BASE_URL=$APP_BASE_URL
- WHITELIST_REGISTER_ENABLED=$WHITELIST_REGISTER_ENABLED
@@ -354,6 +383,24 @@ services:
deploy:
restart_policy:
condition: on-failure
+ auth-gateway:
+ image: ghcr.io/opexdev/auth-gateway
+ environment:
+ - JAVA_OPTS=-Xmx256m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
+ - KAFKA_IP_PORT=kafka-1:29092,kafka-2:29092,kafka-3:29092
+ - CONSUL_HOST=consul
+ - ADMIN_CLIENT_SECRET=${KC_ADMIN_CLIENT_SECRET}
+ volumes:
+ - auth-gateway-keys:/app/keys
+ depends_on:
+ - keycloak
+ networks:
+ - default
+ labels:
+ collect_logs: "true"
+ deploy:
+ restart_policy:
+ condition: on-failure
wallet:
image: ghcr.io/opexdev/wallet
environment:
@@ -364,16 +411,13 @@ services:
- BACKEND_USER=${BACKEND_USER}
- VAULT_HOST=vault
- SWAGGER_AUTH_URL=$KEYCLOAK_FRONTEND_URL
- - PREFERENCES=$PREFERENCES
- DRIVE_FOLDER_ID=$DRIVE_FOLDER_ID
- BACKUP_ENABLED=$WALLET_BACKUP_ENABLED
- configs:
- - preferences.yml
+ - SYMBOLS=BTC_USDT,ETH_USDT,BTC_IRT,ETH_IRT,USDT_IRT,ETH_BUSD,BTC_BUSD,BNB_BUSD
depends_on:
- kafka-1
- kafka-2
- kafka-3
- - auth
- consul
- vault
- postgres-wallet
@@ -394,12 +438,10 @@ services:
- BACKEND_USER=${BACKEND_USER}
- VAULT_HOST=vault
- SWAGGER_AUTH_URL=$KEYCLOAK_FRONTEND_URL
- - PREFERENCES=$PREFERENCES
depends_on:
- kafka-1
- kafka-2
- kafka-3
- - auth
- consul
- vault
- postgres-market
@@ -420,16 +462,8 @@ services:
- BACKEND_USER=${BACKEND_USER}
- VAULT_HOST=vault
- SWAGGER_AUTH_URL=$KEYCLOAK_FRONTEND_URL
- - PREFERENCES=$PREFERENCES
- API_KEY_CLIENT_SECRET=$API_KEY_CLIENT_SECRET
- configs:
- - preferences.yml
depends_on:
- - accountant
- - matching-gateway
- - wallet
- - market
- - auth
- consul
- vault
- postgres-api
@@ -450,16 +484,11 @@ services:
- BACKEND_USER=${BACKEND_USER}
- VAULT_HOST=vault
- SWAGGER_AUTH_URL=$KEYCLOAK_FRONTEND_URL
- - PREFERENCES=$PREFERENCES
- ADDRESS_EXP_TIME=100
- configs:
- - preferences.yml
depends_on:
- kafka-1
- kafka-2
- kafka-3
- - auth
- - wallet
- consul
- vault
- postgres-bc-gateway
@@ -470,6 +499,50 @@ services:
deploy:
restart_policy:
condition: on-failure
+ otp:
+ image: ghcr.io/opexdev/otp
+ environment:
+ - JAVA_OPTS=-Xmx256m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
+ - CONSUL_HOST=consul
+ - DB_IP_PORT=postgres-otp
+ - DB_USER=${DB_USER:-opex}
+ - DB_PASS=${DB_PASS:-hiopex}
+ - SMS_PROVIDER_API_KEY=${SMS_PROVIDER_API_KEY}
+ - SMTP_HOST=${SMTP_HOST}
+ - SMTP_USER=${SMTP_USER}
+ - SMTP_PASS=${SMTP_PASS}
+ depends_on:
+ - consul
+ - postgres-otp
+ networks:
+ - default
+ labels:
+ collect_logs: "true"
+ deploy:
+ restart_policy:
+ condition: on-failure
+ profile:
+ image: ghcr.io/opexdev/profile
+ environment:
+ - JAVA_OPTS=-Xmx256m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
+ - CONSUL_HOST=consul
+ - DB_IP_PORT=postgres-profile
+ - KAFKA_IP_PORT=kafka-1:29092,kafka-2:29092,kafka-3:29092
+ - BACKEND_USER=${BACKEND_USER}
+ - VAULT_HOST=vault
+ - SWAGGER_AUTH_URL=$KEYCLOAK_FRONTEND_URL
+ depends_on:
+ - kafka-1
+ - kafka-2
+ - kafka-3
+ - consul
+ - postgres-profile
+ - vault
+ labels:
+ collect_logs: "true"
+ deploy:
+ restart_policy:
+ condition: on-failure
volumes:
zookeeper-data:
zookeeper-log:
@@ -485,10 +558,15 @@ volumes:
accountant-data:
eventlog-data:
auth-data:
+ keycloak-data:
wallet-data:
market-data:
api-data:
bc-gateway-data:
+ matching-gateway-data:
+ otp-data:
+ auth-gateway-keys:
+ profile-data:
networks:
opex:
external: true
@@ -499,6 +577,3 @@ secrets:
file: opex.dev.crt
private_pem:
file: private.pem
-configs:
- preferences.yml:
- file: preferences.yml
diff --git a/docker-images/kafka/Dockerfile b/docker-images/kafka/Dockerfile
index e3c2623ba..43f699594 100644
--- a/docker-images/kafka/Dockerfile
+++ b/docker-images/kafka/Dockerfile
@@ -1,4 +1,4 @@
-FROM confluentinc/cp-kafka:7.1.1
+FROM confluentinc/cp-kafka:8.0.0
USER root
RUN mkdir /opt/prometheus
RUN chmod +rx /opt/prometheus
diff --git a/docker-images/kafka/kafka-jmx-exporter.yml b/docker-images/kafka/kafka-jmx-exporter.yml
index 4de5dad60..4ef7bcd6d 100644
--- a/docker-images/kafka/kafka-jmx-exporter.yml
+++ b/docker-images/kafka/kafka-jmx-exporter.yml
@@ -172,20 +172,20 @@ rules:
"$3": "$4"
# Quotas
- - pattern : 'kafka.server<>(.+):'
+ - pattern: 'kafka.server<>(.+):'
name: kafka_server_$1_$4
type: GAUGE
labels:
user: "$2"
client-id: "$3"
- - pattern : 'kafka.server<>(.+):'
+ - pattern: 'kafka.server<>(.+):'
name: kafka_server_$1_$3
type: GAUGE
labels:
user: "$2"
- - pattern : 'kafka.server<>(.+):'
+ - pattern: 'kafka.server<>(.+):'
name: kafka_server_$1_$3
type: GAUGE
labels:
diff --git a/docker-images/vault/workflow-vault.sh b/docker-images/vault/workflow-vault.sh
index 3d11ed140..fb8b4d140 100755
--- a/docker-images/vault/workflow-vault.sh
+++ b/docker-images/vault/workflow-vault.sh
@@ -51,6 +51,7 @@ init_secrets() {
vault write auth/app-id/map/app-id/opex-eventlog value=backend-policy display_name=opex-eventlog
vault write auth/app-id/map/app-id/opex-auth value=backend-policy display_name=opex-auth
vault write auth/app-id/map/app-id/opex-wallet value=backend-policy display_name=opex-wallet
+ vault write auth/app-id/map/app-id/opex-matching-gateway value=backend-policy display_name=opex-matching-gateway
vault write auth/app-id/map/app-id/opex-websocket value=backend-policy display_name=opex-websocket
vault write auth/app-id/map/app-id/opex-payment value=backend-policy display_name=opex-payment
vault write auth/app-id/map/app-id/opex-admin value=backend-policy display_name=opex-admin
@@ -62,11 +63,12 @@ init_secrets() {
vault write auth/app-id/map/app-id/opex-referral value=backend-policy display_name=opex-referral
vault write auth/app-id/map/app-id/opex-profile value=backend-policy display_name=opex-profile
vault write auth/app-id/map/app-id/opex-kyc value=backend-policy display_name=opex-kyc
+ vault write auth/app-id/map/app-id/opex-kyc value=backend-policy display_name=opex-kyc
## Enable user-id
vault write auth/app-id/map/user-id/${BACKEND_USER} \
- value=opex-wallet,opex-websocket,opex-eventlog,opex-auth,opex-accountant,opex-api,opex-market,opex-bc-gateway,opex-payment,opex-admin,bitcoin-scanner,ethereum-scanner,tron-scanner,scanner-scheduler,scanner-liaison,opex-referral,opex-profile,opex-kyc
+ value=opex-wallet,opex-websocket,opex-eventlog,opex-auth,opex-accountant,opex-matching-gateway,opex-api,opex-market,opex-bc-gateway,opex-payment,opex-admin,bitcoin-scanner,ethereum-scanner,tron-scanner,scanner-scheduler,scanner-liaison,opex-referral,opex-profile,opex-kyc
## Check login app-id
vault write auth/app-id/login/opex-accountant user_id=${BACKEND_USER}
@@ -76,6 +78,7 @@ init_secrets() {
vault write auth/app-id/login/opex-eventlog user_id=${BACKEND_USER}
vault write auth/app-id/login/opex-auth user_id=${BACKEND_USER}
vault write auth/app-id/login/opex-wallet user_id=${BACKEND_USER}
+ vault write auth/app-id/login/opex-matching-gateway user_id=${BACKEND_USER}
vault write auth/app-id/login/opex-websocket user_id=${BACKEND_USER}
vault write auth/app-id/login/opex-payment user_id=${BACKEND_USER}
vault write auth/app-id/login/opex-admin user_id=${BACKEND_USER}
@@ -97,6 +100,7 @@ init_secrets() {
vault kv put secret/opex-eventlog dbusername=${DB_USER} dbpassword=${DB_PASS} db_read_only_username=${DB_READ_ONLY_USER} db_read_only_pass=${DB_READ_ONLY_PASS}
vault kv put secret/opex-auth dbusername=${DB_USER} dbpassword=${DB_PASS} admin_username=${KEYCLOAK_ADMIN_USERNAME} admin_password=${KEYCLOAK_ADMIN_PASSWORD}
vault kv put secret/opex-wallet dbusername=${DB_USER} dbpassword=${DB_PASS} db_read_only_username=${DB_READ_ONLY_USER} db_read_only_pass=${DB_READ_ONLY_PASS} client_id=${CLIENT_ID} client_secret=${CLIENT_SECRET}
+ vault kv put secret/opex-matching-gateway dbusername=${DB_USER} dbpassword=${DB_PASS} db_read_only_username=${DB_READ_ONLY_USER} db_read_only_pass=${DB_READ_ONLY_PASS} client_id=${CLIENT_ID} client_secret=${CLIENT_SECRET}
vault kv put secret/opex-websocket dbusername=${DB_USER} dbpassword=${DB_PASS} db_read_only_username=${DB_READ_ONLY_USER} db_read_only_pass=${DB_READ_ONLY_PASS}
vault kv put secret/opex-payment dbusername=${DB_USER} dbpassword=${DB_PASS} db_read_only_username=${DB_READ_ONLY_USER} db_read_only_pass=${DB_READ_ONLY_PASS} vandar_api_key=${VANDAR_API_KEY}
vault kv put secret/opex-admin keycloak_client_secret=${OPEX_ADMIN_KEYCLOAK_CLIENT_SECRET}
diff --git a/eventlog/eventlog-app/pom.xml b/eventlog/eventlog-app/pom.xml
index 161a09170..ae93544bc 100644
--- a/eventlog/eventlog-app/pom.xml
+++ b/eventlog/eventlog-app/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
diff --git a/eventlog/eventlog-app/src/main/kotlin/co/nilin/opex/eventlog/app/listeners/DeadLetterListener.kt b/eventlog/eventlog-app/src/main/kotlin/co/nilin/opex/eventlog/app/listeners/DeadLetterListener.kt
index 0a9ce8f5b..9309af238 100644
--- a/eventlog/eventlog-app/src/main/kotlin/co/nilin/opex/eventlog/app/listeners/DeadLetterListener.kt
+++ b/eventlog/eventlog-app/src/main/kotlin/co/nilin/opex/eventlog/app/listeners/DeadLetterListener.kt
@@ -19,28 +19,29 @@ class DeadLetterListener(private val persister: DeadLetterPersister) : DLTListen
return "EventLogDeadLetterListener"
}
- override fun onEvent(event: String?, partition: Int, offset: Long, timestamp: Long, headers: Headers) = runBlocking {
- logger.info("Dead letter event received: $event")
- val map = hashMapOf().apply {
- headers.forEach {
- put(it.key(), it.value().toString(Charsets.UTF_8))
+ override fun onEvent(event: String?, partition: Int, offset: Long, timestamp: Long, headers: Headers) =
+ runBlocking {
+ logger.info("Dead letter event received: $event")
+ val map = hashMapOf().apply {
+ headers.forEach {
+ put(it.key(), it.value().toString(Charsets.UTF_8))
+ }
}
- }
- val dlt = DeadLetterEvent(
- map["dlt-origin-module"]!!,
- map[KafkaHeaders.DLT_ORIGINAL_TOPIC],
- map[KafkaHeaders.DLT_ORIGINAL_CONSUMER_GROUP],
- map[KafkaHeaders.DLT_EXCEPTION_MESSAGE],
- map[KafkaHeaders.DLT_EXCEPTION_STACKTRACE],
- map[KafkaHeaders.DLT_EXCEPTION_FQCN],
- event,
- LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), TimeZone.getDefault().toZoneId())
- )
-
- persister.save(dlt)
- logger.info("DLT persisted")
- }
+ val dlt = DeadLetterEvent(
+ map["dlt-origin-module"]!!,
+ map[KafkaHeaders.DLT_ORIGINAL_TOPIC],
+ map[KafkaHeaders.DLT_ORIGINAL_CONSUMER_GROUP],
+ map[KafkaHeaders.DLT_EXCEPTION_MESSAGE],
+ map[KafkaHeaders.DLT_EXCEPTION_STACKTRACE],
+ map[KafkaHeaders.DLT_EXCEPTION_FQCN],
+ event,
+ LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), TimeZone.getDefault().toZoneId())
+ )
+
+ persister.save(dlt)
+ logger.info("DLT persisted")
+ }
}
\ No newline at end of file
diff --git a/eventlog/eventlog-app/src/main/resources/application.yml b/eventlog/eventlog-app/src/main/resources/application.yml
index 964e5cf10..38f52cf59 100644
--- a/eventlog/eventlog-app/src/main/resources/application.yml
+++ b/eventlog/eventlog-app/src/main/resources/application.yml
@@ -5,7 +5,7 @@ spring:
main:
allow-circular-references: true
kafka:
- bootstrap-servers: ${KAFKA_IP_PORT:localhost:9092}
+ bootstrap-servers: ${KAFKA_IP_PORT:localhost:9092}
consumer:
group-id: eventlog
r2dbc:
@@ -34,7 +34,7 @@ management:
web:
base-path: /actuator
exposure:
- include: ["health", "prometheus", "metrics"]
+ include: [ "health", "prometheus", "metrics" ]
endpoint:
health:
show-details: when_authorized
diff --git a/eventlog/eventlog-core/pom.xml b/eventlog/eventlog-core/pom.xml
index 0b8ad9765..ef376ebc5 100644
--- a/eventlog/eventlog-core/pom.xml
+++ b/eventlog/eventlog-core/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
diff --git a/eventlog/eventlog-ports/eventlog-eventlistener-kafka/pom.xml b/eventlog/eventlog-ports/eventlog-eventlistener-kafka/pom.xml
index 9a6857784..6f88e8565 100644
--- a/eventlog/eventlog-ports/eventlog-eventlistener-kafka/pom.xml
+++ b/eventlog/eventlog-ports/eventlog-eventlistener-kafka/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
diff --git a/eventlog/eventlog-ports/eventlog-eventlistener-kafka/src/main/kotlin/co/nilin/opex/eventlog/ports/kafka/listener/config/KafkaTopicConfig.kt b/eventlog/eventlog-ports/eventlog-eventlistener-kafka/src/main/kotlin/co/nilin/opex/eventlog/ports/kafka/listener/config/KafkaTopicConfig.kt
index 2ac0fab81..0146b4cd2 100644
--- a/eventlog/eventlog-ports/eventlog-eventlistener-kafka/src/main/kotlin/co/nilin/opex/eventlog/ports/kafka/listener/config/KafkaTopicConfig.kt
+++ b/eventlog/eventlog-ports/eventlog-eventlistener-kafka/src/main/kotlin/co/nilin/opex/eventlog/ports/kafka/listener/config/KafkaTopicConfig.kt
@@ -18,7 +18,7 @@ class KafkaTopicConfig {
registerBean("topic_richTrade.DLT", NewTopic::class.java, "richTrade.DLT", 10, 1)
registerBean("topic_richOrder.DLT", NewTopic::class.java, "richOrder.DLT", 10, 1)
registerBean("topic_admin_event.DLT", NewTopic::class.java, "admin_event.DLT", 10, 1)
- registerBean("topic_auth_user_created.DLT", NewTopic::class.java, "auth_user_created.DLT", 10, 1)
+ registerBean("topic_auth.DLT", NewTopic::class.java, "auth.DLT", 10, 1)
}
}
diff --git a/eventlog/eventlog-ports/eventlog-eventlistener-kafka/src/main/kotlin/co/nilin/opex/eventlog/ports/kafka/listener/consumer/DLTKafkaListener.kt b/eventlog/eventlog-ports/eventlog-eventlistener-kafka/src/main/kotlin/co/nilin/opex/eventlog/ports/kafka/listener/consumer/DLTKafkaListener.kt
index aa9ee7a69..6c09d4555 100644
--- a/eventlog/eventlog-ports/eventlog-eventlistener-kafka/src/main/kotlin/co/nilin/opex/eventlog/ports/kafka/listener/consumer/DLTKafkaListener.kt
+++ b/eventlog/eventlog-ports/eventlog-eventlistener-kafka/src/main/kotlin/co/nilin/opex/eventlog/ports/kafka/listener/consumer/DLTKafkaListener.kt
@@ -12,7 +12,15 @@ class DLTKafkaListener : MessageListener {
override fun onMessage(data: ConsumerRecord) {
- listeners.forEach { it.onEvent(data.value(), data.partition(), data.offset(), data.timestamp(), data.headers()) }
+ listeners.forEach {
+ it.onEvent(
+ data.value(),
+ data.partition(),
+ data.offset(),
+ data.timestamp(),
+ data.headers()
+ )
+ }
}
fun addEventListener(tl: DLTListener) {
diff --git a/eventlog/eventlog-ports/eventlog-eventlistener-kafka/src/main/kotlin/co/nilin/opex/eventlog/ports/kafka/listener/inout/OrderRequestEvent.kt b/eventlog/eventlog-ports/eventlog-eventlistener-kafka/src/main/kotlin/co/nilin/opex/eventlog/ports/kafka/listener/inout/OrderRequestEvent.kt
index d0d6381cf..d569df4d1 100644
--- a/eventlog/eventlog-ports/eventlog-eventlistener-kafka/src/main/kotlin/co/nilin/opex/eventlog/ports/kafka/listener/inout/OrderRequestEvent.kt
+++ b/eventlog/eventlog-ports/eventlog-eventlistener-kafka/src/main/kotlin/co/nilin/opex/eventlog/ports/kafka/listener/inout/OrderRequestEvent.kt
@@ -2,4 +2,4 @@ package co.nilin.opex.eventlog.ports.kafka.listener.inout
import co.nilin.opex.matching.engine.core.model.Pair
-abstract class OrderRequestEvent(val ouid:String, val uuid: String, val pair: Pair)
\ No newline at end of file
+abstract class OrderRequestEvent(val ouid: String, val uuid: String, val pair: Pair)
\ No newline at end of file
diff --git a/eventlog/eventlog-ports/eventlog-persister-postgres/pom.xml b/eventlog/eventlog-ports/eventlog-persister-postgres/pom.xml
index 9df95effb..811952e9c 100644
--- a/eventlog/eventlog-ports/eventlog-persister-postgres/pom.xml
+++ b/eventlog/eventlog-ports/eventlog-persister-postgres/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
diff --git a/eventlog/eventlog-ports/eventlog-persister-postgres/src/main/resources/schema.sql b/eventlog/eventlog-ports/eventlog-persister-postgres/src/main/resources/schema.sql
index f4c381101..3f58a1c7f 100644
--- a/eventlog/eventlog-ports/eventlog-persister-postgres/src/main/resources/schema.sql
+++ b/eventlog/eventlog-ports/eventlog-persister-postgres/src/main/resources/schema.sql
@@ -1,79 +1,189 @@
CREATE TABLE IF NOT EXISTS opex_orders
(
- id SERIAL PRIMARY KEY,
- ouid VARCHAR(72) NOT NULL UNIQUE,
- symbol VARCHAR(20) NOT NULL,
- direction VARCHAR(20) NOT NULL,
- match_constraint VARCHAR(20) NOT NULL,
- order_type VARCHAR(20) NOT NULL,
- uuid VARCHAR(72) NOT NULL,
- agent VARCHAR(20),
- ip VARCHAR(11),
- order_date TIMESTAMP NOT NULL,
- create_date TIMESTAMP NOT NULL
-);
+ id
+ SERIAL
+ PRIMARY
+ KEY,
+ ouid
+ VARCHAR
+(
+ 72
+) NOT NULL UNIQUE,
+ symbol VARCHAR
+(
+ 20
+) NOT NULL,
+ direction VARCHAR
+(
+ 20
+) NOT NULL,
+ match_constraint VARCHAR
+(
+ 20
+) NOT NULL,
+ order_type VARCHAR
+(
+ 20
+) NOT NULL,
+ uuid VARCHAR
+(
+ 72
+) NOT NULL,
+ agent VARCHAR
+(
+ 20
+),
+ ip VARCHAR
+(
+ 11
+),
+ order_date TIMESTAMP NOT NULL,
+ create_date TIMESTAMP NOT NULL
+ );
CREATE TABLE IF NOT EXISTS opex_order_events
(
- id SERIAL PRIMARY KEY,
- ouid VARCHAR(72) NOT NULL,
+ id
+ SERIAL
+ PRIMARY
+ KEY,
+ ouid
+ VARCHAR
+(
+ 72
+) NOT NULL,
matching_orderid BIGINT,
- price BIGINT,
- quantity BIGINT,
- filled_quantity BIGINT,
- uuid VARCHAR(72) NOT NULL,
- event VARCHAR(30) NOT NULL,
- agent VARCHAR(20),
- ip VARCHAR(11),
- event_date TIMESTAMP NOT NULL,
- create_date TIMESTAMP NOT NULL
-);
+ price BIGINT,
+ quantity BIGINT,
+ filled_quantity BIGINT,
+ uuid VARCHAR
+(
+ 72
+) NOT NULL,
+ event VARCHAR
+(
+ 30
+) NOT NULL,
+ agent VARCHAR
+(
+ 20
+),
+ ip VARCHAR
+(
+ 11
+),
+ event_date TIMESTAMP NOT NULL,
+ create_date TIMESTAMP NOT NULL
+ );
CREATE TABLE IF NOT EXISTS opex_events
(
- id SERIAL PRIMARY KEY,
- correlation_id VARCHAR(72) NOT NULL,
- ouid VARCHAR(72) NOT NULL,
- uuid VARCHAR(72) NOT NULL,
- symbol VARCHAR(20) NOT NULL,
- event VARCHAR(30) NOT NULL,
- event_json TEXT NOT NULL,
- agent VARCHAR(20),
- ip VARCHAR(11),
- event_date TIMESTAMP NOT NULL,
- create_date TIMESTAMP NOT NULL
-);
+ id
+ SERIAL
+ PRIMARY
+ KEY,
+ correlation_id
+ VARCHAR
+(
+ 72
+) NOT NULL,
+ ouid VARCHAR
+(
+ 72
+) NOT NULL,
+ uuid VARCHAR
+(
+ 72
+) NOT NULL,
+ symbol VARCHAR
+(
+ 20
+) NOT NULL,
+ event VARCHAR
+(
+ 30
+) NOT NULL,
+ event_json TEXT NOT NULL,
+ agent VARCHAR
+(
+ 20
+),
+ ip VARCHAR
+(
+ 11
+),
+ event_date TIMESTAMP NOT NULL,
+ create_date TIMESTAMP NOT NULL
+ );
CREATE TABLE IF NOT EXISTS opex_trades
(
- id SERIAL PRIMARY KEY,
- symbol VARCHAR(20) NOT NULL,
- taker_ouid VARCHAR(72) NOT NULL,
- taker_uuid VARCHAR(72) NOT NULL,
- taker_matching_orderid BIGINT NOT NULL,
- taker_direction VARCHAR(20) NOT NULL,
- taker_price BIGINT NOT NULL,
- taker_remained_quantity BIGINT NOT NULL,
- maker_ouid VARCHAR(72) NOT NULL,
- maker_uuid VARCHAR(72) NOT NULL,
- maker_matching_orderid BIGINT NOT NULL,
- maker_direction VARCHAR(20) NOT NULL,
- maker_price BIGINT NOT NULL,
- maker_remained_quantity BIGINT NOT NULL,
- matched_quantity BIGINT NOT NULL,
- trade_date TIMESTAMP NOT NULL,
- create_date TIMESTAMP NOT NULL
-);
+ id
+ SERIAL
+ PRIMARY
+ KEY,
+ symbol
+ VARCHAR
+(
+ 20
+) NOT NULL,
+ taker_ouid VARCHAR
+(
+ 72
+) NOT NULL,
+ taker_uuid VARCHAR
+(
+ 72
+) NOT NULL,
+ taker_matching_orderid BIGINT NOT NULL,
+ taker_direction VARCHAR
+(
+ 20
+) NOT NULL,
+ taker_price BIGINT NOT NULL,
+ taker_remained_quantity BIGINT NOT NULL,
+ maker_ouid VARCHAR
+(
+ 72
+) NOT NULL,
+ maker_uuid VARCHAR
+(
+ 72
+) NOT NULL,
+ maker_matching_orderid BIGINT NOT NULL,
+ maker_direction VARCHAR
+(
+ 20
+) NOT NULL,
+ maker_price BIGINT NOT NULL,
+ maker_remained_quantity BIGINT NOT NULL,
+ matched_quantity BIGINT NOT NULL,
+ trade_date TIMESTAMP NOT NULL,
+ create_date TIMESTAMP NOT NULL
+ );
CREATE TABLE IF NOT EXISTS dead_letter_events
(
- id SERIAL PRIMARY KEY,
- origin_module VARCHAR(72) NOT NULL,
- origin_topic VARCHAR(72),
- consumer_group VARCHAR(72),
- exception_message TEXT,
+ id
+ SERIAL
+ PRIMARY
+ KEY,
+ origin_module
+ VARCHAR
+(
+ 72
+) NOT NULL,
+ origin_topic VARCHAR
+(
+ 72
+),
+ consumer_group VARCHAR
+(
+ 72
+),
+ exception_message TEXT,
exception_stacktrace TEXT,
exception_class_name TEXT,
- timestamp TIMESTAMP NOT NULL,
- value TEXT
-)
+ timestamp TIMESTAMP NOT NULL,
+ value TEXT
+ )
diff --git a/eventlog/pom.xml b/eventlog/pom.xml
index bc56375fa..0619de2ec 100644
--- a/eventlog/pom.xml
+++ b/eventlog/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
diff --git a/market/market-app/pom.xml b/market/market-app/pom.xml
index 37d7e0e47..071a12447 100644
--- a/market/market-app/pom.xml
+++ b/market/market-app/pom.xml
@@ -67,6 +67,10 @@
co.nilin.opex.market.ports.kafka.listener
market-eventlistener-kafka
+
+ co.nilin.opex.market.ports.kafka.producer
+ market-eventproducer-kafka
+
org.springframework.boot
spring-boot-starter-actuator
@@ -84,15 +88,21 @@
org.springframework.cloud
spring-cloud-starter-vault-config
-
- co.nilin.opex.utility
- preferences
-
io.micrometer
micrometer-registry-prometheus
runtime
+
+ org.jfree
+ jfreechart
+ 1.5.3
+
+
+ org.jfree
+ jfreesvg
+ 3.4.3
+
diff --git a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/MarketAppApplication.kt b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/MarketAppApplication.kt
index c8413c59d..f992edac6 100644
--- a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/MarketAppApplication.kt
+++ b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/MarketAppApplication.kt
@@ -11,5 +11,5 @@ import org.springframework.scheduling.annotation.EnableScheduling
class MarketAppApplication
fun main(args: Array) {
- runApplication(*args)
+ runApplication(*args)
}
diff --git a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/config/InitializeService.kt b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/config/InitializeService.kt
deleted file mode 100644
index edeba1083..000000000
--- a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/config/InitializeService.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package co.nilin.opex.market.app.config
-
-import co.nilin.opex.market.core.inout.RateSource
-import co.nilin.opex.market.ports.postgres.dao.CurrencyRateRepository
-import co.nilin.opex.utility.preferences.Preferences
-import kotlinx.coroutines.reactor.awaitSingleOrNull
-import kotlinx.coroutines.runBlocking
-import org.springframework.beans.factory.annotation.Autowired
-import org.springframework.context.annotation.DependsOn
-import org.springframework.stereotype.Component
-import java.math.BigDecimal
-import javax.annotation.PostConstruct
-
-@Component
-@DependsOn("postgresConfig")
-class InitializeService(private val rateRepository: CurrencyRateRepository) {
-
- @Autowired
- private lateinit var preferences: Preferences
-
- @PostConstruct
- fun init() = runBlocking {
- preferences.currencies.forEach {
- /*rateRepository.createOrUpdate(it.symbol, it.symbol, RateSource.MARKET, BigDecimal.ONE)
- .awaitSingleOrNull()
- rateRepository.createOrUpdate(it.symbol, it.symbol, RateSource.EXTERNAL, BigDecimal.ONE)
- .awaitSingleOrNull()*/
- }
- }
-}
diff --git a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/config/SecurityConfig.kt b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/config/SecurityConfig.kt
index e1851cb6a..054ddeab6 100644
--- a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/config/SecurityConfig.kt
+++ b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/config/SecurityConfig.kt
@@ -32,7 +32,7 @@ class SecurityConfig(private val webClient: WebClient) {
@Throws(Exception::class)
fun reactiveJwtDecoder(): ReactiveJwtDecoder? {
return NimbusReactiveJwtDecoder.withJwkSetUri(jwkUrl)
- .webClient(webClient)
+ .webClient(WebClient.create())
.build()
}
}
diff --git a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/config/WebClientConfig.kt b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/config/WebClientConfig.kt
index 3ac945a3e..1548b9677 100644
--- a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/config/WebClientConfig.kt
+++ b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/config/WebClientConfig.kt
@@ -1,12 +1,10 @@
package co.nilin.opex.market.app.config
import org.springframework.cloud.client.ServiceInstance
-import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
-import org.springframework.http.client.reactive.ReactorClientHttpConnector
import org.springframework.web.reactive.function.client.WebClient
import org.zalando.logbook.Logbook
import org.zalando.logbook.netty.LogbookClientHandler
diff --git a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/controller/AdminController.kt b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/controller/AdminController.kt
new file mode 100644
index 000000000..a0e5af6d4
--- /dev/null
+++ b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/controller/AdminController.kt
@@ -0,0 +1,31 @@
+package co.nilin.opex.market.app.controller
+
+import co.nilin.opex.market.app.data.RecentTradesRequest
+import co.nilin.opex.market.app.utils.asLocalDateTime
+import co.nilin.opex.market.core.inout.TradeData
+import co.nilin.opex.market.core.spi.MarketQueryHandler
+import org.springframework.web.bind.annotation.PostMapping
+import org.springframework.web.bind.annotation.RequestBody
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+@RequestMapping("/v1/admin")
+class AdminController(private val marketQueryHandler: MarketQueryHandler) {
+
+ @PostMapping("/recent-trades")
+ suspend fun getRecentTrades(
+ @RequestBody request: RecentTradesRequest,
+ ): List {
+ return marketQueryHandler.recentTrades(
+ request.symbol,
+ request.makerUuid,
+ request.takerUuid,
+ request.fromDate?.asLocalDateTime(),
+ request.toDate?.asLocalDateTime(),
+ request.excludeSelfTrade,
+ request.limit,
+ request.offset
+ )
+ }
+}
\ No newline at end of file
diff --git a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/controller/ChartController.kt b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/controller/ChartController.kt
index 88cd3f2dc..5caa5f30c 100644
--- a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/controller/ChartController.kt
+++ b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/controller/ChartController.kt
@@ -1,13 +1,31 @@
package co.nilin.opex.market.app.controller
+import co.nilin.opex.common.OpexError
+import co.nilin.opex.market.app.data.SparkLineDataResponse
import co.nilin.opex.market.core.inout.CandleData
+import co.nilin.opex.market.core.inout.PriceTime
import co.nilin.opex.market.core.spi.MarketQueryHandler
+import createLineChart
import org.springframework.web.bind.annotation.*
+import java.math.BigDecimal
+
@RestController
@RequestMapping("/v1/chart")
class ChartController(private val marketQueryHandler: MarketQueryHandler) {
+ enum class Period(val code: String) {
+ DAILY("24h"),
+ WEEKLY("7d"),
+ MONTHLY("1M");
+
+ companion object {
+ fun fromCode(code: String): Period? {
+ return values().find { it.code == code }
+ }
+ }
+ }
+
@GetMapping("/{symbol}/candle")
suspend fun getCandleDataForSymbol(
@PathVariable symbol: String,
@@ -19,4 +37,22 @@ class ChartController(private val marketQueryHandler: MarketQueryHandler) {
return marketQueryHandler.getCandleInfo(symbol, interval, since, until, limit)
}
+ @GetMapping("/spark-line")
+ suspend fun getSparkLineForSymbols(
+ @RequestParam("symbols") symbols: List,
+ @RequestParam("period") periodCode: String
+ ): List {
+ val period = Period.fromCode(periodCode) ?: throw OpexError.BadRequest.exception("Invalid period")
+ return symbols.mapNotNull { symbol ->
+ val priceData: List = when (period) {
+ Period.WEEKLY -> marketQueryHandler.getWeeklyPriceData(symbol)
+ Period.MONTHLY -> marketQueryHandler.getMonthlyPriceData(symbol)
+ Period.DAILY -> marketQueryHandler.getDailyPriceData(symbol)
+ }
+ if (priceData.all { it.closePrice == BigDecimal.ZERO }) return@mapNotNull null
+ val isTrendUp = priceData.last().closePrice >= priceData.first().closePrice
+ val svgData = createLineChart(priceData.map { it.closePrice }, priceData.map { it.closeTime })
+ SparkLineDataResponse(symbol, isTrendUp, svgData)
+ }
+ }
}
\ No newline at end of file
diff --git a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/controller/MarketStatsController.kt b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/controller/MarketStatsController.kt
index ecd3401f7..a3c125119 100644
--- a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/controller/MarketStatsController.kt
+++ b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/controller/MarketStatsController.kt
@@ -3,15 +3,11 @@ package co.nilin.opex.market.app.controller
import co.nilin.opex.common.utils.Interval
import co.nilin.opex.market.core.inout.PriceStat
import co.nilin.opex.market.core.inout.TradeVolumeStat
-import co.nilin.opex.market.core.inout.Transaction
-import co.nilin.opex.market.core.inout.TxOfTrades
import co.nilin.opex.market.core.spi.MarketQueryHandler
import org.springframework.web.bind.annotation.GetMapping
-import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
-import java.time.LocalDateTime
import java.util.*
@RestController
@@ -39,5 +35,4 @@ class MarketStatsController(private val marketQueryHandler: MarketQueryHandler)
}
-
}
\ No newline at end of file
diff --git a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/controller/UserDataController.kt b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/controller/UserDataController.kt
index b65fa1503..fbbf28166 100644
--- a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/controller/UserDataController.kt
+++ b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/controller/UserDataController.kt
@@ -1,6 +1,7 @@
package co.nilin.opex.market.app.controller
import co.nilin.opex.common.OpexError
+import co.nilin.opex.market.app.utils.asLocalDateTime
import co.nilin.opex.market.core.inout.*
import co.nilin.opex.market.core.spi.UserQueryHandler
import org.springframework.security.core.annotation.CurrentSecurityContext
@@ -21,11 +22,16 @@ class UserDataController(private val userQueryHandler: UserQueryHandler) {
return userQueryHandler.queryOrder(uuid, request) ?: throw OpexError.NotFound.exception()
}
+ @GetMapping("/{uuid}/orders/open")
+ suspend fun getUserOpenOrders(@PathVariable uuid: String, @RequestParam limit: Int): List {
+ return userQueryHandler.openOrders(uuid, limit)
+ }
+
@GetMapping("/{uuid}/orders/{symbol}/open")
suspend fun getUserOpenOrders(
- @PathVariable uuid: String,
- @PathVariable symbol: String,
- @RequestParam limit: Int
+ @PathVariable uuid: String,
+ @PathVariable symbol: String,
+ @RequestParam limit: Int,
): List {
return userQueryHandler.openOrders(uuid, symbol, limit)
}
@@ -44,11 +50,91 @@ class UserDataController(private val userQueryHandler: UserQueryHandler) {
suspend fun getTxOfTrades(
@PathVariable user: String,
@RequestBody transactionRequest: TransactionRequest,
- @CurrentSecurityContext securityContext: SecurityContext
+ @CurrentSecurityContext securityContext: SecurityContext,
): TransactionResponse? {
if (securityContext.authentication.name != user)
throw OpexError.Forbidden.exception()
return userQueryHandler.txOfTrades(transactionRequest.apply { owner = user })
}
+ @GetMapping("/order/history/{uuid}")
+ suspend fun getOrderHistory(
+ @RequestParam symbol: String?,
+ @RequestParam startTime: Long?,
+ @RequestParam endTime: Long?,
+ @RequestParam orderType: MatchingOrderType?,
+ @RequestParam direction: OrderDirection?,
+ @RequestParam limit: Int?,
+ @RequestParam offset: Int?,
+ @PathVariable uuid: String,
+ ): List {
+ return userQueryHandler.getOrderHistory(
+ uuid,
+ symbol,
+ startTime?.let { startTime.asLocalDateTime() },
+ endTime?.let { endTime.asLocalDateTime() },
+ orderType,
+ direction,
+ limit,
+ offset
+ )
+ }
+
+ @GetMapping("/order/history/count/{uuid}")
+ suspend fun getOrderHistoryCount(
+ @RequestParam symbol: String?,
+ @RequestParam startTime: Long?,
+ @RequestParam endTime: Long?,
+ @RequestParam orderType: MatchingOrderType?,
+ @RequestParam direction: OrderDirection?,
+ @PathVariable uuid: String,
+ ): Long {
+ return userQueryHandler.getOrderHistoryCount(
+ uuid,
+ symbol,
+ startTime?.let { startTime.asLocalDateTime() },
+ endTime?.let { endTime.asLocalDateTime() },
+ orderType,
+ direction,
+ )
+ }
+
+ @GetMapping("/trade/history/{uuid}")
+ suspend fun getTradeHistory(
+ @PathVariable uuid: String,
+ @RequestParam symbol: String?,
+ @RequestParam startTime: Long?,
+ @RequestParam endTime: Long?,
+ @RequestParam direction: OrderDirection?,
+ @RequestParam limit: Int?,
+ @RequestParam offset: Int?,
+ ): List {
+ return userQueryHandler.getTradeHistory(
+ uuid,
+ symbol,
+ startTime?.let { startTime.asLocalDateTime() },
+ endTime?.let { endTime.asLocalDateTime() },
+ direction,
+ limit,
+ offset
+ )
+ }
+
+ @GetMapping("/trade/history/count/{uuid}")
+ suspend fun getTradeHistoryCount(
+ @PathVariable uuid: String,
+ @RequestParam symbol: String?,
+ @RequestParam startTime: Long?,
+ @RequestParam endTime: Long?,
+ @RequestParam direction: OrderDirection?,
+ ): Long {
+ return userQueryHandler.getTradeHistoryCount(
+ uuid,
+ symbol,
+ startTime?.let { startTime.asLocalDateTime() },
+ endTime?.let { endTime.asLocalDateTime() },
+ direction,
+ )
+ }
+
}
\ No newline at end of file
diff --git a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/data/RecentTradesRequest.kt b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/data/RecentTradesRequest.kt
new file mode 100644
index 000000000..7922d4fde
--- /dev/null
+++ b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/data/RecentTradesRequest.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.market.app.data
+
+data class RecentTradesRequest(
+ val symbol: String?,
+ val makerUuid: String?,
+ val takerUuid: String?,
+ val fromDate: Long?,
+ val toDate: Long?,
+ val excludeSelfTrade : Boolean = true,
+ val limit: Int,
+ val offset: Int,
+
+ )
\ No newline at end of file
diff --git a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/data/SparkLineDataResponse.kt b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/data/SparkLineDataResponse.kt
new file mode 100644
index 000000000..3a54ad1b5
--- /dev/null
+++ b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/data/SparkLineDataResponse.kt
@@ -0,0 +1,7 @@
+package co.nilin.opex.market.app.data
+
+data class SparkLineDataResponse(
+ val symbol: String,
+ val isTrendUp: Boolean,
+ val svgData: String
+)
diff --git a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/service/ReportingService.kt b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/service/ReportingService.kt
index 38c05cbf0..f4a952100 100644
--- a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/service/ReportingService.kt
+++ b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/service/ReportingService.kt
@@ -1,13 +1,11 @@
package co.nilin.opex.market.app.service
import co.nilin.opex.common.utils.Interval
-import co.nilin.opex.market.app.utils.asLocalDateTime
import co.nilin.opex.market.core.spi.MarketQueryHandler
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
-import java.time.LocalDateTime
@Service
class ReportingService(private val marketQueryHandler: MarketQueryHandler) {
diff --git a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/utils/ChartBuilder.kt b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/utils/ChartBuilder.kt
new file mode 100644
index 000000000..6621d2af0
--- /dev/null
+++ b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/utils/ChartBuilder.kt
@@ -0,0 +1,73 @@
+import org.jfree.chart.ChartFactory
+import org.jfree.chart.JFreeChart
+import org.jfree.chart.plot.PlotOrientation
+import org.jfree.chart.plot.XYPlot
+import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer
+import org.jfree.data.xy.XYSeries
+import org.jfree.data.xy.XYSeriesCollection
+import org.jfree.graphics2d.svg.SVGGraphics2D
+import java.awt.Color
+import java.awt.Rectangle
+import java.math.BigDecimal
+import java.time.LocalDateTime
+import java.time.ZoneId
+import java.util.Base64
+
+fun createLineChart(prices: List, times: List): String {
+ val series = XYSeries("Price").apply {
+ prices.zip(times).forEach { (price, time) ->
+ add(time.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(), price.toDouble())
+ }
+ }
+ val dataset = XYSeriesCollection().apply {
+ addSeries(series)
+ }
+ val chart = ChartFactory.createXYLineChart(
+ null, // Chart title
+ null, // X-axis label
+ null, // Y-axis label
+ dataset, // Data
+ PlotOrientation.VERTICAL,
+ false,
+ false,
+ false
+ )
+ val plot: XYPlot = chart.xyPlot
+ val renderer = XYLineAndShapeRenderer(true, false)
+ // Set chart color
+ renderer.setSeriesPaint(0, Color.WHITE)
+ // Set axis ranges to keep the chart logical
+ plot.rangeAxis.range = org.jfree.data.Range(
+ prices.minOrNull()?.toDouble()?.times(0.99) ?: 0.0,
+ prices.maxOrNull()?.toDouble() ?: 0.0
+ )
+
+ val timesInMillis = times.map { it.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() }
+ plot.domainAxis.range = org.jfree.data.Range(
+ timesInMillis.minOrNull()?.toDouble() ?: 0.0,
+ timesInMillis.maxOrNull()?.toDouble() ?: 0.0
+ )
+ plot.domainAxis.lowerMargin = 0.0
+ plot.domainAxis.upperMargin = 0.0
+
+ // Remove gridlines, axis, and background
+ plot.renderer = renderer
+ plot.isDomainGridlinesVisible = false
+ plot.isRangeGridlinesVisible = false
+ plot.backgroundPaint = null
+ plot.domainAxis.isVisible = false
+ plot.rangeAxis.isVisible = false
+ plot.isOutlineVisible = false
+ chart.backgroundPaint = null
+
+ return chartToSvgString(chart, 100, 35)
+}
+
+fun chartToSvgString(chart: JFreeChart, width: Int, height: Int): String {
+ val svg = SVGGraphics2D(width, height)
+ chart.draw(svg, Rectangle(width, height))
+ val svgString = svg.svgElement
+ svg.dispose()
+ val base64SvgString = Base64.getEncoder().encodeToString(svgString.toByteArray(Charsets.UTF_8))
+ return base64SvgString
+}
diff --git a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/utils/PrometheusHealthExtension.kt b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/utils/PrometheusHealthExtension.kt
index 90fe2b630..ab1ca9aa0 100644
--- a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/utils/PrometheusHealthExtension.kt
+++ b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/utils/PrometheusHealthExtension.kt
@@ -6,7 +6,6 @@ import org.springframework.boot.actuate.health.HealthComponent
import org.springframework.boot.actuate.health.HealthEndpoint
import org.springframework.boot.actuate.health.SystemHealth
import org.springframework.context.annotation.Profile
-import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
diff --git a/market/market-app/src/main/resources/application.yml b/market/market-app/src/main/resources/application.yml
index 7da42c161..38e54d518 100644
--- a/market/market-app/src/main/resources/application.yml
+++ b/market/market-app/src/main/resources/application.yml
@@ -91,6 +91,6 @@ logging:
co.nilin: INFO
app:
auth:
- cert-url: lb://opex-auth/auth/realms/opex/protocol/openid-connect/certs
+ cert-url: http://keycloak:8080/realms/opex/protocol/openid-connect/certs
swagger:
authUrl: ${SWAGGER_AUTH_URL:https://api.opex.dev/auth}/realms/opex/protocol/openid-connect/token}
diff --git a/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/MarketOrderEvent.kt b/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/MarketOrderEvent.kt
new file mode 100644
index 000000000..6dd1bd4ef
--- /dev/null
+++ b/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/MarketOrderEvent.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.market.core.inout
+
+import java.time.LocalDateTime
+
+open class MarketOrderEvent {
+
+ val time: LocalDateTime = LocalDateTime.now()
+}
\ No newline at end of file
diff --git a/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/OrderData.kt b/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/OrderData.kt
new file mode 100644
index 000000000..3a0b3debb
--- /dev/null
+++ b/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/OrderData.kt
@@ -0,0 +1,22 @@
+package co.nilin.opex.market.core.inout
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+import java.util.Date
+
+data class OrderData(
+ val symbol: String,
+ val orderId: Long,
+ val orderType: MatchingOrderType,
+ val side: OrderDirection,
+ val price: BigDecimal,
+ val quantity: BigDecimal,
+ val quoteQuantity: BigDecimal,
+ val executedQuantity: BigDecimal,
+ val takerFee: BigDecimal,
+ val makerFee: BigDecimal,
+ val status: Int,
+ val appearance: Int,
+ val createDate: LocalDateTime,
+ val updateDate: LocalDateTime,
+)
diff --git a/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/PriceTime.kt b/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/PriceTime.kt
new file mode 100644
index 000000000..8df9109fc
--- /dev/null
+++ b/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/PriceTime.kt
@@ -0,0 +1,9 @@
+package co.nilin.opex.market.core.inout
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+data class PriceTime(
+ val closeTime: LocalDateTime,
+ val closePrice: BigDecimal,
+)
diff --git a/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/Trade.kt b/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/Trade.kt
index 603284947..9a23460b4 100644
--- a/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/Trade.kt
+++ b/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/Trade.kt
@@ -1,8 +1,10 @@
package co.nilin.opex.market.core.inout
import java.math.BigDecimal
+import java.time.LocalDateTime
import java.util.*
+// User trade data
data class Trade(
val symbol: String,
val id: Long,
@@ -12,7 +14,7 @@ data class Trade(
val quoteQuantity: BigDecimal,
val commission: BigDecimal,
val commissionAsset: String,
- val time: Date,
+ val time: LocalDateTime,
val isBuyer: Boolean,
val isMaker: Boolean,
val isBestMatch: Boolean,
diff --git a/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/TradeData.kt b/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/TradeData.kt
new file mode 100644
index 000000000..be190bfd4
--- /dev/null
+++ b/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/TradeData.kt
@@ -0,0 +1,17 @@
+package co.nilin.opex.market.core.inout
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+// Trade data for admin
+data class TradeData(
+ val tradeId: Long,
+ val symbol: String,
+ val matchedPrice: BigDecimal,
+ val matchedQuantity: BigDecimal,
+ val takerPrice: BigDecimal,
+ val makerPrice: BigDecimal,
+ val tradeDate: LocalDateTime,
+ val makerUuid: String,
+ val takerUuid: String,
+)
diff --git a/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/Transaction.kt b/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/Transaction.kt
index 39b85d1b9..72f715fb0 100644
--- a/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/Transaction.kt
+++ b/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/Transaction.kt
@@ -4,14 +4,14 @@ import java.math.BigDecimal
import java.time.LocalDateTime
data class Transaction(
- var createDate: LocalDateTime,
- var volume: BigDecimal,
- val transactionPrice: BigDecimal,
- var matchedPrice: BigDecimal,
- var side: String,
- var symbol: String,
- var fee: BigDecimal,
- var user: String?=null
+ var createDate: LocalDateTime,
+ var volume: BigDecimal,
+ val transactionPrice: BigDecimal,
+ var matchedPrice: BigDecimal,
+ var side: String,
+ var symbol: String,
+ var fee: BigDecimal,
+ var user: String? = null
)
-data class TxOfTrades(var transactions:List?)
\ No newline at end of file
+data class TxOfTrades(var transactions: List?)
\ No newline at end of file
diff --git a/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/TransactionRequest.kt b/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/TransactionRequest.kt
index d9f66d6a4..2dd1ec454 100644
--- a/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/TransactionRequest.kt
+++ b/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/TransactionRequest.kt
@@ -1,12 +1,12 @@
package co.nilin.opex.market.core.inout
data class TransactionRequest(
- val coin: String?,
- val category: String?,
- val startTime: Long? = null,
- val endTime: Long? = null,
- val limit: Int? = 10,
- val offset: Int? = 0,
- val ascendingByTime: Boolean? = false,
- var owner: String? = null
+ val coin: String?,
+ val category: String?,
+ val startTime: Long? = null,
+ val endTime: Long? = null,
+ val limit: Int? = 10,
+ val offset: Int? = 0,
+ val ascendingByTime: Boolean? = false,
+ var owner: String? = null
)
\ No newline at end of file
diff --git a/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/TransactionResponse.kt b/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/TransactionResponse.kt
index bc50f7f80..b78e4eec0 100644
--- a/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/TransactionResponse.kt
+++ b/market/market-core/src/main/kotlin/co/nilin/opex/market/core/inout/TransactionResponse.kt
@@ -1,19 +1,18 @@
package co.nilin.opex.market.core.inout
import java.math.BigDecimal
-import java.time.LocalDateTime
-import java.util.Date
+import java.util.*
data class TransactionDto(
- var createDate: Date,
- var volume: BigDecimal,
- val transactionPrice: BigDecimal,
- var matchedPrice: BigDecimal,
- var side: String,
- var symbol: String,
- var fee: BigDecimal,
- var user: String?=null
+ var createDate: Date,
+ var volume: BigDecimal,
+ val transactionPrice: BigDecimal,
+ var matchedPrice: BigDecimal,
+ var side: String,
+ var symbol: String,
+ var fee: BigDecimal,
+ var user: String? = null
)
-data class TransactionResponse(var transactions:List?)
\ No newline at end of file
+data class TransactionResponse(var transactions: List?)
\ No newline at end of file
diff --git a/market/market-core/src/main/kotlin/co/nilin/opex/market/core/spi/MarketOrderProducer.kt b/market/market-core/src/main/kotlin/co/nilin/opex/market/core/spi/MarketOrderProducer.kt
new file mode 100644
index 000000000..cdd805bb5
--- /dev/null
+++ b/market/market-core/src/main/kotlin/co/nilin/opex/market/core/spi/MarketOrderProducer.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.market.core.spi
+
+interface MarketOrderProducer {
+
+ suspend fun openOrderUpdate(uuid: String, pair: String)
+}
\ No newline at end of file
diff --git a/market/market-core/src/main/kotlin/co/nilin/opex/market/core/spi/MarketQueryHandler.kt b/market/market-core/src/main/kotlin/co/nilin/opex/market/core/spi/MarketQueryHandler.kt
index 37e644c6c..6e31d84fc 100644
--- a/market/market-core/src/main/kotlin/co/nilin/opex/market/core/spi/MarketQueryHandler.kt
+++ b/market/market-core/src/main/kotlin/co/nilin/opex/market/core/spi/MarketQueryHandler.kt
@@ -18,16 +18,27 @@ interface MarketQueryHandler {
suspend fun recentTrades(symbol: String, limit: Int): List
+ suspend fun recentTrades(
+ symbol: String?,
+ makerUuid: String?,
+ takerUuid: String?,
+ fromDate: LocalDateTime?,
+ toDate: LocalDateTime?,
+ excludeSelfTrade: Boolean,
+ limit: Int,
+ offset: Int,
+ ): List
+
suspend fun lastPrice(symbol: String?): List
suspend fun getBestPriceForSymbols(symbols: List): List
suspend fun getCandleInfo(
- symbol: String,
- interval: String,
- startTime: Long?,
- endTime: Long?,
- limit: Int
+ symbol: String,
+ interval: String,
+ startTime: Long?,
+ endTime: Long?,
+ limit: Int,
): List
suspend fun numberOfActiveUsers(interval: Interval): Long
@@ -44,5 +55,9 @@ interface MarketQueryHandler {
suspend fun mostTrades(interval: Interval): TradeVolumeStat?
+ suspend fun getWeeklyPriceData(symbol: String): List
+
+ suspend fun getMonthlyPriceData(symbol: String): List
+ suspend fun getDailyPriceData(symbol: String): List
}
\ No newline at end of file
diff --git a/market/market-core/src/main/kotlin/co/nilin/opex/market/core/spi/UserQueryHandler.kt b/market/market-core/src/main/kotlin/co/nilin/opex/market/core/spi/UserQueryHandler.kt
index 1a7d097d9..0086ec66e 100644
--- a/market/market-core/src/main/kotlin/co/nilin/opex/market/core/spi/UserQueryHandler.kt
+++ b/market/market-core/src/main/kotlin/co/nilin/opex/market/core/spi/UserQueryHandler.kt
@@ -9,6 +9,8 @@ interface UserQueryHandler {
suspend fun queryOrder(uuid: String, request: QueryOrderRequest): Order?
+ suspend fun openOrders(uuid: String, limit: Int): List
+
suspend fun openOrders(uuid: String, symbol: String?, limit: Int): List
suspend fun allOrders(uuid: String, allOrderRequest: AllOrderRequest): List
@@ -17,4 +19,41 @@ interface UserQueryHandler {
suspend fun txOfTrades(transactionRequest: TransactionRequest): TransactionResponse?
+ suspend fun getOrderHistory(
+ uuid: String,
+ symbol: String?,
+ startTime: LocalDateTime?,
+ endTime: LocalDateTime?,
+ orderType: MatchingOrderType?,
+ direction: OrderDirection?,
+ limit: Int?,
+ offset: Int?,
+ ): List
+
+ suspend fun getOrderHistoryCount(
+ uuid: String,
+ symbol: String?,
+ startTime: LocalDateTime?,
+ endTime: LocalDateTime?,
+ orderType: MatchingOrderType?,
+ direction: OrderDirection?,
+ ): Long
+
+ suspend fun getTradeHistory(
+ uuid: String,
+ symbol: String?,
+ startTime: LocalDateTime?,
+ endTime: LocalDateTime?,
+ direction: OrderDirection?,
+ limit: Int?,
+ offset: Int?,
+ ): List
+
+ suspend fun getTradeHistoryCount(
+ uuid: String,
+ symbol: String?,
+ startTime: LocalDateTime?,
+ endTime: LocalDateTime?,
+ direction: OrderDirection?,
+ ): Long
}
\ No newline at end of file
diff --git a/market/market-ports/market-eventproducer-kafka/pom.xml b/market/market-ports/market-eventproducer-kafka/pom.xml
new file mode 100644
index 000000000..84c9ccd06
--- /dev/null
+++ b/market/market-ports/market-eventproducer-kafka/pom.xml
@@ -0,0 +1,57 @@
+
+
+ 4.0.0
+
+
+ co.nilin.opex.market
+ market
+ 1.0.1-beta.7
+ ../../pom.xml
+
+
+ co.nilin.opex.market.ports.kafka.producer
+ market-eventproducer-kafka
+ market-eventproducer-kafka
+ Market kafka producer of Opex
+
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+ co.nilin.opex.market.core
+ market-core
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+ io.projectreactor.kotlin
+ reactor-kotlin-extensions
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-reactor
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-core
+
+
+ org.springframework.kafka
+ spring-kafka-test
+ test
+
+
+
diff --git a/market/market-ports/market-eventproducer-kafka/src/main/kotlin/co/nilin/opex/market/ports/kafka/producer/config/KafkaProducerConfig.kt b/market/market-ports/market-eventproducer-kafka/src/main/kotlin/co/nilin/opex/market/ports/kafka/producer/config/KafkaProducerConfig.kt
new file mode 100644
index 000000000..439c14839
--- /dev/null
+++ b/market/market-ports/market-eventproducer-kafka/src/main/kotlin/co/nilin/opex/market/ports/kafka/producer/config/KafkaProducerConfig.kt
@@ -0,0 +1,59 @@
+package co.nilin.opex.market.ports.kafka.producer.config
+
+import co.nilin.opex.market.core.inout.MarketOrderEvent
+import org.apache.kafka.clients.admin.NewTopic
+import org.apache.kafka.clients.producer.ProducerConfig
+import org.apache.kafka.common.serialization.StringSerializer
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.support.GenericApplicationContext
+import org.springframework.kafka.config.TopicBuilder
+import org.springframework.kafka.core.DefaultKafkaProducerFactory
+import org.springframework.kafka.core.KafkaTemplate
+import org.springframework.kafka.core.ProducerFactory
+import org.springframework.kafka.support.serializer.JsonSerializer
+import java.util.function.Supplier
+
+object KafkaTopics {
+ const val MARKET_ORDER = "marketOrder"
+}
+
+@Configuration
+class KafkaProducerConfig(
+ @Value("\${spring.kafka.bootstrap-servers}")
+ private val bootstrapServers: String
+) {
+
+ @Bean
+ fun producerConfigs(): Map {
+ return mapOf(
+ ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers,
+ ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java,
+ ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to JsonSerializer::class.java,
+ ProducerConfig.ACKS_CONFIG to "all",
+ JsonSerializer.TYPE_MAPPINGS to "openOrderUpdateEvent:co.nilin.opex.market.ports.kafka.producer.events.OpenOrderUpdateEvent"
+ )
+ }
+
+ @Bean
+ fun producerFactory(producerConfigs: Map): ProducerFactory {
+ return DefaultKafkaProducerFactory(producerConfigs)
+ }
+
+ @Bean
+ fun kafkaTemplate(producerFactory: ProducerFactory): KafkaTemplate {
+ return KafkaTemplate(producerFactory)
+ }
+
+ @Autowired
+ fun createUserCreatedTopics(applicationContext: GenericApplicationContext) {
+ applicationContext.registerBean("topic_marketOrder", NewTopic::class.java, Supplier {
+ TopicBuilder.name(KafkaTopics.MARKET_ORDER)
+ .partitions(1)
+ .replicas(1)
+ .build()
+ })
+ }
+}
\ No newline at end of file
diff --git a/market/market-ports/market-eventproducer-kafka/src/main/kotlin/co/nilin/opex/market/ports/kafka/producer/events/OpenOrderUpdateEvent.kt b/market/market-ports/market-eventproducer-kafka/src/main/kotlin/co/nilin/opex/market/ports/kafka/producer/events/OpenOrderUpdateEvent.kt
new file mode 100644
index 000000000..1f8557293
--- /dev/null
+++ b/market/market-ports/market-eventproducer-kafka/src/main/kotlin/co/nilin/opex/market/ports/kafka/producer/events/OpenOrderUpdateEvent.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.market.ports.kafka.producer.events
+
+import co.nilin.opex.market.core.inout.MarketOrderEvent
+
+data class OpenOrderUpdateEvent(val uuid: String, val pair: String) : MarketOrderEvent()
\ No newline at end of file
diff --git a/market/market-ports/market-eventproducer-kafka/src/main/kotlin/co/nilin/opex/market/ports/kafka/producer/producer/MarketOrderProducer.kt b/market/market-ports/market-eventproducer-kafka/src/main/kotlin/co/nilin/opex/market/ports/kafka/producer/producer/MarketOrderProducer.kt
new file mode 100644
index 000000000..e8e322627
--- /dev/null
+++ b/market/market-ports/market-eventproducer-kafka/src/main/kotlin/co/nilin/opex/market/ports/kafka/producer/producer/MarketOrderProducer.kt
@@ -0,0 +1,35 @@
+package co.nilin.opex.market.ports.kafka.producer.producer
+
+import co.nilin.opex.common.utils.LoggerDelegate
+import co.nilin.opex.market.ports.kafka.producer.config.KafkaTopics
+import co.nilin.opex.market.core.inout.MarketOrderEvent
+import co.nilin.opex.market.core.spi.MarketOrderProducer
+import co.nilin.opex.market.ports.kafka.producer.events.OpenOrderUpdateEvent
+import org.springframework.kafka.core.KafkaTemplate
+import org.springframework.retry.support.RetryTemplate
+import org.springframework.stereotype.Component
+
+@Component
+class MarketOrderProducer(private val template: KafkaTemplate) : MarketOrderProducer {
+
+ private val logger by LoggerDelegate()
+
+ private val retryTemplate = RetryTemplate.builder()
+ .maxAttempts(10)
+ .exponentialBackoff(1000, 1.8, 5 * 60 * 1000)
+ .retryOn(Exception::class.java)
+ .build()
+
+ private suspend fun send(event: MarketOrderEvent) {
+ retryTemplate.execute {
+ template.send(KafkaTopics.MARKET_ORDER, event).addCallback(
+ { logger.info("Market order event sent") },
+ { error -> logger.error("Error sending market order event", error) }
+ )
+ }
+ }
+
+ override suspend fun openOrderUpdate(uuid: String, pair: String) {
+ send(OpenOrderUpdateEvent(uuid, pair))
+ }
+}
\ No newline at end of file
diff --git a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/dao/OrderRepository.kt b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/dao/OrderRepository.kt
index 164984152..2b3eb435b 100644
--- a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/dao/OrderRepository.kt
+++ b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/dao/OrderRepository.kt
@@ -1,6 +1,8 @@
package co.nilin.opex.market.ports.postgres.dao
import co.nilin.opex.market.core.inout.AggregatedOrderPriceModel
+import co.nilin.opex.market.core.inout.MatchingOrderType
+import co.nilin.opex.market.core.inout.OrderData
import co.nilin.opex.market.core.inout.OrderDirection
import co.nilin.opex.market.ports.postgres.model.OrderModel
import kotlinx.coroutines.flow.Flow
@@ -25,8 +27,9 @@ interface OrderRepository : ReactiveCrudRepository {
@Query("select * from orders where symbol = :symbol and order_id = :orderId")
fun findBySymbolAndOrderId(
@Param("symbol")
- symbol: String, @Param("orderId")
- orderId: Long
+ symbol: String,
+ @Param("orderId")
+ orderId: Long,
): Mono
@Query("select * from orders where symbol = :symbol and client_order_id = :origClientOrderId")
@@ -34,7 +37,7 @@ interface OrderRepository : ReactiveCrudRepository {
@Param("symbol")
symbol: String,
@Param("origClientOrderId")
- origClientOrderId: String
+ origClientOrderId: String,
): Mono
@Query(
@@ -53,7 +56,7 @@ interface OrderRepository : ReactiveCrudRepository {
symbol: String?,
@Param("statuses")
status: Collection,
- limit: Int
+ limit: Int,
): Flow
@Query(
@@ -75,7 +78,7 @@ interface OrderRepository : ReactiveCrudRepository {
startTime: Date?,
@Param("endTime")
endTime: Date?,
- limit: Int
+ limit: Int,
): Flow
@Query(
@@ -96,7 +99,7 @@ interface OrderRepository : ReactiveCrudRepository {
@Param("limit")
limit: Int,
@Param("statuses")
- status: Collection
+ status: Collection,
): Flux
@Query(
@@ -117,7 +120,7 @@ interface OrderRepository : ReactiveCrudRepository {
@Param("limit")
limit: Int,
@Param("statuses")
- status: Collection
+ status: Collection,
): Flux
@Query("select * from orders where symbol = :symbol order by create_date desc limit 1")
@@ -131,4 +134,69 @@ interface OrderRepository : ReactiveCrudRepository {
@Query("select count(*) from orders where symbol = :symbol and create_date >= :interval")
fun countBySymbolNewerThan(interval: LocalDateTime, symbol: String): Flow
+
+ @Query(
+ """
+select o.symbol,
+ o.order_id,
+ o.order_type,
+ o.side,
+ o.price,
+ o.quantity,
+ o.quote_quantity,
+ os.executed_quantity,
+ o.taker_fee,
+ o.maker_fee,
+ os.status,
+ os.appearance,
+ o.create_date,
+ os.date as update_date
+from orders o
+ left join (select *
+ from order_status os1
+ where os1.date = (select max(os2.date)
+ from order_status os2
+ where os2.ouid = os1.ouid)) os on o.ouid = os.ouid
+ WHERE uuid = :uuid
+ and (:symbol is null or o.symbol = :symbol)
+ and (:startTime is null or o.create_date >= :startTime)
+ and (:endTime is null or o.create_date <= :endTime)
+ and (:orderType is null or o.order_type = :orderType)
+ and (:direction is null or o.side = :direction)
+order by create_date desc
+ limit :limit offset :offset;
+ """
+ )
+ fun findByCriteria(
+ uuid: String,
+ symbol: String?,
+ startTime: LocalDateTime?,
+ endTime: LocalDateTime?,
+ orderType: MatchingOrderType?,
+ direction: OrderDirection?,
+ limit: Int?,
+ offset: Int?,
+ ): Flow
+
+ @Query(
+ """
+select count(*)
+from orders o
+ WHERE uuid = :uuid
+ and (:symbol is null or o.symbol = :symbol)
+ and (:startTime is null or o.create_date >= :startTime)
+ and (:endTime is null or o.create_date <= :endTime)
+ and (:orderType is null or o.order_type = :orderType)
+ and (:direction is null or o.side = :direction)
+ """
+ )
+ fun countByCriteria(
+ uuid: String,
+ symbol: String?,
+ startTime: LocalDateTime?,
+ endTime: LocalDateTime?,
+ orderType: MatchingOrderType?,
+ direction: OrderDirection?,
+ ): Mono
+
}
\ No newline at end of file
diff --git a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/dao/TradeRepository.kt b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/dao/TradeRepository.kt
index e7e65c16f..4922c195d 100644
--- a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/dao/TradeRepository.kt
+++ b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/dao/TradeRepository.kt
@@ -1,15 +1,13 @@
package co.nilin.opex.market.ports.postgres.dao
import co.nilin.opex.market.core.inout.BestPrice
+import co.nilin.opex.market.core.inout.OrderDirection
import co.nilin.opex.market.core.inout.PriceStat
+import co.nilin.opex.market.core.inout.Trade
import co.nilin.opex.market.core.inout.TradeVolumeStat
import co.nilin.opex.market.core.inout.Transaction
-import co.nilin.opex.market.ports.postgres.model.CandleInfoData
-import co.nilin.opex.market.ports.postgres.model.LastPrice
-import co.nilin.opex.market.ports.postgres.model.TradeModel
-import co.nilin.opex.market.ports.postgres.model.TradeTickerData
+import co.nilin.opex.market.ports.postgres.model.*
import kotlinx.coroutines.flow.Flow
-import org.springframework.data.domain.Pageable
import org.springframework.data.r2dbc.repository.Query
import org.springframework.data.repository.query.Param
import org.springframework.data.repository.reactive.ReactiveCrudRepository
@@ -29,7 +27,7 @@ interface TradeRepository : ReactiveCrudRepository {
fun findMostRecentBySymbol(symbol: String): Flow
@Query(
- """
+ """
select * from trades where :uuid in (taker_uuid, maker_uuid)
and (:fromTrade is null or id > :fromTrade)
and (:symbol is null or symbol = :symbol)
@@ -40,29 +38,29 @@ interface TradeRepository : ReactiveCrudRepository {
"""
)
fun findByUuidAndSymbolAndTimeBetweenAndTradeIdGreaterThan(
- @Param("uuid")
- uuid: String,
- @Param("symbol")
- symbol: String?,
- @Param("fromTrade")
- fromTrade: Long?,
- @Param("startTime")
- startTime: Date?,
- @Param("endTime")
- endTime: Date?,
- limit: Int
+ @Param("uuid")
+ uuid: String,
+ @Param("symbol")
+ symbol: String?,
+ @Param("fromTrade")
+ fromTrade: Long?,
+ @Param("startTime")
+ startTime: Date?,
+ @Param("endTime")
+ endTime: Date?,
+ limit: Int,
): Flow
@Query("select * from trades where symbol = :symbol order by create_date desc limit :limit")
fun findBySymbolSortDescendingByCreateDate(
- @Param("symbol")
- symbol: String,
- @Param("limit")
- limit: Int
+ @Param("symbol")
+ symbol: String,
+ @Param("limit")
+ limit: Int,
): Flow
@Query(
- """
+ """
with first_trade as (select id, symbol, matched_price, matched_quantity from trades where id in (select min(id) from trades where create_date > :date group by symbol)),
last_trade as (select id, symbol, matched_price, matched_quantity from trades where id in (select max(id) from trades where create_date > :date group by symbol))
select symbol,
@@ -103,7 +101,7 @@ interface TradeRepository : ReactiveCrudRepository {
fun tradeTicker(@Param("date") createDate: LocalDateTime): Flux
@Query(
- """
+ """
with first_trade as (select * from trades where create_date > :date and symbol = :symbol order by create_date limit 1),
last_trade as (select * from trades where create_date > :date and symbol = :symbol order by create_date desc limit 1)
select symbol,
@@ -142,14 +140,14 @@ interface TradeRepository : ReactiveCrudRepository {
"""
)
fun tradeTickerBySymbol(
- @Param("symbol")
- symbol: String,
- @Param("date")
- createDate: LocalDateTime,
+ @Param("symbol")
+ symbol: String,
+ @Param("date")
+ createDate: LocalDateTime,
): Mono
@Query(
- """
+ """
select symbol,
(
select price from orders
@@ -170,7 +168,7 @@ interface TradeRepository : ReactiveCrudRepository {
fun bestAskAndBidPrice(): Flux
@Query(
- """
+ """
select symbol,
(
select price from orders
@@ -192,7 +190,7 @@ interface TradeRepository : ReactiveCrudRepository {
fun bestAskAndBidPrice(symbols: List): Flux
@Query(
- """
+ """
select symbol,
(
select price from orders
@@ -220,8 +218,8 @@ interface TradeRepository : ReactiveCrudRepository {
fun findAllGroupBySymbol(): Flux
@Query(
- """
- WITH intervals AS (SELECT * FROM interval_generator((:startTime), (:endTime), :interval ::INTERVAL)),
+ """
+ WITH intervals AS (SELECT * FROM interval_generator((TO_TIMESTAMP(:startTime)) ::TIMESTAMP WITHOUT TIME ZONE, (:endTime), :interval ::INTERVAL)),
first_trade AS (
SELECT DISTINCT ON (f.start_time) f.start_time, f.end_time, t.matched_price AS open_price FROM intervals f
LEFT JOIN trades t ON t.create_date >= f.start_time AND t.create_date < f.end_time AND t.symbol = :symbol
@@ -252,16 +250,16 @@ interface TradeRepository : ReactiveCrudRepository {
"""
)
suspend fun candleData(
- @Param("symbol")
- symbol: String,
- @Param("interval")
- interval: String,
- @Param("startTime")
- startTime: LocalDateTime,
- @Param("endTime")
- endTime: LocalDateTime,
- @Param("limit")
- limit: Int,
+ @Param("symbol")
+ symbol: String,
+ @Param("interval")
+ interval: String,
+ @Param("startTime")
+ startTime: LocalDateTime,
+ @Param("endTime")
+ endTime: LocalDateTime,
+ @Param("limit")
+ limit: Int,
): Flux
@Query("select * from trades order by create_date desc limit 1")
@@ -277,7 +275,7 @@ interface TradeRepository : ReactiveCrudRepository {
fun countBySymbolNewerThan(interval: LocalDateTime, symbol: String): Flow
@Query(
- """
+ """
WITH first_trade AS (SELECT symbol, MIN(id) AS min_id FROM trades WHERE create_date > :since GROUP BY symbol),
last_trade AS (SELECT symbol, MAX(id) AS max_id FROM trades WHERE create_date > :since GROUP BY symbol),
first_trade_details AS (SELECT ft.symbol, t.matched_price AS first_price FROM first_trade ft JOIN trades t ON ft.min_id = t.id),
@@ -299,7 +297,7 @@ interface TradeRepository : ReactiveCrudRepository {
fun findByMostIncreasedPrice(since: LocalDateTime, limit: Int): Flux
@Query(
- """
+ """
WITH first_trade AS (SELECT symbol, MIN(id) AS min_id FROM trades WHERE create_date > :since GROUP BY symbol),
last_trade AS (SELECT symbol, MAX(id) AS max_id FROM trades WHERE create_date > :since GROUP BY symbol),
first_trade_details AS (SELECT ft.symbol, t.matched_price AS first_price FROM first_trade ft JOIN trades t ON ft.min_id = t.id),
@@ -321,7 +319,7 @@ interface TradeRepository : ReactiveCrudRepository {
fun findByMostDecreasedPrice(since: LocalDateTime, limit: Int): Flux
@Query(
- """
+ """
with first_trade as (select symbol, matched_quantity mq from trades where id in (select min(id) from trades where create_date > :since group by symbol)),
last_trade as (select symbol, matched_quantity mq from trades where id in (select max(id) from trades where create_date > :since group by symbol))
select
@@ -345,7 +343,7 @@ interface TradeRepository : ReactiveCrudRepository {
fun findByMostVolume(since: LocalDateTime): Mono
@Query(
- """
+ """
with first_trade as (select symbol, matched_quantity mq from trades where id in (select min(id) from trades where create_date > :since group by symbol)),
last_trade as (select symbol, matched_quantity mq from trades where id in (select max(id) from trades where create_date > :since group by symbol))
select
@@ -369,7 +367,8 @@ interface TradeRepository : ReactiveCrudRepository {
fun findByMostTrades(since: LocalDateTime): Mono
- @Query(""" select t.trade_date As create_date,
+ @Query(
+ """ select t.trade_date As create_date,
t.matched_quantity AS volume,
t.matched_price AS matched_price,
CASE
@@ -411,13 +410,20 @@ interface TradeRepository : ReactiveCrudRepository {
and (:startDate is null or trade_date >=:startDate)
and (:endDate is null or trade_date <=:endDate)
- order by create_date ASC offset :offset limit :limit """)
-
- fun findTxOfTradesAsc(user: String, startDate: LocalDateTime?, endDate: LocalDateTime?, offset: Int?, limit: Int?): Flux
+ order by create_date ASC offset :offset limit :limit """
+ )
+ fun findTxOfTradesAsc(
+ user: String,
+ startDate: LocalDateTime?,
+ endDate: LocalDateTime?,
+ offset: Int?,
+ limit: Int?,
+ ): Flux
- @Query(""" select t.trade_date As create_date,
+ @Query(
+ """ select t.trade_date As create_date,
t.matched_quantity AS volume,
t.matched_price AS matched_price,
CASE
@@ -459,9 +465,135 @@ interface TradeRepository : ReactiveCrudRepository {
and (:startDate is null or trade_date >=:startDate)
and (:endDate is null or trade_date <=:endDate)
- order by create_date DESC offset :offset limit :limit """)
+ order by create_date DESC offset :offset limit :limit """
+ )
- fun findTxOfTradesDesc(user: String, startDate: LocalDateTime?, endDate: LocalDateTime?, offset: Int?, limit: Int?): Flux
+ fun findTxOfTradesDesc(
+ user: String,
+ startDate: LocalDateTime?,
+ endDate: LocalDateTime?,
+ offset: Int?,
+ limit: Int?,
+ ): Flux
+
+ @Query(
+ """
+ WITH intervals AS (SELECT * FROM interval_generator((:startTime), (:endTime), :interval ::INTERVAL)),
+ last_trade AS (
+ SELECT DISTINCT ON (f.start_time) f.start_time, f.end_time, t.matched_price AS close_price FROM intervals f
+ LEFT JOIN trades t ON t.create_date >= f.start_time AND t.create_date < f.end_time AND t.symbol = :symbol
+ ORDER BY f.start_time, t.create_date DESC
+ )
+ SELECT
+ i.end_time AS close_time,
+ lt.close_price AS close_price
+ FROM intervals i
+ LEFT JOIN trades t
+ ON t.create_date >= i.start_time AND t.create_date < i.end_time AND t.symbol = :symbol
+ LEFT JOIN last_trade lt
+ ON i.start_time = lt.start_time
+ GROUP BY i.start_time, i.end_time, lt.close_price
+ ORDER BY i.start_time;
+ """
+ )
+ suspend fun getPriceTimeData(
+ @Param("symbol")
+ symbol: String,
+ @Param("interval")
+ interval: String,
+ @Param("startTime")
+ startTime: LocalDateTime,
+ @Param("endTime")
+ endTime: LocalDateTime,
+ ): Flux
+ @Query(
+ """
+ select * from trades where
+ (:symbol is null or symbol = :symbol)
+ and (:makerUuid is null or maker_uuid = :makerUuid)
+ and (:takerUuid is null or taker_uuid = :takerUuid)
+ and (:fromDate is null or trade_date >= :fromDate)
+ and (:toDate is null or trade_date <= :toDate)
+ and (:excludeSelfTrade is false or maker_uuid != taker_uuid)
+ order by trade_date DESC
+ limit :limit
+ offset :offset
+ """
+ )
+ suspend fun findByCriteria(
+ symbol: String?,
+ makerUuid: String?,
+ takerUuid: String?,
+ fromDate: LocalDateTime?,
+ toDate: LocalDateTime?,
+ excludeSelfTrade: Boolean,
+ limit: Int,
+ offset: Int,
+ ): Flow
+
+
+ @Query(
+ """
+select t.symbol,
+ t.id,
+ o.order_id,
+ case when :uuid = t.maker_uuid then t.maker_price else t.taker_price end as price,
+ t.matched_quantity as quantity,
+ o.quote_quantity,
+ case when :uuid = t.maker_uuid then t.maker_commission else t.taker_commission end as commission,
+ case
+ when :uuid = t.maker_uuid then t.maker_commission_asset
+ else t.taker_commission_asset end as commission_asset,
+ t.trade_date as time,
+ o.side = 'BID' as is_buyer,
+ t.maker_uuid = :uuid as is_maker,
+ true as is_best_match,
+ case when o.side = 'BID' and t.maker_uuid = :uuid then true else false end as is_maker_buyer
+from trades t
+ inner join orders o on
+ (t.maker_uuid = :uuid and o.ouid = t.maker_ouid) or
+ (t.taker_uuid = :uuid and o.ouid = t.taker_ouid)
+where :uuid in (t.maker_uuid, t.taker_uuid)
+ and (:symbol is null or t.symbol = :symbol)
+ and (:startTime is null or t.trade_date >= :startTime)
+ and (:endTime is null or t.trade_date <= :endTime)
+ and (:direction is null or o.side = :direction)
+ order by t.trade_date desc
+ limit :limit
+ offset :offset
+ """
+ )
+ suspend fun findByCriteria(
+ uuid: String,
+ symbol: String?,
+ startTime: LocalDateTime?,
+ endTime: LocalDateTime?,
+ direction: OrderDirection?,
+ limit: Int?,
+ offset: Int?,
+ ): Flow
+
+ @Query(
+ """
+select count(*)
+from trades t
+ inner join orders o on
+ (t.maker_uuid = :uuid and o.ouid = t.maker_ouid) or
+ (t.taker_uuid = :uuid and o.ouid = t.taker_ouid)
+where :uuid in (t.maker_uuid, t.taker_uuid)
+ and (:symbol is null or t.symbol = :symbol)
+ and (:startTime is null or t.trade_date >= :startTime)
+ and (:endTime is null or t.trade_date <= :endTime)
+ and (:direction is null or o.side = :direction)
+ """
+ )
+ suspend fun countByCriteria(
+ uuid: String,
+ symbol: String?,
+ startTime: LocalDateTime?,
+ endTime: LocalDateTime?,
+ direction: OrderDirection?,
+ ): Mono
}
\ No newline at end of file
diff --git a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/impl/MarketQueryHandlerImpl.kt b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/impl/MarketQueryHandlerImpl.kt
index 477a4d770..7947f6ae4 100644
--- a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/impl/MarketQueryHandlerImpl.kt
+++ b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/impl/MarketQueryHandlerImpl.kt
@@ -19,7 +19,6 @@ import kotlinx.coroutines.reactive.awaitFirstOrElse
import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.springframework.stereotype.Component
-import java.lang.StringBuilder
import java.math.BigDecimal
import java.time.Instant
import java.time.LocalDateTime
@@ -31,7 +30,7 @@ class MarketQueryHandlerImpl(
private val orderRepository: OrderRepository,
private val tradeRepository: TradeRepository,
private val orderStatusRepository: OrderStatusRepository,
- private val redisCacheHelper: RedisCacheHelper
+ private val redisCacheHelper: RedisCacheHelper,
) : MarketQueryHandler {
//TODO merge order and status fetching in query
@@ -91,7 +90,7 @@ class MarketQueryHandlerImpl(
val cacheKey = "recentTrades:${symbol.lowercase()}"
val recentTradesCache = redisCacheHelper.getList(cacheKey)
if (!recentTradesCache.isNullOrEmpty())
- return recentTradesCache.toList()
+ return recentTradesCache.toList().take(limit)
return tradeRepository.findBySymbolSortDescendingByCreateDate(symbol, limit)
.map {
@@ -118,6 +117,34 @@ class MarketQueryHandlerImpl(
.also { redisCacheHelper.setExpiration(cacheKey, 60.minutes()) }
}
+ override suspend fun recentTrades(
+ symbol: String?,
+ makerUuid: String?,
+ takerUuid: String?,
+ fromDate: LocalDateTime?,
+ toDate: LocalDateTime?,
+ excludeSelfTrade: Boolean,
+ limit: Int,
+ offset: Int,
+ ): List {
+ return tradeRepository.findByCriteria(symbol, makerUuid, takerUuid, fromDate, toDate,excludeSelfTrade, limit, offset)
+ .map {
+ TradeData(
+ tradeId = it.tradeId,
+ symbol = it.symbol,
+ matchedPrice = it.matchedPrice,
+ matchedQuantity = it.matchedQuantity,
+ takerPrice = it.takerPrice,
+ makerPrice = it.makerPrice,
+ tradeDate = it.tradeDate,
+ makerUuid = it.makerUuid,
+ takerUuid = it.takerUuid,
+ )
+ }
+ .toList()
+ }
+
+
override suspend fun lastPrice(symbol: String?): List {
val list = redisCacheHelper.getOrElse("lastPrice", 1.minutes()) {
if (symbol.isNullOrEmpty())
@@ -142,7 +169,7 @@ class MarketQueryHandlerImpl(
interval: String,
startTime: Long?,
endTime: Long?,
- limit: Int
+ limit: Int,
): List {
val st = if (startTime == null)
tradeRepository.findFirstByCreateDate().awaitFirstOrNull()?.createDate ?: LocalDateTime.now()
@@ -239,6 +266,64 @@ class MarketQueryHandlerImpl(
}
}
+ override suspend fun getWeeklyPriceData(symbol: String): List {
+ return getPriceDataWithCache(
+ symbol = symbol,
+ cacheKeyPrefix = "weeklyPriceData",
+ interval = "4h",
+ fromDate = LocalDateTime.now().minusDays(7)
+ )
+ }
+
+ override suspend fun getMonthlyPriceData(symbol: String): List {
+ return getPriceDataWithCache(
+ symbol = symbol,
+ cacheKeyPrefix = "monthlyPriceData",
+ interval = "24h",
+ fromDate = LocalDateTime.now().minusDays(30)
+ )
+ }
+
+ override suspend fun getDailyPriceData(symbol: String): List {
+ return getPriceDataWithCache(
+ symbol = symbol,
+ cacheKeyPrefix = "dailyPriceData",
+ interval = "1h",
+ fromDate = LocalDateTime.now().minusDays(1)
+ )
+ }
+
+ private suspend fun getPriceDataWithCache(
+ symbol: String,
+ cacheKeyPrefix: String,
+ interval: String,
+ fromDate: LocalDateTime,
+ ): List {
+ val cacheKey = "${cacheKeyPrefix}:${symbol.lowercase()}"
+ val cachedData = redisCacheHelper.getList(cacheKey)
+ if (!cachedData.isNullOrEmpty()) {
+ return cachedData.toList()
+ }
+
+ return tradeRepository.getPriceTimeData(symbol, interval, fromDate, LocalDateTime.now())
+ .collectList()
+ .awaitFirstOrElse { emptyList() }
+ .let { priceTimes ->
+ var lastNonNullPrice: BigDecimal? = null
+ val firstNonNullPrice = priceTimes.firstOrNull { it.closePrice != null }?.closePrice ?: BigDecimal.ZERO
+ priceTimes.map { item ->
+ val price = item.closePrice ?: lastNonNullPrice ?: firstNonNullPrice
+ lastNonNullPrice = price
+ PriceTime(
+ item.closeTime,
+ price
+ )
+ }
+ .onEach { redisCacheHelper.putListItem(cacheKey, it) }
+ .also { redisCacheHelper.setExpiration(cacheKey, 1.hours()) }
+ }
+ }
+
private fun TradeTickerData.asPriceChangeResponse(openTime: Long, closeTime: Long) = PriceChange(
symbol,
priceChange ?: BigDecimal.ZERO,
diff --git a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/impl/OrderPersisterImpl.kt b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/impl/OrderPersisterImpl.kt
index dbefd9f26..c6870c2cf 100644
--- a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/impl/OrderPersisterImpl.kt
+++ b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/impl/OrderPersisterImpl.kt
@@ -5,6 +5,7 @@ import co.nilin.opex.market.core.event.RichOrder
import co.nilin.opex.market.core.event.RichOrderUpdate
import co.nilin.opex.market.core.inout.Order
import co.nilin.opex.market.core.inout.OrderStatus
+import co.nilin.opex.market.core.spi.MarketOrderProducer
import co.nilin.opex.market.core.spi.OrderPersister
import co.nilin.opex.market.ports.postgres.dao.OpenOrderRepository
import co.nilin.opex.market.ports.postgres.dao.OrderRepository
@@ -25,7 +26,8 @@ class OrderPersisterImpl(
private val orderRepository: OrderRepository,
private val orderStatusRepository: OrderStatusRepository,
private val openOrderRepository: OpenOrderRepository,
- private val redisCacheHelper: RedisCacheHelper
+ private val redisCacheHelper: RedisCacheHelper,
+ private val marketOrderProducer: MarketOrderProducer
) : OrderPersister {
private val logger = LoggerFactory.getLogger(OrderPersisterImpl::class.java)
@@ -75,6 +77,7 @@ class OrderPersisterImpl(
logger.info("Order ${order.ouid} deleted from open orders")
}
+ marketOrderProducer.openOrderUpdate(order.uuid, order.pair)
justTry { redisCacheHelper.put("lastOrder", orderModel.asOrderDTO(lastStatus)) }
}
@@ -98,6 +101,8 @@ class OrderPersisterImpl(
openOrderRepository.delete(orderUpdate.ouid).awaitSingleOrNull()
logger.info("Order ${orderUpdate.ouid} deleted from open orders")
}
+ val order = orderRepository.findByOuid(orderUpdate.ouid).awaitFirstOrNull() ?: return
+ marketOrderProducer.openOrderUpdate(order.uuid, order.symbol)
}
override suspend fun load(ouid: String): Order? {
diff --git a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/impl/UserQueryHandlerImpl.kt b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/impl/UserQueryHandlerImpl.kt
index bf2c934d6..87ee17cac 100644
--- a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/impl/UserQueryHandlerImpl.kt
+++ b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/impl/UserQueryHandlerImpl.kt
@@ -12,10 +12,9 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.reactive.awaitFirst
+import kotlinx.coroutines.reactive.awaitFirstOrElse
import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.reactor.awaitSingleOrNull
-import org.springframework.data.domain.PageRequest
-import org.springframework.data.domain.Sort
import org.springframework.stereotype.Component
import java.time.Instant
import java.time.LocalDateTime
@@ -24,17 +23,17 @@ import java.util.*
@Component
class UserQueryHandlerImpl(
- private val orderRepository: OrderRepository,
- private val tradeRepository: TradeRepository,
- private val orderStatusRepository: OrderStatusRepository
+ private val orderRepository: OrderRepository,
+ private val tradeRepository: TradeRepository,
+ private val orderStatusRepository: OrderStatusRepository,
) : UserQueryHandler {
//TODO merge order and status fetching in query
override suspend fun getOrder(uuid: String, ouid: String): Order? {
return orderRepository.findByUUIDAndOUID(uuid, ouid)
- .awaitSingleOrNull()
- ?.asOrderDTO(orderStatusRepository.findMostRecentByOUID(ouid).awaitFirstOrNull())
+ .awaitSingleOrNull()
+ ?.asOrderDTO(orderStatusRepository.findMostRecentByOUID(ouid).awaitFirstOrNull())
}
override suspend fun queryOrder(uuid: String, request: QueryOrderRequest): Order? {
@@ -51,53 +50,64 @@ class UserQueryHandlerImpl(
return order.asOrderDTO(status)
}
+ override suspend fun openOrders(uuid: String, limit: Int): List {
+ return orderRepository.findByUuidAndSymbolAndStatus(
+ uuid,
+ null,
+ listOf(OrderStatus.NEW.code, OrderStatus.PARTIALLY_FILLED.code),
+ limit
+ ).filter { orderModel -> orderModel.constraint != null }
+ .map { it.asOrderDTO(orderStatusRepository.findMostRecentByOUID(it.ouid).awaitFirstOrNull()) }
+ .toList()
+ }
+
override suspend fun openOrders(uuid: String, symbol: String?, limit: Int): List {
return orderRepository.findByUuidAndSymbolAndStatus(
- uuid,
- symbol,
- listOf(OrderStatus.NEW.code, OrderStatus.PARTIALLY_FILLED.code),
- limit
+ uuid,
+ symbol,
+ listOf(OrderStatus.NEW.code, OrderStatus.PARTIALLY_FILLED.code),
+ limit
).filter { orderModel -> orderModel.constraint != null }
- .map { it.asOrderDTO(orderStatusRepository.findMostRecentByOUID(it.ouid).awaitFirstOrNull()) }
- .toList()
+ .map { it.asOrderDTO(orderStatusRepository.findMostRecentByOUID(it.ouid).awaitFirstOrNull()) }
+ .toList()
}
override suspend fun allOrders(uuid: String, allOrderRequest: AllOrderRequest): List {
return orderRepository.findByUuidAndSymbolAndTimeBetween(
- uuid,
- allOrderRequest.symbol,
- allOrderRequest.startTime,
- allOrderRequest.endTime,
- allOrderRequest.limit
+ uuid,
+ allOrderRequest.symbol,
+ allOrderRequest.startTime,
+ allOrderRequest.endTime,
+ allOrderRequest.limit
).filter { orderModel -> orderModel.constraint != null }
- .map { it.asOrderDTO(orderStatusRepository.findMostRecentByOUID(it.ouid).awaitFirstOrNull()) }
- .toList()
+ .map { it.asOrderDTO(orderStatusRepository.findMostRecentByOUID(it.ouid).awaitFirstOrNull()) }
+ .toList()
}
override suspend fun allTrades(uuid: String, request: TradeRequest): List {
return tradeRepository.findByUuidAndSymbolAndTimeBetweenAndTradeIdGreaterThan(
- uuid, request.symbol, request.fromTrade, request.startTime, request.endTime, request.limit
+ uuid, request.symbol, request.fromTrade, request.startTime, request.endTime, request.limit
).map {
val takerOrder = orderRepository.findByOuid(it.takerOuid).awaitFirst()
val makerOrder = orderRepository.findByOuid(it.makerOuid).awaitFirst()
val isMakerBuyer = makerOrder.direction == OrderDirection.BID
Trade(
- it.symbol,
- it.tradeId,
- if (it.takerUuid == uuid) takerOrder.orderId!! else makerOrder.orderId!!,
- if (it.takerUuid == uuid) it.takerPrice else it.makerPrice,
- it.matchedQuantity,
- if (isMakerBuyer) makerOrder.quoteQuantity!! else takerOrder.quoteQuantity!!,
- if (it.takerUuid == uuid) it.takerCommission!! else it.makerCommission!!,
- if (it.takerUuid == uuid) it.takerCommissionAsset!! else it.makerCommissionAsset!!,
- Date.from(it.createDate.atZone(ZoneId.systemDefault()).toInstant()),
- if (it.takerUuid == uuid)
- OrderDirection.ASK == takerOrder.direction
- else
- OrderDirection.ASK == makerOrder.direction,
- it.makerUuid == uuid,
- true,
- isMakerBuyer
+ it.symbol,
+ it.tradeId,
+ if (it.takerUuid == uuid) takerOrder.orderId!! else makerOrder.orderId!!,
+ if (it.takerUuid == uuid) it.takerPrice else it.makerPrice,
+ it.matchedQuantity,
+ if (isMakerBuyer) makerOrder.quoteQuantity!! else takerOrder.quoteQuantity!!,
+ if (it.takerUuid == uuid) it.takerCommission!! else it.makerCommission!!,
+ if (it.takerUuid == uuid) it.takerCommissionAsset!! else it.makerCommissionAsset!!,
+ it.createDate,
+ if (it.takerUuid == uuid)
+ OrderDirection.ASK == takerOrder.direction
+ else
+ OrderDirection.ASK == makerOrder.direction,
+ it.makerUuid == uuid,
+ true,
+ isMakerBuyer
)
}.toList()
}
@@ -105,20 +115,122 @@ class UserQueryHandlerImpl(
override suspend fun txOfTrades(transactionRequest: TransactionRequest): TransactionResponse? {
if (transactionRequest.ascendingByTime == true)
- return TransactionResponse(tradeRepository.findTxOfTradesAsc(transactionRequest.owner!!,
- transactionRequest.startTime?.let { LocalDateTime.ofInstant(Instant.ofEpochMilli(transactionRequest.startTime!!), ZoneId.systemDefault()) }
- ?: null,
- transactionRequest.endTime?.let { LocalDateTime.ofInstant(Instant.ofEpochMilli(transactionRequest.endTime!!), ZoneId.systemDefault()) }
- ?: null,
+ return TransactionResponse(
+ tradeRepository.findTxOfTradesAsc(
+ transactionRequest.owner!!,
+ transactionRequest.startTime?.let {
+ LocalDateTime.ofInstant(
+ Instant.ofEpochMilli(transactionRequest.startTime!!),
+ ZoneId.systemDefault()
+ )
+ }
+ ?: null,
+ transactionRequest.endTime?.let {
+ LocalDateTime.ofInstant(
+ Instant.ofEpochMilli(transactionRequest.endTime!!),
+ ZoneId.systemDefault()
+ )
+ }
+ ?: null,
transactionRequest.offset, transactionRequest.limit
- ).map { it.toDto() }.collectList()?.awaitFirstOrNull())
+ ).map { it.toDto() }.collectList()?.awaitFirstOrNull()
+ )
else
- return TransactionResponse(tradeRepository.findTxOfTradesDesc(transactionRequest.owner!!,
- transactionRequest.startTime?.let { LocalDateTime.ofInstant(Instant.ofEpochMilli(transactionRequest.startTime!!), ZoneId.systemDefault()) }
- ?: null,
- transactionRequest.endTime?.let { LocalDateTime.ofInstant(Instant.ofEpochMilli(transactionRequest.endTime!!), ZoneId.systemDefault()) }
- ?: null,
+ return TransactionResponse(
+ tradeRepository.findTxOfTradesDesc(
+ transactionRequest.owner!!,
+ transactionRequest.startTime?.let {
+ LocalDateTime.ofInstant(
+ Instant.ofEpochMilli(transactionRequest.startTime!!),
+ ZoneId.systemDefault()
+ )
+ }
+ ?: null,
+ transactionRequest.endTime?.let {
+ LocalDateTime.ofInstant(
+ Instant.ofEpochMilli(transactionRequest.endTime!!),
+ ZoneId.systemDefault()
+ )
+ }
+ ?: null,
transactionRequest.offset, transactionRequest.limit
- ).map { it.toDto() }.collectList()?.awaitFirstOrNull())
+ ).map { it.toDto() }.collectList()?.awaitFirstOrNull()
+ )
+ }
+
+ override suspend fun getOrderHistory(
+ uuid: String,
+ symbol: String?,
+ startTime: LocalDateTime?,
+ endTime: LocalDateTime?,
+ orderType: MatchingOrderType?,
+ direction: OrderDirection?,
+ limit: Int?,
+ offset: Int?,
+ ): List {
+ return orderRepository.findByCriteria(
+ uuid,
+ symbol,
+ startTime,
+ endTime,
+ orderType,
+ direction,
+ limit,
+ offset,
+ ).toList()
+ }
+
+ override suspend fun getOrderHistoryCount(
+ uuid: String,
+ symbol: String?,
+ startTime: LocalDateTime?,
+ endTime: LocalDateTime?,
+ orderType: MatchingOrderType?,
+ direction: OrderDirection?
+ ): Long {
+ return orderRepository.countByCriteria(
+ uuid,
+ symbol,
+ startTime,
+ endTime,
+ orderType,
+ direction,
+ ).awaitFirstOrElse { 0L }
+ }
+
+ override suspend fun getTradeHistory(
+ uuid: String,
+ symbol: String?,
+ startTime: LocalDateTime?,
+ endTime: LocalDateTime?,
+ direction: OrderDirection?,
+ limit: Int?,
+ offset: Int?,
+ ): List {
+ return tradeRepository.findByCriteria(
+ uuid,
+ symbol,
+ startTime,
+ endTime,
+ direction,
+ limit,
+ offset
+ ).toList()
+ }
+
+ override suspend fun getTradeHistoryCount(
+ uuid: String,
+ symbol: String?,
+ startTime: LocalDateTime?,
+ endTime: LocalDateTime?,
+ direction: OrderDirection?
+ ): Long {
+ return tradeRepository.countByCriteria(
+ uuid,
+ symbol,
+ startTime,
+ endTime,
+ direction,
+ ).awaitFirst()
}
}
\ No newline at end of file
diff --git a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/model/CandleInfoData.kt b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/model/CandleInfoData.kt
index f8c106add..d86457ade 100644
--- a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/model/CandleInfoData.kt
+++ b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/model/CandleInfoData.kt
@@ -1,6 +1,5 @@
package co.nilin.opex.market.ports.postgres.model
-import org.springframework.data.relational.core.mapping.Column
import java.math.BigDecimal
import java.time.LocalDateTime
diff --git a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/model/LastPrice.kt b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/model/LastPrice.kt
index 5e9799e9d..ef43d7d9c 100644
--- a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/model/LastPrice.kt
+++ b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/model/LastPrice.kt
@@ -2,4 +2,4 @@ package co.nilin.opex.market.ports.postgres.model
import java.math.BigDecimal
-data class LastPrice(val symbol:String, val matchedPrice: BigDecimal)
\ No newline at end of file
+data class LastPrice(val symbol: String, val matchedPrice: BigDecimal)
\ No newline at end of file
diff --git a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/model/PriceTimeData.kt b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/model/PriceTimeData.kt
new file mode 100644
index 000000000..875e3d76e
--- /dev/null
+++ b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/model/PriceTimeData.kt
@@ -0,0 +1,9 @@
+package co.nilin.opex.market.ports.postgres.model
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+data class PriceTimeData(
+ val closeTime: LocalDateTime,
+ val closePrice: BigDecimal?,
+)
\ No newline at end of file
diff --git a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/model/TradeModel.kt b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/model/TradeModel.kt
index e434f1c43..7c6bccaa9 100644
--- a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/model/TradeModel.kt
+++ b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/model/TradeModel.kt
@@ -1,7 +1,6 @@
package co.nilin.opex.market.ports.postgres.model
import org.springframework.data.annotation.Id
-import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.math.BigDecimal
import java.time.LocalDateTime
diff --git a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/util/Convertor.kt b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/util/Convertor.kt
index ddb42a1b9..0cb69e71d 100644
--- a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/util/Convertor.kt
+++ b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/util/Convertor.kt
@@ -2,20 +2,21 @@ package co.nilin.opex.market.ports.postgres.util
import co.nilin.opex.market.core.inout.Transaction
import co.nilin.opex.market.core.inout.TransactionDto
-import java.math.BigDecimal
import java.time.ZoneId
import java.util.*
- fun Transaction.toDto(): TransactionDto {
- return TransactionDto(createDate = Date.from(createDate.atZone(ZoneId.systemDefault()).toInstant()),
- volume,
- transactionPrice,
- matchedPrice,
- side,
- symbol,
- fee,
- user)
+fun Transaction.toDto(): TransactionDto {
+ return TransactionDto(
+ createDate = Date.from(createDate.atZone(ZoneId.systemDefault()).toInstant()),
+ volume,
+ transactionPrice,
+ matchedPrice,
+ side,
+ symbol,
+ fee,
+ user
+ )
- }
+}
diff --git a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/util/RedisCacheHelper.kt b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/util/RedisCacheHelper.kt
index 31bac2103..e74096417 100644
--- a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/util/RedisCacheHelper.kt
+++ b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/util/RedisCacheHelper.kt
@@ -24,8 +24,6 @@ class RedisCacheHelper(private val redisTemplate: RedisTemplate) {
}
fun putList(key: String, values: List, expireAt: DynamicInterval? = null) {
- // Why the fuck this doesn't work?
- // listOps.rightPushAll(key, values)
try {
values.forEach { listOps.rightPush(key, it) }
expireAt?.let { redisTemplate.expireAt(key, it.dateInFuture()) }
diff --git a/market/market-ports/market-persister-postgres/src/main/resources/schema.sql b/market/market-ports/market-persister-postgres/src/main/resources/schema.sql
index 8d9195525..e90dda326 100644
--- a/market/market-ports/market-persister-postgres/src/main/resources/schema.sql
+++ b/market/market-ports/market-persister-postgres/src/main/resources/schema.sql
@@ -1,83 +1,184 @@
CREATE TABLE IF NOT EXISTS orders
(
- id SERIAL PRIMARY KEY,
- ouid VARCHAR(72) NOT NULL UNIQUE,
- uuid VARCHAR(72) NOT NULL,
- client_order_id VARCHAR(72),
- symbol VARCHAR(20) NOT NULL,
- order_id INTEGER,
- maker_fee DECIMAL,
- taker_fee DECIMAL,
- left_side_fraction DECIMAL,
+ id
+ SERIAL
+ PRIMARY
+ KEY,
+ ouid
+ VARCHAR
+(
+ 72
+) NOT NULL UNIQUE,
+ uuid VARCHAR
+(
+ 72
+) NOT NULL,
+ client_order_id VARCHAR
+(
+ 72
+),
+ symbol VARCHAR
+(
+ 20
+) NOT NULL,
+ order_id INTEGER,
+ maker_fee DECIMAL,
+ taker_fee DECIMAL,
+ left_side_fraction DECIMAL,
right_side_fraction DECIMAL,
- user_level VARCHAR(20),
- side VARCHAR(20),
- match_constraint VARCHAR(20),
- order_type VARCHAR(20),
- price DECIMAL,
- quantity DECIMAL,
- quote_quantity DECIMAL,
- create_date TIMESTAMP,
- update_date TIMESTAMP NOT NULL,
- version INTEGER
-);
+ user_level VARCHAR
+(
+ 20
+),
+ side VARCHAR
+(
+ 20
+),
+ match_constraint VARCHAR
+(
+ 20
+),
+ order_type VARCHAR
+(
+ 20
+),
+ price DECIMAL,
+ quantity DECIMAL,
+ quote_quantity DECIMAL,
+ create_date TIMESTAMP,
+ update_date TIMESTAMP NOT NULL,
+ version INTEGER
+ );
CREATE TABLE IF NOT EXISTS order_status
(
- id SERIAL PRIMARY KEY,
- ouid VARCHAR(72) NOT NULL,
- executed_quantity DECIMAL,
+ id
+ SERIAL
+ PRIMARY
+ KEY,
+ ouid
+ VARCHAR
+(
+ 72
+) NOT NULL,
+ executed_quantity DECIMAL,
accumulative_quote_qty DECIMAL,
- status INTEGER NOT NULL,
- appearance INTEGER NOT NULL,
- date TIMESTAMP NOT NULL,
- UNIQUE (ouid, status, appearance, executed_quantity)
-);
+ status INTEGER NOT NULL,
+ appearance INTEGER NOT NULL,
+ date TIMESTAMP NOT NULL,
+ UNIQUE
+(
+ ouid,
+ status,
+ appearance,
+ executed_quantity
+)
+ );
CREATE TABLE IF NOT EXISTS open_orders
(
- id SERIAL PRIMARY KEY,
- ouid VARCHAR(72) NOT NULL UNIQUE,
+ id
+ SERIAL
+ PRIMARY
+ KEY,
+ ouid
+ VARCHAR
+(
+ 72
+) NOT NULL UNIQUE,
executed_quantity DECIMAL,
- status INTEGER NOT NULL
-);
+ status INTEGER NOT NULL
+ );
CREATE TABLE IF NOT EXISTS trades
(
- id SERIAL PRIMARY KEY,
- trade_id INTEGER NOT NULL,
- symbol VARCHAR(20) NOT NULL,
- base_asset VARCHAR(20) NOT NULL,
- quote_asset VARCHAR(20) NOT NULL,
- matched_price DECIMAL NOT NULL,
- matched_quantity DECIMAL NOT NULL,
- taker_price DECIMAL NOT NULL,
- maker_price DECIMAL NOT NULL,
- taker_commission DECIMAL,
- maker_commission DECIMAL,
- taker_commission_asset VARCHAR(20),
- maker_commission_asset VARCHAR(20),
- trade_date TIMESTAMP NOT NULL,
- maker_ouid VARCHAR(72) NOT NULL,
- taker_ouid VARCHAR(72) NOT NULL,
- maker_uuid VARCHAR(72) NOT NULL,
- taker_uuid VARCHAR(72) NOT NULL,
- create_date TIMESTAMP
-);
+ id
+ SERIAL
+ PRIMARY
+ KEY,
+ trade_id
+ INTEGER
+ NOT
+ NULL,
+ symbol
+ VARCHAR
+(
+ 20
+) NOT NULL,
+ base_asset VARCHAR
+(
+ 20
+) NOT NULL,
+ quote_asset VARCHAR
+(
+ 20
+) NOT NULL,
+ matched_price DECIMAL NOT NULL,
+ matched_quantity DECIMAL NOT NULL,
+ taker_price DECIMAL NOT NULL,
+ maker_price DECIMAL NOT NULL,
+ taker_commission DECIMAL,
+ maker_commission DECIMAL,
+ taker_commission_asset VARCHAR
+(
+ 20
+),
+ maker_commission_asset VARCHAR
+(
+ 20
+),
+ trade_date TIMESTAMP NOT NULL,
+ maker_ouid VARCHAR
+(
+ 72
+) NOT NULL,
+ taker_ouid VARCHAR
+(
+ 72
+) NOT NULL,
+ maker_uuid VARCHAR
+(
+ 72
+) NOT NULL,
+ taker_uuid VARCHAR
+(
+ 72
+) NOT NULL,
+ create_date TIMESTAMP
+ );
CREATE INDEX IF NOT EXISTS idx_trades_symbol on trades (symbol);
CREATE INDEX IF NOT EXISTS idx_trades_create_date on trades (create_date);
CREATE TABLE IF NOT EXISTS currency_rate
(
- id SERIAL PRIMARY KEY,
- base VARCHAR(25) NOT NULL,
- quote VARCHAR(25) NOT NULL,
- source VARCHAR(25) NOT NULL,
- rate DECIMAL NOT NULL,
- UNIQUE (base, quote, source)
-);
+ id
+ SERIAL
+ PRIMARY
+ KEY,
+ base
+ VARCHAR
+(
+ 25
+) NOT NULL,
+ quote VARCHAR
+(
+ 25
+) NOT NULL,
+ source VARCHAR
+(
+ 25
+) NOT NULL,
+ rate DECIMAL NOT NULL,
+ UNIQUE
+(
+ base,
+ quote,
+ source
+)
+ );
-CREATE OR REPLACE FUNCTION interval_generator(
+CREATE
+OR REPLACE FUNCTION interval_generator(
start_ts TIMESTAMP without TIME ZONE,
end_ts TIMESTAMP without TIME ZONE,
round_interval INTERVAL
@@ -90,15 +191,16 @@ CREATE OR REPLACE FUNCTION interval_generator(
as
$$
BEGIN
- RETURN QUERY
- SELECT (n) start_time,
- (n + round_interval) end_time
- FROM generate_series(
- date_trunc('minute', start_ts),
- end_ts,
- round_interval
- ) n;
+RETURN QUERY
+SELECT (n) start_time,
+ (n + round_interval) end_time
+FROM generate_series(
+ date_trunc('minute', start_ts),
+ end_ts,
+ round_interval
+ ) n;
END;
-$$ LANGUAGE 'plpgsql';
+$$
+LANGUAGE 'plpgsql';
diff --git a/market/market-ports/market-persister-postgres/src/test/kotlin/co/nilin/opex/market/ports/postgres/impl/MarketQueryHandlerTest.kt b/market/market-ports/market-persister-postgres/src/test/kotlin/co/nilin/opex/market/ports/postgres/impl/MarketQueryHandlerTest.kt
index 6b64ebeea..6de56844a 100644
--- a/market/market-ports/market-persister-postgres/src/test/kotlin/co/nilin/opex/market/ports/postgres/impl/MarketQueryHandlerTest.kt
+++ b/market/market-ports/market-persister-postgres/src/test/kotlin/co/nilin/opex/market/ports/postgres/impl/MarketQueryHandlerTest.kt
@@ -1,6 +1,9 @@
package co.nilin.opex.market.ports.postgres.impl
-import co.nilin.opex.market.core.inout.*
+import co.nilin.opex.market.core.inout.MarketTrade
+import co.nilin.opex.market.core.inout.Order
+import co.nilin.opex.market.core.inout.OrderDirection
+import co.nilin.opex.market.core.inout.OrderStatus
import co.nilin.opex.market.ports.postgres.dao.OrderRepository
import co.nilin.opex.market.ports.postgres.dao.OrderStatusRepository
import co.nilin.opex.market.ports.postgres.dao.TradeRepository
diff --git a/market/market-ports/market-persister-postgres/src/test/kotlin/co/nilin/opex/market/ports/postgres/impl/OrderPersisterTest.kt b/market/market-ports/market-persister-postgres/src/test/kotlin/co/nilin/opex/market/ports/postgres/impl/OrderPersisterTest.kt
index 48e71a9f1..0a533641a 100644
--- a/market/market-ports/market-persister-postgres/src/test/kotlin/co/nilin/opex/market/ports/postgres/impl/OrderPersisterTest.kt
+++ b/market/market-ports/market-persister-postgres/src/test/kotlin/co/nilin/opex/market/ports/postgres/impl/OrderPersisterTest.kt
@@ -1,10 +1,12 @@
package co.nilin.opex.market.ports.postgres.impl
+import co.nilin.opex.market.core.spi.MarketOrderProducer
import co.nilin.opex.market.ports.postgres.dao.OpenOrderRepository
import co.nilin.opex.market.ports.postgres.dao.OrderRepository
import co.nilin.opex.market.ports.postgres.dao.OrderStatusRepository
import co.nilin.opex.market.ports.postgres.impl.sample.VALID
import co.nilin.opex.market.ports.postgres.util.RedisCacheHelper
+import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
@@ -16,9 +18,16 @@ class OrderPersisterTest {
private val orderRepository = mockk()
private val orderStatusRepository = mockk()
private val openOrderRepository = mockk()
+ private val marketOrderProducer = mockk()
private val redisCacheHelper = mockk()
private val orderPersister =
- OrderPersisterImpl(orderRepository, orderStatusRepository, openOrderRepository, redisCacheHelper)
+ OrderPersisterImpl(
+ orderRepository,
+ orderStatusRepository,
+ openOrderRepository,
+ redisCacheHelper,
+ marketOrderProducer
+ )
@Test
fun givenOrderRepo_whenSaveRichOrder_thenSuccess(): Unit = runBlocking {
@@ -38,6 +47,10 @@ class OrderPersisterTest {
openOrderRepository.delete(any())
} returns Mono.empty()
every { redisCacheHelper.put(any(), any()) } returns Unit
+ every {
+ orderRepository.findByOuid(any())
+ } returns Mono.just(VALID.MAKER_ORDER_MODEL)
+ coEvery { marketOrderProducer.openOrderUpdate(any(), any()) } returns Unit
assertThatNoException().isThrownBy { runBlocking { orderPersister.save(VALID.RICH_ORDER) } }
}
@@ -56,6 +69,10 @@ class OrderPersisterTest {
every {
openOrderRepository.delete(any())
} returns Mono.empty()
+ every {
+ orderRepository.findByOuid(any())
+ } returns Mono.just(VALID.MAKER_ORDER_MODEL)
+ coEvery { marketOrderProducer.openOrderUpdate(any(), any()) } returns Unit
assertThatNoException().isThrownBy { runBlocking { orderPersister.update(VALID.RICH_ORDER_UPDATE) } }
}
diff --git a/market/market-ports/market-persister-postgres/src/test/kotlin/co/nilin/opex/market/ports/postgres/impl/UserQueryHandlerTest.kt b/market/market-ports/market-persister-postgres/src/test/kotlin/co/nilin/opex/market/ports/postgres/impl/UserQueryHandlerTest.kt
index c30d04944..26eda70c6 100644
--- a/market/market-ports/market-persister-postgres/src/test/kotlin/co/nilin/opex/market/ports/postgres/impl/UserQueryHandlerTest.kt
+++ b/market/market-ports/market-persister-postgres/src/test/kotlin/co/nilin/opex/market/ports/postgres/impl/UserQueryHandlerTest.kt
@@ -7,8 +7,6 @@ import co.nilin.opex.market.ports.postgres.dao.TradeRepository
import co.nilin.opex.market.ports.postgres.impl.sample.VALID
import io.mockk.every
import io.mockk.mockk
-import kotlinx.coroutines.flow.count
-import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
diff --git a/market/market-ports/market-persister-postgres/src/test/kotlin/co/nilin/opex/market/ports/postgres/impl/sample/Samples.kt b/market/market-ports/market-persister-postgres/src/test/kotlin/co/nilin/opex/market/ports/postgres/impl/sample/Samples.kt
index c5538f441..f0d550546 100644
--- a/market/market-ports/market-persister-postgres/src/test/kotlin/co/nilin/opex/market/ports/postgres/impl/sample/Samples.kt
+++ b/market/market-ports/market-persister-postgres/src/test/kotlin/co/nilin/opex/market/ports/postgres/impl/sample/Samples.kt
@@ -8,7 +8,6 @@ import co.nilin.opex.market.ports.postgres.model.LastPrice
import co.nilin.opex.market.ports.postgres.model.OrderModel
import co.nilin.opex.market.ports.postgres.model.OrderStatusModel
import co.nilin.opex.market.ports.postgres.model.TradeModel
-import co.nilin.opex.market.ports.postgres.util.isWorking
import java.math.BigDecimal
import java.security.Principal
import java.time.LocalDateTime
diff --git a/market/pom.xml b/market/pom.xml
index e64de9679..5013b6fdd 100644
--- a/market/pom.xml
+++ b/market/pom.xml
@@ -19,6 +19,7 @@
market-app
market-core
market-ports/market-eventlistener-kafka
+ market-ports/market-eventproducer-kafka
market-ports/market-persister-postgres
@@ -50,6 +51,11 @@
market-eventlistener-kafka
${project.version}
+
+ co.nilin.opex.market.ports.kafka.producer
+ market-eventproducer-kafka
+ 1.0.1-beta.7
+
co.nilin.opex.market.ports.binance
market-binance-rest
@@ -70,11 +76,6 @@
interceptors
${interceptor.version}
-
- co.nilin.opex.utility
- preferences
- ${preferences.version}
-
org.springframework.cloud
spring-cloud-dependencies
diff --git a/matching-engine/matching-engine-app/pom.xml b/matching-engine/matching-engine-app/pom.xml
index 1ca63381a..7bd61ad6e 100644
--- a/matching-engine/matching-engine-app/pom.xml
+++ b/matching-engine/matching-engine-app/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
@@ -47,10 +47,6 @@
org.springframework.boot
spring-boot-starter-actuator
-
- co.nilin.opex.utility
- preferences
-
io.micrometer
micrometer-registry-prometheus
diff --git a/matching-engine/matching-engine-app/src/main/kotlin/co/nilin/opex/matching/engine/app/config/InitializeService.kt b/matching-engine/matching-engine-app/src/main/kotlin/co/nilin/opex/matching/engine/app/config/InitializeService.kt
index 02c3beb49..7caf5f262 100644
--- a/matching-engine/matching-engine-app/src/main/kotlin/co/nilin/opex/matching/engine/app/config/InitializeService.kt
+++ b/matching-engine/matching-engine-app/src/main/kotlin/co/nilin/opex/matching/engine/app/config/InitializeService.kt
@@ -1,7 +1,5 @@
package co.nilin.opex.matching.engine.app.config
-import co.nilin.opex.utility.preferences.Preferences
-import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@@ -9,12 +7,6 @@ import org.springframework.context.annotation.Configuration
@Configuration
class InitializeService {
- @Autowired
- private lateinit var preferences: Preferences
-
- /*@Bean("symbols")
- fun getSymbols(): List = preferences.markets.map { it.pair ?: "${it.leftSide}_${it.rightSide}" }*/
-
@Bean("symbols")
fun getSymbols(@Value("\${app.symbols}") symbols: String): List {
return symbols.split(",").map { it.trim() }.map { it.uppercase() }
diff --git a/matching-engine/matching-engine-app/src/main/resources/application.yml b/matching-engine/matching-engine-app/src/main/resources/application.yml
index 4b384fdef..9a4ed5b32 100644
--- a/matching-engine/matching-engine-app/src/main/resources/application.yml
+++ b/matching-engine/matching-engine-app/src/main/resources/application.yml
@@ -16,7 +16,7 @@ management:
web:
base-path: /actuator
exposure:
- include: ["health", "prometheus", "metrics"]
+ include: [ "health", "prometheus", "metrics" ]
endpoint:
health:
show-details: when_authorized
diff --git a/matching-engine/matching-engine-core/pom.xml b/matching-engine/matching-engine-core/pom.xml
index 25c222bae..e9c95993c 100644
--- a/matching-engine/matching-engine-core/pom.xml
+++ b/matching-engine/matching-engine-core/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
diff --git a/matching-engine/matching-engine-core/src/main/kotlin/co/nilin/opex/matching/engine/core/inout/OrderRequestEvent.kt b/matching-engine/matching-engine-core/src/main/kotlin/co/nilin/opex/matching/engine/core/inout/OrderRequestEvent.kt
index 3ec793a34..236a74b36 100644
--- a/matching-engine/matching-engine-core/src/main/kotlin/co/nilin/opex/matching/engine/core/inout/OrderRequestEvent.kt
+++ b/matching-engine/matching-engine-core/src/main/kotlin/co/nilin/opex/matching/engine/core/inout/OrderRequestEvent.kt
@@ -2,4 +2,4 @@ package co.nilin.opex.matching.engine.core.inout
import co.nilin.opex.matching.engine.core.model.Pair
-abstract class OrderRequestEvent(val ouid:String, val uuid: String, val pair: Pair)
\ No newline at end of file
+abstract class OrderRequestEvent(val ouid: String, val uuid: String, val pair: Pair)
\ No newline at end of file
diff --git a/matching-engine/matching-engine-ports/matching-engine-eventlistener-kafka/pom.xml b/matching-engine/matching-engine-ports/matching-engine-eventlistener-kafka/pom.xml
index 981aaeb33..876f69b94 100644
--- a/matching-engine/matching-engine-ports/matching-engine-eventlistener-kafka/pom.xml
+++ b/matching-engine/matching-engine-ports/matching-engine-eventlistener-kafka/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
diff --git a/matching-engine/matching-engine-ports/matching-engine-eventlistener-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/listener/config/OrderKafkaConfig.kt b/matching-engine/matching-engine-ports/matching-engine-eventlistener-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/listener/config/OrderKafkaConfig.kt
index 6262ba1df..ec63df8aa 100644
--- a/matching-engine/matching-engine-ports/matching-engine-eventlistener-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/listener/config/OrderKafkaConfig.kt
+++ b/matching-engine/matching-engine-ports/matching-engine-eventlistener-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/listener/config/OrderKafkaConfig.kt
@@ -2,7 +2,6 @@ package co.nilin.opex.matching.engine.ports.kafka.listener.config
import co.nilin.opex.matching.engine.core.eventh.events.CoreEvent
import co.nilin.opex.matching.engine.core.inout.OrderRequestEvent
-import co.nilin.opex.matching.engine.core.inout.OrderSubmitRequestEvent
import co.nilin.opex.matching.engine.ports.kafka.listener.consumer.EventKafkaListener
import co.nilin.opex.matching.engine.ports.kafka.listener.consumer.OrderKafkaListener
import org.apache.kafka.clients.consumer.ConsumerConfig
diff --git a/matching-engine/matching-engine-ports/matching-engine-snapshots-redis/pom.xml b/matching-engine/matching-engine-ports/matching-engine-snapshots-redis/pom.xml
index 042b22c13..1dd1e88ca 100644
--- a/matching-engine/matching-engine-ports/matching-engine-snapshots-redis/pom.xml
+++ b/matching-engine/matching-engine-ports/matching-engine-snapshots-redis/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
diff --git a/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/pom.xml b/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/pom.xml
index b13662c63..ad9220e32 100644
--- a/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/pom.xml
+++ b/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
diff --git a/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/EventsKafkaConfig.kt b/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/EventsKafkaConfig.kt
index 6fdf44e04..c11c225e8 100644
--- a/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/EventsKafkaConfig.kt
+++ b/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/EventsKafkaConfig.kt
@@ -2,7 +2,6 @@ package co.nilin.opex.matching.engine.ports.kafka.submitter.config
import co.nilin.opex.matching.engine.core.eventh.events.CoreEvent
import co.nilin.opex.matching.engine.core.inout.OrderRequestEvent
-import co.nilin.opex.matching.engine.core.inout.OrderSubmitRequestEvent
import org.apache.kafka.clients.producer.ProducerConfig
import org.apache.kafka.common.serialization.StringSerializer
import org.springframework.beans.factory.annotation.Qualifier
diff --git a/matching-engine/pom.xml b/matching-engine/pom.xml
index 737dabe1d..010d84c41 100644
--- a/matching-engine/pom.xml
+++ b/matching-engine/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
@@ -62,11 +62,6 @@
interceptors
${interceptor.version}
-
- co.nilin.opex.utility
- preferences
- ${preferences.version}
-
diff --git a/matching-gateway/matching-gateway-app/pom.xml b/matching-gateway/matching-gateway-app/pom.xml
index 821456c1b..24e94ab58 100644
--- a/matching-gateway/matching-gateway-app/pom.xml
+++ b/matching-gateway/matching-gateway-app/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
@@ -39,6 +39,10 @@
co.nilin.opex.matching.gateway.ports.kafka.submitter
matching-gateway-submitter-kafka
+
+ co.nilin.opex.matching.gateway.ports.postgres
+ matching-gateway-persister-postgres
+
org.springframework.cloud
spring-cloud-starter-consul-all
@@ -55,6 +59,10 @@
org.springframework.boot
spring-boot-starter-oauth2-resource-server
+
+ org.springframework.cloud
+ spring-cloud-starter-vault-config
+
org.bouncycastle
bcprov-jdk15on
diff --git a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/config/SecurityConfig.kt b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/config/SecurityConfig.kt
index ed4019789..b7bea8a0e 100644
--- a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/config/SecurityConfig.kt
+++ b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/config/SecurityConfig.kt
@@ -1,7 +1,10 @@
package co.nilin.opex.matching.gateway.app.config
+import co.nilin.opex.common.security.ReactiveCustomJwtConverter
+import co.nilin.opex.matching.gateway.app.utils.hasRole
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
+import org.springframework.http.HttpMethod
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder
@@ -23,11 +26,12 @@ class SecurityConfig(private val webClient: WebClient) {
.pathMatchers("/swagger-ui/**").permitAll()
.pathMatchers("/swagger-resources/**").permitAll()
.pathMatchers("/v2/api-docs").permitAll()
- .pathMatchers("/**").hasAuthority("SCOPE_trust")
+ .pathMatchers(HttpMethod.GET, "/pair-setting/**").permitAll()
+ .pathMatchers(HttpMethod.PUT, "/pair-setting/**").hasAuthority("ROLE_admin")
.anyExchange().authenticated()
.and()
.oauth2ResourceServer()
- .jwt()
+ .jwt { it.jwtAuthenticationConverter(ReactiveCustomJwtConverter()) }
return http.build()
}
@@ -35,7 +39,7 @@ class SecurityConfig(private val webClient: WebClient) {
@Throws(Exception::class)
fun reactiveJwtDecoder(): ReactiveJwtDecoder? {
return NimbusReactiveJwtDecoder.withJwkSetUri(jwkUrl)
- .webClient(webClient)
+ .webClient(WebClient.create())
.build()
}
}
diff --git a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/config/WebClientConfig.kt b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/config/WebClientConfig.kt
index 0fd266d83..9a7e82138 100644
--- a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/config/WebClientConfig.kt
+++ b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/config/WebClientConfig.kt
@@ -5,7 +5,6 @@ import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalanc
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
-import org.springframework.http.client.reactive.ReactorClientHttpConnector
import org.springframework.web.reactive.function.client.WebClient
import org.zalando.logbook.Logbook
import org.zalando.logbook.netty.LogbookClientHandler
diff --git a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/controller/ControllerExceptionHandler.kt b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/controller/ControllerExceptionHandler.kt
deleted file mode 100644
index 4e61b272c..000000000
--- a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/controller/ControllerExceptionHandler.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-package co.nilin.opex.matching.gateway.app.controller
-
-import co.nilin.opex.matching.gateway.app.exception.NotAllowedToSubmitOrderException
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties
-import com.fasterxml.jackson.databind.ObjectMapper
-import org.slf4j.LoggerFactory
-import org.springframework.http.ResponseEntity
-import org.springframework.web.bind.annotation.ExceptionHandler
-import org.springframework.web.reactive.function.client.WebClientResponseException
-import java.nio.charset.StandardCharsets
-import java.util.*
-
-//@RestControllerAdvice
-class ControllerExceptionHandler {
-
- data class ErrorResponse(
- val timestamp: Date, val status: Int, val error: String, val message: String
- )
-
- val logger = LoggerFactory.getLogger(ControllerExceptionHandler::class.java)
-
- val objectMapper: ObjectMapper = ObjectMapper()
-
- @ExceptionHandler(NotAllowedToSubmitOrderException::class)
- fun handle(ex: NotAllowedToSubmitOrderException): ResponseEntity {
- logger.error("Trace Error {}", ex)
- val ret = ResponseEntity.status(500).body(
- ErrorResponse(
- Date(), -1, ex::class.qualifiedName ?: "", ex.message ?: ""
- )
- )
- logger.debug("return error response:{}", ret)
- return ret
- }
-
- @JsonIgnoreProperties(ignoreUnknown = true)
- class WebClientErrorResponse {
- constructor() {
-
- }
-
- constructor(timestamp: Date?, path: String?, status: Int?, error: String?, message: String?) {
- this.timestamp = timestamp
- this.path = path
- this.status = status
- this.error = error
- this.message = message
- }
-
- var timestamp: Date? = null
- var path: String? = null
- var status: Int? = null
- var error: String? = null
- var message: String? = null
- }
-
- @ExceptionHandler(WebClientResponseException::class)
- fun handle(ex: WebClientResponseException): ResponseEntity {
- logger.error("Trace Error {}", ex)
- try {
- val body = objectMapper.readValue(
- ex.responseBodyAsByteArray.toString(StandardCharsets.UTF_8),
- WebClientErrorResponse::class.java
- )
- val ret = ResponseEntity.status(body.status ?: ex.rawStatusCode).body(
- ErrorResponse(
- Date(),
- body.status ?: ex.rawStatusCode,
- body.error ?: ex::class.qualifiedName ?: "",
- body.message ?: "Internal Server Error"
- )
- )
- logger.debug("return error response:{}", ret)
- return ret
- } catch (je: Exception) {
- logger.error("Trace Error {}", je)
- val ret = ResponseEntity.status(ex.statusCode).body(
- ErrorResponse(
- Date(), ex.rawStatusCode, ex::class.qualifiedName ?: "", "Internal Server Error"
- )
- )
- logger.debug("return error response:{}", ret)
- return ret
- }
- }
-
- @ExceptionHandler(Throwable::class)
- fun handle(ex: Throwable): ResponseEntity {
- logger.error("Trace Error {}", ex)
- val ret = ResponseEntity.status(500).body(
- ErrorResponse(
- Date(), 500, ex::class.qualifiedName ?: "", "Internal Server Error"
- )
- )
- logger.debug("return error response:{}", ret)
- return ret
- }
-}
diff --git a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/controller/PairSettingController.kt b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/controller/PairSettingController.kt
new file mode 100644
index 000000000..1a578a945
--- /dev/null
+++ b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/controller/PairSettingController.kt
@@ -0,0 +1,26 @@
+package co.nilin.opex.matching.gateway.app.controller
+
+import co.nilin.opex.matching.gateway.ports.postgres.dto.PairSetting
+import co.nilin.opex.matching.gateway.ports.postgres.service.PairSettingService
+import org.springframework.web.bind.annotation.*
+import java.math.BigDecimal
+
+@RestController
+@RequestMapping("/pair-setting")
+class PairSettingController(private val pairSettingService: PairSettingService) {
+
+ @GetMapping("/{pair}")
+ suspend fun getPairSetting(@PathVariable pair: String): PairSetting {
+ return pairSettingService.load(pair)
+ }
+
+ @GetMapping
+ suspend fun getPairSettings(): List {
+ return pairSettingService.loadAll()
+ }
+
+ @PutMapping
+ suspend fun updatePairSetting(@RequestBody pairSetting: PairSetting): PairSetting {
+ return pairSettingService.update(pairSetting)
+ }
+}
\ No newline at end of file
diff --git a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/proxy/PairConfigLoaderImpl.kt b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/proxy/PairConfigLoaderImpl.kt
index 27379e3c4..342d4a55a 100644
--- a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/proxy/PairConfigLoaderImpl.kt
+++ b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/proxy/PairConfigLoaderImpl.kt
@@ -4,11 +4,25 @@ import co.nilin.opex.matching.engine.core.model.OrderDirection
import co.nilin.opex.matching.gateway.app.inout.PairConfig
import co.nilin.opex.matching.gateway.app.spi.AccountantApiProxy
import co.nilin.opex.matching.gateway.app.spi.PairConfigLoader
+import co.nilin.opex.matching.gateway.ports.postgres.util.CacheManager
import org.springframework.stereotype.Service
+import java.util.concurrent.TimeUnit
@Service
-class PairConfigLoaderImpl(private val accountantApiProxy: AccountantApiProxy) : PairConfigLoader {
+class PairConfigLoaderImpl(
+ private val accountantApiProxy: AccountantApiProxy,
+ private val cacheManager: CacheManager
+) : PairConfigLoader {
override suspend fun load(pair: String, direction: OrderDirection): PairConfig {
- return accountantApiProxy.fetchPairConfig(pair, direction)
+ return cacheManager.get("pair-config:$pair-$direction")
+ ?: accountantApiProxy.fetchPairConfig(pair, direction)
+ .also {
+ cacheManager.put(
+ "pair-config:$pair-$direction",
+ it,
+ 5, TimeUnit.MINUTES
+ )
+
+ }
}
}
\ No newline at end of file
diff --git a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/service/OrderService.kt b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/service/OrderService.kt
index 79f999546..ec1fba6e0 100644
--- a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/service/OrderService.kt
+++ b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/service/OrderService.kt
@@ -12,6 +12,7 @@ import co.nilin.opex.matching.gateway.ports.kafka.submitter.inout.OrderSubmitReq
import co.nilin.opex.matching.gateway.ports.kafka.submitter.inout.OrderSubmitResult
import co.nilin.opex.matching.gateway.ports.kafka.submitter.service.KafkaHealthIndicator
import co.nilin.opex.matching.gateway.ports.kafka.submitter.service.OrderRequestEventSubmitter
+import co.nilin.opex.matching.gateway.ports.postgres.service.PairSettingService
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.math.BigDecimal
@@ -21,6 +22,7 @@ class OrderService(
val accountantApiProxy: AccountantApiProxy,
val orderRequestEventSubmitter: OrderRequestEventSubmitter,
val pairConfigLoader: PairConfigLoader,
+ val pairSettingService: PairSettingService,
private val kafkaHealthIndicator: KafkaHealthIndicator,
) {
@@ -28,13 +30,25 @@ class OrderService(
suspend fun submitNewOrder(createOrderRequest: CreateOrderRequest): OrderSubmitResult {
require(createOrderRequest.price >= BigDecimal.ZERO)
+
+ val pairSetting = pairSettingService.load(createOrderRequest.pair)
+ if (!pairSetting.isAvailable)
+ throw OpexError.PairIsNotAvailable.exception()
+ if (!pairSetting.orderTypes.split(",").contains(createOrderRequest.orderType.name)) {
+ throw OpexError.InvalidOrderType.exception()
+ }
+ if ((createOrderRequest.quantity * createOrderRequest.price) > pairSetting.maxOrder ||
+ (createOrderRequest.quantity * createOrderRequest.price) < pairSetting.minOrder) {
+ throw OpexError.InvalidQuantity.exception()
+ }
+
+
val symbolSides = createOrderRequest.pair.split("_")
val symbol = if (createOrderRequest.direction == OrderDirection.ASK)
symbolSides[0]
else
symbolSides[1]
- //TODO cache
val pairConfig = pairConfigLoader.load(createOrderRequest.pair, createOrderRequest.direction)
val canCreateOrder = runCatching {
diff --git a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/utils/Extensions.kt b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/utils/Extensions.kt
new file mode 100644
index 000000000..f78d4fc5a
--- /dev/null
+++ b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/utils/Extensions.kt
@@ -0,0 +1,29 @@
+package co.nilin.opex.matching.gateway.app.utils
+
+import com.nimbusds.jose.shaded.json.JSONArray
+import org.springframework.security.authorization.AuthorizationDecision
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.oauth2.jwt.Jwt
+
+fun ServerHttpSecurity.AuthorizeExchangeSpec.Access.hasRole(
+ authority: String,
+ role: String
+): ServerHttpSecurity.AuthorizeExchangeSpec = access { mono, _ ->
+ mono.map { auth ->
+ val hasAuthority = auth.authorities.any { it.authority == authority }
+ val hasRole = ((auth.principal as Jwt).claims["roles"] as JSONArray?)?.contains(role) == true
+ AuthorizationDecision(hasAuthority && hasRole)
+ }
+}
+
+fun ServerHttpSecurity.AuthorizeExchangeSpec.Access.hasRoleAndLevel(
+ role: String? = null,
+ level: String? = null
+): ServerHttpSecurity.AuthorizeExchangeSpec = access { mono, _ ->
+ mono.map { auth ->
+ val hasLevel = level?.let { ((auth.principal as Jwt).claims["level"] as String?)?.equals(level) == true }
+ ?: true
+ val hasRole = ((auth.principal as Jwt).claims["roles"] as JSONArray?)?.contains(role) == true
+ AuthorizationDecision(hasLevel && hasRole)
+ }
+}
diff --git a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/utils/VaultUserIdMechanism.kt b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/utils/VaultUserIdMechanism.kt
new file mode 100644
index 000000000..969895854
--- /dev/null
+++ b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/utils/VaultUserIdMechanism.kt
@@ -0,0 +1,9 @@
+package co.nilin.opex.matching.gateway.app.utils
+
+import org.springframework.vault.authentication.AppIdUserIdMechanism
+
+class VaultUserIdMechanism() : AppIdUserIdMechanism {
+ override fun createUserId(): String {
+ return System.getenv("BACKEND_USER")
+ }
+}
\ No newline at end of file
diff --git a/matching-gateway/matching-gateway-app/src/main/resources/application.yml b/matching-gateway/matching-gateway-app/src/main/resources/application.yml
index 15e19d34e..5d36a1a7b 100644
--- a/matching-gateway/matching-gateway-app/src/main/resources/application.yml
+++ b/matching-gateway/matching-gateway-app/src/main/resources/application.yml
@@ -9,9 +9,27 @@ spring:
bootstrap-servers: ${KAFKA_IP_PORT:localhost:9092}
consumer:
group-id: gateway
+ r2dbc:
+ url: r2dbc:postgresql://${DB_IP_PORT:localhost}/opex
+ username: ${dbusername:opex}
+ password: ${DB_PASS:hiopex}
+ initialization-mode: always
cloud:
bootstrap:
enabled: true
+ vault:
+ host: ${VAULT_HOST}
+ port: 8200
+ scheme: http
+ authentication: APPID
+ app-id:
+ user-id: co.nilin.opex.matching.gateway.app.utils.VaultUserIdMechanism
+ fail-fast: true
+ kv:
+ enabled: true
+ backend: secret
+ profile-separator: '/'
+ application-name: ${spring.application.name}
consul:
host: ${CONSUL_HOST:localhost}
port: 8500
@@ -25,7 +43,7 @@ management:
web:
base-path: /actuator
exposure:
- include: ["health", "prometheus", "metrics"]
+ include: [ "health", "prometheus", "metrics" ]
endpoint:
health:
show-details: when_authorized
@@ -64,9 +82,12 @@ logging:
co.nilin: INFO
org.zalando.logbook: TRACE
app:
+ symbols: ${SYMBOLS}
accountant:
url: lb://opex-accountant
auth:
- cert-url: lb://opex-auth/auth/realms/opex/protocol/openid-connect/certs
+ cert-url: http://keycloak:8080/realms/opex/protocol/openid-connect/certs
swagger:
authUrl: ${SWAGGER_AUTH_URL:https://api.opex.dev/auth}/realms/opex/protocol/openid-connect/token}
+
+
diff --git a/matching-gateway/matching-gateway-app/src/test/kotlin/co/nilin/opex/matching/gateway/app/service/OrderServiceTest.kt b/matching-gateway/matching-gateway-app/src/test/kotlin/co/nilin/opex/matching/gateway/app/service/OrderServiceTest.kt
index 306586dff..176393526 100644
--- a/matching-gateway/matching-gateway-app/src/test/kotlin/co/nilin/opex/matching/gateway/app/service/OrderServiceTest.kt
+++ b/matching-gateway/matching-gateway-app/src/test/kotlin/co/nilin/opex/matching/gateway/app/service/OrderServiceTest.kt
@@ -5,10 +5,13 @@ import co.nilin.opex.matching.gateway.app.service.sample.VALID
import co.nilin.opex.matching.gateway.app.spi.AccountantApiProxy
import co.nilin.opex.matching.gateway.app.spi.PairConfigLoader
import co.nilin.opex.matching.gateway.ports.kafka.submitter.inout.OrderSubmitResult
-import co.nilin.opex.matching.gateway.ports.kafka.submitter.service.EventSubmitter
import co.nilin.opex.matching.gateway.ports.kafka.submitter.service.KafkaHealthIndicator
import co.nilin.opex.matching.gateway.ports.kafka.submitter.service.OrderRequestEventSubmitter
-import io.mockk.*
+import co.nilin.opex.matching.gateway.ports.postgres.service.PairSettingService
+import io.mockk.MockKException
+import io.mockk.clearMocks
+import io.mockk.coEvery
+import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
@@ -21,20 +24,28 @@ private class OrderServiceTest {
private val eventSubmitter: OrderRequestEventSubmitter = mockk()
private val pairConfigLoader: PairConfigLoader = mockk()
private val kafkaHealthIndicator: KafkaHealthIndicator = mockk()
+ private val pairSettingService: PairSettingService = mockk()
+
private val orderService: OrderService = OrderService(
accountantApiProxy,
orderRequestEventSubmitter,
pairConfigLoader,
- kafkaHealthIndicator
+ pairSettingService,
+ kafkaHealthIndicator,
)
private fun stubASK() {
+ coEvery {
+ pairSettingService.load(VALID.ETH_USDT)
+ } returns VALID.PAIR_SETTING
+
coEvery {
pairConfigLoader.load(
VALID.ETH_USDT,
OrderDirection.ASK
)
} returns VALID.PAIR_CONFIG
+
coEvery {
accountantApiProxy.canCreateOrder(
VALID.CREATE_ORDER_REQUEST_ASK.uuid!!,
@@ -42,15 +53,20 @@ private class OrderServiceTest {
VALID.CREATE_ORDER_REQUEST_ASK.quantity
)
} returns true
+
coEvery {
orderRequestEventSubmitter.submit(any())
} returns OrderSubmitResult(null)
+
coEvery {
kafkaHealthIndicator.isHealthy
} returns true
}
private fun stubBID() {
+ coEvery {
+ pairSettingService.load(VALID.ETH_USDT)
+ } returns VALID.PAIR_SETTING
coEvery {
pairConfigLoader.load(
VALID.ETH_USDT,
@@ -84,11 +100,13 @@ private class OrderServiceTest {
@Test
fun givenPair_whenSubmitNewOrderByInvalidSymbol_thenThrow(): Unit = runBlocking {
stubASK()
- clearMocks(pairConfigLoader)
+ clearMocks(pairConfigLoader, pairSettingService)
coEvery {
pairConfigLoader.load("BTC_ETH", OrderDirection.ASK)
} throws Exception()
-
+ coEvery {
+ pairSettingService.load("BTC_ETH")
+ } throws Exception()
assertThatThrownBy {
runBlocking {
orderService.submitNewOrder(VALID.CREATE_ORDER_REQUEST_ASK.copy(pair = "BTC_ETH"))
@@ -130,11 +148,13 @@ private class OrderServiceTest {
@Test
fun givenPair_whenSubmitNewOrderByBIDAndInvalidSymbol_thenThrow(): Unit = runBlocking {
stubBID()
- clearMocks(pairConfigLoader)
+ clearMocks(pairConfigLoader, pairSettingService)
coEvery {
pairConfigLoader.load("BTC_USDT", OrderDirection.BID)
} throws Exception()
-
+ coEvery {
+ pairSettingService.load("BTC_USDT")
+ } throws Exception()
assertThatThrownBy {
runBlocking {
orderService.submitNewOrder(VALID.CREATE_ORDER_REQUEST_BID.copy(pair = "BTC_USDT"))
diff --git a/matching-gateway/matching-gateway-app/src/test/kotlin/co/nilin/opex/matching/gateway/app/service/sample/Samples.kt b/matching-gateway/matching-gateway-app/src/test/kotlin/co/nilin/opex/matching/gateway/app/service/sample/Samples.kt
index db4aaba58..ea9f1ab47 100644
--- a/matching-gateway/matching-gateway-app/src/test/kotlin/co/nilin/opex/matching/gateway/app/service/sample/Samples.kt
+++ b/matching-gateway/matching-gateway-app/src/test/kotlin/co/nilin/opex/matching/gateway/app/service/sample/Samples.kt
@@ -6,6 +6,7 @@ import co.nilin.opex.matching.engine.core.model.OrderType
import co.nilin.opex.matching.gateway.app.inout.CancelOrderRequest
import co.nilin.opex.matching.gateway.app.inout.CreateOrderRequest
import co.nilin.opex.matching.gateway.app.inout.PairConfig
+import co.nilin.opex.matching.gateway.ports.postgres.dto.PairSetting
import java.math.BigDecimal
object VALID {
@@ -23,6 +24,8 @@ object VALID {
val PAIR_CONFIG = PairConfig(ETH_USDT, ETH, USDT, BigDecimal.valueOf(0.01), BigDecimal.valueOf(0.0001))
+ val PAIR_SETTING = PairSetting(ETH_USDT, true, 0.0000001.toBigDecimal(), 100.toBigDecimal(), "LIMIT_ORDER,MARKET_ORDER", null)
+
val CREATE_ORDER_REQUEST_ASK = CreateOrderRequest(
UUID,
ETH_USDT,
diff --git a/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/pom.xml b/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/pom.xml
new file mode 100644
index 000000000..5b4b55151
--- /dev/null
+++ b/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/pom.xml
@@ -0,0 +1,93 @@
+
+
+ 4.0.0
+
+
+ co.nilin.opex.matching.gateway
+ matching-gateway
+ 1.0.1-beta.7
+ ../../pom.xml
+
+
+ co.nilin.opex.matching.gateway.ports.postgres
+ matching-gateway-persister-postgres
+ matching-gateway-persister-postgres
+ Persist items of Opex Matching gateway on Postgres
+
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+
+
+ org.springframework.boot
+ spring-boot-starter-data-r2dbc
+
+
+ io.r2dbc
+ r2dbc-pool
+
+
+ io.r2dbc
+ r2dbc-spi
+
+
+
+
+ org.postgresql
+ r2dbc-postgresql
+ runtime
+
+
+ io.r2dbc
+ r2dbc-pool
+
+
+ io.r2dbc
+ r2dbc-spi
+
+
+
+
+ io.r2dbc
+ r2dbc-pool
+ 1.0.1.RELEASE
+
+
+ io.r2dbc
+ r2dbc-spi
+ 1.0.0.RELEASE
+
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+ io.projectreactor.kotlin
+ reactor-kotlin-extensions
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-reactor
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-core
+
+
+ com.google.code.gson
+ gson
+
+
+ co.nilin.opex.utility
+ error-handler
+
+
+ io.projectreactor
+ reactor-test
+ test
+
+
+
diff --git a/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/config/PostgresConfig.kt b/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/config/PostgresConfig.kt
new file mode 100644
index 000000000..c364136b8
--- /dev/null
+++ b/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/config/PostgresConfig.kt
@@ -0,0 +1,22 @@
+package co.nilin.opex.matching.gateway.ports.postgres.config
+
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.io.Resource
+import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories
+import org.springframework.r2dbc.core.DatabaseClient
+
+@Configuration
+@EnableR2dbcRepositories(basePackages = ["co.nilin.opex"])
+class PostgresConfig(
+ db: DatabaseClient,
+ @Value("classpath:schema.sql") private val schemaResource: Resource
+) {
+ init {
+ val schemaReader = schemaResource.inputStream.reader()
+ val schema = schemaReader.readText().trim()
+ schemaReader.close()
+ val initDb = db.sql { schema }
+ initDb.then().block()
+ }
+}
diff --git a/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/dao/PairSettingRepository.kt b/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/dao/PairSettingRepository.kt
new file mode 100644
index 000000000..12fcff723
--- /dev/null
+++ b/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/dao/PairSettingRepository.kt
@@ -0,0 +1,16 @@
+package co.nilin.opex.matching.gateway.ports.postgres.dao
+
+import co.nilin.opex.matching.gateway.ports.postgres.model.PairSettingModel
+import org.springframework.data.r2dbc.repository.Query
+import org.springframework.data.repository.reactive.ReactiveCrudRepository
+import org.springframework.stereotype.Repository
+import reactor.core.publisher.Mono
+import java.math.BigDecimal
+
+@Repository
+interface PairSettingRepository : ReactiveCrudRepository {
+ fun findByPair(pair: String): Mono
+
+ @Query("insert into pair_setting(pair,is_available,min_order,max_order,order_types) values(:pair,:isAvailable,:minOrder,:maxOrder,:orderTypes) ")
+ fun insert(pair: String, isAvailable: Boolean , minOrder : BigDecimal, maxOrder : BigDecimal,orderTypes : String): Mono
+}
diff --git a/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/dto/PairSetting.kt b/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/dto/PairSetting.kt
new file mode 100644
index 000000000..1fd568629
--- /dev/null
+++ b/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/dto/PairSetting.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.matching.gateway.ports.postgres.dto
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+class PairSetting(
+ val pair: String,
+ val isAvailable: Boolean,
+ val minOrder : BigDecimal,
+ val maxOrder : BigDecimal,
+ val orderTypes : String,
+ val updateDate: LocalDateTime? = null,
+)
\ No newline at end of file
diff --git a/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/impl/PairSettingServiceImpl.kt b/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/impl/PairSettingServiceImpl.kt
new file mode 100644
index 000000000..0e1ccaa80
--- /dev/null
+++ b/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/impl/PairSettingServiceImpl.kt
@@ -0,0 +1,62 @@
+package co.nilin.opex.matching.gateway.ports.postgres.impl
+
+import co.nilin.opex.common.OpexError
+import co.nilin.opex.matching.gateway.ports.postgres.dao.PairSettingRepository
+import co.nilin.opex.matching.gateway.ports.postgres.dto.PairSetting
+import co.nilin.opex.matching.gateway.ports.postgres.service.PairSettingService
+import co.nilin.opex.matching.gateway.ports.postgres.util.CacheManager
+import co.nilin.opex.matching.gateway.ports.postgres.util.toPairSetting
+import kotlinx.coroutines.reactive.awaitFirst
+import kotlinx.coroutines.reactive.awaitFirstOrNull
+import org.springframework.stereotype.Service
+import java.time.LocalDateTime
+import java.util.concurrent.TimeUnit
+
+@Service
+class PairSettingServiceImpl(
+ private val pairSettingRepository: PairSettingRepository,
+ private val cacheManager: CacheManager
+) : PairSettingService {
+
+ override suspend fun load(pair: String): PairSetting {
+ return cacheManager.get("pair-setting:$pair")
+ ?: pairSettingRepository.findByPair(pair)
+ .awaitFirstOrNull()
+ ?.let {
+ it.toPairSetting().also {
+ cacheManager.put(
+ "pair-setting:${it.pair}",
+ it,
+ 5, TimeUnit.MINUTES
+ )
+ }
+ }
+ ?: throw OpexError.PairNotFound.exception()
+ }
+
+ override suspend fun loadAll(): List {
+ return pairSettingRepository.findAll()
+ .map { it.toPairSetting() }
+ .collectList()
+ .awaitFirstOrNull() ?: emptyList()
+ }
+
+ override suspend fun update(pairSetting: PairSetting): PairSetting {
+ val pairSetting =
+ pairSettingRepository.findByPair(pairSetting.pair).awaitFirstOrNull() ?: throw OpexError.PairNotFound.exception()
+ pairSetting.apply {
+ this.isAvailable = pairSetting.isAvailable
+ this.minOrder = pairSetting.minOrder
+ this.maxOrder = pairSetting.maxOrder
+ this.orderTypes = pairSetting.orderTypes
+ this.updateDate = LocalDateTime.now()
+ }
+ return pairSettingRepository.save(pairSetting).awaitFirst().toPairSetting().also {
+ cacheManager.put(
+ "pair-setting:${it.pair}",
+ it,
+ 5, TimeUnit.MINUTES
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/model/PairSettingModel.kt b/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/model/PairSettingModel.kt
new file mode 100644
index 000000000..f62832c5d
--- /dev/null
+++ b/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/model/PairSettingModel.kt
@@ -0,0 +1,17 @@
+package co.nilin.opex.matching.gateway.ports.postgres.model
+
+import org.springframework.data.annotation.Id
+import org.springframework.data.relational.core.mapping.Table
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+@Table("pair_setting")
+data class PairSettingModel(
+ @Id
+ val pair: String,
+ var isAvailable: Boolean,
+ var minOrder : BigDecimal,
+ var maxOrder : BigDecimal,
+ var orderTypes : String,
+ var updateDate: LocalDateTime? = null,
+)
\ No newline at end of file
diff --git a/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/service/PairSettingInitializer.kt b/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/service/PairSettingInitializer.kt
new file mode 100644
index 000000000..0137f55f9
--- /dev/null
+++ b/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/service/PairSettingInitializer.kt
@@ -0,0 +1,70 @@
+package co.nilin.opex.matching.gateway.ports.postgres.service
+
+import co.nilin.opex.matching.gateway.ports.postgres.dao.PairSettingRepository
+import co.nilin.opex.matching.gateway.ports.postgres.dto.PairSetting
+import co.nilin.opex.matching.gateway.ports.postgres.util.CacheManager
+import co.nilin.opex.matching.gateway.ports.postgres.util.toPairSetting
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.reactive.awaitFirstOrNull
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.stereotype.Service
+import java.math.BigDecimal
+import java.util.concurrent.TimeUnit
+import javax.annotation.PostConstruct
+
+@Service
+class PairSettingInitializer(
+ private val pairSettingRepository: PairSettingRepository,
+ private val cacheManager: CacheManager,
+ @Value("\${app.symbols}")
+ private val symbols: String
+) {
+
+ private val logger = LoggerFactory.getLogger(PairSettingInitializer::class.java)
+ val scope = CoroutineScope(Dispatchers.IO)
+
+ @PostConstruct
+ fun initialize() {
+ logger.info(
+ """
+================================================================================================
+ Initialize Pair Settings
+================================================================================================
+ """
+ )
+ scope.launch {
+ try {
+ symbols.split(",").forEach { pair ->
+ val existingPair = pairSettingRepository.findByPair(pair).awaitFirstOrNull()
+
+ val pairToCache = existingPair ?: pairSettingRepository.insert(
+ pair,
+ false,
+ BigDecimal.ONE,
+ BigDecimal.ONE,
+ "LIMIT_ORDER,MARKET_ORDER"
+ ).then(pairSettingRepository.findByPair(pair)).awaitFirstOrNull()
+ .also { if (it == null) logger.warn("Failed to insert pair: $pair") }
+ ?: return@forEach
+
+ if (existingPair != null) logger.info("Pair already exists: $pair") else logger.info("Added Pair: $pair")
+
+ cacheManager.put(pair, pairToCache.toPairSetting(), 5, TimeUnit.MINUTES)
+ }
+ logger.info(
+ """
+================================================================================================
+ Completed Successfully
+================================================================================================
+ """
+ )
+ } catch (e: Exception) {
+ logger.error("Error initializing Pair Settings: ${e.message}")
+ throw e
+ }
+ }
+ }
+}
diff --git a/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/service/PairSettingService.kt b/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/service/PairSettingService.kt
new file mode 100644
index 000000000..723271184
--- /dev/null
+++ b/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/service/PairSettingService.kt
@@ -0,0 +1,9 @@
+package co.nilin.opex.matching.gateway.ports.postgres.service
+
+import co.nilin.opex.matching.gateway.ports.postgres.dto.PairSetting
+
+interface PairSettingService {
+ suspend fun load(pair: String): PairSetting
+ suspend fun loadAll(): List
+ suspend fun update(pairSetting: PairSetting): PairSetting
+}
\ No newline at end of file
diff --git a/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/util/CacheManager.kt b/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/util/CacheManager.kt
new file mode 100644
index 000000000..18b6fdffd
--- /dev/null
+++ b/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/util/CacheManager.kt
@@ -0,0 +1,43 @@
+package co.nilin.opex.matching.gateway.ports.postgres.util
+
+import org.springframework.stereotype.Component
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.TimeUnit
+
+@Component
+class CacheManager {
+
+ private val cacheMap = ConcurrentHashMap>()
+
+ data class CacheEntry(
+ val value: T,
+ val timestamp: Long
+ )
+
+ fun put(key: K, value: V, expirationTime: Long, timeUnit: TimeUnit) {
+ val expirationMillis = timeUnit.toMillis(expirationTime)
+ cacheMap[key] = CacheEntry(value, System.currentTimeMillis() + expirationMillis)
+ }
+
+ fun get(key: K): V? {
+ val entry = cacheMap[key]
+ return if (entry != null && !isExpired(entry)) {
+ entry.value
+ } else {
+ cacheMap.remove(key)
+ null
+ }
+ }
+
+ private fun isExpired(entry: CacheEntry): Boolean {
+ return System.currentTimeMillis() > entry.timestamp
+ }
+
+ fun remove(key: K) {
+ cacheMap.remove(key)
+ }
+
+ fun clear() {
+ cacheMap.clear()
+ }
+}
diff --git a/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/util/Convertor.kt b/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/util/Convertor.kt
new file mode 100644
index 000000000..c039102d7
--- /dev/null
+++ b/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/matching/gateway/ports/postgres/util/Convertor.kt
@@ -0,0 +1,17 @@
+package co.nilin.opex.matching.gateway.ports.postgres.util
+
+import co.nilin.opex.matching.gateway.ports.postgres.dto.PairSetting
+import co.nilin.opex.matching.gateway.ports.postgres.model.PairSettingModel
+
+
+fun PairSettingModel.toPairSetting(): PairSetting {
+ return PairSetting(
+ pair,
+ isAvailable,
+ minOrder,
+ maxOrder,
+ orderTypes,
+ updateDate,
+ )
+}
+
diff --git a/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/resources/schema.sql b/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/resources/schema.sql
new file mode 100644
index 000000000..c8a595b04
--- /dev/null
+++ b/matching-gateway/matching-gateway-port/matching-gateway-persister-postgres/src/main/resources/schema.sql
@@ -0,0 +1,27 @@
+CREATE TABLE IF NOT EXISTS pair_setting
+(
+ pair VARCHAR(72) PRIMARY KEY,
+ is_available BOOLEAN NOT NULL DEFAULT FALSE,
+ update_date TIMESTAMP
+);
+
+DO
+$$
+ BEGIN
+ IF NOT EXISTS (SELECT 1
+ FROM information_schema.columns
+ WHERE table_name = 'pair_setting' AND column_name = 'min_order') THEN ALTER TABLE pair_setting
+ ADD COLUMN min_order DECIMAL NOT NULL default 1;
+ END IF;
+ IF NOT EXISTS (SELECT 1
+ FROM information_schema.columns
+ WHERE table_name = 'pair_setting' AND column_name = 'max_order') THEN ALTER TABLE pair_setting
+ ADD COLUMN max_order DECIMAL NOT NULL default 1;
+ END IF;
+ IF NOT EXISTS (SELECT 1
+ FROM information_schema.columns
+ WHERE table_name = 'pair_setting' AND column_name = 'order_types') THEN ALTER TABLE pair_setting
+ ADD COLUMN order_types varchar(255) NOT NULL default 'LIMIT_ORDER, MARKET_ORDER' ;
+ END IF;
+ END
+$$;
\ No newline at end of file
diff --git a/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/pom.xml b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/pom.xml
index 9ef385922..bd520bfd6 100644
--- a/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/pom.xml
+++ b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
diff --git a/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/config/OrderKafkaConfig.kt b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/config/OrderKafkaConfig.kt
index 0e0bf5189..4f1e96623 100644
--- a/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/config/OrderKafkaConfig.kt
+++ b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/config/OrderKafkaConfig.kt
@@ -2,7 +2,6 @@ package co.nilin.opex.matching.gateway.ports.kafka.submitter.config
import co.nilin.opex.matching.engine.core.eventh.events.CoreEvent
import co.nilin.opex.matching.gateway.ports.kafka.submitter.inout.OrderRequestEvent
-import co.nilin.opex.matching.gateway.ports.kafka.submitter.inout.OrderSubmitRequestEvent
import org.apache.kafka.clients.producer.ProducerConfig
import org.apache.kafka.common.serialization.StringSerializer
import org.springframework.beans.factory.annotation.Qualifier
diff --git a/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/inout/OrderRequestEvent.kt b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/inout/OrderRequestEvent.kt
index 462f2f58a..88bb3f7b7 100644
--- a/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/inout/OrderRequestEvent.kt
+++ b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/inout/OrderRequestEvent.kt
@@ -2,4 +2,4 @@ package co.nilin.opex.matching.gateway.ports.kafka.submitter.inout
import co.nilin.opex.matching.engine.core.model.Pair
-abstract class OrderRequestEvent(val ouid:String, val uuid: String, val pair: Pair)
\ No newline at end of file
+abstract class OrderRequestEvent(val ouid: String, val uuid: String, val pair: Pair)
\ No newline at end of file
diff --git a/matching-gateway/pom.xml b/matching-gateway/pom.xml
index 3fd2fe323..56e9c1b09 100644
--- a/matching-gateway/pom.xml
+++ b/matching-gateway/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
@@ -18,6 +18,7 @@
matching-gateway-app
matching-gateway-port/matching-gateway-submitter-kafka
+ matching-gateway-port/matching-gateway-persister-postgres
@@ -48,6 +49,11 @@
matching-gateway-submitter-kafka
${project.version}
+
+ co.nilin.opex.matching.gateway.ports.postgres
+ matching-gateway-persister-postgres
+ ${project.version}
+
co.nilin.opex.utility
error-handler
diff --git a/otp/otp-app/Dockerfile b/otp/otp-app/Dockerfile
new file mode 100644
index 000000000..9c566c1b2
--- /dev/null
+++ b/otp/otp-app/Dockerfile
@@ -0,0 +1,5 @@
+FROM openjdk:11
+ARG JAR_FILE=target/*.jar
+COPY ${JAR_FILE} app.jar
+ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"]
+HEALTHCHECK --interval=45s --start-period=30s --retries=5 CMD curl -sf 'http://localhost:8080/actuator/health' >/dev/null || exit 1
diff --git a/otp/otp-app/pom.xml b/otp/otp-app/pom.xml
new file mode 100644
index 000000000..1d3d72124
--- /dev/null
+++ b/otp/otp-app/pom.xml
@@ -0,0 +1,121 @@
+
+
+ 4.0.0
+
+
+ co.nilin.opex.matching.otp
+ otp
+ 1.0.1-beta.7
+
+
+ otp-app
+ otp-app
+
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+
+
+ com.fasterxml.jackson.module
+ jackson-module-kotlin
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+ org.springframework.cloud
+ spring-cloud-starter-consul-all
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
+
+
+ org.springframework.boot
+ spring-boot-starter-data-r2dbc
+
+
+ org.postgresql
+ r2dbc-postgresql
+ runtime
+
+
+ io.springfox
+ springfox-boot-starter
+ 3.0.0
+
+
+ com.sun.mail
+ jakarta.mail
+ 2.0.1
+
+
+ jakarta.activation
+ jakarta.activation-api
+ 2.0.1
+
+
+ dev.samstevens.totp
+ totp
+ 1.7.1
+
+
+ com.google.zxing
+ core
+ 3.5.3
+
+ com.google.zxing
+ javase
+ 3.5.3
+
+
+ co.nilin.opex.utility
+ error-handler
+
+
+ io.mockk
+ mockk
+
+
+ io.micrometer
+ micrometer-registry-prometheus
+ runtime
+
+
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-dependencies
+ ${spring-cloud.version}
+ pom
+ import
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/OTPApp.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/OTPApp.kt
new file mode 100644
index 000000000..0e161d374
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/OTPApp.kt
@@ -0,0 +1,16 @@
+package co.nilin.opex.otp.app
+
+import co.nilin.opex.utility.error.EnableOpexErrorHandler
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+import org.springframework.context.annotation.ComponentScan
+import org.springframework.scheduling.annotation.EnableScheduling
+
+@SpringBootApplication
+@ComponentScan("co.nilin.opex")
+@EnableOpexErrorHandler
+class OTPApp
+
+fun main(args: Array) {
+ runApplication(*args)
+}
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/config/PostgresConfig.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/config/PostgresConfig.kt
new file mode 100644
index 000000000..0c554085b
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/config/PostgresConfig.kt
@@ -0,0 +1,22 @@
+package co.nilin.opex.otp.app.config
+
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.io.Resource
+import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories
+import org.springframework.r2dbc.core.DatabaseClient
+
+@Configuration
+@EnableR2dbcRepositories(basePackages = ["co.nilin.opex"])
+class PostgresConfig(
+ db: DatabaseClient,
+ @Value("classpath:schema.sql") private val schemaResource: Resource
+) {
+ init {
+ val schemaReader = schemaResource.inputStream.reader()
+ val schema = schemaReader.readText().trim()
+ schemaReader.close()
+ val initDb = db.sql { schema }
+ initDb.then().block()
+ }
+}
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/config/SecurityConfig.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/config/SecurityConfig.kt
new file mode 100644
index 000000000..ae5337bd5
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/config/SecurityConfig.kt
@@ -0,0 +1,48 @@
+package co.nilin.opex.otp.app.config
+
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Profile
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
+import org.springframework.security.crypto.password.PasswordEncoder
+import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder
+import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.web.reactive.function.client.WebClient
+
+@EnableWebFluxSecurity
+@Profile("!test")
+class SecurityConfig(private val webClient: WebClient) {
+
+ @Value("\${app.auth.cert-url}")
+ private lateinit var jwkUrl: String
+
+ @Bean
+ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain? {
+ http.csrf().disable()
+ .authorizeExchange()
+ .pathMatchers("/actuator/**").permitAll()
+ .pathMatchers("/v1/otp/**").permitAll()
+ .pathMatchers("/v1/totp/**").permitAll()
+ .anyExchange().authenticated()
+ .and()
+ .oauth2ResourceServer()
+ .jwt()
+ return http.build()
+ }
+
+ @Bean
+ @Throws(Exception::class)
+ fun reactiveJwtDecoder(): ReactiveJwtDecoder? {
+ return NimbusReactiveJwtDecoder.withJwkSetUri(jwkUrl)
+ .webClient(webClient)
+ .build()
+ }
+
+ @Bean
+ fun passwordEncoder(): BCryptPasswordEncoder {
+ return BCryptPasswordEncoder()
+ }
+}
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/config/WebclientConfig.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/config/WebclientConfig.kt
new file mode 100644
index 000000000..7abff0f3f
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/config/WebclientConfig.kt
@@ -0,0 +1,14 @@
+package co.nilin.opex.otp.app.config
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.web.reactive.function.client.WebClient
+
+@Configuration
+class WebclientConfig {
+
+ @Bean
+ fun webClient(): WebClient {
+ return WebClient.builder().build()
+ }
+}
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/controller/OTPController.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/controller/OTPController.kt
new file mode 100644
index 000000000..635a0509f
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/controller/OTPController.kt
@@ -0,0 +1,58 @@
+package co.nilin.opex.otp.app.controller
+
+import co.nilin.opex.common.OpexError
+import co.nilin.opex.otp.app.data.*
+import co.nilin.opex.otp.app.model.OTPType
+import co.nilin.opex.otp.app.service.OTPService
+import org.springframework.http.HttpStatus
+import org.springframework.http.ResponseEntity
+import org.springframework.web.bind.annotation.PostMapping
+import org.springframework.web.bind.annotation.RequestBody
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+@RequestMapping("/v1/otp")
+class OTPController(private val otpService: OTPService) {
+
+ //TODO IMPORTANT: remove in production
+ data class TempOtpResponse(val otp: String)
+ //TODO IMPORTANT: remove in production
+
+ //TODO IMPORTANT: remove in production
+ @PostMapping
+ suspend fun requestOTP(@RequestBody request: NewOTPRequest): ResponseEntity {
+ validateOTPRequest(request.receivers.map { it.type })
+ val code = if (request.receivers.size == 1)
+ otpService.requestOTP(
+ request.receivers[0].receiver,
+ request.receivers[0].type,
+ request.userId,
+ request.action
+ )
+ else
+ otpService.requestCompositeOTP(request.receivers.toSet(), request.userId, request.action)
+ return ResponseEntity.status(HttpStatus.CREATED).body(TempOtpResponse(code))
+ }
+
+ @PostMapping("/verify")
+ suspend fun verifyOTP(@RequestBody request: VerifyOTPRequest): OTPResult {
+ validateOTPRequest(request.otpCodes.map { it.type })
+ val result = if (request.otpCodes.size == 1)
+ otpService.verifyOTP(request.otpCodes[0].code, request.userId)
+ else
+ otpService.verifyCompositeOTP(request.otpCodes.toSet(), request.userId)
+ return OTPResult(result.isValid, result)
+ }
+
+ private fun validateOTPRequest(request: List) {
+ if (request.isEmpty() || request.contains(OTPType.COMPOSITE))
+ throw OpexError.BadRequest.exception()
+
+ val map = request.groupingBy { it }.eachCount()
+ map.forEach { entry ->
+ if (entry.value > 1)
+ throw OpexError.BadRequest.exception()
+ }
+ }
+}
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/controller/TOTPController.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/controller/TOTPController.kt
new file mode 100644
index 000000000..115db992e
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/controller/TOTPController.kt
@@ -0,0 +1,52 @@
+package co.nilin.opex.otp.app.controller
+
+import co.nilin.opex.otp.app.data.SetupTOTPRequest
+import co.nilin.opex.otp.app.data.SetupTOTPResponse
+import co.nilin.opex.otp.app.data.VerifyOTPResponse
+import co.nilin.opex.otp.app.data.VerifyTOTPRequest
+import co.nilin.opex.otp.app.model.TOTPQueryResponse
+import co.nilin.opex.otp.app.service.TOTPService
+import org.springframework.web.bind.annotation.DeleteMapping
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.PathVariable
+import org.springframework.web.bind.annotation.PostMapping
+import org.springframework.web.bind.annotation.RequestBody
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+@RequestMapping("/v1/totp")
+class TOTPController(private val service: TOTPService) {
+
+ @PostMapping("/setup")
+ suspend fun setup(@RequestBody request: SetupTOTPRequest): SetupTOTPResponse {
+ val uri = service.setupTOTP(request.userId, request.label)
+ return SetupTOTPResponse(uri)
+ }
+
+ @PostMapping("/setup/verify")
+ suspend fun verifySetup(@RequestBody request: VerifyTOTPRequest) {
+ service.verifyAndMarkActivated(request.userId, request.code)
+ }
+
+ @PostMapping("/verify")
+ suspend fun verify(@RequestBody request: VerifyTOTPRequest): VerifyOTPResponse {
+ val isVerified = service.verifyTOTP(request.userId, request.code)
+ return VerifyOTPResponse(isVerified)
+ }
+
+ @GetMapping("/query/{userId}")
+ suspend fun query(@PathVariable userId: String): TOTPQueryResponse {
+ val totp = service.findTOTP(userId)
+ return TOTPQueryResponse(
+ totp?.userId ?: userId,
+ totp?.isEnabled ?: false,
+ totp?.isActivated ?: false,
+ )
+ }
+
+ @DeleteMapping
+ suspend fun remove(@RequestBody request: VerifyTOTPRequest) {
+ service.removeTOTP(request.userId, request.code)
+ }
+}
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/NewOTPRequest.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/NewOTPRequest.kt
new file mode 100644
index 000000000..533213744
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/NewOTPRequest.kt
@@ -0,0 +1,7 @@
+package co.nilin.opex.otp.app.data
+
+data class NewOTPRequest(
+ val userId: String,
+ val receivers: List,
+ val action: String?
+)
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/NewOTPResponse.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/NewOTPResponse.kt
new file mode 100644
index 000000000..7dd115c30
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/NewOTPResponse.kt
@@ -0,0 +1,3 @@
+package co.nilin.opex.otp.app.data
+
+data class NewOTPResponse(val tracingCode: String)
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/OTPCode.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/OTPCode.kt
new file mode 100644
index 000000000..499fe8565
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/OTPCode.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.otp.app.data
+
+import co.nilin.opex.otp.app.model.OTPType
+
+data class OTPCode(
+ val type: OTPType,
+ val code: String
+)
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/OTPReceiver.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/OTPReceiver.kt
new file mode 100644
index 000000000..86a3657da
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/OTPReceiver.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.otp.app.data
+
+import co.nilin.opex.otp.app.model.OTPType
+
+data class OTPReceiver(
+ val receiver: String,
+ val type: OTPType,
+)
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/OTPResult.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/OTPResult.kt
new file mode 100644
index 000000000..3266d7c75
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/OTPResult.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.otp.app.data
+
+data class OTPResult(val result: Boolean, val type: OTPResultType)
+
+enum class OTPResultType(val isValid: Boolean = false) {
+
+ VALID(true), EXPIRED, INCORRECT, INVALID
+}
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/SetupTOTPRequest.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/SetupTOTPRequest.kt
new file mode 100644
index 000000000..368e43706
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/SetupTOTPRequest.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.otp.app.data
+
+data class SetupTOTPRequest(
+ val userId: String,
+ val label: String?
+)
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/SetupTOTPResponse.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/SetupTOTPResponse.kt
new file mode 100644
index 000000000..00751c14f
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/SetupTOTPResponse.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.otp.app.data
+
+data class SetupTOTPResponse(
+ val uri: String
+)
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/VerifyOTPRequest.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/VerifyOTPRequest.kt
new file mode 100644
index 000000000..636060259
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/VerifyOTPRequest.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.otp.app.data
+
+data class VerifyOTPRequest(
+ val userId: String,
+ val otpCodes: List
+)
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/VerifyOTPResponse.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/VerifyOTPResponse.kt
new file mode 100644
index 000000000..7ab95c8dd
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/VerifyOTPResponse.kt
@@ -0,0 +1,3 @@
+package co.nilin.opex.otp.app.data
+
+data class VerifyOTPResponse(val result: Boolean)
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/VerifyTOTPRequest.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/VerifyTOTPRequest.kt
new file mode 100644
index 000000000..4931e111a
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/VerifyTOTPRequest.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.otp.app.data
+
+data class VerifyTOTPRequest(
+ val userId: String,
+ val code: String
+)
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/OTP.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/OTP.kt
new file mode 100644
index 000000000..96e24135d
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/OTP.kt
@@ -0,0 +1,26 @@
+package co.nilin.opex.otp.app.model
+
+import org.springframework.data.annotation.Id
+import org.springframework.data.relational.core.mapping.Table
+import java.time.LocalDateTime
+
+@Table("otp")
+data class OTP(
+ val code: String,
+ val receiver: String,
+ val userId: String,
+ val action: String,
+ val tracingCode: String,
+ val type: OTPType,
+ val expiresAt: LocalDateTime,
+ val requestDate: LocalDateTime = LocalDateTime.now(),
+ val isVerified: Boolean = false,
+ val isActive: Boolean = true,
+ val verifyTime: LocalDateTime? = null,
+ @Id val id: Long? = null,
+) {
+
+ fun isExpired(): Boolean {
+ return expiresAt.isBefore(LocalDateTime.now())
+ }
+}
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/OTPConfig.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/OTPConfig.kt
new file mode 100644
index 000000000..712bda511
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/OTPConfig.kt
@@ -0,0 +1,15 @@
+package co.nilin.opex.otp.app.model
+
+import org.springframework.data.annotation.Id
+import org.springframework.data.relational.core.mapping.Table
+
+@Table("otp_config")
+data class OTPConfig(
+ @Id val type: OTPType,
+ var expireTimeSeconds: Int = 60,
+ var charCount: Int = 6,
+ var includeAlphabetChars: Boolean = false,
+ var isEnabled: Boolean = true,
+ var isActivated: Boolean = false,
+ var messageTemplate: String = "%s",
+)
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/OTPType.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/OTPType.kt
new file mode 100644
index 000000000..938049cc6
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/OTPType.kt
@@ -0,0 +1,7 @@
+package co.nilin.opex.otp.app.model
+
+enum class OTPType(val compositeOrder: Int) {
+
+ SMS(0), EMAIL(1),
+ COMPOSITE(99)
+}
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/TOTP.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/TOTP.kt
new file mode 100644
index 000000000..d7aa0494b
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/TOTP.kt
@@ -0,0 +1,16 @@
+package co.nilin.opex.otp.app.model
+
+import org.springframework.data.annotation.Id
+import org.springframework.data.relational.core.mapping.Table
+import java.time.LocalDateTime
+
+@Table("totp")
+data class TOTP(
+ val userId: String,
+ val secret: String,
+ val label: String? = null,
+ var isEnabled: Boolean = true,
+ var isActivated: Boolean = false,
+ val createdAt: LocalDateTime = LocalDateTime.now(),
+ @Id val id: Long? = null
+)
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/TOTPConfig.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/TOTPConfig.kt
new file mode 100644
index 000000000..06bb0e792
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/TOTPConfig.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.otp.app.model
+
+import org.springframework.data.annotation.Id
+import org.springframework.data.relational.core.mapping.Table
+
+@Table("TOTP")
+data class TOTPConfig(
+ var secretChars: Int,
+ var issuer: String,
+ @Id val id: Boolean = true
+)
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/TOTPQueryResponse.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/TOTPQueryResponse.kt
new file mode 100644
index 000000000..81dfa98b5
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/TOTPQueryResponse.kt
@@ -0,0 +1,7 @@
+package co.nilin.opex.otp.app.model
+
+data class TOTPQueryResponse(
+ val userId: String,
+ val isEnabled: Boolean,
+ val isActivated: Boolean,
+)
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/proxy/KaveNegarProxy.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/proxy/KaveNegarProxy.kt
new file mode 100644
index 000000000..0fd926534
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/proxy/KaveNegarProxy.kt
@@ -0,0 +1,43 @@
+package co.nilin.opex.otp.app.proxy
+
+import co.nilin.opex.common.utils.LoggerDelegate
+import kotlinx.coroutines.reactor.awaitSingleOrNull
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.stereotype.Component
+import org.springframework.web.reactive.function.client.WebClient
+import org.springframework.web.reactive.function.client.bodyToMono
+import org.springframework.web.util.UriComponentsBuilder
+
+@Component
+class KaveNegarProxy(
+ @Value("\${otp.sms.provider.api-key}")
+ private val apiKey: String,
+ private val webClient: WebClient
+) {
+
+ private val logger by LoggerDelegate()
+ private val baseUrl = "https://api.kavenegar.com/v1/$apiKey/"
+
+ suspend fun send(receiver: String, message: String, sender: String? = null, type: String? = null): Boolean {
+ val uri = UriComponentsBuilder.fromUriString("$baseUrl/sms/send.json")
+ .queryParam("receptor", receiver)
+ .queryParam("message", message)
+ .queryParam("sender", sender)
+ .queryParam("type", type)
+ .build().toUri()
+
+ return try {
+ val response = webClient.get()
+ .uri(uri)
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToMono()
+ .awaitSingleOrNull()
+ logger.debug("Message sent to receiver $receiver.\n$response")
+ true
+ } catch (e: Exception) {
+ logger.error("Failed to send SMS", e)
+ false
+ }
+ }
+}
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/proxy/data/KaveSendResponse.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/proxy/data/KaveSendResponse.kt
new file mode 100644
index 000000000..ba1f9da58
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/proxy/data/KaveSendResponse.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.otp.app.proxy.data
+
+data class KaveSendResponse(
+ val messageId: Long,
+ val message: String?,
+ val status: Int,
+ val statusText: String?,
+ val sender: String?,
+ val receptor: String?,
+ val cost: Long
+)
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/repository/OTPConfigRepository.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/repository/OTPConfigRepository.kt
new file mode 100644
index 000000000..0d28a1652
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/repository/OTPConfigRepository.kt
@@ -0,0 +1,9 @@
+package co.nilin.opex.otp.app.repository
+
+import co.nilin.opex.otp.app.model.OTPConfig
+import co.nilin.opex.otp.app.model.OTPType
+import org.springframework.data.repository.reactive.ReactiveCrudRepository
+import org.springframework.stereotype.Repository
+
+@Repository
+interface OTPConfigRepository : ReactiveCrudRepository
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/repository/OTPRepository.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/repository/OTPRepository.kt
new file mode 100644
index 000000000..b95e2ec61
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/repository/OTPRepository.kt
@@ -0,0 +1,26 @@
+package co.nilin.opex.otp.app.repository
+
+import co.nilin.opex.otp.app.model.OTP
+import co.nilin.opex.otp.app.model.OTPType
+import org.springframework.data.r2dbc.repository.Query
+import org.springframework.data.repository.kotlin.CoroutineCrudRepository
+import org.springframework.stereotype.Repository
+
+@Repository
+interface OTPRepository : CoroutineCrudRepository {
+
+ suspend fun findByTracingCode(traceCode: String): OTP?
+
+ @Query("select * from otp where user_id = :userId and is_active is true")
+ suspend fun findActiveByUserId(userId: String): OTP?
+
+ @Query("select * from otp where is_active is true and ((receiver = :receiver and type = :type) or user_id = :userId)")
+ suspend fun findActiveByReceiverAndTypeOrUserId(receiver: String, type: OTPType, userId: String): OTP?
+
+ @Query("update otp set is_active = false where id = :id")
+ suspend fun markInactive(id: Long)
+
+ @Query("update otp set is_verified = true, is_active = false, verify_time = current_timestamp where id = :id")
+ suspend fun markVerified(id: Long)
+
+}
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/repository/TOTPConfigRepository.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/repository/TOTPConfigRepository.kt
new file mode 100644
index 000000000..280827b3f
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/repository/TOTPConfigRepository.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.otp.app.repository
+
+import co.nilin.opex.otp.app.model.TOTPConfig
+import org.springframework.data.r2dbc.repository.Query
+import org.springframework.data.repository.kotlin.CoroutineCrudRepository
+import org.springframework.stereotype.Repository
+
+@Repository
+interface TOTPConfigRepository : CoroutineCrudRepository {
+
+ @Query("select * from totp_config limit 1")
+ suspend fun findOne(): TOTPConfig
+}
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/repository/TOTPRepository.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/repository/TOTPRepository.kt
new file mode 100644
index 000000000..521302e22
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/repository/TOTPRepository.kt
@@ -0,0 +1,15 @@
+package co.nilin.opex.otp.app.repository
+
+import co.nilin.opex.otp.app.model.TOTP
+import org.springframework.data.r2dbc.repository.Query
+import org.springframework.data.repository.kotlin.CoroutineCrudRepository
+import org.springframework.stereotype.Repository
+
+@Repository
+interface TOTPRepository : CoroutineCrudRepository {
+
+ suspend fun findByUserId(userId: String): TOTP?
+
+ @Query("update totp set is_activated = true where id = :id")
+ suspend fun markActivated(id:Long)
+}
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/OTPService.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/OTPService.kt
new file mode 100644
index 000000000..fcc8383ef
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/OTPService.kt
@@ -0,0 +1,177 @@
+package co.nilin.opex.otp.app.service
+
+import co.nilin.opex.common.OpexError
+import co.nilin.opex.common.utils.LoggerDelegate
+import co.nilin.opex.otp.app.data.OTPCode
+import co.nilin.opex.otp.app.data.OTPReceiver
+import co.nilin.opex.otp.app.data.OTPResultType
+import co.nilin.opex.otp.app.model.OTP
+import co.nilin.opex.otp.app.model.OTPConfig
+import co.nilin.opex.otp.app.model.OTPType
+import co.nilin.opex.otp.app.repository.OTPConfigRepository
+import co.nilin.opex.otp.app.repository.OTPRepository
+import co.nilin.opex.otp.app.service.message.MessageManager
+import kotlinx.coroutines.reactor.awaitSingleOrNull
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
+import org.springframework.stereotype.Service
+import java.time.LocalDateTime
+import java.util.UUID
+import kotlin.math.pow
+import kotlin.random.Random
+
+@Service
+class OTPService(
+ private val repository: OTPRepository,
+ private val configRepository: OTPConfigRepository,
+ private val messageManager: MessageManager,
+ private val encoder: BCryptPasswordEncoder
+) {
+
+ private val logger by LoggerDelegate()
+
+ suspend fun requestOTP(receiver: String, type: OTPType, userId: String, action: String?): String {
+ checkActiveOTP(receiver, type, userId)
+ val config = getConfig(type)
+ val code = generateCode(config.charCount, config.includeAlphabetChars)
+ messageManager.sendMessage(config, type, code, receiver)
+ storeOTP(receiver, type, code, config, userId, action)
+ return code
+ }
+
+ suspend fun requestCompositeOTP(receivers: Set, userId: String, action: String?): String {
+ val type = OTPType.COMPOSITE
+ val mainConfig = getConfig(type)
+ val receiver = receivers.joinToString(",") { it.receiver }
+ checkActiveOTP(receiver, type, userId)
+
+ val compositeCode = StringBuilder()
+ receivers.forEach {
+ val config = getConfig(it.type)
+ val code = generateCode(config.charCount, config.includeAlphabetChars)
+ messageManager.sendMessage(config, type, code, receiver)
+ compositeCode.append(code)
+ }
+
+ storeOTP(receiver, type, compositeCode.toString(), mainConfig, userId, action)
+ return compositeCode.toString()
+ }
+
+ private suspend fun storeOTP(
+ receiver: String,
+ type: OTPType,
+ code: String,
+ config: OTPConfig,
+ userId: String,
+ action: String?
+ ): String {
+ val expireTime = LocalDateTime.now().plusSeconds(config.expireTimeSeconds.toLong())
+ val newOtp = OTP(
+ code.encode(),
+ receiver,
+ userId,
+ action ?: "UNSPECIFIED",
+ UUID.randomUUID().toString(),
+ type,
+ expireTime,
+ )
+
+ logger.debug("${newOtp.tracingCode} -> $code")
+ val otp = repository.save(newOtp)
+ return otp.tracingCode
+ }
+
+ private suspend fun checkActiveOTP(receiver: String, type: OTPType, userId: String) {
+ // Check whether receiver has an active otp of specified type
+ repository.findActiveByReceiverAndTypeOrUserId(receiver, type, userId)?.let {
+ if (it.isExpired())
+ repository.markInactive(it.id!!)
+ else
+ throw OpexError.OTPAlreadyRequested.exception()
+ }
+ }
+
+ private suspend fun getConfig(type: OTPType): OTPConfig {
+ return configRepository.findById(type).awaitSingleOrNull()?.apply {
+ if (!isEnabled)
+ throw OpexError.OTPDisabled.exception()
+ } ?: throw OpexError.OTPConfigNotFound.exception()
+ }
+
+ suspend fun verifyOTP(code: String, userId: String): OTPResultType {
+ val otp = repository.findActiveByUserId(userId)
+ return verifyOtp(code, otp, userId)
+ }
+
+ suspend fun verifyCompositeOTP(codes: Set, userId: String): OTPResultType {
+ repository.findActiveByUserId(userId)?.let {
+ if (it.type != OTPType.COMPOSITE)
+ throw OpexError.BadRequest.exception()
+
+ val code = reconstructCode(codes)
+ return verifyOtp(code, it, userId)
+ }
+ return OTPResultType.INVALID
+ }
+
+ @Deprecated("Use userId instead")
+ suspend fun verifyOTP(code: String, userId: String, tracingCode: String?): OTPResultType {
+ val otp = repository.findByTracingCode(tracingCode!!)
+ return verifyOtp(code, otp, userId)
+ }
+
+ @Deprecated("Use userId instead")
+ suspend fun verifyCompositeOTP(codes: Set, userId: String, tracingCode: String?): OTPResultType {
+ repository.findByTracingCode(tracingCode!!)?.let {
+ if (it.type != OTPType.COMPOSITE)
+ throw OpexError.BadRequest.exception()
+
+ val code = reconstructCode(codes)
+ return verifyOtp(code, it, userId)
+ }
+ return OTPResultType.INVALID
+ }
+
+ private suspend fun reconstructCode(codes: Set): String {
+ return codes.sortedBy { it.type.compositeOrder }.joinToString("") { it.code }
+ }
+
+ private suspend fun verifyOtp(code: String, otp: OTP?, userId: String): OTPResultType {
+ if (otp == null) {
+ logger.warn("Otp request not found")
+ return OTPResultType.INVALID
+ }
+
+ if (otp.userId != userId) {
+ logger.warn("Otp userId mismatch")
+ return OTPResultType.INVALID
+ }
+
+ if (otp.isExpired()) {
+ logger.warn("Otp request expired. tracingCode: ${otp.tracingCode}")
+ return OTPResultType.EXPIRED
+ }
+
+ if (!encoder.matches(code, otp.code)) {
+ logger.warn("Otp request invalid")
+ return OTPResultType.INCORRECT
+ }
+
+ repository.markVerified(otp.id!!)
+ return OTPResultType.VALID
+ }
+
+ private suspend fun generateCode(length: Int): String {
+ val min = 10.0.pow(length - 1).toInt()
+ val max = 10.0.pow(length).toInt() - 1
+ return Random.nextInt(min, max + 1).toString()
+ }
+
+ private fun generateCode(length: Int, includeAlpha: Boolean): String {
+ val chars = if (includeAlpha) ('A'..'Z') + ('a'..'z') + ('0'..'9') else ('0'..'9').toList()
+ return (1..length).map { chars.random() }.joinToString("")
+ }
+
+ private fun String.encode(): String {
+ return encoder.encode(this)
+ }
+}
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/TOTPService.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/TOTPService.kt
new file mode 100644
index 000000000..7ee56a0eb
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/TOTPService.kt
@@ -0,0 +1,89 @@
+package co.nilin.opex.otp.app.service
+
+import co.nilin.opex.common.OpexError
+import co.nilin.opex.otp.app.model.TOTP
+import co.nilin.opex.otp.app.repository.TOTPConfigRepository
+import co.nilin.opex.otp.app.repository.TOTPRepository
+import dev.samstevens.totp.code.DefaultCodeGenerator
+import dev.samstevens.totp.code.DefaultCodeVerifier
+import dev.samstevens.totp.code.HashingAlgorithm
+import dev.samstevens.totp.qr.QrData
+import dev.samstevens.totp.secret.DefaultSecretGenerator
+import dev.samstevens.totp.time.SystemTimeProvider
+import org.springframework.stereotype.Service
+
+@Service
+class TOTPService(
+ private val repository: TOTPRepository,
+ private val configRepository: TOTPConfigRepository
+) {
+
+ suspend fun setupTOTP(userId: String, label: String? = null): String {
+ val config = configRepository.findOne()
+ repository.findByUserId(userId)?.let { throw OpexError.TOTPAlreadyRegistered.exception() }
+ val secret = generateSecret()
+ val uri = generateUri(userId, config.issuer, secret, label)
+ repository.save(TOTP(userId, secret, label))
+ return uri
+ }
+
+ suspend fun verifyAndMarkActivated(userId: String, code: String) {
+ val totp = repository.findByUserId(userId) ?: throw OpexError.TOTPNotFound.exception()
+ val isValid = isCodeValid(totp.secret, code.trim())
+ if (isValid)
+ repository.markActivated(totp.id!!)
+ else
+ throw OpexError.InvalidTOTPCode.exception()
+ }
+
+ suspend fun verifyTOTP(userId: String, code: String): Boolean {
+ val totp = repository.findByUserId(userId) ?: throw OpexError.TOTPNotFound.exception()
+ if (!totp.isActivated) throw OpexError.TOTPSetupIncomplete.exception()
+ return isCodeValid(totp.secret, code.trim())
+ }
+
+ suspend fun removeTOTP(userId: String, code: String) {
+ val totp = repository.findByUserId(userId) ?: throw OpexError.TOTPNotFound.exception()
+ if (!totp.isActivated)
+ repository.deleteById(totp.id!!)
+ else {
+ val isValid = isCodeValid(totp.secret, code.trim())
+ if (isValid)
+ repository.deleteById(totp.id!!)
+ else
+ throw OpexError.InvalidTOTPCode.exception()
+ }
+ }
+
+ suspend fun findTOTP(userId: String): TOTP? {
+ return repository.findByUserId(userId)
+ }
+
+ private suspend fun generateSecret(): String {
+ val config = configRepository.findOne()
+ val generator = DefaultSecretGenerator(config.secretChars)
+ return generator.generate()
+ }
+
+ private fun generateUri(userId: String, issuer: String, secret: String, label: String? = null): String {
+ val data = QrData.Builder()
+ .label(label ?: userId)
+ .secret(secret)
+ .issuer(issuer)
+ .algorithm(HashingAlgorithm.SHA1)
+ .digits(6)
+ .period(30)
+ .build()
+ return data.uri
+ }
+
+ private fun isCodeValid(secret: String, code: String): Boolean {
+ val timeProvider = SystemTimeProvider()
+ val generator = DefaultCodeGenerator(HashingAlgorithm.SHA1, 6)
+ val verifier = DefaultCodeVerifier(generator, timeProvider).apply {
+ setTimePeriod(30)
+ setAllowedTimePeriodDiscrepancy(3)
+ }
+ return verifier.isValidCode(secret, code)
+ }
+}
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/message/EmailSender.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/message/EmailSender.kt
new file mode 100644
index 000000000..4ec465a2f
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/message/EmailSender.kt
@@ -0,0 +1,56 @@
+package co.nilin.opex.otp.app.service.message
+
+import co.nilin.opex.common.utils.LoggerDelegate
+import jakarta.mail.Authenticator
+import jakarta.mail.Message
+import jakarta.mail.PasswordAuthentication
+import jakarta.mail.Session
+import jakarta.mail.Transport
+import jakarta.mail.internet.InternetAddress
+import jakarta.mail.internet.MimeMessage
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.stereotype.Component
+import java.util.*
+
+@Component
+class EmailSender(
+ @Value("\${otp.email.host}")
+ private val host: String,
+ @Value("\${otp.email.username}")
+ private val username: String,
+ @Value("\${otp.email.password}")
+ private val password: String
+) : MessageSender {
+
+ private val logger by LoggerDelegate()
+
+ override suspend fun send(receiver: String, message: String, metadata: Map): Boolean {
+ val subject = "Your otp code"
+ val props = Properties().apply {
+ put("mail.smtp.host", host)
+ put("mail.smtp.port", "2525")
+ put("mail.smtp.auth", "true")
+ put("mail.smtp.username", username)
+ }
+
+ val auth = object : Authenticator() {
+ override fun getPasswordAuthentication() = PasswordAuthentication(username, password)
+ }
+ val session = Session.getInstance(props, auth)
+ return try {
+ val emailMessage = MimeMessage(session).apply {
+ setFrom(InternetAddress(username))
+ setSubject(subject)
+ setRecipient(Message.RecipientType.TO, InternetAddress(receiver))
+ setContent(message, "text/html; charset=utf-8")
+ }
+ Transport.send(emailMessage)
+ logger.info("Successfully sent email message")
+ true
+ } catch (e: Exception) {
+ logger.error("Failed to send email message", e)
+ false
+ }
+ }
+}
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/message/MessageManager.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/message/MessageManager.kt
new file mode 100644
index 000000000..00a0f5869
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/message/MessageManager.kt
@@ -0,0 +1,35 @@
+package co.nilin.opex.otp.app.service.message
+
+import co.nilin.opex.common.OpexError
+import co.nilin.opex.common.utils.LoggerDelegate
+import co.nilin.opex.otp.app.model.OTPConfig
+import co.nilin.opex.otp.app.model.OTPType
+import org.springframework.stereotype.Component
+
+@Component
+class MessageManager(
+ private val smsSender: SMSSender,
+ private val emailSender: EmailSender
+) {
+
+ private val logger by LoggerDelegate()
+
+ suspend fun sendMessage(config: OTPConfig, otpType: OTPType, code: String, receiver: String) {
+ val message = String.format(config.messageTemplate, code)
+ if (config.isActivated) {
+ val result = getSender(otpType).send(receiver, message)
+ if (!result)
+ throw OpexError.UnableToSendOTP.exception()
+ } else {
+ logger.warn("OTP for type ${otpType.name} is not activated. Message will not be sent. $message -> $receiver")
+ }
+ }
+
+ suspend fun getSender(type: OTPType): MessageSender {
+ return when (type) {
+ OTPType.SMS -> smsSender
+ OTPType.EMAIL -> emailSender
+ OTPType.COMPOSITE -> throw IllegalStateException("Composite sender not supported")
+ }
+ }
+}
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/message/MessageSender.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/message/MessageSender.kt
new file mode 100644
index 000000000..2fc474f40
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/message/MessageSender.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.otp.app.service.message
+
+interface MessageSender {
+
+ suspend fun send(receiver: String, message: String, metadata: Map = emptyMap()): Boolean
+}
\ No newline at end of file
diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/message/SMSSender.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/message/SMSSender.kt
new file mode 100644
index 000000000..9132f1527
--- /dev/null
+++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/message/SMSSender.kt
@@ -0,0 +1,12 @@
+package co.nilin.opex.otp.app.service.message
+
+import co.nilin.opex.otp.app.proxy.KaveNegarProxy
+import org.springframework.stereotype.Component
+
+@Component
+class SMSSender(private val smsProxy: KaveNegarProxy) : MessageSender {
+
+ override suspend fun send(receiver: String, message: String, metadata: Map): Boolean {
+ return smsProxy.send(receiver, message)
+ }
+}
\ No newline at end of file
diff --git a/otp/otp-app/src/main/resources/application.yml b/otp/otp-app/src/main/resources/application.yml
new file mode 100644
index 000000000..5a7a1e9bc
--- /dev/null
+++ b/otp/otp-app/src/main/resources/application.yml
@@ -0,0 +1,49 @@
+server:
+ port: 8080
+spring:
+ application:
+ name: opex-otp
+ r2dbc:
+ url: r2dbc:postgresql://${DB_IP_PORT:localhost}/opex
+ username: ${DB_USER}
+ password: ${DB_PASS}
+ initialization-mode: always
+ cloud:
+ bootstrap:
+ enabled: true
+ consul:
+ host: ${CONSUL_HOST:localhost}
+ port: 8500
+ discovery:
+ #healthCheckPath: ${management.context-path}/health
+ instance-id: ${spring.application.name}:${server.port}
+ healthCheckInterval: 20s
+ prefer-ip-address: true
+management:
+ endpoints:
+ web:
+ base-path: /actuator
+ exposure:
+ include: [ "health", "prometheus", "metrics", "logging" ]
+ endpoint:
+ health:
+ show-details: when_authorized
+ metrics:
+ enabled: true
+ prometheus:
+ enabled: true
+logging:
+ level:
+ co.nilin: INFO
+ # org.zalando.logbook: TRACE
+app:
+ auth:
+ cert-url: http://keycloak:8080/realms/opex/protocol/openid-connect/certs
+otp:
+ sms:
+ provider:
+ api-key: ${SMS_PROVIDER_API_KEY}
+ email:
+ host: ${SMTP_HOST}
+ username: ${SMTP_USER}
+ password: ${SMTP_PASS}
diff --git a/otp/otp-app/src/main/resources/schema.sql b/otp/otp-app/src/main/resources/schema.sql
new file mode 100644
index 000000000..30589c13c
--- /dev/null
+++ b/otp/otp-app/src/main/resources/schema.sql
@@ -0,0 +1,62 @@
+create table if not exists otp
+(
+ id serial primary key,
+ code text not null,
+ receiver text not null,
+ user_id text not null,
+ action text not null,
+ tracing_code text not null unique,
+ type varchar(16) not null,
+ expires_at timestamp not null,
+ request_date timestamp not null default current_timestamp,
+ is_verified boolean not null default false,
+ is_active boolean not null default true,
+ verify_time timestamp
+);
+
+create table if not exists otp_config
+(
+ type varchar(16) primary key,
+ expire_time_seconds integer not null default 60,
+ char_count integer not null default 6,
+ include_alphabet_chars boolean not null default false,
+ is_enabled boolean not null default true,
+ is_activated boolean not null default false,
+ message_template text not null default '%s',
+ check (char_count between 4 and 100)
+);
+
+create table if not exists totp
+(
+ id serial primary key,
+ user_id text not null unique,
+ secret text not null unique,
+ label text,
+ is_enabled boolean not null default true,
+ is_activated boolean not null default false,
+ created_at timestamp not null default current_timestamp
+);
+
+create table if not exists totp_config
+(
+ id boolean primary key default true,
+ secret_chars int not null default 64,
+ issuer text not null,
+ constraint id check (id is true)
+);
+
+insert into otp_config
+values ('EMAIL', 60, 8, true)
+on conflict do nothing;
+
+insert into otp_config
+values ('SMS')
+on conflict do nothing;
+
+insert into otp_config
+values ('COMPOSITE', 120)
+on conflict do nothing;
+
+insert into totp_config
+values (true, 128, 'Opex')
+on conflict do nothing;
\ No newline at end of file
diff --git a/otp/pom.xml b/otp/pom.xml
new file mode 100644
index 000000000..0266dd847
--- /dev/null
+++ b/otp/pom.xml
@@ -0,0 +1,52 @@
+
+
+ 4.0.0
+
+
+ core
+ co.nilin.opex
+ 1.0.1-beta.7
+
+
+ co.nilin.opex.matching.otp
+ otp
+ otp
+ pom
+ OTP root of Opex
+
+
+ otp-app
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+
+
+ co.nilin.opex
+ common
+
+
+ org.zalando
+ logbook-spring-boot-webflux-autoconfigure
+ 3.9.0
+
+
+
+
+
+
+ co.nilin.opex.utility
+ error-handler
+ ${error-hanlder.version}
+
+
+ co.nilin.opex.utility
+ interceptors
+ ${interceptor.version}
+
+
+
+
diff --git a/pom.xml b/pom.xml
index 1f7431fd8..a680813c7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,7 +17,6 @@
2.7.6
2021.0.5
1.0.1
- 1.0.0
1.0.0
true
@@ -32,7 +31,10 @@
user-management
wallet
bc-gateway
+ otp
common
+ auth-gateway
+ profile
diff --git a/preferences-demo.yml b/preferences-demo.yml
deleted file mode 100644
index 825039c7c..000000000
--- a/preferences-demo.yml
+++ /dev/null
@@ -1,237 +0,0 @@
-addressTypes:
- - addressType: bitcoin
- addressRegex: "*"
- - addressType: ethereum
- addressRegex: "*"
-chains:
- - name: bitcoin
- addressType: bitcoin
- scanners:
- - url: http://bitcoin-scanner:8080
- maxBlockRange: 30
- delayOnRateLimit: 300
- schedule:
- delay: 600
- errorDelay: 60
- timeout: 30
- maxRetries: 5
- confirmations: 0
- enabled: false
- - name: ethereum
- addressType: ethereum
- scanners:
- - url: http://ethereum-scanner:8080
- maxBlockRange: 30
- delayOnRateLimit: 300
- schedule:
- delay: 15
- errorDelay: 7
- timeout: 30
- maxRetries: 5
- confirmations: 0
- enabled: false
- - name: bsc
- addressType: ethereum
- scanners:
- - url: http://bsc-scanner:8080
- maxBlockRange: 30
- delayOnRateLimit: 300
- schedule:
- delay: 6
- errorDelay: 3
- timeout: 30
- maxRetries: 5
- confirmations: 0
-currencies:
- - symbol: BTC
- name: Bitcoin
- precision: 0.000001
- mainBalance: 10000
- dailyTotal: 1000
- dailyCount: 100
- monthlyTotal: 30000
- monthlyCount: 3000
- implementations:
- - chain: bitcoin
- withdrawFee: 0.0001
- withdrawMin: 0.0001
- decimal: 0
- - symbol: ETH
- name: Ethereum
- precision: 0.000001
- mainBalance: 10000
- dailyTotal: 1000
- dailyCount: 100
- monthlyTotal: 30000
- monthlyCount: 3000
- implementations:
- - chain: ethereum
- withdrawFee: 0.00001
- withdrawMin: 0.000001
- decimal: 18
- - symbol: BNB
- name: Binance
- precision: 0.0001
- mainBalance: 10000
- dailyTotal: 1000
- dailyCount: 100
- monthlyTotal: 30000
- monthlyCount: 3000
- implementations:
- - chain: bsc
- withdrawFee: 0.00001
- withdrawMin: 0.000001
- decimal: 18
- - chain: bsc
- symbol: WBNB
- token: true
- tokenAddress: 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c
- tokenName: Wrapped BNB
- withdrawFee: 0.01
- withdrawMin: 0.01
- decimal: 18
- - symbol: BUSD
- name: Binance USD
- precision: 0.01
- mainBalance: 10000
- dailyTotal: 1000
- dailyCount: 100
- monthlyTotal: 30000
- monthlyCount: 3000
- implementations:
- - chain: bsc
- token: true
- tokenAddress: 0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56
- tokenName: BUSD Token
- withdrawFee: 0.01
- withdrawMin: 0.01
- decimal: 18
- - symbol: IRT
- name: Toman
- precision: 0.1
- mainBalance: 10000
- dailyTotal: 1000
- dailyCount: 100
- monthlyTotal: 30000
- monthlyCount: 3000
-markets:
- - leftSide: BTC
- rightSide: BUSD
- aliases:
- - key: binance
- alias: BTCBUSD
- feeConfigs:
- - direction: ASK
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - direction: BID
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - leftSide: ETH
- rightSide: BUSD
- aliases:
- - key: binance
- alias: ETHBUSD
- feeConfigs:
- - direction: ASK
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - direction: BID
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - leftSide: BNB
- rightSide: BUSD
- aliases:
- - key: binance
- alias: BNBBUSD
- feeConfigs:
- - direction: ASK
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - direction: BID
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - leftSide: BTC
- rightSide: IRT
- aliases:
- - key: binance
- alias: BTCIRT
- feeConfigs:
- - direction: ASK
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - direction: BID
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - leftSide: ETH
- rightSide: IRT
- aliases:
- - key: binance
- alias: ETHIRT
- feeConfigs:
- - direction: ASK
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - direction: BID
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - leftSide: BNB
- rightSide: IRT
- aliases:
- - key: binance
- alias: BNBIRT
- feeConfigs:
- - direction: ASK
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - direction: BID
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - leftSide: BUSD
- rightSide: IRT
- aliases:
- - key: binance
- alias: BUSDIRT
- feeConfigs:
- - direction: ASK
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - direction: BID
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
-userLimits:
- - owner: 1
- action: withdraw
- walletType: main
- withdrawFee: 0.0001
- dailyTotal: 1000
- dailyCount: 100
- monthlyTotal: 30000
- monthlyCount: 3000
-system:
- walletTitle: system
- walletLevel: system
-admin:
- walletTitle: admin
- walletLevel: admin
-userLevels:
- - "*"
- - "nofee"
-auth:
- whitelist:
- enabled: false
- file: /whitelist.txt
diff --git a/preferences-dev.yml b/preferences-dev.yml
deleted file mode 100644
index 920f89144..000000000
--- a/preferences-dev.yml
+++ /dev/null
@@ -1,412 +0,0 @@
-addressTypes:
- - addressType: ethereum
- addressRegex: "*"
- - addressType: test-bitcoin
- addressRegex: "*"
-chains:
- - name: test-bitcoin
- addressType: test-bitcoin
- scanners:
- - url: http://bitcoin-scanner:8080
- maxBlockRange: 30
- delayOnRateLimit: 5
- maxParallelCall: 2
- schedules:
- - workerType: MAIN
- delay: 600
- timeout: 30
- maxRetries: 5
- confirmations: 0
- maxBlockCount: 4
- enabled: false
- - workerType: ERROR
- delay: 600
- timeout: 30
- maxRetries: 5
- confirmations: 0
- maxBlockCount: 4
- enabled: false
- - workerType: DELAYED
- delay: 300
- timeout: 30
- maxRetries: 5
- confirmations: 0
- maxBlockCount: 2
- enabled: false
- - name: test-ethereum
- addressType: ethereum
- scanners:
- - url: http://ethereum-scanner:8080
- maxBlockRange: 30
- delayOnRateLimit: 5
- maxParallelCall: 3
- schedules:
- - workerType: MAIN
- delay: 15
- timeout: 30
- maxRetries: 5
- confirmations: 0
- maxBlockCount: 10
- enabled: false
- - workerType: ERROR
- delay: 7
- timeout: 30
- maxRetries: 5
- confirmations: 0
- maxBlockCount: 10
- enabled: false
- - workerType: DELAYED
- delay: 15
- timeout: 30
- maxRetries: 5
- confirmations: 0
- maxBlockCount: 5
- enabled: false
- - name: test-bsc
- addressType: ethereum
- scanners:
- - url: http://bsc-scanner:8080
- maxBlockRange: 10
- delayOnRateLimit: 300
- maxParallelCall: 5
- schedules:
- - workerType: MAIN
- delay: 6
- timeout: 30
- maxRetries: 5
- confirmations: 0
- maxBlockCount: 30
- - workerType: ERROR
- delay: 3
- timeout: 30
- maxRetries: 5
- confirmations: 0
- maxBlockCount: 20
- - workerType: DELAYED
- delay: 10
- timeout: 30
- maxRetries: 5
- confirmations: 0
- maxBlockCount: 10
-currencies:
- - symbol: IRT
- name: Toman
- precision: 0.1
- mainBalance: 10000
- dailyTotal: 1000
- dailyCount: 100
- monthlyTotal: 30000
- monthlyCount: 3000
- gift: 1000000
- - symbol: BTC
- name: Bitcoin (Test)
- precision: 0.000001
- mainBalance: 10000
- dailyTotal: 1000
- dailyCount: 100
- monthlyTotal: 30000
- monthlyCount: 3000
- implementations:
- - chain: test-bitcoin
- withdrawFee: 0.0001
- withdrawMin: 0.0001
- decimal: 0
- gift: 5
- - symbol: ETH
- name: Ethereum (Test)
- precision: 0.000001
- mainBalance: 10000
- dailyTotal: 1000
- dailyCount: 100
- monthlyTotal: 30000
- monthlyCount: 3000
- implementations:
- - chain: test-ethereum
- withdrawFee: 0.00001
- withdrawMin: 0.000001
- decimal: 18
- gift: 100
- - symbol: USDT
- name: Tether (Test)
- precision: 0.01
- mainBalance: 10000
- dailyTotal: 1000
- dailyCount: 100
- monthlyTotal: 30000
- monthlyCount: 3000
- implementations:
- - chain: test-ethereum
- token: true
- tokenAddress: 0x110a13FC3efE6A245B50102D2d79B3E76125Ae83
- tokenName: Tether USD
- withdrawFee: 0.01
- withdrawMin: 0.01
- decimal: 6
- gift: 1000000
- - symbol: BUSD
- name: Binance USD (Test)
- precision: 0.01
- mainBalance: 10000
- dailyTotal: 1000
- dailyCount: 100
- monthlyTotal: 30000
- monthlyCount: 3000
- implementations:
- - chain: test-bsc
- token: true
- tokenAddress: 0xeD24FC36d5Ee211Ea25A80239Fb8C4Cfd80f12Ee
- tokenName: BUSD Token
- withdrawFee: 0.01
- withdrawMin: 0.01
- decimal: 18
- gift: 1000000
- - symbol: BNB
- name: Binance (Test)
- precision: 0.0001
- mainBalance: 10000
- dailyTotal: 1000
- dailyCount: 100
- monthlyTotal: 30000
- monthlyCount: 3000
- implementations:
- - chain: test-bsc
- withdrawFee: 0.00001
- withdrawMin: 0.000001
- decimal: 18
- - chain: test-bsc
- symbol: WBNB
- token: true
- tokenAddress: 0x5b3e2bc1da86ff6235d9ead4504d598cae77dbcb
- tokenName: Wrapped BNB (Test Net)
- withdrawFee: 0.01
- withdrawMin: 0.01
- decimal: 18
- gift: 2000
- - symbol: SOL
- name: Solana
- precision: 0.00001
- mainBalance: 100000
- dailyTotal: 10000
- dailyCount: 1000
- monthlyTotal: 300000
- monthlyCount: 30000
- implementations:
- - chain: test-bsc
- token: true
- tokenAddress: 0x570A5D26f7765Ecb712C0924E4De545B89fD43dF
- tokenName: Wrapped Solana
- withdrawFee: 0.0001
- withdrawMin: 0.001
- decimal: 18
- gift: 100
- - symbol: DOGE
- name: Dogecoin
- precision: 0.001
- mainBalance: 10000000
- dailyTotal: 1000000
- dailyCount: 100000
- monthlyTotal: 30000000
- monthlyCount: 3000000
- implementations:
- - chain: test-bsc
- token: true
- tokenAddress: 0xbA2aE424d960c26247Dd6c32edC70B295c744C43
- tokenName: Binance-Peg Dogecoin
- withdrawFee: 2
- withdrawMin: 10
- decimal: 8
- gift: 100
- - symbol: TON
- name: Toncoin
- precision: 0.0001
- mainBalance: 1000000
- dailyTotal: 100000
- dailyCount: 10000
- monthlyTotal: 3000000
- monthlyCount: 300000
- implementations:
- - chain: test-ethereum
- token: true
- tokenAddress: 0x582d872a1b094fc48f5de31d3b73f2d9be47def1
- tokenName: Wrapped Ton Coin
- withdrawFee: 0.3
- withdrawMin: 1
- decimal: 9
- gift: 100
-markets:
- - leftSide: BTC
- rightSide: USDT
- aliases:
- - key: binance
- alias: BTCUSDT
- feeConfigs:
- - direction: ASK
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - direction: BID
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - leftSide: ETH
- rightSide: USDT
- aliases:
- - key: binance
- alias: ETHUSDT
- feeConfigs:
- - direction: ASK
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - direction: BID
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - leftSide: BTC
- rightSide: IRT
- aliases:
- - key: binance
- alias: BTCIRT
- feeConfigs:
- - direction: ASK
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - direction: BID
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - leftSide: ETH
- rightSide: IRT
- aliases:
- - key: binance
- alias: ETHIRT
- feeConfigs:
- - direction: ASK
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - direction: BID
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - leftSide: USDT
- rightSide: IRT
- aliases:
- - key: binance
- alias: USDIRT
- feeConfigs:
- - direction: ASK
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - direction: BID
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - leftSide: ETH
- rightSide: BUSD
- aliases:
- - key: binance
- alias: ETHBUSD
- feeConfigs:
- - direction: ASK
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - direction: BID
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - leftSide: BTC
- rightSide: BUSD
- aliases:
- - key: binance
- alias: BTCBUSD
- feeConfigs:
- - direction: ASK
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - direction: BID
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - leftSide: BNB
- rightSide: BUSD
- aliases:
- - key: binance
- alias: BNBBUSD
- feeConfigs:
- - direction: ASK
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - direction: BID
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - leftSide: SOL
- rightSide: USDT
- aliases:
- - key: binance
- alias: SOLUSDT
- feeConfigs:
- - direction: ASK
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - direction: BID
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - leftSide: DOGE
- rightSide: USDT
- aliases:
- - key: binance
- alias: DOGEUSDT
- feeConfigs:
- - direction: ASK
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - direction: BID
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - leftSide: TON
- rightSide: USDT
- aliases:
- - key: binance
- alias: TONUSDT
- feeConfigs:
- - direction: ASK
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
- - direction: BID
- userLevel: "*"
- makerFee: 0.01
- takerFee: 0.01
-userLimits:
- - owner: 1
- action: withdraw
- walletType: main
- withdrawFee: 0.0001
- dailyTotal: 1000
- dailyCount: 100
- monthlyTotal: 30000
- monthlyCount: 3000
-userLevels:
- - "*"
- - "nofee"
-system:
- walletTitle: system
- walletLevel: system
-admin:
- walletTitle: admin
- walletLevel: admin
-auth:
- whitelist:
- enabled: false
- file: /whitelist.txt
\ No newline at end of file
diff --git a/profile/pom.xml b/profile/pom.xml
new file mode 100644
index 000000000..a9bbbabc8
--- /dev/null
+++ b/profile/pom.xml
@@ -0,0 +1,102 @@
+
+
+
+ core
+ co.nilin.opex
+ 1.0.1-beta.7
+
+
+ 4.0.0
+ profile
+ pom
+
+
+ 17
+ 17
+ UTF-8
+
+
+
+ profile-core
+ profile-app
+ profile-ports/profile-postgres
+ profile-ports/profile-eventlistener-kafka
+ profile-ports/profile-submitter-kafka
+ profile-ports/profile-kyc-proxy
+ profile-ports/profile-shahkar-proxy
+
+
+
+
+
+ co.nilin.opex.profile
+ profile-app
+ ${project.version}
+
+
+ co.nilin.opex.profile
+ profile-core
+ ${project.version}
+
+
+ co.nilin.opex.profile.ports
+ profile-eventlistener-kafka
+ ${project.version}
+
+
+ co.nilin.opex.profile.ports
+ profile-submitter-kafka
+ ${project.version}
+
+
+ co.nilin.opex.profile.ports
+ profile-postgres
+ ${project.version}
+
+
+ co.nilin.opex.admin.gateway
+ admin-app
+ ${project.version}
+
+
+ co.nilin.opex.utility
+ logging-handler
+ ${logging-handler.version}
+
+
+ co.nilin.opex.utility
+ interceptors
+ ${interceptor.version}
+
+
+ co.nilin.opex.profile.ports
+ profile-kyc-proxy
+ ${project.version}
+
+
+ co.nilin.opex.profile.ports
+ profile-shahkar-proxy
+ ${project.version}
+
+
+
+
+
+
+
+ com.google.code.gson
+ gson
+ 2.10.1
+
+
+ co.nilin.opex
+ common
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+
+
+
\ No newline at end of file
diff --git a/profile/profile-app/.gitignore b/profile/profile-app/.gitignore
new file mode 100644
index 000000000..549e00a2a
--- /dev/null
+++ b/profile/profile-app/.gitignore
@@ -0,0 +1,33 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
diff --git a/profile/profile-app/Dockerfile b/profile/profile-app/Dockerfile
new file mode 100644
index 000000000..9c566c1b2
--- /dev/null
+++ b/profile/profile-app/Dockerfile
@@ -0,0 +1,5 @@
+FROM openjdk:11
+ARG JAR_FILE=target/*.jar
+COPY ${JAR_FILE} app.jar
+ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"]
+HEALTHCHECK --interval=45s --start-period=30s --retries=5 CMD curl -sf 'http://localhost:8080/actuator/health' >/dev/null || exit 1
diff --git a/profile/profile-app/pom.xml b/profile/profile-app/pom.xml
new file mode 100644
index 000000000..d5e0d7e3c
--- /dev/null
+++ b/profile/profile-app/pom.xml
@@ -0,0 +1,155 @@
+
+
+ 4.0.0
+
+ co.nilin.opex
+ profile
+ 1.0.1-beta.7
+
+ co.nilin.opex.profile
+ profile-app
+ profile-app
+ profile-app
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+ com.fasterxml.jackson.module
+ jackson-module-kotlin
+
+
+ io.projectreactor.kotlin
+ reactor-kotlin-extensions
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-reactor
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-core
+
+
+ io.projectreactor
+ reactor-test
+ test
+
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
+
+
+ com.nimbusds
+ nimbus-jose-jwt
+ 9.22
+
+
+ co.nilin.opex.profile
+ profile-core
+
+
+ co.nilin.opex.profile.ports
+ profile-postgres
+
+
+ co.nilin.opex.profile.ports
+ profile-eventlistener-kafka
+
+
+ co.nilin.opex.profile.ports
+ profile-submitter-kafka
+
+
+ org.springframework
+ spring-tx
+ provided
+
+
+ org.springframework.cloud
+ spring-cloud-starter-vault-config
+
+
+ co.nilin.opex.utility
+ interceptors
+
+
+ org.springframework.cloud
+ spring-cloud-starter-consul-all
+
+
+ io.micrometer
+ micrometer-registry-prometheus
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-dependencies
+ ${spring-cloud.version}
+ pom
+ import
+
+
+
+
+ ${project.basedir}/src/main/kotlin
+ ${project.basedir}/src/test/kotlin
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+
+
+ -Xjsr305=strict
+
+
+ spring
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-allopen
+ ${kotlin.version}
+
+
+
+
+
+
+
diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/ProfileApp.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/ProfileApp.kt
new file mode 100644
index 000000000..be29efc53
--- /dev/null
+++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/ProfileApp.kt
@@ -0,0 +1,15 @@
+package co.nilin.opex.profile.app
+
+import co.nilin.opex.utility.error.EnableOpexErrorHandler
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+import org.springframework.context.annotation.ComponentScan
+
+@SpringBootApplication
+@ComponentScan("co.nilin.opex")
+@EnableOpexErrorHandler
+class ProfileApp
+
+fun main(args: Array) {
+ runApplication(*args)
+}
diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/AppConfig.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/AppConfig.kt
new file mode 100644
index 000000000..f56c6ca80
--- /dev/null
+++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/AppConfig.kt
@@ -0,0 +1,28 @@
+package co.nilin.opex.profile.app.config
+
+import co.nilin.opex.profile.core.spi.KycLevelUpdatedEventListener
+import co.nilin.opex.profile.core.spi.UserCreatedEventListener
+import co.nilin.opex.profile.ports.kafka.consumer.KycLevelUpdatedKafkaListener
+import co.nilin.opex.profile.ports.kafka.consumer.UserCreatedKafkaListener
+import io.r2dbc.spi.ConnectionFactory
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.io.ClassPathResource
+
+
+@Configuration
+class AppConfig {
+ @Autowired
+ fun configureEventListeners(
+ useCreatedKafkaListener: UserCreatedKafkaListener,
+ userCreatedEventListener: UserCreatedEventListener,
+ kycLevelUpdatedKafkaListener: KycLevelUpdatedKafkaListener,
+ kycLevelUpdatedEventListener: KycLevelUpdatedEventListener
+ ) {
+ useCreatedKafkaListener.addEventListener(userCreatedEventListener)
+ kycLevelUpdatedKafkaListener.addEventListener(kycLevelUpdatedEventListener)
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/AppDispatchers.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/AppDispatchers.kt
new file mode 100644
index 000000000..f549323d4
--- /dev/null
+++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/AppDispatchers.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.profile.app.config
+
+import kotlinx.coroutines.asCoroutineDispatcher
+import java.util.concurrent.Executors
+
+object AppDispatchers {
+ val kafkaExecutor = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
+}
\ No newline at end of file
diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/RestConfig.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/RestConfig.kt
new file mode 100644
index 000000000..bd259474f
--- /dev/null
+++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/RestConfig.kt
@@ -0,0 +1,29 @@
+package co.nilin.opex.profile.app.config
+
+import co.nilin.opex.utility.interceptors.FormDataWorkaroundFilter
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.format.Formatter
+import org.springframework.web.server.WebFilter
+import java.util.*
+
+@Configuration
+class RestConfig {
+ @Bean
+ fun dateFormatter(): Formatter? {
+ return object : Formatter {
+ override fun print(date: Date, locale: Locale): String {
+ return date.time.toString()
+ }
+
+ override fun parse(date: String, locale: Locale): Date {
+ return Date(date.toLong())
+ }
+ }
+ }
+
+ @Bean
+ fun formDataWebFilter(): WebFilter {
+ return FormDataWorkaroundFilter()
+ }
+}
diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/SecurityConfig.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/SecurityConfig.kt
new file mode 100644
index 000000000..bb86727e4
--- /dev/null
+++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/SecurityConfig.kt
@@ -0,0 +1,40 @@
+package co.nilin.opex.profile.app.config
+
+import co.nilin.opex.profile.app.utils.hasRole
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.context.annotation.Bean
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder
+import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.web.reactive.function.client.WebClient
+
+@EnableWebFluxSecurity
+class SecurityConfig(private val webClient: WebClient) {
+
+ @Value("\${app.auth.cert-url}")
+ private lateinit var jwkUrl: String
+
+ @Bean
+ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain? {
+ http.csrf().disable()
+ .authorizeExchange()
+ // .pathMatchers("/**").permitAll()
+ .pathMatchers("**/admin/**").hasRole("SCOPE_trust", "admin_system")
+ .pathMatchers("/actuator/**").permitAll()
+ .anyExchange().authenticated()
+ .and()
+ .oauth2ResourceServer()
+ .jwt()
+ return http.build()
+ }
+
+ @Bean
+ @Throws(Exception::class)
+ fun reactiveJwtDecoder(): ReactiveJwtDecoder? {
+ return NimbusReactiveJwtDecoder.withJwkSetUri(jwkUrl)
+ .webClient(WebClient.create())
+ .build()
+ }
+}
diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/WebClientConfig.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/WebClientConfig.kt
new file mode 100644
index 000000000..c1f71c861
--- /dev/null
+++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/WebClientConfig.kt
@@ -0,0 +1,38 @@
+package co.nilin.opex.profile.app.config
+
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.cloud.client.ServiceInstance
+import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties
+import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer
+import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.web.reactive.function.client.ExchangeStrategies
+import org.springframework.web.reactive.function.client.WebClient
+
+@Configuration
+class WebClientConfig {
+ @Bean
+ @Qualifier("loadBalanced")
+ fun loadBalancedWebClient(loadBalancerFactory: ReactiveLoadBalancer.Factory): WebClient {
+ return WebClient.builder()
+ .filter(ReactorLoadBalancerExchangeFilterFunction(loadBalancerFactory, emptyList()))
+ .exchangeStrategies(
+ ExchangeStrategies.builder()
+ .codecs { it.defaultCodecs().maxInMemorySize(20 * 1024 * 1024) }
+ .build()
+ )
+ .build()
+ }
+
+ @Bean
+ fun webClient(loadBalancerFactory: ReactiveLoadBalancer.Factory): WebClient {
+ return WebClient.builder()
+ .filter(
+ ReactorLoadBalancerExchangeFilterFunction(
+ loadBalancerFactory, LoadBalancerProperties(), emptyList()
+ )
+ )
+ .build()
+ }
+}
diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/LimitationController.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/LimitationController.kt
new file mode 100644
index 000000000..5304804de
--- /dev/null
+++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/LimitationController.kt
@@ -0,0 +1,35 @@
+package co.nilin.opex.profile.app.controller
+
+import co.nilin.opex.profile.app.service.LimitationManagement
+import co.nilin.opex.profile.core.data.limitation.ActionType
+import co.nilin.opex.profile.core.data.limitation.LimitationReason
+import co.nilin.opex.profile.core.data.limitation.LimitationResponse
+import kotlinx.coroutines.flow.toList
+import org.springframework.security.core.annotation.CurrentSecurityContext
+import org.springframework.security.core.context.SecurityContext
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RequestParam
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+@RequestMapping("/v2/profile/limitation")
+class LimitationController(private var limitManagement: LimitationManagement) {
+
+ @GetMapping("")
+ suspend fun getLimitation(
+ @RequestParam("action") action: ActionType?,
+ @RequestParam("reason") reason: LimitationReason?,
+ @CurrentSecurityContext securityContext: SecurityContext
+ ): LimitationResponse? {
+ return LimitationResponse(
+ totalData = limitManagement.getLimitation(
+ securityContext.authentication.name,
+ action,
+ reason,
+ 0,
+ 1000
+ )?.toList()
+ )
+ }
+}
\ No newline at end of file
diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/LinkedAccountController.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/LinkedAccountController.kt
new file mode 100644
index 000000000..1f67dc9f9
--- /dev/null
+++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/LinkedAccountController.kt
@@ -0,0 +1,53 @@
+package co.nilin.opex.profile.app.controller
+
+import co.nilin.opex.profile.app.service.LinkAccountManagement
+import co.nilin.opex.profile.core.data.linkedbankAccount.*
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.reactive.awaitFirst
+import kotlinx.coroutines.reactive.awaitFirstOrNull
+import org.springframework.security.core.annotation.CurrentSecurityContext
+import org.springframework.security.core.context.SecurityContext
+import org.springframework.web.bind.annotation.*
+
+@RestController
+@RequestMapping("/v2/profile/linked-account")
+class LinkedAccountController(val linkedAccountManagement: LinkAccountManagement) {
+
+ @PostMapping("")
+ suspend fun addLinkedAccount(
+ @RequestBody linkedBankAccountRequest: LinkedBankAccountRequest,
+ @CurrentSecurityContext securityContext: SecurityContext
+ ): LinkedAccountResponse? {
+ linkedBankAccountRequest.userId = securityContext.authentication.name
+ return linkedAccountManagement.addNewAccount(linkedBankAccountRequest)?.awaitFirstOrNull()
+ }
+
+ enum class Status { Enable, Disable }
+
+ @PutMapping("/{accountId}")
+ suspend fun updateLinkedAccount(
+ @PathVariable accountId: String,
+ @RequestBody updateRelatedAccountRequest: UpdateRelatedAccountRequest,
+ @CurrentSecurityContext securityContext: SecurityContext
+ ): LinkedAccountResponse? {
+ return linkedAccountManagement.updateAccount(updateRelatedAccountRequest.apply {
+ this.accountId = accountId
+ userId = securityContext.authentication.name
+ })?.awaitFirstOrNull()
+ }
+
+ @GetMapping("")
+ suspend fun getLinkedAccount(@CurrentSecurityContext securityContext: SecurityContext): Flow? {
+ return linkedAccountManagement.getAccounts(securityContext.authentication.name)
+ }
+
+ @DeleteMapping("/{accountId}")
+ suspend fun deleteAccount(
+ @PathVariable accountId: String,
+ @CurrentSecurityContext securityContext: SecurityContext
+ ): DeleteAccountResponse? {
+ return linkedAccountManagement
+ .deleteAccount(DeleteLinkedAccountRequest(accountId, securityContext.authentication.name))
+ ?.let { ac -> DeleteAccountResponse(ac.awaitFirst()) }
+ }
+}
\ No newline at end of file
diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileAdminController.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileAdminController.kt
new file mode 100644
index 000000000..0623942be
--- /dev/null
+++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileAdminController.kt
@@ -0,0 +1,190 @@
+package co.nilin.opex.profile.app.controller
+
+import co.nilin.opex.profile.app.service.LinkAccountManagement
+import co.nilin.opex.profile.app.service.ProfileApprovalRequestManagement
+import co.nilin.opex.profile.app.service.ProfileManagement
+import co.nilin.opex.profile.core.data.limitation.*
+import co.nilin.opex.profile.core.data.linkedbankAccount.LinkedAccountHistoryResponse
+import co.nilin.opex.profile.core.data.linkedbankAccount.LinkedAccountResponse
+import co.nilin.opex.profile.core.data.linkedbankAccount.LinkedBankAccountRequest
+import co.nilin.opex.profile.core.data.linkedbankAccount.VerifyLinkedAccountRequest
+import co.nilin.opex.profile.core.data.profile.*
+import co.nilin.opex.profile.ports.postgres.imp.LimitationManagementImp
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.reactive.awaitFirstOrNull
+import org.springframework.security.core.annotation.CurrentSecurityContext
+import org.springframework.security.core.context.SecurityContext
+import org.springframework.web.bind.annotation.*
+
+@RestController
+@RequestMapping("/v2/admin/profile")
+
+class ProfileAdminController(
+ val profileManagement: ProfileManagement,
+ val linkAccountManagement: LinkAccountManagement,
+ val profileApprovalRequestManagement: ProfileApprovalRequestManagement,
+ val limitManagement: LimitationManagementImp
+) {
+
+ data class ChangeRequestStatusBody(
+ val id: Long,
+ val description: String
+ )
+
+ @PostMapping("/{userId}")
+ suspend fun createManually(@PathVariable("userId") userId: String, @RequestBody newProfile: Profile): Profile? {
+ return profileManagement.create(userId, newProfile)?.awaitFirstOrNull()
+ }
+
+ @PutMapping("/{userId}")
+ suspend fun updateAsAdmin(@PathVariable("userId") userId: String, @RequestBody newProfile: Profile): Profile? {
+ return profileManagement.updateAsAdmin(userId, newProfile)?.awaitFirstOrNull()
+ }
+
+ @GetMapping("/history/{userId}")
+ suspend fun getHistory(
+ @PathVariable("userId") userId: String,
+ @RequestParam offset: Int?, @RequestParam size: Int?
+ ): List? {
+ return profileManagement.getHistory(userId, offset ?: 0, size ?: 1000)
+ }
+
+ @PostMapping("")
+ suspend fun getProfiles(
+ @RequestParam offset: Int?, @RequestParam size: Int?,
+ @RequestBody profileRequest: ProfileRequest
+ ): List? {
+ return profileManagement.getAllProfiles(offset ?: 0, size ?: 1000, profileRequest)?.toList()
+ }
+
+
+ @GetMapping("/{userId}")
+ suspend fun getProfile(@PathVariable("userId") userId: String): Profile? {
+ return profileManagement.getProfile(userId)?.awaitFirstOrNull()
+ }
+
+ // =====================================Approval Requests====================================
+
+ @GetMapping("/approval-requests/{status}")
+ suspend fun getApprovalRequests(@PathVariable("status") status: ProfileApprovalRequestStatus): List {
+ return profileApprovalRequestManagement.getApprovalRequests(status)
+ }
+
+ @GetMapping("/approval-request/{id}")
+ suspend fun getApprovalRequest(@PathVariable("id") id: Long): ProfileApprovalResponse {
+ return profileApprovalRequestManagement.getApprovalRequest(id)
+ }
+
+ @PostMapping("/approve-request")
+ suspend fun approveRequest(
+ @RequestBody changeRequestStatusBody: ChangeRequestStatusBody,
+ @CurrentSecurityContext securityContext: SecurityContext
+ ): ProfileApprovalResponse {
+ return profileApprovalRequestManagement.approveRequest(
+ changeRequestStatusBody.id,
+ securityContext.authentication.name,
+ changeRequestStatusBody.description
+ )
+ }
+
+ @PostMapping("/reject-request")
+ suspend fun rejectRequest(
+ @RequestBody changeRequestStatusBody: ChangeRequestStatusBody,
+ @CurrentSecurityContext securityContext: SecurityContext
+ ): ProfileApprovalResponse {
+ return profileApprovalRequestManagement.rejectRequest(
+ changeRequestStatusBody.id,
+ securityContext.authentication.name,
+ changeRequestStatusBody.description
+ )
+ }
+ // =====================================linked accounts====================================
+
+ @GetMapping("/linked-account/{userId}")
+ suspend fun getLinkedAccount(@PathVariable userId: String): Flow? {
+ return linkAccountManagement.getAccounts(userId)
+ }
+
+ @GetMapping("/linked-account/history/{accountId}")
+ suspend fun getHistoryLinkedAccount(@PathVariable accountId: String): Flow? {
+
+ return linkAccountManagement.getHistoryLinkedAccount(accountId)
+ }
+
+ @PostMapping("/linked-account/{userId}")
+ suspend fun addLinkedAccount(
+ @PathVariable userId: String,
+ @RequestBody linkedBankAccountRequest: LinkedBankAccountRequest,
+ @CurrentSecurityContext securityContext: SecurityContext
+ ): LinkedAccountResponse? {
+ linkedBankAccountRequest.userId = userId
+ linkedBankAccountRequest.description = "Inserted by admin: ${securityContext.authentication.name}"
+ return linkAccountManagement.addNewAccount(linkedBankAccountRequest)?.awaitFirstOrNull()
+ }
+
+ @PutMapping("/linked-account/verify/{accountId}")
+ suspend fun verifyLinkedAccount(
+ @PathVariable accountId: String, @RequestBody verifyRequest: VerifyLinkedAccountRequest,
+ @CurrentSecurityContext securityContext: SecurityContext
+ ): LinkedAccountResponse? {
+ verifyRequest.accountId = accountId
+ verifyRequest.verifier = securityContext.authentication.name
+ return linkAccountManagement.verifyAccount(verifyRequest)?.awaitFirstOrNull()
+
+ }
+
+ //==============================================limitation services=================================================
+
+
+ @PostMapping("/limitation")
+ suspend fun updateLimitation(@RequestBody permissionRequest: UpdateLimitationRequest) {
+ permissionRequest.reason ?: LimitationReason.Other
+ limitManagement.updateLimitation(permissionRequest)
+ }
+
+ @GetMapping("/limitation")
+ suspend fun getLimitation(
+ @RequestParam("userId") userId: String?,
+ @RequestParam("action") action: ActionType?,
+ @RequestParam("reason") reason: LimitationReason?,
+ @RequestParam("groupBy") groupBy: String?,
+ @RequestParam("size") size: Int?,
+ @RequestParam("offset") offset: Int?
+ ): LimitationResponse? {
+
+ var res = limitManagement.getLimitation(userId, action, reason, offset ?: 0, size ?: 1000)?.toList()
+
+ return when (groupBy) {
+ "user" -> LimitationResponse(res?.groupBy { r -> r.userId })
+ "action" -> LimitationResponse(res?.groupBy { r -> r.actionType?.name })
+ "reason" -> LimitationResponse(res?.groupBy { r -> (r.reason ?: LimitationReason.Other).name })
+ else -> {
+ LimitationResponse(totalData = res)
+ }
+ }
+
+ }
+
+ @GetMapping("/limitation/history")
+ suspend fun getLimitationHistory(
+ @RequestParam("userId") userId: String?,
+ @RequestParam("action") action: ActionType?,
+ @RequestParam("reason") reason: LimitationReason?,
+ @RequestParam("groupBy") groupBy: String?,
+ @RequestParam("size") size: Int?,
+ @RequestParam("offset") offset: Int?
+ ): LimitationHistoryResponse? {
+
+ var res = limitManagement.getLimitationHistory(userId, action, reason, offset ?: 0, size ?: 1000)?.toList()
+ return when (groupBy) {
+ "user" -> LimitationHistoryResponse(res?.groupBy { r -> r.userId })
+ "action" -> LimitationHistoryResponse(res?.groupBy { r -> r.actionType?.name })
+ "reason" -> LimitationHistoryResponse(res?.groupBy { r -> (r.reason ?: LimitationReason.Other).name })
+ else -> {
+ LimitationHistoryResponse(totalData = res)
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt
new file mode 100644
index 000000000..c2c7d907e
--- /dev/null
+++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt
@@ -0,0 +1,56 @@
+package co.nilin.opex.profile.app.controller
+
+import co.nilin.opex.profile.app.service.ProfileManagement
+import co.nilin.opex.profile.core.data.profile.CompleteProfileRequest
+import co.nilin.opex.profile.core.data.profile.CompleteProfileResponse
+import co.nilin.opex.profile.core.data.profile.Profile
+import co.nilin.opex.profile.core.data.profile.UpdateProfileRequest
+import kotlinx.coroutines.reactive.awaitFirstOrNull
+import org.springframework.security.core.annotation.CurrentSecurityContext
+import org.springframework.security.core.context.SecurityContext
+import org.springframework.web.bind.annotation.*
+
+@RestController
+@RequestMapping("/v2/profile")
+
+class ProfileController(val profileManagement: ProfileManagement) {
+
+ @GetMapping("")
+ suspend fun getProfile(@CurrentSecurityContext securityContext: SecurityContext): Profile? {
+ return profileManagement.getProfile(securityContext.authentication.name)?.awaitFirstOrNull()
+ }
+
+
+ @PutMapping("")
+ suspend fun update(
+ @RequestBody newProfile: UpdateProfileRequest,
+ @CurrentSecurityContext securityContext: SecurityContext
+ ): Profile? {
+ return profileManagement.update(securityContext.authentication.name, newProfile)?.awaitFirstOrNull()
+ }
+
+ @PostMapping("/Completion")
+ suspend fun completeProfile(
+ @RequestBody completeProfileRequest: CompleteProfileRequest,
+ @CurrentSecurityContext securityContext: SecurityContext
+ ): CompleteProfileResponse? {
+ return profileManagement.completeProfile(securityContext.authentication.name, completeProfileRequest)
+ }
+
+ //TODO update mobile and email need improvement
+ @PutMapping("/mobile/{mobile}")
+ suspend fun updateMobile(
+ @PathVariable mobile: String,
+ @CurrentSecurityContext securityContext: SecurityContext
+ ) {
+ profileManagement.updateMobile(securityContext.authentication.name, mobile)
+ }
+
+ @PutMapping("/email/{email}")
+ suspend fun updateEmail(
+ @PathVariable email: String,
+ @CurrentSecurityContext securityContext: SecurityContext
+ ) {
+ profileManagement.updateEmail(securityContext.authentication.name, email)
+ }
+}
\ No newline at end of file
diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/listener/KycLevelUpdatedListener.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/listener/KycLevelUpdatedListener.kt
new file mode 100644
index 000000000..d0ac41932
--- /dev/null
+++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/listener/KycLevelUpdatedListener.kt
@@ -0,0 +1,32 @@
+package co.nilin.opex.profile.app.listener
+
+import co.nilin.opex.profile.app.service.ProfileManagement
+import co.nilin.opex.profile.core.data.event.KycLevelUpdatedEvent
+import org.slf4j.LoggerFactory
+import org.springframework.stereotype.Component
+import co.nilin.opex.profile.core.spi.KycLevelUpdatedEventListener
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+@Component
+class KycLevelUpdatedListener(val userRegistrationService: ProfileManagement) : KycLevelUpdatedEventListener {
+
+ private val logger = LoggerFactory.getLogger(KycLevelUpdatedListener::class.java)
+ val scope = CoroutineScope(Dispatchers.IO)
+ override fun id(): String {
+ return "KycLevelUpdatedListener"
+ }
+
+ override fun onEvent(event: KycLevelUpdatedEvent,
+ partition: Int, offset: Long, timestamp: Long, eventId: String) {
+ logger.info("==========================================================================")
+ logger.info("Incoming UserLevelUpdated event: $event")
+ logger.info("==========================================================================")
+ scope.launch {
+ userRegistrationService.updateUserLevel(event.userId, event.kycLevel)
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/listener/UserCreatedListener.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/listener/UserCreatedListener.kt
new file mode 100644
index 000000000..f6db67720
--- /dev/null
+++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/listener/UserCreatedListener.kt
@@ -0,0 +1,31 @@
+package co.nilin.opex.profile.app.listener
+
+import co.nilin.opex.profile.app.service.ProfileManagement
+import co.nilin.opex.profile.core.spi.UserCreatedEventListener
+import kotlinx.coroutines.runBlocking
+import org.slf4j.LoggerFactory
+import org.springframework.stereotype.Component
+import co.nilin.opex.profile.core.data.event.UserCreatedEvent
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+@Component
+class UserCreatedListener(val userRegistrationService: ProfileManagement) : UserCreatedEventListener {
+
+ private val logger = LoggerFactory.getLogger(UserCreatedListener::class.java)
+ val scope = CoroutineScope(Dispatchers.IO)
+ override fun id(): String {
+ return "UserCreatedEventListener"
+ }
+
+
+ override fun onEvent(event: UserCreatedEvent, partition: Int, offset: Long, timestamp: Long, eventId: String) {
+ logger.info("==========================================================================")
+ logger.info("Incoming UserCreated event: $event")
+ logger.info("==========================================================================")
+ scope.launch {
+ userRegistrationService.registerNewUser(event)
+ }
+ }
+}
\ No newline at end of file
diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/LimitationManagement.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/LimitationManagement.kt
new file mode 100644
index 000000000..57bfc9758
--- /dev/null
+++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/LimitationManagement.kt
@@ -0,0 +1,23 @@
+package co.nilin.opex.profile.app.service
+
+import co.nilin.opex.profile.core.data.limitation.*
+import co.nilin.opex.profile.core.spi.LimitationPersister
+import kotlinx.coroutines.flow.Flow
+import org.springframework.stereotype.Component
+
+@Component
+class LimitationManagement(private var limitationPersister: LimitationPersister) {
+ suspend fun updateLimitation(permissionControlRequest: UpdateLimitationRequest) {
+ limitationPersister.updateLimitation(permissionControlRequest)
+
+ }
+
+ suspend fun getLimitation(userId: String?, action: ActionType?, reason: LimitationReason?, offset: Int, size: Int): Flow? {
+ return limitationPersister.getLimitation(userId, action, reason, offset, size)
+ }
+
+ suspend fun getLimitationHistory(userId: String?, action: ActionType?, reason: LimitationReason?, offset: Int, size: Int): Flow? {
+ return limitationPersister.getLimitationHistory(userId, action, reason, offset, size)
+ }
+
+}
\ No newline at end of file
diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/LinkAccountManagement.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/LinkAccountManagement.kt
new file mode 100644
index 000000000..3f20df5a6
--- /dev/null
+++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/LinkAccountManagement.kt
@@ -0,0 +1,48 @@
+package co.nilin.opex.profile.app.service
+
+import co.nilin.opex.common.OpexError
+import co.nilin.opex.profile.core.data.linkedbankAccount.*
+import co.nilin.opex.profile.core.spi.LinkedAccountPersister
+import co.nilin.opex.profile.core.utils.isValidCardNumber
+import co.nilin.opex.profile.core.utils.isValidIBAN
+import kotlinx.coroutines.flow.Flow
+import org.springframework.stereotype.Service
+import reactor.core.publisher.Mono
+
+@Service
+class LinkAccountManagement(val linkedAccountPersister: LinkedAccountPersister) {
+
+ suspend fun addNewAccount(linkedBankAccountRequest: LinkedBankAccountRequest): Mono? {
+ linkedBankAccountRequest.verifyRegisterNewAccount()
+ return linkedAccountPersister.addNewAccount(linkedBankAccountRequest)
+ }
+
+ suspend fun updateAccount(updateRelatedAccountRequest: UpdateRelatedAccountRequest): Mono? {
+ return linkedAccountPersister.updateAccount(updateRelatedAccountRequest)
+ }
+
+ suspend fun getAccounts(userId: String): Flow? {
+ return linkedAccountPersister.getAccounts(userId)
+ }
+
+ suspend fun getHistoryLinkedAccount(accountId: String): Flow? {
+ return linkedAccountPersister.getHistory(accountId)
+ }
+
+ suspend fun verifyAccount(verifyRequest: VerifyLinkedAccountRequest): Mono? {
+ return linkedAccountPersister.verifyAccount(verifyRequest)
+ }
+
+ suspend fun deleteAccount(deleteLinkedAccountRequest: DeleteLinkedAccountRequest): Mono? {
+ return linkedAccountPersister.deleteAccount(deleteLinkedAccountRequest)
+ }
+
+ private fun LinkedBankAccountRequest.verifyRegisterNewAccount() {
+ when (bankAccountType) {
+ BankAccountType.Sheba -> if (!number.isValidIBAN()) throw OpexError.InvalidIban.exception()
+ BankAccountType.Card -> if (!number.isValidCardNumber()) throw OpexError.InvalidCard.exception()
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileApprovalRequestManagement.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileApprovalRequestManagement.kt
new file mode 100644
index 000000000..11a8e7e0d
--- /dev/null
+++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileApprovalRequestManagement.kt
@@ -0,0 +1,63 @@
+package co.nilin.opex.profile.app.service
+
+import co.nilin.opex.common.OpexError
+import co.nilin.opex.profile.core.data.event.KycLevelUpdatedEvent
+import co.nilin.opex.profile.core.data.kyc.KycLevel
+import co.nilin.opex.profile.core.data.profile.ProfileApprovalRequestStatus
+import co.nilin.opex.profile.core.data.profile.ProfileApprovalResponse
+import co.nilin.opex.profile.core.spi.KycLevelUpdatedPublisher
+import co.nilin.opex.profile.core.spi.ProfileApprovalRequestPersister
+import co.nilin.opex.profile.core.spi.ProfilePersister
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.reactive.awaitFirstOrNull
+import org.springframework.stereotype.Component
+import java.time.LocalDateTime
+
+@Component
+class ProfileApprovalRequestManagement(
+ private val profileApprovalRequestPersister: ProfileApprovalRequestPersister,
+ private val kycLevelUpdatedPublisher: KycLevelUpdatedPublisher,
+ private val profilePersister: ProfilePersister,
+) {
+
+ suspend fun getApprovalRequests(status: ProfileApprovalRequestStatus): List {
+ return profileApprovalRequestPersister.getRequests(status)?.toList() ?: emptyList()
+ }
+
+ suspend fun getApprovalRequest(id: Long): ProfileApprovalResponse {
+ return profileApprovalRequestPersister.getRequestById(id).awaitFirstOrNull()
+ ?: throw OpexError.NotFound.exception("Request not found")
+ }
+
+ suspend fun approveRequest(id: Long, updater: String, description: String): ProfileApprovalResponse {
+ val request = changeRequestStatus(id, updater, ProfileApprovalRequestStatus.APPROVED, description)
+ val profile = profilePersister.getProfile(request.profileId)?.awaitFirstOrNull()
+ ?: throw OpexError.NotFound.exception("Request not found")
+ kycLevelUpdatedPublisher.publish(KycLevelUpdatedEvent(profile.userId!!, KycLevel.Level2, LocalDateTime.now()))
+ return request
+ }
+
+ suspend fun rejectRequest(id: Long, updater: String, description: String): ProfileApprovalResponse {
+ return changeRequestStatus(id, updater, ProfileApprovalRequestStatus.REJECTED, description)
+ }
+
+ private suspend fun changeRequestStatus(
+ id: Long,
+ updater: String,
+ newStatus: ProfileApprovalRequestStatus,
+ description: String
+ ): ProfileApprovalResponse {
+ var request = (profileApprovalRequestPersister.getRequestById(id).awaitFirstOrNull()
+ ?: throw OpexError.NotFound.exception("Request not found"))
+ if (request.status != ProfileApprovalRequestStatus.PENDING)
+ throw OpexError.BadRequest.exception("Only pending requests can be changed")
+ request.apply {
+ status = newStatus
+ updateDate = LocalDateTime.now()
+ this.updater = updater
+ this.description = description
+ }
+ return profileApprovalRequestPersister.update(request)
+ }
+
+}
\ No newline at end of file
diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt
new file mode 100644
index 000000000..c48dd718c
--- /dev/null
+++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt
@@ -0,0 +1,164 @@
+package co.nilin.opex.profile.app.service
+
+
+import co.nilin.opex.common.OpexError
+import co.nilin.opex.profile.core.data.event.KycLevelUpdatedEvent
+import co.nilin.opex.profile.core.data.event.UserCreatedEvent
+import co.nilin.opex.profile.core.data.kyc.KycLevel
+import co.nilin.opex.profile.core.data.profile.*
+import co.nilin.opex.profile.core.spi.*
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.reactive.awaitFirstOrNull
+import org.slf4j.LoggerFactory
+import org.springframework.stereotype.Component
+import reactor.core.publisher.Mono
+import java.time.LocalDateTime
+
+@Component
+class ProfileManagement(
+ private val profilePersister: ProfilePersister,
+ private val linkedAccountPersister: LinkedAccountPersister,
+ private val limitationPersister: LimitationPersister,
+ private val profileApprovalRequestPersister: ProfileApprovalRequestPersister,
+ private val shahkarInquiry: ShahkarInquiry,
+ private val kycLevelUpdatedPublisher: KycLevelUpdatedPublisher
+) {
+ private val logger = LoggerFactory.getLogger(ProfileManagement::class.java)
+ suspend fun registerNewUser(event: UserCreatedEvent) {
+ with(event) {
+ profilePersister.createProfile(
+ Profile(
+ firstName = firstName,
+ lastName = lastName,
+ email = email,
+ mobile = mobile,
+ userId = uuid,
+ status = UserStatus.Active,
+ createDate = LocalDateTime.now(),
+ lastUpdateDate = LocalDateTime.now(),
+ creator = "system",
+ kycLevel = KycLevel.Level1
+ )
+ )
+ }
+ }
+
+ suspend fun getAllProfiles(offset: Int, size: Int, profileRequest: ProfileRequest): List? {
+ profileRequest.accountNumber?.let {
+ val res = profilePersister.getAllProfile(offset, size, profileRequest)?.toList()
+ val accountOwner =
+ linkedAccountPersister.getOwner(profileRequest.accountNumber!!, profileRequest.partialSearch)
+ ?.map { profilePersister.getProfile(it.userId)?.awaitFirstOrNull() }?.toList()
+ if (res?.isEmpty() == true || accountOwner?.isEmpty() == true) {
+ return null
+ } else {
+ return addDetail(accountOwner!!::contains?.let { it1 -> res?.filter(it1) }, profileRequest)
+ }
+ } ?: run {
+ return addDetail(profilePersister.getAllProfile(offset, size, profileRequest)?.toList(), profileRequest)
+
+ }
+ }
+
+
+ private suspend fun addDetail(res: List?, profileRequest: ProfileRequest): List? {
+ if (profileRequest.includeLinkedAccount == true) {
+ res?.forEach {
+ it?.linkedAccounts = linkedAccountPersister.getAccounts(it?.userId!!)?.toList()
+ }
+ }
+ if (profileRequest.includeLimitation == true) {
+ res?.forEach {
+ it?.limitations = limitationPersister.getLimitation(it?.userId)?.toList()
+ }
+ }
+ return res;
+ }
+
+ suspend fun getProfile(userId: String): Mono? {
+ return profilePersister.getProfile(userId)
+ }
+
+ suspend fun update(userId: String, newProfile: UpdateProfileRequest): Mono? {
+ return profilePersister.updateProfile(userId, newProfile)
+ }
+
+ suspend fun updateAsAdmin(userId: String, newProfile: Profile): Mono? {
+ return profilePersister.updateProfileAsAdmin(userId, newProfile)
+ }
+
+ suspend fun create(userId: String, newProfile: Profile): Mono? {
+ newProfile.userId = userId
+ return profilePersister.createProfile(newProfile)
+ }
+
+ suspend fun getHistory(userId: String, offset: Int, size: Int): List? {
+ return profilePersister.getHistory(userId, offset, size)
+ }
+
+ suspend fun updateUserLevel(userId: String, userLevel: KycLevel) {
+ profilePersister.updateUserLevel(userId, userLevel)
+ }
+
+ //TODO Need OTP
+ suspend fun updateMobile(userId: String, mobile: String) {
+ val profile = profilePersister.getProfile(userId)?.awaitFirstOrNull()
+ ?: throw OpexError.NotFound.exception("profile not found")
+ if (profile.mobile.isNullOrEmpty())
+ profilePersister.updateMobile(userId, mobile)
+ else
+ throw OpexError.BadRequest.exception("Mobile cannot be changed")
+ }
+
+ //TODO Need OTP
+ suspend fun updateEmail(userId: String, email: String) {
+ val profile = profilePersister.getProfile(userId)?.awaitFirstOrNull()
+ ?: throw OpexError.NotFound.exception("profile not found")
+ if (profile.email.isNullOrEmpty())
+ profilePersister.updateEmail(userId, email)
+ else
+ throw OpexError.BadRequest.exception("Email cannot be changed")
+ }
+
+ suspend fun completeProfile(
+ userId: String,
+ completeProfileRequest: CompleteProfileRequest
+ ): CompleteProfileResponse {
+ val profile = profilePersister.getProfile(userId)?.awaitFirstOrNull()
+ ?: throw OpexError.NotFound.exception("profile not found")
+ if (profile.kycLevel == KycLevel.Level2) {
+ throw OpexError.BadRequest.exception("Profile already completed")
+ }
+ val isIranian = completeProfileRequest.nationality == "Iranian"
+ if (isIranian) {
+ if (!shahkarInquiry.getInquiryResult(
+ completeProfileRequest.identifier,
+ profile.mobile ?: throw OpexError.BadRequest.exception("Profile mobile is empty")
+ )
+ ) {
+ throw OpexError.VerificationFailed.exception("Mobile and identifier do not match")
+ }
+ completeProfileRequest.verificationStatus = true
+ }
+ val completedProfile = profilePersister.completeProfile(userId, completeProfileRequest).awaitFirstOrNull()
+ ?: throw OpexError.BadRequest.exception("profile not found for userId: $userId")
+ if (isIranian)
+ kycLevelUpdatedPublisher.publish(KycLevelUpdatedEvent(userId, KycLevel.Level2, LocalDateTime.now()))
+ else
+ saveProfileApprovalRequest(completedProfile.id)
+ return completedProfile
+ }
+
+ private suspend fun saveProfileApprovalRequest(profileId: Long) {
+ profileApprovalRequestPersister.save(
+ ProfileApprovalRequest(
+ profileId = profileId,
+ status = ProfileApprovalRequestStatus.PENDING,
+ createDate = LocalDateTime.now(),
+ updateDate = null,
+ updater = null
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/utils/Extensions.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/utils/Extensions.kt
new file mode 100644
index 000000000..49cc02cc3
--- /dev/null
+++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/utils/Extensions.kt
@@ -0,0 +1,18 @@
+package co.nilin.opex.profile.app.utils
+
+
+import com.nimbusds.jose.shaded.json.JSONArray
+import org.springframework.security.authorization.AuthorizationDecision
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.oauth2.jwt.Jwt
+
+fun ServerHttpSecurity.AuthorizeExchangeSpec.Access.hasRole(
+ authority: String,
+ role: String
+): ServerHttpSecurity.AuthorizeExchangeSpec = access { mono, _ ->
+ mono.map { auth ->
+ val hasAuthority = auth.authorities.any { it.authority == authority }
+ val hasRole = ((auth.principal as Jwt).claims["roles"] as JSONArray?)?.contains(role) == true
+ AuthorizationDecision(hasAuthority && hasRole)
+ }
+}
\ No newline at end of file
diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/utils/PrometheusHealthExtension.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/utils/PrometheusHealthExtension.kt
new file mode 100644
index 000000000..c6c017a8a
--- /dev/null
+++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/utils/PrometheusHealthExtension.kt
@@ -0,0 +1,63 @@
+package co.nilin.opex.profile.app.utils
+
+import io.micrometer.core.instrument.Gauge
+import io.micrometer.core.instrument.MeterRegistry
+import org.springframework.boot.actuate.health.HealthComponent
+import org.springframework.boot.actuate.health.HealthEndpoint
+import org.springframework.boot.actuate.health.SystemHealth
+import org.springframework.scheduling.annotation.Scheduled
+import org.springframework.stereotype.Component
+
+@Component
+class PrometheusHealthExtension(
+ private val registry: MeterRegistry,
+ private val endpoint: HealthEndpoint
+) {
+
+ private var consulHealth = -1
+ private var r2dbcHealth = -1
+ private var vaultHealth = -1
+ private var vaultReactiveHealth = -1
+ private val service = "PROFILE"
+
+ init {
+ Gauge.builder("consul_health", consulHealth) { consulHealth.toDouble() }
+ .description("Health of consul connection")
+ .tag("Service", service)
+ .register(registry)
+
+ Gauge.builder("r2dbc_health", r2dbcHealth) { r2dbcHealth.toDouble() }
+ .description("Health of r2dbc connection")
+ .tag("Service", service)
+ .register(registry)
+
+ Gauge.builder("vault_health", vaultHealth) { vaultHealth.toDouble() }
+ .description("Health of vault connection")
+ .tag("Service", service)
+ .register(registry)
+
+ Gauge.builder("vaultReactive_health", vaultReactiveHealth) { vaultReactiveHealth.toDouble() }
+ .description("Health of vaultReactive connection")
+ .tag("Service", service)
+ .register(registry)
+ }
+
+ @Scheduled(initialDelay = 1000, fixedDelay = 5000)
+ fun updateHealth() {
+ try {
+ val health = endpoint.health() as SystemHealth
+ consulHealth = getHealthValue(health.components["consul"])
+ r2dbcHealth = getHealthValue(health.components["r2dbc"])
+ vaultHealth = getHealthValue(health.components["vault"])
+ vaultReactiveHealth = getHealthValue(health.components["vaultReactive"])
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ private fun getHealthValue(health: HealthComponent?): Int {
+ health ?: return -1
+ return if (health.status.code == "UP") 1 else 0
+ }
+
+}
\ No newline at end of file
diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/utils/VaultUserIdMechanism.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/utils/VaultUserIdMechanism.kt
new file mode 100644
index 000000000..d5d7523c9
--- /dev/null
+++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/utils/VaultUserIdMechanism.kt
@@ -0,0 +1,9 @@
+package co.nilin.opex.profile.app.utils
+
+import org.springframework.vault.authentication.AppIdUserIdMechanism
+
+class VaultUserIdMechanism : AppIdUserIdMechanism {
+ override fun createUserId(): String {
+ return System.getenv("BACKEND_USER")
+ }
+}
\ No newline at end of file
diff --git a/profile/profile-app/src/main/resources/application.yml b/profile/profile-app/src/main/resources/application.yml
new file mode 100644
index 000000000..00398823d
--- /dev/null
+++ b/profile/profile-app/src/main/resources/application.yml
@@ -0,0 +1,66 @@
+server.port: 8080
+logging:
+ level:
+ co.nilin: DEBUG
+ reactor.netty.http.client: DEBUG
+spring:
+ application:
+ name: opex-profile
+ main:
+ allow-bean-definition-overriding: false
+ allow-circular-references: true
+ kafka:
+ bootstrap-servers: ${KAFKA_IP_PORT:localhost:9092}
+ consumer:
+ group-id: profile
+ r2dbc:
+ url: r2dbc:postgresql://${DB_IP_PORT:localhost}/opex
+ username: ${dbusername:opex}
+ password: ${dbpassword:hiopex}
+ initialization-mode: always
+
+ cloud:
+ bootstrap:
+ enabled: true
+ vault:
+ host: ${VAULT_HOST}
+ port: 8200
+ scheme: http
+ authentication: APPID
+ app-id:
+ user-id: co.nilin.opex.profile.app.utils.VaultUserIdMechanism
+ fail-fast: true
+ kv:
+ enabled: true
+ backend: secret
+ profile-separator: '/'
+ application-name: ${spring.application.name}
+ consul:
+ host: ${CONSUL_HOST:localhost}
+ port: 8500
+ discovery:
+ #healthCheckPath: ${management.context-path}/health
+ instance-id: ${spring.application.name}:${server.port}
+ healthCheckInterval: 20s
+ prefer-ip-address: true
+ config:
+ import: vault://secret/${spring.application.name}
+swagger.authUrl: ${SWAGGER_AUTH_URL:https://api.opex.dev/auth}/realms/opex/protocol/openid-connect/token
+app:
+ auth:
+ cert-url: http://keycloak:8080/realms/opex/protocol/openid-connect/certs
+ kyc:
+ url: lb://opex-kyc/v2/admin/kyc/internal
+management:
+ endpoints:
+ web:
+ base-path: /actuator
+ exposure:
+ include: ["health", "prometheus", "metrics"]
+ endpoint:
+ health:
+ show-details: when_authorized
+ metrics:
+ enabled: true
+ prometheus:
+ enabled: true
\ No newline at end of file
diff --git a/profile/profile-app/src/test/kotlin/co/nilin/opex/profile/app/ProfileAppApplicationTests.kt b/profile/profile-app/src/test/kotlin/co/nilin/opex/profile/app/ProfileAppApplicationTests.kt
new file mode 100644
index 000000000..abcbcd7fa
--- /dev/null
+++ b/profile/profile-app/src/test/kotlin/co/nilin/opex/profile/app/ProfileAppApplicationTests.kt
@@ -0,0 +1,12 @@
+//package co.nilin.opex.profile.app
+//
+//import org.junit.jupiter.api.Test
+//import org.springframework.boot.test.context.SpringBootTest
+//import java.time.LocalDate
+//import java.time.LocalDateTime
+//
+//
+//class ProfileAppApplicationTests {
+//
+//
+//}
diff --git a/profile/profile-core/.gitignore b/profile/profile-core/.gitignore
new file mode 100644
index 000000000..549e00a2a
--- /dev/null
+++ b/profile/profile-core/.gitignore
@@ -0,0 +1,33 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
diff --git a/profile/profile-core/pom.xml b/profile/profile-core/pom.xml
new file mode 100644
index 000000000..794519145
--- /dev/null
+++ b/profile/profile-core/pom.xml
@@ -0,0 +1,38 @@
+
+
+ 4.0.0
+
+ co.nilin.opex
+ profile
+ 1.0.1-beta.7
+
+ co.nilin.opex.profile
+ profile-core
+ profile-core
+ profile-core
+
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+ io.projectreactor.kotlin
+ reactor-kotlin-extensions
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-reactor
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-core
+
+
+
+
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/event/KycLevelUpdatedEvent.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/event/KycLevelUpdatedEvent.kt
new file mode 100644
index 000000000..d22aae3e8
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/event/KycLevelUpdatedEvent.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.profile.core.data.event
+
+import co.nilin.opex.profile.core.data.kyc.KycLevel
+import java.time.LocalDateTime
+
+data class KycLevelUpdatedEvent(var userId: String, var kycLevel: KycLevel, var updateDate: LocalDateTime)
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/event/UserCreatedEvent.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/event/UserCreatedEvent.kt
new file mode 100644
index 000000000..0b4415049
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/event/UserCreatedEvent.kt
@@ -0,0 +1,28 @@
+package co.nilin.opex.profile.core.data.event
+
+import java.time.LocalDateTime
+
+class UserCreatedEvent {
+ var eventDate: LocalDateTime = LocalDateTime.now()
+ lateinit var uuid: String
+ lateinit var username: String
+ var firstName: String? = null
+ var lastName: String? = null
+ var email: String? = null
+ var mobile: String? = null
+
+
+ constructor(uuid: String, firstName: String?, lastName: String?, email: String?, mobile: String?) : super() {
+ this.uuid = uuid
+ this.firstName = firstName
+ this.lastName = lastName
+ this.email = email
+ this.mobile = mobile
+ }
+
+ constructor() : super()
+
+ override fun toString(): String {
+ return "UserCreatedEvent(uuid='$uuid', firstName='$firstName', lastName='$lastName', email='$email' , mobile='$mobile')"
+ }
+}
\ No newline at end of file
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/kyc/KycLevel.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/kyc/KycLevel.kt
new file mode 100644
index 000000000..10e9e9c93
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/kyc/KycLevel.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.profile.core.data.kyc
+
+enum class KycLevel {
+ Level1, Level2, Level3
+}
+
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/kyc/KycLevelDetail.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/kyc/KycLevelDetail.kt
new file mode 100644
index 000000000..67b3bdf61
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/kyc/KycLevelDetail.kt
@@ -0,0 +1,26 @@
+package co.nilin.opex.profile.core.data.kyc
+
+enum class KycLevelDetail(val kycLevel: KycLevel) {
+ Registered(KycLevel.Level1),
+ ProfileCompleted(KycLevel.Level2),
+ UploadDataLevel3(KycLevel.Level2),
+ AcceptedManualReview(KycLevel.Level3),
+ RejectedManualReview(KycLevel.Level2),
+ ManualUpdateLevel1(KycLevel.Level1),
+ ManualUpdateLevel2(KycLevel.Level2),
+ ManualUpdateLevel3(KycLevel.Level3);
+
+
+ public val previousValidSteps: List?
+ get() = when (this) {
+ Registered -> null
+ ProfileCompleted -> arrayOf(Registered).asList()
+ UploadDataLevel3 -> arrayOf(Registered, RejectedManualReview, ManualUpdateLevel1, ManualUpdateLevel3,ProfileCompleted).asList()
+ AcceptedManualReview -> arrayOf(UploadDataLevel3, RejectedManualReview, ManualUpdateLevel1,ManualUpdateLevel2, ManualUpdateLevel3).asList()
+ RejectedManualReview -> arrayOf(UploadDataLevel3, AcceptedManualReview, ManualUpdateLevel1,ManualUpdateLevel2, ManualUpdateLevel3).asList()
+ else -> {
+ null
+ }
+ }
+
+}
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/kyc/KycRequest.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/kyc/KycRequest.kt
new file mode 100644
index 000000000..9e8950f5c
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/kyc/KycRequest.kt
@@ -0,0 +1,14 @@
+package co.nilin.opex.profile.core.data.kyc
+
+import java.time.LocalDateTime
+
+open class KycRequest {
+ lateinit var userId: String
+ var stepId: String? = null
+ var referenceId: String? = null
+ var issuer: String? = null
+ var step: KycStep? = null
+ var createDate: LocalDateTime? = LocalDateTime.now()
+ var description: String? = null
+ var method: KycMethod? = null
+}
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/kyc/KycStep.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/kyc/KycStep.kt
new file mode 100644
index 000000000..f0341b176
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/kyc/KycStep.kt
@@ -0,0 +1,14 @@
+package co.nilin.opex.profile.core.data.kyc
+
+
+enum class KycStep {
+ UploadDataForLevel3(), ManualReview(), Register(), ManualUpdate() , ProfileCompleted()
+}
+
+enum class KycStatus {
+ Successful, Failed, Rejected, Accepted
+}
+
+enum class KycMethod{
+ METHOD_1 , METHOD_2
+}
\ No newline at end of file
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/kyc/ManualUpdateRequest.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/kyc/ManualUpdateRequest.kt
new file mode 100644
index 000000000..c2341f3bc
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/kyc/ManualUpdateRequest.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.profile.core.data.kyc
+
+data class ManualUpdateRequest(
+ var level: KycLevelDetail,
+) : KycRequest()
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/ActionType.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/ActionType.kt
new file mode 100644
index 000000000..71a56ec66
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/ActionType.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.profile.core.data.limitation
+
+enum class ActionType {
+ Login, Buy, Sell, Withdraw, CashOut, All, Deposit
+}
\ No newline at end of file
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/Limitation.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/Limitation.kt
new file mode 100644
index 000000000..e121b53dd
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/Limitation.kt
@@ -0,0 +1,15 @@
+package co.nilin.opex.profile.core.data.limitation
+
+import com.fasterxml.jackson.annotation.JsonInclude
+import java.time.LocalDateTime
+
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+data class Limitation(
+ var expTime: LocalDateTime?,
+ var userId: String?,
+ var actionType: ActionType?,
+ var createDate: LocalDateTime?,
+ var detail: String?,
+ var description: String?,
+ var reason: LimitationReason?
+)
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/LimitationHistory.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/LimitationHistory.kt
new file mode 100644
index 000000000..bf5778069
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/LimitationHistory.kt
@@ -0,0 +1,19 @@
+package co.nilin.opex.profile.core.data.limitation
+
+import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
+import java.time.LocalDateTime
+
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+data class LimitationHistory(
+ var expTime: LocalDateTime?,
+ var userId: String?,
+ var actionType: ActionType?,
+ var createDate: LocalDateTime?,
+ var detail: String?,
+ var description: String?,
+ var issuer: String?,
+ var changeRequestDate: LocalDateTime?,
+ var changeRequestType: String?,
+ var reason: LimitationReason?
+)
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/LimitationHistoryResponse.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/LimitationHistoryResponse.kt
new file mode 100644
index 000000000..3fe9e4336
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/LimitationHistoryResponse.kt
@@ -0,0 +1,9 @@
+package co.nilin.opex.profile.core.data.limitation
+
+import com.fasterxml.jackson.annotation.JsonInclude
+
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+data class LimitationHistoryResponse(
+ var response: Map>? = null,
+ var totalData: List? = null
+)
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/LimitationReason.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/LimitationReason.kt
new file mode 100644
index 000000000..4dc123957
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/LimitationReason.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.profile.core.data.limitation
+
+enum class LimitationReason {
+ MajorProfileChange, ContactProfileChange, Other
+}
\ No newline at end of file
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/LimitationResponse.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/LimitationResponse.kt
new file mode 100644
index 000000000..f71c6995d
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/LimitationResponse.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.profile.core.data.limitation
+
+import com.fasterxml.jackson.annotation.JsonInclude
+
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+data class LimitationResponse(var response: Map>? = null, var totalData: List? = null)
\ No newline at end of file
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/LimitationUpdateType.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/LimitationUpdateType.kt
new file mode 100644
index 000000000..83dcf3ad5
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/LimitationUpdateType.kt
@@ -0,0 +1,3 @@
+package co.nilin.opex.profile.core.data.limitation
+
+enum class LimitationUpdateType { Revoke, Access }
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/UpdateLimitationRequest.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/UpdateLimitationRequest.kt
new file mode 100644
index 000000000..48c28150c
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/limitation/UpdateLimitationRequest.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.profile.core.data.limitation
+
+data class UpdateLimitationRequest(
+ var userId: String?,
+ var actions: List?,
+ var exprTime: Long?,
+ var updateType: LimitationUpdateType,
+ var description: String?,
+ var detail: String?,
+ var reason: LimitationReason?
+)
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/BankAccountType.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/BankAccountType.kt
new file mode 100644
index 000000000..861fb05c4
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/BankAccountType.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.profile.core.data.linkedbankAccount
+
+enum class BankAccountType {
+ Card, Account, Sheba
+}
\ No newline at end of file
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/DeleteAccountResponse.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/DeleteAccountResponse.kt
new file mode 100644
index 000000000..4f5e582d6
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/DeleteAccountResponse.kt
@@ -0,0 +1,3 @@
+package co.nilin.opex.profile.core.data.linkedbankAccount
+
+data class DeleteAccountResponse(var accountId: String?, var status: String = "Deleted")
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/DeleteLinkedAccountRequest.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/DeleteLinkedAccountRequest.kt
new file mode 100644
index 000000000..80b519318
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/DeleteLinkedAccountRequest.kt
@@ -0,0 +1,3 @@
+package co.nilin.opex.profile.core.data.linkedbankAccount
+
+data class DeleteLinkedAccountRequest(var accountId: String, var userId: String)
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/LinkedAccountHistoryResponse.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/LinkedAccountHistoryResponse.kt
new file mode 100644
index 000000000..d2a870f5a
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/LinkedAccountHistoryResponse.kt
@@ -0,0 +1,17 @@
+package co.nilin.opex.profile.core.data.linkedbankAccount
+
+import java.time.LocalDateTime
+
+data class LinkedAccountHistoryResponse(
+ var userId: String?,
+ var bankAccountType: BankAccountType,
+ var registerDate: LocalDateTime? = null,
+ var verifiedDate: LocalDateTime? = null,
+ var number: String,
+ var accountId: String? = null,
+ var enabled: Boolean?,
+ var verified: Boolean?,
+ var description: String?,
+ var changeRequestDate: LocalDateTime?,
+ var changeRequestType: String?
+)
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/LinkedAccountResponse.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/LinkedAccountResponse.kt
new file mode 100644
index 000000000..57784316b
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/LinkedAccountResponse.kt
@@ -0,0 +1,15 @@
+package co.nilin.opex.profile.core.data.linkedbankAccount
+
+import java.time.LocalDateTime
+
+data class LinkedAccountResponse(
+ var userId: String,
+ var bankAccountType: BankAccountType,
+ var registerDate: LocalDateTime? = null,
+ var verifiedDate: LocalDateTime? = null,
+ var number: String,
+ var accountId: String? = null,
+ var enabled: Boolean?,
+ var verified: Boolean?,
+ var description: String?
+)
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/LinkedBankAccountRequest.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/LinkedBankAccountRequest.kt
new file mode 100644
index 000000000..49806468f
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/LinkedBankAccountRequest.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.profile.core.data.linkedbankAccount
+
+import java.time.LocalDateTime
+
+data class LinkedBankAccountRequest(
+ var userId: String?,
+ var bankAccountType: BankAccountType,
+ var registerDate: LocalDateTime? = null,
+ var verifiedDate: LocalDateTime? = null,
+ var number: String,
+ var accountId: String? = null,
+ var description: String?
+)
\ No newline at end of file
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/UpdateRelatedAccountRequest.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/UpdateRelatedAccountRequest.kt
new file mode 100644
index 000000000..2435e2cae
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/UpdateRelatedAccountRequest.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.profile.core.data.linkedbankAccount
+
+data class UpdateRelatedAccountRequest(var userId: String?, var accountId: String?, var status: Status)
+
+enum class Status { Enable, Disable }
\ No newline at end of file
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/VerifyLinkedAccountRequest.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/VerifyLinkedAccountRequest.kt
new file mode 100644
index 000000000..0fa9d24c2
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/linkedbankAccount/VerifyLinkedAccountRequest.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.profile.core.data.linkedbankAccount
+
+data class VerifyLinkedAccountRequest(
+ val verified: Boolean,
+ var description: String?,
+ var accountId: String?,
+ var verifier: String?
+)
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/CompleteProfileRequest.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/CompleteProfileRequest.kt
new file mode 100644
index 000000000..12dd887c7
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/CompleteProfileRequest.kt
@@ -0,0 +1,18 @@
+package co.nilin.opex.profile.core.data.profile
+
+import co.nilin.opex.profile.core.data.kyc.KycLevel
+import java.time.LocalDateTime
+
+data class CompleteProfileRequest(
+ var firstName: String,
+ var lastName: String,
+ var address: String ? = null,
+ var telephone: String? = null,
+ var postalCode: String? = null,
+ var nationality: String,
+ var identifier: String,
+ var gender: Gender,
+ var birthDate: LocalDateTime,
+ var kycLevel: KycLevel? = KycLevel.Level1,
+ var verificationStatus: Boolean? = false
+)
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/CompleteProfileResponse.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/CompleteProfileResponse.kt
new file mode 100644
index 000000000..49d8d44a6
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/CompleteProfileResponse.kt
@@ -0,0 +1,23 @@
+package co.nilin.opex.profile.core.data.profile
+
+import co.nilin.opex.profile.core.data.kyc.KycLevel
+import java.time.LocalDateTime
+
+open class CompleteProfileResponse(
+ var id: Long,
+ var email: String?,
+ var userId: String?,
+ var firstName: String? = null,
+ var lastName: String? = null,
+ var address: String? = null,
+ var mobile: String? = null,
+ var telephone: String? = null,
+ var postalCode: String? = null,
+ var nationality: String? = null,
+ var identifier: String? = null,
+ var gender: Gender? = null,
+ var birthDate: LocalDateTime? = null,
+ var status: UserStatus? = null,
+ var kycLevel: KycLevel? = null,
+ var verificationStatus: Boolean? = false
+)
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/Gender.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/Gender.kt
new file mode 100644
index 000000000..82d852345
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/Gender.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.profile.core.data.profile
+
+enum class Gender {
+ FEMALE, MALE
+}
\ No newline at end of file
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/Profile.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/Profile.kt
new file mode 100644
index 000000000..51d27d288
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/Profile.kt
@@ -0,0 +1,32 @@
+package co.nilin.opex.profile.core.data.profile
+
+import co.nilin.opex.profile.core.data.kyc.KycLevel
+import co.nilin.opex.profile.core.data.limitation.Limitation
+import co.nilin.opex.profile.core.data.linkedbankAccount.LinkedAccountResponse
+import com.fasterxml.jackson.annotation.JsonInclude
+import java.time.LocalDateTime
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class Profile(
+ var email: String?,
+ var userId: String?,
+ var firstName: String? = null,
+ var lastName: String? = null,
+ var address: String? = null,
+ var mobile: String? = null,
+ var telephone: String? = null,
+ var postalCode: String? = null,
+ var nationality: String? = null,
+ var identifier: String? = null,
+ var gender: Gender? = null,
+ var birthDate: LocalDateTime? = null,
+ var status: UserStatus? = null,
+ var createDate: LocalDateTime? = null,
+ var lastUpdateDate: LocalDateTime? = null,
+ var creator: String? = null,
+ var kycLevel: KycLevel? = null,
+ var linkedAccounts: List? = null,
+ var limitations: List? = null,
+ var verificationStatus : Boolean? = false
+
+)
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/ProfileApprovalRequest.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/ProfileApprovalRequest.kt
new file mode 100644
index 000000000..c57c47918
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/ProfileApprovalRequest.kt
@@ -0,0 +1,12 @@
+package co.nilin.opex.profile.core.data.profile
+
+import java.time.LocalDateTime
+
+data class ProfileApprovalRequest(
+ var profileId: Long,
+ var status: ProfileApprovalRequestStatus? = ProfileApprovalRequestStatus.PENDING,
+ var createDate: LocalDateTime? = null,
+ var updateDate: LocalDateTime? = null,
+ var updater: String? = null,
+ var description: String?=null
+)
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/ProfileApprovalRequestStatus.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/ProfileApprovalRequestStatus.kt
new file mode 100644
index 000000000..4e30230e4
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/ProfileApprovalRequestStatus.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.profile.core.data.profile
+
+enum class ProfileApprovalRequestStatus {
+ PENDING, APPROVED, REJECTED
+}
\ No newline at end of file
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/ProfileApprovalResponse.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/ProfileApprovalResponse.kt
new file mode 100644
index 000000000..213bc6248
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/ProfileApprovalResponse.kt
@@ -0,0 +1,14 @@
+package co.nilin.opex.profile.core.data.profile
+
+import java.time.LocalDateTime
+
+data class ProfileApprovalResponse(
+ var id : Long,
+ var profileId: Long,
+ var status: ProfileApprovalRequestStatus,
+ var createDate: LocalDateTime,
+ var updateDate: LocalDateTime? = null,
+ var updater: String? = null,
+ var description: String?=null
+
+)
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/ProfileHistory.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/ProfileHistory.kt
new file mode 100644
index 000000000..0dfc3058a
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/ProfileHistory.kt
@@ -0,0 +1,32 @@
+package co.nilin.opex.profile.core.data.profile
+
+import co.nilin.opex.profile.core.data.kyc.KycLevel
+import com.fasterxml.jackson.annotation.JsonInclude
+import java.time.LocalDateTime
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class ProfileHistory(
+ var email: String?,
+ var userId: String?,
+ var firstName: String? = null,
+ var lastName: String? = null,
+ var address: String? = null,
+ var mobile: String? = null,
+ var telephone: String? = null,
+ var postalCode: String? = null,
+ var nationality: String? = null,
+ var identifier: String? = null,
+ var gender: Boolean? = null,
+ var birthDate: LocalDateTime? = null,
+ var status: UserStatus? = null,
+ var createDate: LocalDateTime? = null,
+ var lastUpdateDate: LocalDateTime? = null,
+ var creator: String? = null,
+ var issuer: String?,
+ var changeRequestDate: LocalDateTime?,
+ var changeRequestType: String?,
+ var updatedItem: List?,
+ var kycLevel: KycLevel? = null,
+ var verificationStatus : Boolean? = false
+
+)
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/ProfileRequest.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/ProfileRequest.kt
new file mode 100644
index 000000000..919924182
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/ProfileRequest.kt
@@ -0,0 +1,19 @@
+package co.nilin.opex.profile.core.data.profile
+
+import java.time.LocalDateTime
+
+data class ProfileRequest(
+ var userId: String?,
+ var mobile: String?,
+ var email: String?,
+ var linkedAccount: String?,
+ var nationalCode: String?,
+ var firstName: String?,
+ var lastName: String?,
+ var createDateFrom: LocalDateTime?,
+ var accountNumber: String?,
+ var createDateTo: LocalDateTime?,
+ var includeLimitation: Boolean?,
+ var includeLinkedAccount: Boolean?,
+ var partialSearch: Boolean? = false
+)
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/UpdateProfileRequest.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/UpdateProfileRequest.kt
new file mode 100644
index 000000000..4aa769d56
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/UpdateProfileRequest.kt
@@ -0,0 +1,16 @@
+package co.nilin.opex.profile.core.data.profile
+
+import java.time.LocalDateTime
+
+data class UpdateProfileRequest(
+ var firstName: String? = null,
+ var lastName: String? = null,
+ var address: String? = null,
+ var telephone: String? = null,
+ var postalCode: String? = null,
+ var nationality: String? = null,
+ var identifier: String? = null,
+ var gender: Gender? = null,
+ val mobile: String? = null,
+ var birthDate: LocalDateTime? = null,
+)
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/UserStatus.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/UserStatus.kt
new file mode 100644
index 000000000..6ba3400fd
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/UserStatus.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.profile.core.data.profile
+
+enum class UserStatus {
+ Active, Inactive, Blocked, PartialBlocked
+}
\ No newline at end of file
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/AccessManagement.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/AccessManagement.kt
new file mode 100644
index 000000000..f32829e1e
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/AccessManagement.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.profile.core.spi
+
+interface AccessManagement {
+ fun grantPermission()
+ fun revokePermission()
+}
\ No newline at end of file
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/KycLevelUpdatedEventListener.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/KycLevelUpdatedEventListener.kt
new file mode 100644
index 000000000..c09548c9c
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/KycLevelUpdatedEventListener.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.profile.core.spi
+
+import co.nilin.opex.profile.core.data.event.KycLevelUpdatedEvent
+
+
+interface KycLevelUpdatedEventListener {
+ fun id(): String
+ fun onEvent(event: KycLevelUpdatedEvent, partition: Int, offset: Long, timestamp: Long, eventId: String)
+
+}
\ No newline at end of file
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/KycLevelUpdatedPublisher.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/KycLevelUpdatedPublisher.kt
new file mode 100644
index 000000000..8a3c23b9f
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/KycLevelUpdatedPublisher.kt
@@ -0,0 +1,9 @@
+package co.nilin.opex.profile.core.spi
+
+import co.nilin.opex.profile.core.data.event.KycLevelUpdatedEvent
+
+
+interface KycLevelUpdatedPublisher {
+ suspend fun publish(order: KycLevelUpdatedEvent)
+
+}
\ No newline at end of file
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/KycProxy.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/KycProxy.kt
new file mode 100644
index 000000000..5536a9c59
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/KycProxy.kt
@@ -0,0 +1,7 @@
+package co.nilin.opex.profile.core.spi
+
+import co.nilin.opex.profile.core.data.kyc.ManualUpdateRequest
+
+interface KycProxy {
+ suspend fun updateKycLevel(updateKycLevelRequest: ManualUpdateRequest)
+}
\ No newline at end of file
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/LimitationPersister.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/LimitationPersister.kt
new file mode 100644
index 000000000..0b535f9ba
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/LimitationPersister.kt
@@ -0,0 +1,14 @@
+package co.nilin.opex.profile.core.spi
+
+import co.nilin.opex.profile.core.data.limitation.*
+import kotlinx.coroutines.flow.Flow
+
+interface LimitationPersister {
+ suspend fun updateLimitation(updatePermissionRequest: UpdateLimitationRequest)
+
+ suspend fun getLimitation(userId: String?, action: ActionType? = null, reason: LimitationReason? = null, offset: Int? = 0, size: Int? = 1000): Flow?
+
+ suspend fun getLimitationHistory(userId: String?, action: ActionType?, reason: LimitationReason?, offset: Int, size: Int): Flow?
+
+
+}
\ No newline at end of file
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/LinkedAccountPersister.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/LinkedAccountPersister.kt
new file mode 100644
index 000000000..caa123feb
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/LinkedAccountPersister.kt
@@ -0,0 +1,25 @@
+package co.nilin.opex.profile.core.spi
+
+import co.nilin.opex.profile.core.data.linkedbankAccount.*
+import kotlinx.coroutines.flow.Flow
+import reactor.core.publisher.Mono
+
+interface LinkedAccountPersister {
+ suspend fun addNewAccount(linkedBankAccountRequest: LinkedBankAccountRequest): Mono?
+
+ suspend fun updateAccount(updateRelatedAccountRequest: UpdateRelatedAccountRequest): Mono?
+
+
+ suspend fun getAccounts(userId: String): Flow?
+
+ suspend fun getOwner(accountNumber: String, partialSearch: Boolean?): Flow?
+
+
+ suspend fun getHistory(userId: String): Flow?
+
+
+ suspend fun verifyAccount(verifyRequest: VerifyLinkedAccountRequest): Mono?
+
+ suspend fun deleteAccount(deleteLinkedAccountRequest: DeleteLinkedAccountRequest): Mono?
+
+}
\ No newline at end of file
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/ProfileApprovalRequestPersister.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/ProfileApprovalRequestPersister.kt
new file mode 100644
index 000000000..d5cdfc05c
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/ProfileApprovalRequestPersister.kt
@@ -0,0 +1,14 @@
+package co.nilin.opex.profile.core.spi
+
+import co.nilin.opex.profile.core.data.profile.ProfileApprovalRequest
+import co.nilin.opex.profile.core.data.profile.ProfileApprovalRequestStatus
+import co.nilin.opex.profile.core.data.profile.ProfileApprovalResponse
+import kotlinx.coroutines.flow.Flow
+import reactor.core.publisher.Mono
+
+interface ProfileApprovalRequestPersister {
+ suspend fun save(request: ProfileApprovalRequest) : Mono
+ suspend fun getRequests(status : ProfileApprovalRequestStatus): Flow?
+ suspend fun getRequestById(id: Long): Mono
+ suspend fun update(request: ProfileApprovalResponse) :ProfileApprovalResponse
+}
\ No newline at end of file
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/ProfilePersister.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/ProfilePersister.kt
new file mode 100644
index 000000000..77f6b1775
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/ProfilePersister.kt
@@ -0,0 +1,22 @@
+package co.nilin.opex.profile.core.spi
+
+import co.nilin.opex.profile.core.data.kyc.KycLevel
+import co.nilin.opex.profile.core.data.profile.*
+import kotlinx.coroutines.flow.Flow
+import reactor.core.publisher.Mono
+
+interface ProfilePersister {
+
+ suspend fun updateProfile(id: String, data: UpdateProfileRequest): Mono
+ suspend fun completeProfile(id: String, data: CompleteProfileRequest): Mono
+ suspend fun updateProfileAsAdmin(id: String, data: Profile): Mono
+ suspend fun createProfile(data: Profile): Mono
+ suspend fun getProfile(userId: String): Mono?
+ suspend fun getProfile(id: Long): Mono?
+ suspend fun getAllProfile(offset: Int, size: Int, profileRequest: ProfileRequest): Flow?
+ suspend fun getHistory(userId: String, offset: Int, size: Int): List
+ suspend fun updateUserLevel(userId: String, userLevel: KycLevel)
+ suspend fun updateMobile(userId: String, mobile: String)
+ suspend fun updateEmail(userId: String, email: String)
+}
+
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/ShahkarInquiry.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/ShahkarInquiry.kt
new file mode 100644
index 000000000..9032799d5
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/ShahkarInquiry.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.profile.core.spi
+
+interface ShahkarInquiry {
+ suspend fun getInquiryResult(identifier: String, mobile: String): Boolean
+}
\ No newline at end of file
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/UserCreatedEventListener.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/UserCreatedEventListener.kt
new file mode 100644
index 000000000..d62f0789b
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/UserCreatedEventListener.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.profile.core.spi
+
+import co.nilin.opex.profile.core.data.event.UserCreatedEvent
+
+
+interface UserCreatedEventListener {
+ fun id(): String
+ fun onEvent(event: UserCreatedEvent, partition: Int, offset: Long, timestamp: Long, eventId: String)
+
+}
\ No newline at end of file
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/utils/Compare.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/utils/Compare.kt
new file mode 100644
index 000000000..95b9259e2
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/utils/Compare.kt
@@ -0,0 +1,20 @@
+package co.nilin.opex.profile.core.utils
+
+import kotlin.reflect.full.memberProperties
+
+fun Any.compare(s2: Any): List? {
+ val changedProperties: MutableList = ArrayList()
+ for (field in this::class.memberProperties) {
+// // You might want to set modifier to public first (if it is not public yet)
+// field.isAccessible = true
+ val value1: Any? = field.getter.call(this)
+ val value2: Any? = field.getter.call(s2)
+ // if (value1 != null && value2 != null) {
+ if (value1 != value2) {
+ if (!field.name.lowercase().contains("date"))
+ changedProperties.add(field.name)
+ }
+ // }
+ }
+ return changedProperties
+}
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/utils/Convertor.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/utils/Convertor.kt
new file mode 100644
index 000000000..2d4363bbc
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/utils/Convertor.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.profile.core.utils
+
+import com.google.gson.Gson
+
+fun Any.convert(classOfT: Class): T = Gson().fromJson(Gson().toJson(this), classOfT)
diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/utils/Validation.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/utils/Validation.kt
new file mode 100644
index 000000000..3ccf3818a
--- /dev/null
+++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/utils/Validation.kt
@@ -0,0 +1,61 @@
+package co.nilin.opex.profile.core.utils
+
+
+import java.math.BigInteger
+import java.util.Locale
+
+
+private val IBAN_VALIDATION_DIVISOR = BigInteger("97")
+private val IBAN_VALIDATION_REMAINDER = BigInteger("1")
+
+/**
+ * Check Bank card number validation
+ *
+ * @return return true if it is valid card number otherwise false
+ */
+fun CharSequence?.isValidCardNumber(): Boolean {
+ this ?: return false
+ if (!Regex("\\d{16}").matches(this))
+ return false
+ var sum = 0
+ for (i in indices) sum += try {
+ val character = get(i).toString().toInt()
+ if (i % 2 == 0) {
+ val temp = character * 2
+ if (temp > 9) temp - 9 else temp
+ } else character
+ } catch (e: NumberFormatException) {
+ return false
+ }
+ return sum % 10 == 0
+}
+
+/**
+ * Check Bank IBAN number validation
+ *
+ * @return return true if it is valid IBAN number otherwise false
+ */
+fun CharSequence?.isValidIBAN(): Boolean {
+ this ?: return false
+
+ val iban = if (!toString().toUpperCase(Locale.ENGLISH).startsWith("IR")) {
+ "IR$this"
+ } else {
+ this
+ }
+ if (iban.length != 26) return false
+ if (!iban.startsWith("IR")) return false
+ val cc = iban.substring(0, 2)
+ val cd = iban.substring(2, 4)
+ val bban = iban.substring(4, 26)
+ val ccFirstCharValue = getCountryCodeValue(cc[0])
+ val ccSecondCharValue = getCountryCodeValue(cc[1])
+ val newIBAN = BigInteger(bban + ccFirstCharValue + ccSecondCharValue + cd)
+ return newIBAN.mod(IBAN_VALIDATION_DIVISOR).compareTo(IBAN_VALIDATION_REMAINDER) == 0
+}
+
+private fun getCountryCodeValue(c: Char): Int {
+ return c - 'A' + 10
+}
+
+
diff --git a/profile/profile-core/src/main/resources/application.properties b/profile/profile-core/src/main/resources/application.properties
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/profile/profile-core/src/main/resources/application.properties
@@ -0,0 +1 @@
+
diff --git a/profile/profile-core/src/test/kotlin/co/nilin/opex/profile/core/ProfileCoreApplicationTests.kt b/profile/profile-core/src/test/kotlin/co/nilin/opex/profile/core/ProfileCoreApplicationTests.kt
new file mode 100644
index 000000000..25cf38ccf
--- /dev/null
+++ b/profile/profile-core/src/test/kotlin/co/nilin/opex/profile/core/ProfileCoreApplicationTests.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.profile.core
+
+import org.junit.jupiter.api.Test
+import org.springframework.boot.test.context.SpringBootTest
+
+//@SpringBootTest
+//class ProfileCoreApplicationTests {
+//
+// @Test
+// fun contextLoads() {
+// }
+//
+//}
diff --git a/profile/profile-ports/profile-eventlistener-kafka/.gitignore b/profile/profile-ports/profile-eventlistener-kafka/.gitignore
new file mode 100644
index 000000000..549e00a2a
--- /dev/null
+++ b/profile/profile-ports/profile-eventlistener-kafka/.gitignore
@@ -0,0 +1,33 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
diff --git a/profile/profile-ports/profile-eventlistener-kafka/pom.xml b/profile/profile-ports/profile-eventlistener-kafka/pom.xml
new file mode 100644
index 000000000..a6077d0ef
--- /dev/null
+++ b/profile/profile-ports/profile-eventlistener-kafka/pom.xml
@@ -0,0 +1,45 @@
+
+
+ 4.0.0
+
+ co.nilin.opex
+ profile
+ 1.0.1-beta.7
+ ../../pom.xml
+
+ co.nilin.opex.profile.ports
+ profile-eventlistener-kafka
+ profile-eventlistener-kafka
+ profile-kafka
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ co.nilin.opex.profile
+ profile-core
+
+
+
+
+
diff --git a/profile/profile-ports/profile-eventlistener-kafka/src/main/kotlin/co/nilin/opex/profile/ports/kafka/config/KafkaListenerConfig.kt b/profile/profile-ports/profile-eventlistener-kafka/src/main/kotlin/co/nilin/opex/profile/ports/kafka/config/KafkaListenerConfig.kt
new file mode 100644
index 000000000..0af20064b
--- /dev/null
+++ b/profile/profile-ports/profile-eventlistener-kafka/src/main/kotlin/co/nilin/opex/profile/ports/kafka/config/KafkaListenerConfig.kt
@@ -0,0 +1,110 @@
+package co.nilin.opex.profile.ports.kafka.config
+
+
+import co.nilin.opex.profile.core.data.event.KycLevelUpdatedEvent
+import co.nilin.opex.profile.core.data.event.UserCreatedEvent
+import co.nilin.opex.profile.ports.kafka.consumer.KycLevelUpdatedKafkaListener
+import co.nilin.opex.profile.ports.kafka.consumer.UserCreatedKafkaListener
+import org.apache.kafka.clients.consumer.ConsumerConfig
+import org.apache.kafka.common.TopicPartition
+import org.apache.kafka.common.serialization.StringDeserializer
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.kafka.core.ConsumerFactory
+import org.springframework.kafka.core.DefaultKafkaConsumerFactory
+import org.springframework.kafka.core.DefaultKafkaProducerFactory
+import org.springframework.kafka.core.KafkaTemplate
+import org.springframework.kafka.core.ProducerFactory
+import org.springframework.kafka.listener.*
+import org.springframework.kafka.support.serializer.JsonDeserializer
+import org.springframework.util.backoff.FixedBackOff
+import java.util.regex.Pattern
+
+@Configuration
+class KafkaListenerConfig {
+ private val logger = LoggerFactory.getLogger(KafkaListenerConfig::class.java)
+
+ @Value("\${spring.kafka.bootstrap-servers}")
+ private lateinit var bootstrapServers: String
+
+ @Value("\${spring.kafka.consumer.group-id}")
+ private lateinit var groupId: String
+
+ @Bean("consumerConfigs")
+ fun consumerConfigs(): Map {
+
+ return mapOf(
+ ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers,
+ ConsumerConfig.GROUP_ID_CONFIG to groupId,
+ ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java,
+ ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to JsonDeserializer::class.java,
+ JsonDeserializer.TRUSTED_PACKAGES to "co.nilin.opex.*",
+ JsonDeserializer.TYPE_MAPPINGS to "userCreatedEvent:co.nilin.opex.profile.core.data.event.UserCreatedEvent,kyc_level_updated_event:co.nilin.opex.profile.core.data.event.KycLevelUpdatedEvent"
+ )
+ }
+
+
+ @Bean("profileConsumerFactory")
+ fun consumerFactory(@Qualifier("consumerConfigs") consumerConfigs: Map): ConsumerFactory {
+ return DefaultKafkaConsumerFactory(consumerConfigs)
+ }
+ @Bean("profileProducerFactory")
+ fun producerFactory(@Qualifier("consumerConfigs") producerConfigs: Map): ProducerFactory {
+ return DefaultKafkaProducerFactory(producerConfigs)
+ }
+
+ @Bean("profileKafkaTemplate")
+ fun kafkaTemplate(@Qualifier("profileProducerFactory") producerFactory: ProducerFactory): KafkaTemplate {
+ return KafkaTemplate(producerFactory)
+ }
+
+ @Bean("kycConsumerFactory")
+ fun kycConsumerFactory(@Qualifier("consumerConfigs") consumerConfigs: Map): ConsumerFactory {
+ return DefaultKafkaConsumerFactory(consumerConfigs)
+ }
+
+ @Autowired
+ @ConditionalOnBean(UserCreatedKafkaListener::class)
+ fun configureUserCreatedListener(
+ listener: UserCreatedKafkaListener,
+ @Qualifier("profileKafkaTemplate") template: KafkaTemplate,
+ @Qualifier("profileConsumerFactory") consumerFactory: ConsumerFactory
+ ) {
+ val containerProps = ContainerProperties(Pattern.compile("auth"))
+ containerProps.messageListener = listener
+ val container = ConcurrentMessageListenerContainer(consumerFactory, containerProps)
+ container.setBeanName("UserCreatedKafkaListenerContainer")
+ container.commonErrorHandler = createConsumerErrorHandler(template, "auth.DLT")
+ container.start()
+ }
+
+
+ @Autowired
+ @ConditionalOnBean(KycLevelUpdatedKafkaListener::class)
+ fun configureKycLevelUpdatedListener(
+ listener: KycLevelUpdatedKafkaListener,
+ template: KafkaTemplate,
+ @Qualifier("kycConsumerFactory") consumerFactory: ConsumerFactory
+ ) {
+ val containerProps = ContainerProperties(Pattern.compile("kyc_level_updated"))
+ containerProps.messageListener = listener
+ val container = ConcurrentMessageListenerContainer(consumerFactory, containerProps)
+ container.setBeanName("KycLevelUpdatedKafkaListenerContainer")
+ container.commonErrorHandler = createConsumerErrorHandler(template, "kyc_level_updated.DLT")
+ container.start()
+ }
+
+ private fun createConsumerErrorHandler(kafkaTemplate: KafkaTemplate<*, *>, dltTopic: String): CommonErrorHandler {
+ val recoverer = DeadLetterPublishingRecoverer(kafkaTemplate) { cr, _ ->
+ cr.headers().add("dlt-origin-module", "PROFILE".toByteArray())
+ TopicPartition(dltTopic, cr.partition())
+ }
+ return DefaultErrorHandler(recoverer, FixedBackOff(5_000, 20))
+ }
+
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-eventlistener-kafka/src/main/kotlin/co/nilin/opex/profile/ports/kafka/consumer/KycLevelUpdatedKafkaListener.kt b/profile/profile-ports/profile-eventlistener-kafka/src/main/kotlin/co/nilin/opex/profile/ports/kafka/consumer/KycLevelUpdatedKafkaListener.kt
new file mode 100644
index 000000000..12f0dc59a
--- /dev/null
+++ b/profile/profile-ports/profile-eventlistener-kafka/src/main/kotlin/co/nilin/opex/profile/ports/kafka/consumer/KycLevelUpdatedKafkaListener.kt
@@ -0,0 +1,34 @@
+package co.nilin.opex.profile.ports.kafka.consumer
+
+import co.nilin.opex.profile.core.data.event.KycLevelUpdatedEvent
+import co.nilin.opex.profile.core.spi.KycLevelUpdatedEventListener
+import org.apache.kafka.clients.consumer.ConsumerRecord
+import org.slf4j.LoggerFactory
+import org.springframework.kafka.listener.MessageListener
+import org.springframework.stereotype.Component
+
+@Component
+class KycLevelUpdatedKafkaListener : MessageListener {
+ val eventListeners = arrayListOf()
+ private val logger = LoggerFactory.getLogger(KycLevelUpdatedKafkaListener::class.java)
+ override fun onMessage(data: ConsumerRecord) {
+
+ eventListeners.forEach { tl ->
+ logger.info("incoming new event " + tl.id())
+ tl.onEvent(data.value(), data.partition(), data.offset(), data.timestamp(), tl.id())
+ }
+ }
+
+ fun addEventListener(tl: KycLevelUpdatedEventListener) {
+ eventListeners.add(tl)
+ }
+
+ fun removeEventListener(tl: KycLevelUpdatedEventListener) {
+ eventListeners.removeIf { item ->
+ item.id() == tl.id()
+ }
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-eventlistener-kafka/src/main/kotlin/co/nilin/opex/profile/ports/kafka/consumer/UserCreatedKafkaListener.kt b/profile/profile-ports/profile-eventlistener-kafka/src/main/kotlin/co/nilin/opex/profile/ports/kafka/consumer/UserCreatedKafkaListener.kt
new file mode 100644
index 000000000..4aab935e1
--- /dev/null
+++ b/profile/profile-ports/profile-eventlistener-kafka/src/main/kotlin/co/nilin/opex/profile/ports/kafka/consumer/UserCreatedKafkaListener.kt
@@ -0,0 +1,32 @@
+package co.nilin.opex.profile.ports.kafka.consumer
+
+
+import co.nilin.opex.profile.core.spi.UserCreatedEventListener
+import org.apache.kafka.clients.consumer.ConsumerRecord
+import org.slf4j.LoggerFactory
+import org.springframework.kafka.listener.MessageListener
+import org.springframework.stereotype.Component
+import co.nilin.opex.profile.core.data.event.UserCreatedEvent
+
+@Component
+class UserCreatedKafkaListener : MessageListener {
+ val eventListeners = arrayListOf()
+ private val logger = LoggerFactory.getLogger(UserCreatedKafkaListener::class.java)
+ override fun onMessage(data: ConsumerRecord) {
+
+ eventListeners.forEach { tl ->
+ logger.info("incoming new event " + tl.id())
+ tl.onEvent(data.value(), data.partition(), data.offset(), data.timestamp(), tl.id())
+ }
+ }
+
+ fun addEventListener(tl: UserCreatedEventListener) {
+ eventListeners.add(tl)
+ }
+
+ fun removeEventListener(tl: UserCreatedEventListener) {
+ eventListeners.removeIf { item ->
+ item.id() == tl.id()
+ }
+ }
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-eventlistener-kafka/src/main/resources/application.properties b/profile/profile-ports/profile-eventlistener-kafka/src/main/resources/application.properties
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/profile/profile-ports/profile-eventlistener-kafka/src/main/resources/application.properties
@@ -0,0 +1 @@
+
diff --git a/profile/profile-ports/profile-kyc-proxy/.gitignore b/profile/profile-ports/profile-kyc-proxy/.gitignore
new file mode 100644
index 000000000..549e00a2a
--- /dev/null
+++ b/profile/profile-ports/profile-kyc-proxy/.gitignore
@@ -0,0 +1,33 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
diff --git a/profile/profile-ports/profile-kyc-proxy/pom.xml b/profile/profile-ports/profile-kyc-proxy/pom.xml
new file mode 100644
index 000000000..caefb1761
--- /dev/null
+++ b/profile/profile-ports/profile-kyc-proxy/pom.xml
@@ -0,0 +1,43 @@
+
+
+ 4.0.0
+
+ co.nilin.opex
+ profile
+ 1.0.1-beta.7
+ ../../pom.xml
+
+ co.nilin.opex.profile.ports
+ profile-kyc-proxy
+ profile-kyc-proxy
+ profile-kyc-proxy
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ co.nilin.opex.profile
+ profile-core
+
+
+
+
+
diff --git a/profile/profile-ports/profile-kyc-proxy/src/main/kotlin/co/nilin/opex/profile/ports/kyc/imp/KycProxyImp.kt b/profile/profile-ports/profile-kyc-proxy/src/main/kotlin/co/nilin/opex/profile/ports/kyc/imp/KycProxyImp.kt
new file mode 100644
index 000000000..983ef438e
--- /dev/null
+++ b/profile/profile-ports/profile-kyc-proxy/src/main/kotlin/co/nilin/opex/profile/ports/kyc/imp/KycProxyImp.kt
@@ -0,0 +1,37 @@
+package co.nilin.opex.profile.ports.kyc.imp
+
+import co.nilin.opex.profile.core.data.kyc.ManualUpdateRequest
+import co.nilin.opex.profile.core.spi.KycProxy
+import kotlinx.coroutines.reactive.awaitFirst
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.core.ParameterizedTypeReference
+import org.springframework.http.HttpStatus
+import org.springframework.http.MediaType
+import org.springframework.stereotype.Component
+import org.springframework.web.reactive.function.BodyInserters
+import org.springframework.web.reactive.function.client.WebClient
+import org.springframework.web.reactive.function.client.bodyToMono
+import java.net.URI
+
+inline fun typeRef(): ParameterizedTypeReference = object : ParameterizedTypeReference() {}
+
+@Component
+class KycProxyImp(@Qualifier("loadBalanced") private val webClient: WebClient) : KycProxy {
+ @Value("\${app.kyc.url}")
+ private lateinit var baseUrl: String
+ private val logger = LoggerFactory.getLogger(KycProxyImp::class.java)
+
+
+ override suspend fun updateKycLevel(updateKycLevelRequest: ManualUpdateRequest) {
+ webClient.put()
+ .uri(URI.create("$baseUrl/${updateKycLevelRequest.userId}"))
+ .contentType(MediaType.APPLICATION_JSON)
+ .body(BodyInserters.fromValue(updateKycLevelRequest))
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToMono()
+ .awaitFirst()
+ }
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-kyc-proxy/src/main/resources/application.properties b/profile/profile-ports/profile-kyc-proxy/src/main/resources/application.properties
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/profile/profile-ports/profile-kyc-proxy/src/main/resources/application.properties
@@ -0,0 +1 @@
+
diff --git a/profile/profile-ports/profile-kyc-proxy/src/test/kotlin/co/nilin/opex/profile/ports/kafka/ProfilePostgressApplicationTests.kt b/profile/profile-ports/profile-kyc-proxy/src/test/kotlin/co/nilin/opex/profile/ports/kafka/ProfilePostgressApplicationTests.kt
new file mode 100644
index 000000000..577be1be0
--- /dev/null
+++ b/profile/profile-ports/profile-kyc-proxy/src/test/kotlin/co/nilin/opex/profile/ports/kafka/ProfilePostgressApplicationTests.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.profile.ports.kafka
+
+//@SpringBootTest
+//class ProfilePostgressApplicationTests {
+//
+// @Test
+// fun contextLoads() {
+// }
+//
+//}
diff --git a/profile/profile-ports/profile-postgres/.gitignore b/profile/profile-ports/profile-postgres/.gitignore
new file mode 100644
index 000000000..549e00a2a
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/.gitignore
@@ -0,0 +1,33 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
diff --git a/profile/profile-ports/profile-postgres/pom.xml b/profile/profile-ports/profile-postgres/pom.xml
new file mode 100644
index 000000000..bc2e91dc9
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/pom.xml
@@ -0,0 +1,65 @@
+
+
+ 4.0.0
+
+ co.nilin.opex
+ profile
+ 1.0.1-beta.7
+ ../../pom.xml
+
+ co.nilin.opex.profile.ports
+ profile-postgres
+ profile-postgres
+ profile-postgres
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-r2dbc
+
+
+ org.postgresql
+ r2dbc-postgresql
+ runtime
+
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ co.nilin.opex.profile
+ profile-core
+
+
+ co.nilin.opex.profile.ports
+ profile-kyc-proxy
+
+
+ co.nilin.opex.profile.ports
+ profile-shahkar-proxy
+
+
+
+
+
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/config/PostgresConfig.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/config/PostgresConfig.kt
new file mode 100644
index 000000000..fa2c85d03
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/config/PostgresConfig.kt
@@ -0,0 +1,25 @@
+package co.nilin.opex.profile.ports.postgres.config
+
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.io.Resource
+import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories
+import org.springframework.r2dbc.core.DatabaseClient
+
+@Configuration
+@EnableR2dbcRepositories(basePackages = ["co.nilin.opex"])
+
+class PostgresConfig(
+ db: DatabaseClient,
+ @Value("classpath:schema.sql") private val schemaResource: Resource
+) {
+ init {
+ val schemaReader = schemaResource.inputStream.reader()
+ val schema = schemaReader.readText().trim()
+ schemaReader.close()
+ val initDb = db.sql { schema }
+ initDb // initialize the database
+ .then()
+ .subscribe() // execute
+ }
+}
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/convertor/ProfileConvertor.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/convertor/ProfileConvertor.kt
new file mode 100644
index 000000000..aa1478210
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/convertor/ProfileConvertor.kt
@@ -0,0 +1,25 @@
+package co.nilin.opex.profile.ports.postgres.convertor
+
+import co.nilin.opex.profile.core.data.profile.CompleteProfileResponse
+import co.nilin.opex.profile.ports.postgres.model.entity.ProfileModel
+
+fun convertProfileModelToCompleteProfileResponse(profileModel: ProfileModel): CompleteProfileResponse {
+ return CompleteProfileResponse(
+ id = profileModel.id,
+ email = profileModel.email,
+ userId = profileModel.userId,
+ firstName = profileModel.firstName,
+ lastName = profileModel.lastName,
+ address = profileModel.address,
+ mobile = profileModel.mobile,
+ telephone = profileModel.telephone,
+ postalCode = profileModel.postalCode,
+ nationality = profileModel.nationality,
+ identifier = profileModel.identifier,
+ gender = profileModel.gender,
+ birthDate = profileModel.birthDate,
+ status = profileModel.status,
+ kycLevel = profileModel.kycLevel,
+ verificationStatus = profileModel.verificationStatus
+ )
+}
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/LimitationHistoryRepository.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/LimitationHistoryRepository.kt
new file mode 100644
index 000000000..ece2e5595
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/LimitationHistoryRepository.kt
@@ -0,0 +1,17 @@
+package co.nilin.opex.profile.ports.postgres.dao
+
+import co.nilin.opex.profile.core.data.limitation.ActionType
+import co.nilin.opex.profile.core.data.limitation.LimitationReason
+import co.nilin.opex.profile.ports.postgres.model.history.LimitationHistory
+import kotlinx.coroutines.flow.Flow
+import org.springframework.data.domain.Pageable
+import org.springframework.data.r2dbc.repository.Query
+import org.springframework.data.repository.reactive.ReactiveCrudRepository
+import org.springframework.stereotype.Repository
+
+@Repository
+interface LimitationHistoryRepository : ReactiveCrudRepository {
+ @Query("select * from limitation_history l where (:userId is NULL or l.user_id= :userId) And (:action is NULL or l.action_type=:action) And (:reason is NULL or l.reason=:reason) OFFSET :offset LIMIT :size; ")
+ fun findAllLimitationHistory(userId: String?, action: ActionType?, reason: LimitationReason?, offset: Int, size: Int, pageable: Pageable): Flow?
+
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/LimitationRepository.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/LimitationRepository.kt
new file mode 100644
index 000000000..66c182b5c
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/LimitationRepository.kt
@@ -0,0 +1,25 @@
+package co.nilin.opex.profile.ports.postgres.dao
+
+import co.nilin.opex.profile.core.data.limitation.ActionType
+import co.nilin.opex.profile.core.data.limitation.LimitationReason
+import co.nilin.opex.profile.ports.postgres.model.entity.LimitationModel
+import kotlinx.coroutines.flow.Flow
+import org.springframework.data.domain.Pageable
+import org.springframework.data.r2dbc.repository.Query
+import org.springframework.data.repository.reactive.ReactiveCrudRepository
+import org.springframework.stereotype.Repository
+import reactor.core.publisher.Mono
+
+@Repository
+interface LimitationRepository : ReactiveCrudRepository {
+ fun findByLimitationOn(data: String): Mono?
+ fun deleteByLimitationOn(data: String): Mono
+ fun deleteByUserId(userId: String): Mono
+
+ fun deleteByActionType(actionType: ActionType): Mono
+
+ @Query("select * from limitation l where (:userId is NULL or l.user_id= :userId) And (:action is NULL or l.action_type=:action) And (:reason is NULL or l.reason=:reason) OFFSET :offset LIMIT :size; ")
+ fun findAllLimitation(userId: String?, action: ActionType?, reason: LimitationReason?, offset: Int, size: Int, pageable: Pageable): Flow?
+
+
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/LinkedAccountHistoryRepository.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/LinkedAccountHistoryRepository.kt
new file mode 100644
index 000000000..4af1cf80c
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/LinkedAccountHistoryRepository.kt
@@ -0,0 +1,16 @@
+package co.nilin.opex.profile.ports.postgres.dao
+
+import co.nilin.opex.profile.ports.postgres.model.history.LinkedBankAccountHistory
+import kotlinx.coroutines.flow.Flow
+import org.springframework.data.repository.reactive.ReactiveCrudRepository
+import org.springframework.stereotype.Repository
+
+@Repository
+interface LinkedAccountHistoryRepository : ReactiveCrudRepository {
+
+ fun findAllByUserId(userId: String): Flow?
+
+ fun findAllByAccountId(accountId: String): Flow?
+
+
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/LinkedAccountRepository.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/LinkedAccountRepository.kt
new file mode 100644
index 000000000..7acbf2a94
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/LinkedAccountRepository.kt
@@ -0,0 +1,28 @@
+package co.nilin.opex.profile.ports.postgres.dao
+
+import co.nilin.opex.profile.ports.postgres.model.entity.LinkedBankAccountModel
+import kotlinx.coroutines.flow.Flow
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.data.r2dbc.repository.Query
+import org.springframework.data.repository.reactive.ReactiveCrudRepository
+import org.springframework.stereotype.Repository
+import reactor.core.publisher.Mono
+
+@Repository
+interface LinkedAccountRepository : ReactiveCrudRepository {
+
+ fun findAllByUserIdAndAccountId(userId: String, accountId: String): Mono?
+
+ fun findAllByUserId(userId: String): Flow?
+
+ fun findAllByNumber(accountNumber: String): Flow?
+
+ @Query("select * from linked_bank_account lbc where position(lower(:accountNumber) in lower(lbc.number))>0 ")
+ fun searchAllByNumber(accountNumber: String): Flow?
+
+
+ fun findByAccountId(accountId: String): Mono?
+
+ fun deleteByAccountIdAndUserId(accountId: String, userId: String): Mono
+
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/ProfileApprovalRequestRepository.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/ProfileApprovalRequestRepository.kt
new file mode 100644
index 000000000..54bc17d38
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/ProfileApprovalRequestRepository.kt
@@ -0,0 +1,16 @@
+package co.nilin.opex.profile.ports.postgres.dao
+
+import co.nilin.opex.profile.core.data.profile.ProfileApprovalRequestStatus
+import co.nilin.opex.profile.ports.postgres.model.entity.ProfileApprovalRequestModel
+import kotlinx.coroutines.flow.Flow
+import org.springframework.data.r2dbc.repository.Query
+import org.springframework.data.repository.reactive.ReactiveCrudRepository
+import reactor.core.publisher.Mono
+
+interface ProfileApprovalRequestRepository : ReactiveCrudRepository {
+
+ fun findByProfileIdAndStatus(profileId: Long, status: ProfileApprovalRequestStatus): Mono
+
+ @Query("select * from profile_approval_request p where p.status = :status order by create_date desc")
+ fun findByStatus(status: ProfileApprovalRequestStatus): Flow?
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/ProfileHistoryRepository.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/ProfileHistoryRepository.kt
new file mode 100644
index 000000000..437478a17
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/ProfileHistoryRepository.kt
@@ -0,0 +1,12 @@
+package co.nilin.opex.profile.ports.postgres.dao
+
+import co.nilin.opex.profile.ports.postgres.model.history.ProfileHistory
+import kotlinx.coroutines.flow.Flow
+import org.springframework.data.domain.Pageable
+import org.springframework.data.repository.reactive.ReactiveCrudRepository
+import org.springframework.stereotype.Repository
+
+@Repository
+interface ProfileHistoryRepository : ReactiveCrudRepository {
+ fun findByUserId(userId: String, pageable: Pageable): Flow
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/ProfileRepository.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/ProfileRepository.kt
new file mode 100644
index 000000000..c4c512299
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/ProfileRepository.kt
@@ -0,0 +1,51 @@
+package co.nilin.opex.profile.ports.postgres.dao
+
+import co.nilin.opex.profile.ports.postgres.model.entity.ProfileModel
+import kotlinx.coroutines.flow.Flow
+import org.springframework.data.domain.Pageable
+import org.springframework.data.r2dbc.repository.Query
+import org.springframework.data.repository.reactive.ReactiveCrudRepository
+import org.springframework.stereotype.Repository
+import reactor.core.publisher.Mono
+import java.time.LocalDateTime
+
+@Repository
+interface ProfileRepository : ReactiveCrudRepository {
+
+ fun findByUserId(userId: String): Mono?
+
+ fun findBy(pageable: Pageable): Flow
+
+ @Query("select * from profile p where (:userId is null or p.user_id=:userId ) " +
+ "and (:mobile is null or p.mobile=:mobile )" +
+ " and (:email is null or p.email=:email )" +
+ " and (:firstName is null or p.first_name=:firstName )" +
+ " and (:lastName is null or p.last_name=:lastName )" +
+ " and (:nationalCode is null or p.identifier=:nationalCode )" +
+ " and (:createDateFrom is null or p.create_date > :createDateFrom )" +
+ " and (:createDateTo is null or p.create_date < :createDateTo ) ")
+
+ fun findUsersBy(userId: String?, mobile: String?, email: String?, firstName: String?, lastName: String?, nationalCode: String?, createDateFrom: LocalDateTime?, createDateTo: LocalDateTime?, pageable: Pageable): Flow?
+
+ @Query("select * from profile p where (:userId is null or position(lower(:userId) in lower(p.user_id))>0 ) " +
+ "and (:mobile is null or position(lower(:mobile) in lower(p.mobile))>0 )" +
+ " and (:email is null or position(lower(:email) in lower(p.email))>0 )" +
+ " and (:firstName is null or position(lower(:firstName) in lower(p.first_name))>0 )" +
+ " and (:lastName is null or position(lower(:lastName) in lower(p.last_name))>0 )" +
+ " and (:nationalCode is null or position(lower(:nationalCode) in lower(p.identifier))>0 )" +
+ " and (:createDateFrom is null or p.create_date > :createDateFrom )" +
+ " and (:createDateTo is null or p.create_date < :createDateTo ) ")
+
+ fun searchUsersBy(userId: String?, mobile: String?, email: String?, firstName: String?, lastName: String?, nationalCode: String?, createDateFrom: LocalDateTime?, createDateTo: LocalDateTime?, pageable: Pageable): Flow?
+
+
+ @Query("""
+ SELECT * FROM profile
+ WHERE user_id = :userId
+ OR ( :mobile IS NOT NULL AND mobile = :mobile )
+ OR ( :email IS NOT NULL AND lower(email) = lower(:email) )
+ """)
+ fun findByUserIdOrEmailOrMobile(userId: String, email: String? , mobile : String?): Mono?
+
+
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/LimitationManagementImp.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/LimitationManagementImp.kt
new file mode 100644
index 000000000..4973cda25
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/LimitationManagementImp.kt
@@ -0,0 +1,131 @@
+package co.nilin.opex.profile.ports.postgres.imp
+
+import co.nilin.opex.common.OpexError
+import co.nilin.opex.profile.core.data.limitation.*
+import co.nilin.opex.profile.core.spi.LimitationPersister
+import co.nilin.opex.profile.ports.postgres.dao.LimitationHistoryRepository
+import co.nilin.opex.profile.ports.postgres.dao.ProfileRepository
+import co.nilin.opex.profile.ports.postgres.dao.LimitationRepository
+import co.nilin.opex.profile.ports.postgres.model.entity.LimitationModel
+import co.nilin.opex.profile.core.utils.convert
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.reactive.awaitFirstOrNull
+import org.slf4j.LoggerFactory
+import org.springframework.data.domain.PageRequest
+import org.springframework.data.domain.Sort
+import org.springframework.stereotype.Service
+import org.springframework.transaction.annotation.Transactional
+import java.time.LocalDateTime
+
+@Service
+class LimitationManagementImp(
+ private var limitationRepository: LimitationRepository,
+ private var profileRepository: ProfileRepository,
+ private var limitationHistoryRepository: LimitationHistoryRepository
+) : LimitationPersister {
+ private val logger = LoggerFactory.getLogger(LimitationManagementImp::class.java)
+
+ @Transactional
+ override suspend fun updateLimitation(updatePermissionRequest: UpdateLimitationRequest) {
+ var BreakException = {};
+
+ //is there particular user? yes
+ updatePermissionRequest.userId?.let {
+ profileRepository.findByUserId(updatePermissionRequest.userId!!)?.awaitFirstOrNull()
+ ?.let {
+ //set limitations for specific user on some actions
+ logger.info("set limitations for specific user on some actions")
+ updatePermissionRequest.actions?.forEach {
+ if (updatePermissionRequest.updateType == LimitationUpdateType.Revoke) {
+ limitationRepository.findByLimitationOn(updatePermissionRequest.userId + "_$it")
+ ?.awaitFirstOrNull()
+ ?: run {
+ with(updatePermissionRequest) {
+ var limit = updatePermissionRequest.convert(LimitationModel::class.java)
+ limit.actionType = it
+ limit.limitationOn = userId + "_$it"
+ limit.createDate = LocalDateTime.now()
+ limitationRepository.save(limit).awaitFirstOrNull()
+
+ }
+ }
+ } else {
+ logger.info("reset limitations for specific user on some actions")
+ //reset limitations for specific user on some actions/all
+ if (updatePermissionRequest.actions?.contains(ActionType.All) == true) {
+ limitationRepository.deleteByUserId(updatePermissionRequest.userId!!).awaitFirstOrNull()
+ //todo break
+ }
+ limitationRepository.deleteByLimitationOn(updatePermissionRequest.userId + "_$it")
+ .awaitFirstOrNull()
+ }
+ }
+ } ?: throw OpexError.UserNotFound.exception()
+ //is there particular user? no
+ } ?: run {
+ //set limitations for all users on some actions
+ logger.info("set limitations for all users on some actions")
+ updatePermissionRequest.actions?.forEach {
+ if (updatePermissionRequest.updateType == LimitationUpdateType.Revoke) {
+ limitationRepository.findByLimitationOn("All_$it")?.awaitFirstOrNull()
+ ?: run {
+ with(updatePermissionRequest) {
+ var limit = updatePermissionRequest.convert(LimitationModel::class.java)
+ limit.userId = "All"
+ limit.actionType = it
+ limit.limitationOn = "All_$it"
+ limit.createDate = LocalDateTime.now()
+ limitationRepository.save(limit).awaitFirstOrNull()
+ }
+ }
+ } else {
+ //reset limitations for all users on some actions/all
+ logger.info("reset limitations for all users on some actions")
+ if (updatePermissionRequest.actions?.contains(ActionType.All) == true) {
+ limitationRepository.deleteAll().awaitFirstOrNull()
+ //break
+ }
+ limitationRepository.deleteByActionType(it).awaitFirstOrNull()
+ }
+ }
+ }
+
+ }
+
+ override suspend fun getLimitation(
+ userId: String?,
+ action: ActionType?,
+ reason: LimitationReason?,
+ offset: Int?,
+ size: Int?
+ ): Flow? {
+ return limitationRepository.findAllLimitation(
+ userId,
+ action,
+ reason,
+ offset!!,
+ size!!,
+ PageRequest.of(offset, size, Sort.by(Sort.Direction.DESC, "id"))
+ )?.map { l -> l.convert(Limitation::class.java) }
+ }
+
+
+ override suspend fun getLimitationHistory(
+ userId: String?,
+ action: ActionType?,
+ reason: LimitationReason?,
+ offset: Int,
+ size: Int
+ ): Flow? {
+ return limitationHistoryRepository.findAllLimitationHistory(
+ userId,
+ action,
+ reason,
+ offset,
+ size,
+ PageRequest.of(offset, size, Sort.by(Sort.Direction.DESC, "id"))
+ )?.map { l -> l.convert(LimitationHistory::class.java) }
+ }
+
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/LinkAccountManagementImp.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/LinkAccountManagementImp.kt
new file mode 100644
index 000000000..393afb6a1
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/LinkAccountManagementImp.kt
@@ -0,0 +1,102 @@
+package co.nilin.opex.profile.ports.postgres.imp
+
+import co.nilin.opex.common.OpexError
+import co.nilin.opex.profile.core.data.linkedbankAccount.*
+import co.nilin.opex.profile.ports.postgres.utils.convert
+import co.nilin.opex.profile.core.spi.LinkedAccountPersister
+import co.nilin.opex.profile.ports.postgres.dao.LinkedAccountHistoryRepository
+import co.nilin.opex.profile.ports.postgres.dao.LinkedAccountRepository
+import co.nilin.opex.profile.ports.postgres.dao.ProfileRepository
+import co.nilin.opex.profile.ports.postgres.model.entity.LinkedBankAccountModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.reactive.awaitFirstOrNull
+import org.slf4j.LoggerFactory
+import org.springframework.stereotype.Component
+import reactor.core.publisher.Mono
+import java.time.LocalDateTime
+import java.util.*
+
+@Component
+class LinkAccountManagementImp(
+ val linkedAccountRepository: LinkedAccountRepository,
+ val profileRepository: ProfileRepository,
+ val linkedAccountHistoryRepository: LinkedAccountHistoryRepository
+) : LinkedAccountPersister {
+ private val logger = LoggerFactory.getLogger(LinkAccountManagementImp::class.java)
+
+ override suspend fun addNewAccount(linkedBankAccountRequest: LinkedBankAccountRequest): Mono {
+ return profileRepository.findByUserId(linkedBankAccountRequest.userId!!)?.let {
+ linkedAccountRepository.save(linkedBankAccountRequest.convert(LinkedBankAccountModel::class.java).apply {
+ enabled = true
+ verified = false
+ accountId = UUID.randomUUID().toString()
+ registerDate = LocalDateTime.now()
+ }).doOnError { throw OpexError.DuplicateAccount.exception() }
+ .map { d -> d.convert(LinkedAccountResponse::class.java) }
+ } ?: throw OpexError.UserNotFound.exception()
+ }
+
+ override suspend fun updateAccount(updateRelatedAccountRequest: UpdateRelatedAccountRequest): Mono? {
+ return linkedAccountRepository.findAllByUserIdAndAccountId(
+ updateRelatedAccountRequest.userId!!,
+ updateRelatedAccountRequest.accountId!!
+ )?.awaitFirstOrNull()
+ ?.let { d ->
+ d.enabled = updateRelatedAccountRequest.status == Status.Enable
+ linkedAccountRepository.save(d)
+
+ }?.map { d -> d.convert(LinkedAccountResponse::class.java) }
+ ?: throw OpexError.InvalidLinkedAccount.exception()
+ }
+
+ override suspend fun getOwner(accountNumber: String, partialSearch: Boolean?): Flow? {
+ if (partialSearch == false) {
+ logger.info("==========================$accountNumber")
+ return linkedAccountRepository.findAllByNumber(accountNumber)
+ ?.map { d -> d.convert(LinkedAccountResponse::class.java) }
+
+ } else {
+ logger.info("==========------------==========$accountNumber")
+
+ return linkedAccountRepository.searchAllByNumber(accountNumber)
+ ?.map { d -> d.convert(LinkedAccountResponse::class.java) }
+ }
+ }
+
+ override suspend fun getAccounts(userId: String): Flow? {
+ return profileRepository.findByUserId(userId)?.awaitFirstOrNull()?.let {
+ linkedAccountRepository.findAllByUserId(userId)?.map { d -> d.convert(LinkedAccountResponse::class.java) }
+ } ?: throw OpexError.UserNotFound.exception()
+ }
+
+ override suspend fun getHistory(userId: String): Flow? {
+ return linkedAccountHistoryRepository.findAllByAccountId(userId)
+ ?.map { d -> d.convert(LinkedAccountHistoryResponse::class.java) }
+ }
+
+ override suspend fun verifyAccount(verifyRequest: VerifyLinkedAccountRequest): Mono? {
+ return linkedAccountRepository.save(linkedAccountRepository.findByAccountId(verifyRequest.accountId!!)
+ ?.awaitFirstOrNull()?.let { d ->
+ d.apply {
+ verified = verifyRequest.verified
+ verifier = verifyRequest.verifier
+ description = verifyRequest.description
+ }
+ }
+ ?: throw OpexError.AccountNotFound.exception())?.map { d -> d?.convert(LinkedAccountResponse::class.java) }
+ }
+
+ override suspend fun deleteAccount(deleteLinkedAccountRequest: DeleteLinkedAccountRequest): Mono? {
+
+ return linkedAccountRepository.findAllByUserIdAndAccountId(
+ deleteLinkedAccountRequest.userId,
+ deleteLinkedAccountRequest.accountId
+ )?.awaitFirstOrNull()?.let {
+ linkedAccountRepository.deleteByAccountIdAndUserId(
+ deleteLinkedAccountRequest.accountId,
+ deleteLinkedAccountRequest.userId
+ ).awaitFirstOrNull().run { return Mono.just(deleteLinkedAccountRequest.accountId) }
+ } ?: throw OpexError.InvalidLinkedAccount.exception()
+ }
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/ProfileApprovalRequestManagementImp.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/ProfileApprovalRequestManagementImp.kt
new file mode 100644
index 000000000..53a5f2032
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/ProfileApprovalRequestManagementImp.kt
@@ -0,0 +1,57 @@
+package co.nilin.opex.profile.ports.postgres.imp
+
+import co.nilin.opex.common.OpexError
+import co.nilin.opex.profile.core.data.profile.ProfileApprovalRequest
+import co.nilin.opex.profile.core.data.profile.ProfileApprovalRequestStatus
+import co.nilin.opex.profile.core.data.profile.ProfileApprovalResponse
+import co.nilin.opex.profile.core.spi.ProfileApprovalRequestPersister
+import co.nilin.opex.profile.core.utils.convert
+import co.nilin.opex.profile.ports.postgres.dao.ProfileApprovalRequestRepository
+import co.nilin.opex.profile.ports.postgres.model.entity.ProfileApprovalRequestModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.reactive.awaitFirstOrNull
+import org.springframework.stereotype.Service
+import reactor.core.publisher.Mono
+
+@Service
+class ProfileApprovalRequestManagementImp(
+ private var profileApprovalRequestRepository: ProfileApprovalRequestRepository,
+) : ProfileApprovalRequestPersister {
+ override suspend fun save(request: ProfileApprovalRequest): Mono {
+ profileApprovalRequestRepository.findByProfileIdAndStatus(
+ request.profileId,
+ ProfileApprovalRequestStatus.PENDING
+ ).awaitFirstOrNull()?.let {
+ throw OpexError.ProfileApprovalRequestAlreadyExists.exception()
+ } ?: run {
+ val requestApprovalRequest: ProfileApprovalRequestModel =
+ request.convert(ProfileApprovalRequestModel::class.java)
+ profileApprovalRequestRepository.save(requestApprovalRequest).awaitFirstOrNull()
+ return Mono.just(request)
+ }
+ }
+
+ override suspend fun getRequests(status: ProfileApprovalRequestStatus): Flow? {
+ return profileApprovalRequestRepository.findByStatus(status)?.map { p ->
+ p.convert(
+ ProfileApprovalResponse::class.java
+ )
+ }
+ }
+
+ override suspend fun getRequestById(id: Long): Mono {
+ return profileApprovalRequestRepository.findById(id).map { p ->
+ p.convert(
+ ProfileApprovalResponse::class.java
+ )
+ }
+ }
+
+ override suspend fun update(request: ProfileApprovalResponse): ProfileApprovalResponse {
+ val requestApprovalRequest: ProfileApprovalRequestModel =
+ request.convert(ProfileApprovalRequestModel::class.java)
+ profileApprovalRequestRepository.save(requestApprovalRequest).awaitFirstOrNull()
+ return request
+ }
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/ProfileManagementImp.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/ProfileManagementImp.kt
new file mode 100644
index 000000000..df35d92dc
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/ProfileManagementImp.kt
@@ -0,0 +1,261 @@
+package co.nilin.opex.profile.ports.postgres.imp
+
+import co.nilin.opex.common.OpexError
+import co.nilin.opex.profile.core.data.kyc.KycLevel
+import co.nilin.opex.profile.core.data.kyc.KycLevelDetail
+import co.nilin.opex.profile.core.data.kyc.ManualUpdateRequest
+import co.nilin.opex.profile.core.data.limitation.ActionType
+import co.nilin.opex.profile.core.data.limitation.LimitationReason
+import co.nilin.opex.profile.core.data.limitation.LimitationUpdateType
+import co.nilin.opex.profile.core.data.limitation.UpdateLimitationRequest
+import co.nilin.opex.profile.core.data.profile.*
+import co.nilin.opex.profile.core.spi.ProfilePersister
+import co.nilin.opex.profile.core.utils.compare
+import co.nilin.opex.profile.core.utils.convert
+import co.nilin.opex.profile.ports.kyc.imp.KycProxyImp
+import co.nilin.opex.profile.ports.postgres.convertor.convertProfileModelToCompleteProfileResponse
+import co.nilin.opex.profile.ports.postgres.dao.ProfileHistoryRepository
+import co.nilin.opex.profile.ports.postgres.dao.ProfileRepository
+import co.nilin.opex.profile.ports.postgres.model.entity.ProfileModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.reactive.awaitFirstOrNull
+import org.slf4j.LoggerFactory
+import org.springframework.data.domain.PageRequest
+import org.springframework.data.domain.Sort
+import org.springframework.stereotype.Service
+import org.springframework.transaction.annotation.Transactional
+import reactor.core.publisher.Mono
+import java.time.LocalDateTime
+
+@Service
+class ProfileManagementImp(
+ private var profileRepository: ProfileRepository,
+ private var profileHistoryRepository: ProfileHistoryRepository,
+ private var limitationManagementImp: LimitationManagementImp,
+ private var kycProxyImp: KycProxyImp,
+) : ProfilePersister {
+ private val logger = LoggerFactory.getLogger(ProfileManagementImp::class.java)
+
+ @Transactional
+ override suspend fun updateProfile(id: String, data: UpdateProfileRequest): Mono {
+ var newKycLevel: KycLevel? = null
+ return profileRepository.findByUserId(id)?.awaitFirstOrNull()?.let { it ->
+ with(data) {
+
+ if (isMajorChanges(it, this)) {
+ newKycLevel = applyMajorChangesRequirements(it, this)
+ }
+ if (isContactChanges(it, this))
+ newKycLevel = applyContactChangesRequirements(it, this)
+ }
+ var newProfileModel = data.convert(ProfileModel::class.java)
+
+ newProfileModel.id = it.id
+ newProfileModel.kycLevel = it.kycLevel
+ newProfileModel.userId = it.userId
+ newProfileModel.email = it.email
+ newProfileModel.status = it.status
+ newProfileModel.createDate = it.createDate
+ newProfileModel.lastUpdateDate = LocalDateTime.now()
+
+ // 1.new kyc level was sent to kyc module
+ // 2.kyc module as soon as possible will push that message into all module includes profile
+ // 3. we return new kyc level to user locally and based on changes of close future in database
+
+ profileRepository.save(newProfileModel).map { convert(Profile::class.java) }.map { d ->
+ newKycLevel.let { d.kycLevel = newKycLevel }
+ d
+ }
+
+
+ } ?: throw OpexError.UserNotFound.exception()
+ }
+
+ override suspend fun completeProfile(id: String, data: CompleteProfileRequest): Mono {
+ return profileRepository.findByUserId(id)?.awaitFirstOrNull()?.let { it ->
+
+ var newProfileModel = data.convert(ProfileModel::class.java)
+ newProfileModel.email = it.email
+ newProfileModel.mobile = it.mobile
+ newProfileModel.id = it.id
+ newProfileModel.userId = it.userId
+ newProfileModel.status = it.status
+ newProfileModel.createDate = it.createDate
+ newProfileModel.lastUpdateDate = LocalDateTime.now()
+ profileRepository.save(newProfileModel)
+ .map { savedProfile ->
+ convertProfileModelToCompleteProfileResponse(savedProfile).apply {
+ if (it.nationality == "Iranian")
+ kycLevel = KycLevel.Level2
+ }
+ }
+ } ?: throw OpexError.UserNotFound.exception()
+ }
+
+ //todo
+ //update shared fields in keycloak
+ override suspend fun updateProfileAsAdmin(id: String, data: Profile): Mono {
+
+ return profileRepository.findByUserId(id)?.awaitFirstOrNull()?.let {
+ with(data) {
+ this.lastUpdateDate = LocalDateTime.now()
+ this.createDate = createDate
+ this.kycLevel = kycLevel
+ this.email = email
+ this.userId = userId
+ }
+ var newProfileModel = data.convert(ProfileModel::class.java)
+ newProfileModel.id = it.id
+ profileRepository.save(newProfileModel).map { convert(Profile::class.java) }
+ } ?: throw OpexError.UserNotFound.exception()
+ }
+
+ override suspend fun createProfile(data: Profile): Mono {
+ if (data.email.isNullOrBlank() && data.mobile.isNullOrBlank()) {
+ throw OpexError.BadRequest.exception("email and mobile is null or empty")
+ }
+ profileRepository.findByUserIdOrEmailOrMobile(data.userId!!, data.email, data.mobile)?.awaitFirstOrNull()?.let {
+ throw OpexError.UserIdAlreadyExists.exception()
+ } ?: run {
+ val profile: ProfileModel = data.convert(ProfileModel::class.java)
+ profileRepository.save(profile).awaitFirstOrNull()
+ return Mono.just(data)
+ }
+ }
+
+ override suspend fun getProfile(userId: String): Mono? {
+
+ return profileRepository.findByUserId(userId)?.map {
+ it.convert(Profile::class.java)
+ } ?: throw OpexError.UserNotFound.exception()
+
+ }
+
+ override suspend fun getProfile(id: Long): Mono {
+ val profile: Profile =
+ profileRepository.findById(id).awaitFirstOrNull()?.convert(Profile::class.java)
+ ?: throw OpexError.UserNotFound.exception()
+ return Mono.just(profile)
+ }
+
+ override suspend fun getAllProfile(offset: Int, size: Int, profileRequest: ProfileRequest): Flow? {
+ if (profileRequest.partialSearch == false)
+ return profileRepository.findUsersBy(
+ profileRequest.userId, profileRequest.mobile,
+ profileRequest.email, profileRequest.firstName, profileRequest.lastName,
+ profileRequest.nationalCode, profileRequest.createDateFrom, profileRequest.createDateTo,
+ PageRequest.of(offset, size, Sort.by(Sort.Direction.ASC, "id"))
+ )?.map { p -> p.convert(Profile::class.java) }
+ else {
+ return profileRepository.searchUsersBy(
+ profileRequest.userId, profileRequest.mobile,
+ profileRequest.email, profileRequest.firstName, profileRequest.lastName,
+ profileRequest.nationalCode, profileRequest.createDateFrom, profileRequest.createDateTo,
+ PageRequest.of(offset, size, Sort.by(Sort.Direction.ASC, "id"))
+ )?.map { p -> p.convert(Profile::class.java) }
+ }
+ }
+
+ override suspend fun getHistory(userId: String, offset: Int, size: Int): List {
+ val resp: MutableList = ArrayList()
+
+ profileRepository.findByUserId(userId)?.awaitFirstOrNull() ?: throw OpexError.UserNotFound.exception()
+ profileHistoryRepository.findByUserId(
+ userId,
+ PageRequest.of(offset, size, Sort.by(Sort.Direction.DESC, "changeRequestDate"))
+ )
+ .map { p ->
+ p.convert(ProfileHistory::class.java)
+ }
+ .toList()
+ .windowed(2, 1, true)
+ .forEach { window: List ->
+ val new = window.first()
+ val past = window.last()
+ if (past.userId?.isNotBlank() == true) {
+ new.updatedItem = new.compare(past)
+ resp.add(new)
+ } else
+ resp.add(past)
+ }
+
+ return resp.toList()
+ }
+
+ override suspend fun updateUserLevel(userId: String, userLevel: KycLevel) {
+ profileRepository.findByUserId(userId)?.block()?.let { profileModel ->
+ profileModel.kycLevel = userLevel
+ profileRepository.save(profileModel).awaitFirstOrNull()
+
+ } ?: throw OpexError.UserNotFound.exception()
+ }
+
+ override suspend fun updateMobile(userId: String, mobile: String) {
+ val profileModel = profileRepository.findByUserId(userId)?.awaitFirstOrNull()
+ ?: throw OpexError.UserNotFound.exception()
+ profileModel.mobile = mobile
+ profileRepository.save(profileModel).awaitFirstOrNull()
+ }
+
+ override suspend fun updateEmail(userId: String, email: String) {
+ val profileModel = profileRepository.findByUserId(userId)?.awaitFirstOrNull()
+ ?: throw OpexError.UserNotFound.exception()
+ profileModel.email = email
+ profileRepository.save(profileModel).awaitFirstOrNull()
+ }
+
+ fun isMajorChanges(oldData: ProfileModel, newData: UpdateProfileRequest): Boolean {
+ return !oldData.firstName.equals(newData.firstName) || !oldData.lastName.equals(newData.lastName)
+ }
+
+ fun isContactChanges(oldData: ProfileModel, newData: UpdateProfileRequest): Boolean {
+ // return oldData.email != newData.email ||
+ return !oldData.mobile.equals(newData.mobile)
+ }
+
+ suspend fun applyMajorChangesRequirements(oldData: ProfileModel, newData: UpdateProfileRequest): KycLevel? {
+ //todo
+ //read from panel
+ val newKycLevel = KycLevel.Level1
+
+ updateKycLevel(userId = oldData.userId!!, kycLevel = newKycLevel, LimitationReason.MajorProfileChange.name)
+ limitationManagementImp.updateLimitation(
+ UpdateLimitationRequest(
+ oldData.userId, arrayOf(
+
+ ActionType.CashOut, ActionType.Withdraw
+ ).asList(), null, LimitationUpdateType.Revoke, null, null, LimitationReason.MajorProfileChange
+ )
+ )
+
+ return newKycLevel
+ }
+
+ suspend fun applyContactChangesRequirements(oldData: ProfileModel, newData: UpdateProfileRequest): KycLevel? {
+ //todo
+ //read from panel
+ val newKycLevel = KycLevel.Level1
+
+ updateKycLevel(userId = oldData.userId!!, kycLevel = newKycLevel, LimitationReason.MajorProfileChange.name)
+ limitationManagementImp.updateLimitation(
+ UpdateLimitationRequest(
+ oldData.userId, arrayOf(
+
+ ActionType.Withdraw
+ ).asList(), null, LimitationUpdateType.Revoke, null, null, LimitationReason.ContactProfileChange
+ )
+ )
+ return newKycLevel
+ }
+
+ suspend fun updateKycLevel(userId: String, kycLevel: KycLevel, reason: String?) {
+ val kycLevelDetail =
+ if (kycLevel == KycLevel.Level1) KycLevelDetail.ManualUpdateLevel1 else KycLevelDetail.ManualUpdateLevel3
+ kycProxyImp.updateKycLevel(ManualUpdateRequest(kycLevelDetail).apply {
+ this.userId = userId
+ this.description = reason
+ })
+ }
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/HistoryTracker.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/HistoryTracker.kt
new file mode 100644
index 000000000..7e44b03ae
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/HistoryTracker.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.profile.ports.postgres.model
+
+import java.util.*
+
+data class HistoryTracker(
+ var originalDataId: Long?,
+ var issuer: String?,
+ var changeRequestDate: Date?,
+ var changeRequestType: String?
+)
\ No newline at end of file
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/Limitation.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/Limitation.kt
new file mode 100644
index 000000000..b63ccbc37
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/Limitation.kt
@@ -0,0 +1,17 @@
+package co.nilin.opex.profile.ports.postgres.model.base
+
+import co.nilin.opex.profile.core.data.limitation.ActionType
+import co.nilin.opex.profile.core.data.limitation.LimitationReason
+import java.time.LocalDateTime
+import java.util.*
+
+open class Limitation {
+ lateinit var userId: String;
+ var actionType: ActionType? = null;
+ var createDate: LocalDateTime? = null;
+ var expTime: Long? = null;
+ var detail: String? = null
+ var limitationOn: String? = null
+ var description: String? = null
+ var reason: LimitationReason? = null
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/LinkedBankAccount.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/LinkedBankAccount.kt
new file mode 100644
index 000000000..1bb7e2910
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/LinkedBankAccount.kt
@@ -0,0 +1,17 @@
+package co.nilin.opex.profile.ports.postgres.model.base
+
+import co.nilin.opex.profile.core.data.linkedbankAccount.BankAccountType
+import java.time.LocalDateTime
+
+open class LinkedBankAccount {
+ lateinit var userId: String
+ var bankAccountType: BankAccountType? = null
+ var registerDate: LocalDateTime? = null
+ var verifiedDate: LocalDateTime? = null
+ var enabled: Boolean? = false
+ var verified: Boolean? = false
+ var verifier: String? = null
+ var number: String? = null
+ var accountId: String? = null
+ var description: String? = null
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/Profile.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/Profile.kt
new file mode 100644
index 000000000..133bf7dcb
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/Profile.kt
@@ -0,0 +1,28 @@
+package co.nilin.opex.profile.ports.postgres.model.base
+
+import co.nilin.opex.profile.core.data.profile.Gender
+import co.nilin.opex.profile.core.data.kyc.KycLevel
+import co.nilin.opex.profile.core.data.profile.UserStatus
+import java.time.LocalDateTime
+
+open class Profile {
+ lateinit var email: String
+ lateinit var userId: String
+ var firstName: String? = null
+ var lastName: String? = null
+ var address: String? = null
+ var mobile: String? = null
+ var telephone: String? = null
+ var postalCode: String? = null
+ var nationality: String? = null
+ var identifier: String? = null
+ var gender: Gender? = null
+ lateinit var birthDate: LocalDateTime
+ var status: UserStatus? = null
+ var createDate: LocalDateTime? = null
+ var lastUpdateDate: LocalDateTime? = null
+ var creator: String? = null
+ var kycLevel: KycLevel? = KycLevel.Level1
+ var verificationStatus : Boolean? = false
+
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/ProfileApprovalRequest.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/ProfileApprovalRequest.kt
new file mode 100644
index 000000000..6803cdcfc
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/ProfileApprovalRequest.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.profile.ports.postgres.model.base
+
+import co.nilin.opex.profile.core.data.profile.ProfileApprovalRequestStatus
+import java.time.LocalDateTime
+
+open class ProfileApprovalRequest {
+ var profileId: Long = 0L
+ var status: ProfileApprovalRequestStatus? = ProfileApprovalRequestStatus.PENDING
+ var createDate: LocalDateTime? = null
+ var updateDate: LocalDateTime? = null
+ var updater: String? = null
+ var description: String?=null
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/entity/LimitationModel.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/entity/LimitationModel.kt
new file mode 100644
index 000000000..ac0782354
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/entity/LimitationModel.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.profile.ports.postgres.model.entity
+
+import co.nilin.opex.profile.ports.postgres.model.base.Limitation
+import org.springframework.data.annotation.Id
+import org.springframework.data.relational.core.mapping.Table
+
+@Table("limitation")
+data class LimitationModel(@Id var id: Long) : Limitation()
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/entity/LinkedBankAccountModel.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/entity/LinkedBankAccountModel.kt
new file mode 100644
index 000000000..e70352cc7
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/entity/LinkedBankAccountModel.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.profile.ports.postgres.model.entity
+
+import co.nilin.opex.profile.ports.postgres.model.base.Limitation
+import co.nilin.opex.profile.ports.postgres.model.base.LinkedBankAccount
+import org.springframework.data.annotation.Id
+import org.springframework.data.relational.core.mapping.Table
+
+@Table("linked_bank_account")
+data class LinkedBankAccountModel(
+ @Id var id: Long) : LinkedBankAccount()
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/entity/ProfileApprovalRequestModel.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/entity/ProfileApprovalRequestModel.kt
new file mode 100644
index 000000000..599910755
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/entity/ProfileApprovalRequestModel.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.profile.ports.postgres.model.entity
+
+import co.nilin.opex.profile.ports.postgres.model.base.ProfileApprovalRequest
+import org.springframework.data.annotation.Id
+import org.springframework.data.relational.core.mapping.Table
+
+@Table("profile_approval_request")
+data class ProfileApprovalRequestModel(
+ @Id var id: Long
+) : ProfileApprovalRequest()
\ No newline at end of file
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/entity/ProfileModel.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/entity/ProfileModel.kt
new file mode 100644
index 000000000..03db781ef
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/entity/ProfileModel.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.profile.ports.postgres.model.entity
+
+import co.nilin.opex.profile.ports.postgres.model.base.Profile
+import org.springframework.data.annotation.Id
+import org.springframework.data.relational.core.mapping.Table
+
+@Table("profile")
+data class ProfileModel(
+ @Id var id: Long
+) : Profile()
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/history/LimitationHistory.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/history/LimitationHistory.kt
new file mode 100644
index 000000000..f9bc18518
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/history/LimitationHistory.kt
@@ -0,0 +1,16 @@
+package co.nilin.opex.profile.ports.postgres.model.history
+
+import co.nilin.opex.profile.ports.postgres.model.base.Limitation
+import org.springframework.data.annotation.Id
+import org.springframework.data.relational.core.mapping.Table
+import java.time.LocalDateTime
+import java.util.*
+
+@Table("limitation_history")
+data class LimitationHistory(
+ @Id
+ var id: Long,
+ var issuer: String?,
+ var changeRequestDate: LocalDateTime?,
+ var changeRequestType: String?
+) : Limitation()
\ No newline at end of file
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/history/LinkedBankAccountHistory.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/history/LinkedBankAccountHistory.kt
new file mode 100644
index 000000000..880197388
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/history/LinkedBankAccountHistory.kt
@@ -0,0 +1,16 @@
+package co.nilin.opex.profile.ports.postgres.model.history
+
+import co.nilin.opex.profile.ports.postgres.model.base.Limitation
+import co.nilin.opex.profile.ports.postgres.model.base.LinkedBankAccount
+import org.springframework.data.annotation.Id
+import org.springframework.data.relational.core.mapping.Table
+import java.time.LocalDateTime
+import java.util.*
+
+@Table("linked_bank_account_history")
+data class LinkedBankAccountHistory(
+ @Id
+ var id: Long,
+ var changeRequestDate: LocalDateTime?,
+ var changeRequestType: String?
+) : LinkedBankAccount()
\ No newline at end of file
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/history/ProfileHistory.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/history/ProfileHistory.kt
new file mode 100644
index 000000000..2cef13f11
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/history/ProfileHistory.kt
@@ -0,0 +1,21 @@
+package co.nilin.opex.profile.ports.postgres.model.history
+
+import co.nilin.opex.profile.ports.postgres.model.base.Profile
+import org.springframework.data.annotation.Id
+import org.springframework.data.relational.core.mapping.Table
+import java.time.LocalDateTime
+import java.util.*
+
+@Table("profile_history")
+data class ProfileHistory(
+ @Id
+ var id: Long,
+ var originalDataId: Long?,
+ var issuer: String?,
+ var changeRequestDate: LocalDateTime?,
+ var changeRequestType: String?
+) : Profile()
+
+
+
+
diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/utils/Convertor.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/utils/Convertor.kt
new file mode 100644
index 000000000..e7d99d76a
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/utils/Convertor.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.profile.ports.postgres.utils
+
+import com.google.gson.Gson
+import reactor.core.publisher.Mono
+
+fun Any.convert(classOfT: Class): T = Gson().fromJson(Gson().toJson(this), classOfT)
+
+fun Mono.convert(classOfT: Class): Mono = Mono.just(Gson().fromJson(Gson().toJson(this), classOfT))
diff --git a/profile/profile-ports/profile-postgres/src/main/resources/schema.sql b/profile/profile-ports/profile-postgres/src/main/resources/schema.sql
new file mode 100644
index 000000000..a4eb00a93
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/main/resources/schema.sql
@@ -0,0 +1,325 @@
+CREATE TABLE IF NOT EXISTS profile
+(
+ id SERIAL PRIMARY KEY,
+ email VARCHAR(100) NOT NULL UNIQUE,
+ last_name VARCHAR(256),
+ user_id VARCHAR(100) NOT NULL UNIQUE,
+ create_date TIMESTAMP,
+ identifier VARCHAR(100),
+ address VARCHAR(256),
+ first_name VARCHAR(256),
+ telephone VARCHAR(256),
+ mobile VARCHAR(256),
+ nationality VARCHAR(256),
+ gender VARCHAR(50),
+ birth_date TIMESTAMP,
+ status VARCHAR(100),
+ postal_code VARCHAR(100),
+ creator VARCHAR(100),
+ last_update_date TIMESTAMP DEFAULT CURRENT_DATE,
+ kyc_level varchar(100)
+);
+
+CREATE TABLE IF NOT EXISTS profile_history
+(
+ id SERIAL PRIMARY KEY,
+ email VARCHAR(100) NOT NULL,
+ last_name VARCHAR(256),
+ user_id VARCHAR(100) NOT NULL,
+ create_date TIMESTAMP,
+ identifier VARCHAR(100),
+ address VARCHAR(256),
+ first_name VARCHAR(256),
+ telephone VARCHAR(256),
+ mobile VARCHAR(256),
+ nationality VARCHAR(256),
+ gender BOOLEAN,
+ birth_date TIMESTAMP,
+ status VARCHAR(100),
+ last_update_date TIMESTAMP,
+ original_data_id VARCHAR(100) NOT NULL,
+ creator VARCHAR(100),
+ issuer VARCHAR(100),
+ postal_code VARCHAR(100),
+ change_request_date TIMESTAMP,
+ change_request_type VARCHAR(100),
+ kyc_level varchar(100)
+);
+
+
+CREATE TABLE IF NOT EXISTS limitation
+(
+ id SERIAL PRIMARY KEY,
+ user_id VARCHAR(100) NOT NULL,
+ action_type VARCHAR(100),
+ create_date TIMESTAMP,
+ exp_time VARCHAR(100),
+ detail VARCHAR(100),
+ limitation_on VARCHAR(100) UNIQUE NOT NULL,
+ description VARCHAR(100),
+ reason VARCHAR(100)
+);
+
+
+CREATE TABLE IF NOT EXISTS limitation_history
+(
+ id SERIAL PRIMARY KEY,
+ user_id VARCHAR(100) NOT NULL,
+ action_type VARCHAR(100),
+ create_date TIMESTAMP,
+ exp_time VARCHAR(100),
+ detail VARCHAR(100),
+ issuer VARCHAR(100),
+ change_request_date TIMESTAMP,
+ change_request_type VARCHAR(100),
+ limitation_on VARCHAR(100),
+ description VARCHAR(100),
+ reason VARCHAR(100)
+);
+
+CREATE TABLE IF NOT EXISTS linked_bank_account
+(
+ id SERIAL PRIMARY KEY,
+ user_id VARCHAR(100) NOT NULL,
+ bank_account_type VARCHAR(100),
+ register_date TIMESTAMP,
+ verified_date TIMESTAMP,
+ enabled BOOLEAN,
+ verified BOOLEAN,
+ verifier VARCHAR(100),
+ number VARCHAR(100),
+ account_id VARCHAR(100) UNIQUE,
+ description VARCHAR(100)
+);
+
+ALTER TABLE linked_bank_account
+ DROP CONSTRAINT IF EXISTS unique_account;
+ALTER TABLE linked_bank_account
+ ADD CONSTRAINT unique_account UNIQUE (user_id, number);
+
+
+CREATE TABLE IF NOT EXISTS linked_bank_account_history
+(
+ id SERIAL PRIMARY KEY,
+ user_id VARCHAR(100) NOT NULL,
+ bank_account_type VARCHAR(100),
+ register_date TIMESTAMP,
+ verified_date TIMESTAMP,
+ enabled BOOLEAN,
+ verifid BOOLEAN,
+ verifier VARCHAR(100),
+ number VARCHAR(100),
+ account_id VARCHAR(100),
+ description VARCHAR(100),
+ change_request_date TIMESTAMP,
+ change_request_type VARCHAR(100)
+);
+
+-- Alter table limitation_history add column reason Varchar(100);
+
+
+DROP TRIGGER IF EXISTS profile_log_update on public.profile;
+DROP TRIGGER IF EXISTS profile_log_delete on public.profile;
+
+DROP TRIGGER IF EXISTS limitation_log_update on public.limitation;
+DROP TRIGGER IF EXISTS limitation_log_delete on public.limitation;
+
+DROP TRIGGER IF EXISTS linked_account_log_update on public.linked_bank_account;
+DROP TRIGGER IF EXISTS linked_account_log_delete on public.linked_bank_account;
+
+
+CREATE OR REPLACE FUNCTION triger_function() RETURNS TRIGGER AS
+$BODY$
+BEGIN
+ INSERT INTO public.profile_history (original_data_id, change_request_date, change_request_type, email, user_id,
+ create_date, identifier, address, first_name, last_name, mobile, telephone,
+ nationality, gender, birth_date, status, postal_code, creator, kyc_level)
+ VALUES (OLD.id, now(), 'UPDATE', OLD.email, OLD.user_id, OLD.create_date, OLD.identifier, OLD.address,
+ OLD.first_name,
+ OLD.last_name, OLD.mobile, OLD.telephone, OLD.nationality, OLD.gender, OLD.birth_date, OLD.status,
+ OLD.postal_code, OLD.creator, OLD.kyc_level);
+ RETURN NULL;
+END;
+$BODY$ language plpgsql;
+
+
+
+CREATE OR REPLACE FUNCTION triger_delete_function() RETURNS TRIGGER AS
+$BODY$
+BEGIN
+ INSERT INTO public.profile_history (original_data_id, change_request_date, change_request_type, email, user_id,
+ create_date, identifier, address, first_name, last_name, mobile, telephone,
+ nationality, gender, birth_date, status, postal_code, creator, kyc_level)
+ VALUES (OLD.id, now(), 'DELETE', OLD.email, OLD.user_id, OLD.create_date, OLD.identifier, OLD.address,
+ OLD.first_name,
+ OLD.last_name, OLD.mobile, OLD.telephone, OLD.nationality, OLD.gender, OLD.birth_date, OLD.status,
+ OLD.postal_code, OLD.creator, OLD.kyc_level);
+ RETURN NULL;
+END;
+$BODY$ language plpgsql;
+
+
+
+CREATE OR REPLACE FUNCTION triger_limitation_function() RETURNS TRIGGER AS
+$BODY$
+BEGIN
+ INSERT INTO public.limitation_history (change_request_date, change_request_type, user_id, create_date, action_type,
+ exp_time, detail, limitation_on, description, reason)
+ VALUES (now(), 'UPDATE', OLD.user_id, OLD.create_date, OLD.action_type, OLD.exp_time, OLD.detail, OLD.limitation_on,
+ OLD.description, OLD.reason);
+ RETURN NULL;
+END;
+$BODY$ language plpgsql;
+
+
+
+CREATE OR REPLACE FUNCTION triger_delete_limitation_function() RETURNS TRIGGER AS
+$BODY$
+BEGIN
+ INSERT INTO public.limitation_history (change_request_date, change_request_type, user_id, create_date, action_type,
+ exp_time, detail, limitation_on, description, reason)
+ VALUES (now(), 'DELETE', OLD.user_id, OLD.create_date, OLD.action_type, OLD.exp_time, OLD.detail, OLD.limitation_on,
+ OLD.description, OLD.reason);
+ RETURN NULL;
+END;
+$BODY$ language plpgsql;
+
+
+
+CREATE OR REPLACE FUNCTION triger_linked_account_function() RETURNS TRIGGER AS
+$BODY$
+BEGIN
+ INSERT INTO public.linked_bank_account_history (change_request_date, change_request_type, user_id, verified_date,
+ enabled, verified, verifier, number, description, account_id)
+ VALUES (now(), 'UPDATE', OLD.user_id, OLD.verified_date, OLD.enabled, OLD.verified, OLD.verifier, OLD.number,
+ OLD.description, OLD.account_id);
+ RETURN NULL;
+END;
+$BODY$ language plpgsql;
+
+
+CREATE OR REPLACE FUNCTION triger_delete_linked_account_function() RETURNS TRIGGER AS
+$BODY$
+BEGIN
+ INSERT INTO public.linked_bank_account_history (change_request_date, change_request_type, user_id, verified_date,
+ enabled, verified, verifier, number, description, account_id)
+ VALUES (now(), 'DELETE', OLD.user_id, OLD.verified_date, OLD.enabled, OLD.verified, OLD.verifier, OLD.number,
+ OLD.description, OLD.account_id);
+ RETURN NULL;
+END;
+$BODY$ language plpgsql;
+
+
+
+CREATE TRIGGER profile_log_update
+ AFTER UPDATE
+ ON profile
+ FOR EACH ROW
+EXECUTE PROCEDURE triger_function();
+CREATE TRIGGER profile_log_delete
+ AFTER DELETE
+ ON profile
+ FOR EACH ROW
+EXECUTE PROCEDURE triger_delete_function();
+
+
+CREATE TRIGGER limitation_log_update
+ AFTER UPDATE
+ ON limitation
+ FOR EACH ROW
+EXECUTE PROCEDURE triger_limitation_function();
+CREATE TRIGGER limitation_log_delete
+ AFTER DELETE
+ ON limitation
+ FOR EACH ROW
+EXECUTE PROCEDURE triger_delete_limitation_function();
+
+
+
+CREATE TRIGGER linked_account_log_update
+ AFTER UPDATE
+ ON linked_bank_account
+ FOR EACH ROW
+EXECUTE PROCEDURE triger_linked_account_function();
+CREATE TRIGGER linked_account_log_delete
+ AFTER DELETE
+ ON linked_bank_account
+ FOR EACH ROW
+EXECUTE PROCEDURE triger_delete_linked_account_function();
+
+CREATE TABLE IF NOT EXISTS profile_approval_request
+(
+ id SERIAL PRIMARY KEY,
+ profile_id BIGINT NOT NULL,
+ status VARCHAR(100) NOT NULL,
+ create_date TIMESTAMP,
+ update_date TIMESTAMP,
+ updater VARCHAR(100),
+ description VARCHAR(255)
+);
+
+DO
+$$
+ BEGIN
+ IF NOT EXISTS (SELECT 1
+ FROM information_schema.columns
+ WHERE table_name = 'profile' AND column_name = 'verification_status') THEN ALTER TABLE profile
+ ADD COLUMN verification_status VARCHAR(255);
+ END IF;
+ END
+$$;
+
+DO
+$$
+ BEGIN
+ IF NOT EXISTS (SELECT 1
+ FROM information_schema.columns
+ WHERE table_name = 'profile_history'
+ AND column_name = 'verification_status') THEN ALTER TABLE profile_history
+ ADD COLUMN verification_status VARCHAR(255);
+ END IF;
+ END
+$$;
+
+DO
+$$
+ BEGIN
+ IF EXISTS (SELECT 1
+ FROM information_schema.columns
+ WHERE table_name = 'profile_history'
+ AND column_name = 'gender'
+ AND data_type != 'character varying') THEN ALTER TABLE profile_history
+ ALTER COLUMN gender SET DATA TYPE VARCHAR(50);
+ END IF;
+ END
+$$;
+
+DO
+$$
+BEGIN
+ IF EXISTS (SELECT 1
+ FROM information_schema.columns
+ WHERE table_name = 'profile'
+ AND column_name = 'email') THEN ALTER TABLE profile
+ ALTER COLUMN email DROP NOT NULL;
+ END IF;
+ IF EXISTS (SELECT 1
+ FROM information_schema.columns
+ WHERE table_name = 'profile'
+ AND column_name = 'identifier') THEN ALTER TABLE profile
+ ADD CONSTRAINT unique_identifier UNIQUE (identifier);
+ END IF;
+ IF EXISTS (SELECT 1
+ FROM information_schema.columns
+ WHERE table_name = 'profile'
+ AND column_name = 'mobile') THEN ALTER TABLE profile
+ ADD CONSTRAINT unique_mobile UNIQUE (mobile);
+ END IF;
+ IF EXISTS (SELECT 1
+ FROM information_schema.columns
+ WHERE table_name = 'profile_history'
+ AND column_name = 'email') THEN ALTER TABLE profile_history
+ ALTER COLUMN email DROP NOT NULL;
+ END IF;
+END
+$$;
\ No newline at end of file
diff --git a/profile/profile-ports/profile-postgres/src/test/kotlin/co/nilin/opex/profile/ports/postgres/ProfilePostgressApplicationTests.kt b/profile/profile-ports/profile-postgres/src/test/kotlin/co/nilin/opex/profile/ports/postgres/ProfilePostgressApplicationTests.kt
new file mode 100644
index 000000000..b1fecce81
--- /dev/null
+++ b/profile/profile-ports/profile-postgres/src/test/kotlin/co/nilin/opex/profile/ports/postgres/ProfilePostgressApplicationTests.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.profile.ports.postgres
+
+//@SpringBootTest
+//class ProfilePostgressApplicationTests {
+//
+// @Test
+// fun contextLoads() {
+// }
+//
+//}
diff --git a/profile/profile-ports/profile-shahkar-proxy/.gitignore b/profile/profile-ports/profile-shahkar-proxy/.gitignore
new file mode 100644
index 000000000..549e00a2a
--- /dev/null
+++ b/profile/profile-ports/profile-shahkar-proxy/.gitignore
@@ -0,0 +1,33 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
diff --git a/profile/profile-ports/profile-shahkar-proxy/pom.xml b/profile/profile-ports/profile-shahkar-proxy/pom.xml
new file mode 100644
index 000000000..0f6207882
--- /dev/null
+++ b/profile/profile-ports/profile-shahkar-proxy/pom.xml
@@ -0,0 +1,43 @@
+
+
+ 4.0.0
+
+ co.nilin.opex
+ profile
+ 1.0.1-beta.7
+ ../../pom.xml
+
+ co.nilin.opex.profile.ports
+ profile-shahkar-proxy
+ profile-shahkar-proxy
+ profile-shahkar-proxy
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ co.nilin.opex.profile
+ profile-core
+
+
+
+
+
diff --git a/profile/profile-ports/profile-shahkar-proxy/src/main/kotlin/co/nilin/opex/profile/ports/shahkar/imp/ShahkarProxyImp.kt b/profile/profile-ports/profile-shahkar-proxy/src/main/kotlin/co/nilin/opex/profile/ports/shahkar/imp/ShahkarProxyImp.kt
new file mode 100644
index 000000000..41b89a0f8
--- /dev/null
+++ b/profile/profile-ports/profile-shahkar-proxy/src/main/kotlin/co/nilin/opex/profile/ports/shahkar/imp/ShahkarProxyImp.kt
@@ -0,0 +1,12 @@
+package co.nilin.opex.profile.ports.shahkar.imp
+
+import co.nilin.opex.profile.core.spi.ShahkarInquiry
+import org.springframework.stereotype.Component
+
+@Component
+class ShahkarProxyImp : ShahkarInquiry {
+ override suspend fun getInquiryResult(identifier: String, mobile: String): Boolean {
+ //TODO implement
+ return true
+ }
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-shahkar-proxy/src/main/resources/application.properties b/profile/profile-ports/profile-shahkar-proxy/src/main/resources/application.properties
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/profile/profile-ports/profile-shahkar-proxy/src/main/resources/application.properties
@@ -0,0 +1 @@
+
diff --git a/profile/profile-ports/profile-submitter-kafka/.gitignore b/profile/profile-ports/profile-submitter-kafka/.gitignore
new file mode 100644
index 000000000..549e00a2a
--- /dev/null
+++ b/profile/profile-ports/profile-submitter-kafka/.gitignore
@@ -0,0 +1,33 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
diff --git a/profile/profile-ports/profile-submitter-kafka/pom.xml b/profile/profile-ports/profile-submitter-kafka/pom.xml
new file mode 100644
index 000000000..3d6909fc1
--- /dev/null
+++ b/profile/profile-ports/profile-submitter-kafka/pom.xml
@@ -0,0 +1,45 @@
+
+
+ 4.0.0
+
+ co.nilin.opex
+ profile
+ 1.0.1-beta.7
+ ../../pom.xml
+
+ co.nilin.opex.profile.ports
+ profile-submitter-kafka
+ profile-submitter-kafka
+ profile-kafka
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ co.nilin.opex.profile
+ profile-core
+
+
+
+
+
diff --git a/profile/profile-ports/profile-submitter-kafka/src/main/kotlin/co/nilin/opex/profile/ports/kafka/config/KafkaProducerConfig.kt b/profile/profile-ports/profile-submitter-kafka/src/main/kotlin/co/nilin/opex/profile/ports/kafka/config/KafkaProducerConfig.kt
new file mode 100644
index 000000000..ac144cd69
--- /dev/null
+++ b/profile/profile-ports/profile-submitter-kafka/src/main/kotlin/co/nilin/opex/profile/ports/kafka/config/KafkaProducerConfig.kt
@@ -0,0 +1,46 @@
+package co.nilin.opex.profile.ports.kafka.config
+
+
+import co.nilin.opex.profile.core.data.event.KycLevelUpdatedEvent
+import org.apache.kafka.clients.producer.ProducerConfig
+import org.apache.kafka.common.serialization.StringSerializer
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.kafka.core.DefaultKafkaProducerFactory
+import org.springframework.kafka.core.KafkaTemplate
+import org.springframework.kafka.core.ProducerFactory
+import org.springframework.kafka.support.serializer.JsonDeserializer
+import org.springframework.kafka.support.serializer.JsonSerializer
+
+@Configuration
+class KafkaProducerConfig {
+ private val logger = LoggerFactory.getLogger(KafkaProducerConfig::class.java)
+
+ @Value("\${spring.kafka.bootstrap-servers}")
+ private lateinit var bootstrapServers: String
+
+ @Bean("producerConfigs")
+ fun producerConfigs(): Map {
+ return mapOf(
+ ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers,
+ ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java,
+ ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to JsonSerializer::class.java,
+ ProducerConfig.ACKS_CONFIG to "all",
+ JsonDeserializer.TRUSTED_PACKAGES to "co.nilin.opex.*",
+ JsonSerializer.TYPE_MAPPINGS to "kyc_level_updated_event:co.nilin.opex.profile.core.data.event.KycLevelUpdatedEvent"
+ )
+ }
+
+ @Bean("kycEventProducerFactory")
+ fun producerFactory(@Qualifier("producerConfigs") producerConfigs: Map): ProducerFactory {
+ return DefaultKafkaProducerFactory(producerConfigs)
+ }
+
+ @Bean("kycEventKafkaTemplate")
+ fun kafkaTemplate(@Qualifier("kycEventProducerFactory") producerFactory: ProducerFactory): KafkaTemplate {
+ return KafkaTemplate(producerFactory)
+ }
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-submitter-kafka/src/main/kotlin/co/nilin/opex/profile/ports/kafka/config/KafkaTopicConfig.kt b/profile/profile-ports/profile-submitter-kafka/src/main/kotlin/co/nilin/opex/profile/ports/kafka/config/KafkaTopicConfig.kt
new file mode 100644
index 000000000..c334419cd
--- /dev/null
+++ b/profile/profile-ports/profile-submitter-kafka/src/main/kotlin/co/nilin/opex/profile/ports/kafka/config/KafkaTopicConfig.kt
@@ -0,0 +1,23 @@
+package co.nilin.opex.profile.ports.kafka.config
+
+import org.apache.kafka.clients.admin.NewTopic
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.support.GenericApplicationContext
+import org.springframework.kafka.config.TopicBuilder
+import java.util.function.Supplier
+
+@Configuration
+class KafkaTopicConfig {
+
+ @Autowired
+ fun createTopics(applicationContext: GenericApplicationContext) {
+ applicationContext.registerBean("topic_kyc_level_updated", NewTopic::class.java, Supplier {
+ TopicBuilder.name("kyc_level_updated")
+ .partitions(1)
+ .replicas(1)
+ .build()
+ })
+ }
+
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-submitter-kafka/src/main/kotlin/co/nilin/opex/profile/ports/kafka/publisher/KycLevelSubmitter.kt b/profile/profile-ports/profile-submitter-kafka/src/main/kotlin/co/nilin/opex/profile/ports/kafka/publisher/KycLevelSubmitter.kt
new file mode 100644
index 000000000..91e2f081e
--- /dev/null
+++ b/profile/profile-ports/profile-submitter-kafka/src/main/kotlin/co/nilin/opex/profile/ports/kafka/publisher/KycLevelSubmitter.kt
@@ -0,0 +1,36 @@
+package co.nilin.opex.profile.ports.kafka.publisher
+
+
+import co.nilin.opex.profile.core.data.event.KycLevelUpdatedEvent
+import co.nilin.opex.profile.core.spi.KycLevelUpdatedPublisher
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.kafka.core.KafkaTemplate
+import org.springframework.stereotype.Component
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+
+@Component
+class KycLevelSubmitter(
+ @Qualifier("kycEventKafkaTemplate") private val kafkaTemplate: KafkaTemplate,
+) : KycLevelUpdatedPublisher {
+
+ private val logger = LoggerFactory.getLogger(KycLevelSubmitter::class.java)
+
+ val topic = "kyc_level_updated"
+
+ override suspend fun publish(update: KycLevelUpdatedEvent): Unit = suspendCoroutine { cont ->
+ logger.info("Submitting kycLevelUpdated")
+
+ val sendFuture = kafkaTemplate.send(topic, update)
+ sendFuture.addCallback({
+ cont.resume(Unit)
+ }, {
+ logger.error("Error submitting kycLevelChange", it)
+ cont.resumeWithException(it)
+ })
+ }
+
+
+}
\ No newline at end of file
diff --git a/profile/profile-ports/profile-submitter-kafka/src/main/resources/application.properties b/profile/profile-ports/profile-submitter-kafka/src/main/resources/application.properties
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/profile/profile-ports/profile-submitter-kafka/src/main/resources/application.properties
@@ -0,0 +1 @@
+
diff --git a/user-management/keycloak-gateway/pom.xml b/user-management/keycloak-gateway/pom.xml
index 94be9f19c..9023304ca 100644
--- a/user-management/keycloak-gateway/pom.xml
+++ b/user-management/keycloak-gateway/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/KeycloakGatewayApp.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/KeycloakGatewayApp.kt
index 67ec6be6a..be3a628dc 100644
--- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/KeycloakGatewayApp.kt
+++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/KeycloakGatewayApp.kt
@@ -7,7 +7,6 @@ import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfigurati
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication
import org.springframework.context.annotation.ComponentScan
-import org.springframework.data.jpa.repository.config.EnableJpaRepositories
import org.springframework.scheduling.annotation.EnableScheduling
@SpringBootApplication(exclude = [LiquibaseAutoConfiguration::class])
diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/config/EmbeddedKeycloakConfig.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/config/EmbeddedKeycloakConfig.kt
index dfef8a24e..415a7cb60 100644
--- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/config/EmbeddedKeycloakConfig.kt
+++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/config/EmbeddedKeycloakConfig.kt
@@ -2,12 +2,8 @@ package co.nilin.opex.auth.gateway.config
import org.jboss.resteasy.plugins.server.servlet.HttpServlet30Dispatcher
import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters
-import org.keycloak.admin.client.Keycloak
-import org.keycloak.admin.client.resource.RealmResource
import org.keycloak.connections.jpa.updater.liquibase.ThreadLocalSessionContext
import org.keycloak.models.KeycloakSession
-import org.keycloak.services.DefaultKeycloakSessionFactory
-import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.web.servlet.FilterRegistrationBean
import org.springframework.boot.web.servlet.ServletRegistrationBean
import org.springframework.context.annotation.Bean
@@ -54,14 +50,13 @@ class EmbeddedKeycloakConfig {
filter.addUrlPatterns(keycloakServerProperties.contextPath + "/*")
return filter
}
+
@Bean
fun keycloakSession(): KeycloakSession? {
- return ThreadLocalSessionContext.getCurrentSession();
+ return ThreadLocalSessionContext.getCurrentSession();
}
-
-
@Throws(NamingException::class)
private fun mockJndiEnvironment() {
NamingManager.setInitialContextFactoryBuilder {
diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/config/KafkaConfig.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/config/KafkaConfig.kt
index a5eca210d..55f855b53 100644
--- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/config/KafkaConfig.kt
+++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/config/KafkaConfig.kt
@@ -28,11 +28,11 @@ class KafkaConfig {
@Bean("authProducerConfigs")
fun producerConfigs(): Map {
return mapOf(
- ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers,
- ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java,
- ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to JsonSerializer::class.java,
- ProducerConfig.ACKS_CONFIG to "all",
- JsonSerializer.TYPE_MAPPINGS to "user_created_event:co.nilin.opex.auth.gateway.model.UserCreatedEvent"
+ ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers,
+ ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java,
+ ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to JsonSerializer::class.java,
+ ProducerConfig.ACKS_CONFIG to "all",
+ JsonSerializer.TYPE_MAPPINGS to "user_created_event:co.nilin.opex.auth.gateway.model.UserCreatedEvent"
)
}
@@ -48,19 +48,19 @@ class KafkaConfig {
@Autowired
fun createUserCreatedTopics(applicationContext: GenericApplicationContext) {
- applicationContext.registerBean("topic_auth_user_created", NewTopic::class.java, Supplier {
- TopicBuilder.name("auth_user_created")
- .partitions(1)
- .replicas(1)
- .build()
+ applicationContext.registerBean("topic_auth", NewTopic::class.java, Supplier {
+ TopicBuilder.name("auth")
+ .partitions(1)
+ .replicas(1)
+ .build()
})
}
@Autowired
fun configureEventListeners(
- kycLevelUpdatedKafkaListener: KycLevelUpdatedKafkaListener,
- kycLevelUpdatedEventListener: KycLevelUpdatedEventListener
+ kycLevelUpdatedKafkaListener: KycLevelUpdatedKafkaListener,
+ kycLevelUpdatedEventListener: KycLevelUpdatedEventListener
) {
kycLevelUpdatedKafkaListener.addEventListener(kycLevelUpdatedEventListener)
diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/CaptchaType.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/CaptchaType.kt
new file mode 100644
index 000000000..61d4ef57b
--- /dev/null
+++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/CaptchaType.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.auth.gateway.data
+
+enum class CaptchaType {
+ INTERNAL, ARCAPTCHA, HCAPTCHA
+}
\ No newline at end of file
diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/ChangePasswordRequest.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/ChangePasswordRequest.kt
index df866b20f..2b1716dec 100644
--- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/ChangePasswordRequest.kt
+++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/ChangePasswordRequest.kt
@@ -1,10 +1,10 @@
package co.nilin.opex.auth.gateway.data
-class ChangePasswordRequest{
+class ChangePasswordRequest {
- var password: String?=null
- var newPassword: String?=null
- var confirmation: String?=null
+ var password: String? = null
+ var newPassword: String? = null
+ var confirmation: String? = null
constructor()
diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/RegisterUserRequest.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/RegisterUserRequest.kt
index a07914a5a..ab913476b 100644
--- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/RegisterUserRequest.kt
+++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/RegisterUserRequest.kt
@@ -6,6 +6,7 @@ class RegisterUserRequest {
var lastName: String? = null
var email: String? = null
var captchaAnswer: String? = null
+ var captchaType: CaptchaType? = CaptchaType.INTERNAL
var password: String? = null
var passwordConfirmation: String? = null
@@ -16,6 +17,7 @@ class RegisterUserRequest {
lastName: String?,
email: String?,
captchaAnswer: String?,
+ captchaType: CaptchaType?,
password: String?,
passwordConfirmation: String?
) {
@@ -23,6 +25,7 @@ class RegisterUserRequest {
this.lastName = lastName
this.email = email
this.captchaAnswer = captchaAnswer
+ this.captchaType = captchaType
this.password = password
this.passwordConfirmation = passwordConfirmation
}
diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/WhiteListAdaptor.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/WhiteListAdaptor.kt
index 45287565d..c8a5663f2 100644
--- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/WhiteListAdaptor.kt
+++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/WhiteListAdaptor.kt
@@ -1,7 +1,7 @@
package co.nilin.opex.auth.gateway.data
class WhiteListAdaptor {
- var data: MutableList?=null
+ var data: MutableList? = null
constructor()
constructor(data: MutableList) {
diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/ExtendedEventListenerProvider.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/ExtendedEventListenerProvider.kt
index 496e4c6c0..248b9944a 100644
--- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/ExtendedEventListenerProvider.kt
+++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/ExtendedEventListenerProvider.kt
@@ -51,7 +51,7 @@ class ExtendedEventListenerProvider(private val session: KeycloakSession) : Even
val realm = model.getRealm(event.realmId)
val user = session.users().getUserById(event.userId, realm)
if (user != null && user.email != null && user.isEmailVerified) {
- logger.info("USER HAS VERIFIED EMAIL : ${event.userId}" )
+ logger.info("USER HAS VERIFIED EMAIL : ${event.userId}")
// Example of adding an attribute when this event happens
user.setSingleAttribute("attribute-key", "attribute-value")
diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/HashicorpVaultProvider.java b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/HashicorpVaultProvider.java
index 735858fef..443ffdb5c 100644
--- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/HashicorpVaultProvider.java
+++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/HashicorpVaultProvider.java
@@ -20,6 +20,15 @@ public class HashicorpVaultProvider implements VaultProvider {
private String vaultSecretEngineName;
private VaultService service;
+ public HashicorpVaultProvider(String vaultUrl, String vaultAppId, String vaultUserId, String realmName, String vaultSecretEngineName, VaultService service) {
+ this.vaultUrl = vaultUrl;
+ this.vaultAppId = vaultAppId;
+ this.vaultUserId = vaultUserId;
+ this.realmName = realmName;
+ this.vaultSecretEngineName = vaultSecretEngineName;
+ this.service = service;
+ }
+
@Override
public VaultRawSecret obtainSecret(String vaultSecretId) {
int secretVersion = 0;
@@ -40,13 +49,4 @@ public VaultRawSecret obtainSecret(String vaultSecretId) {
public void close() {
}
- public HashicorpVaultProvider(String vaultUrl, String vaultAppId, String vaultUserId, String realmName, String vaultSecretEngineName, VaultService service) {
- this.vaultUrl = vaultUrl;
- this.vaultAppId = vaultAppId;
- this.vaultUserId = vaultUserId;
- this.realmName = realmName;
- this.vaultSecretEngineName = vaultSecretEngineName;
- this.service = service;
- }
-
}
\ No newline at end of file
diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/HashicorpVaultProviderFactory.java b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/HashicorpVaultProviderFactory.java
index c546f1646..2a9aad484 100644
--- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/HashicorpVaultProviderFactory.java
+++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/HashicorpVaultProviderFactory.java
@@ -9,68 +9,66 @@
import org.keycloak.vault.VaultProviderFactory;
public class HashicorpVaultProviderFactory implements VaultProviderFactory {
- private static final Logger logger = Logger.getLogger(HashicorpVaultProviderFactory.class);
+ public static final String PROVIDER_ID = "hachicorp-vault";
+ private static final Logger logger = Logger.getLogger(HashicorpVaultProviderFactory.class);
+ private String vaultAppId;
+ private String vaultUserId;
+ private String vaultUrl;
+ private String vaultSecretEngineName;
- public static final String PROVIDER_ID = "hachicorp-vault";
+ private static String format(String url) {
+ if (!(url.charAt(url.length() - 1) == '/')) {
+ return url.concat("/");
+ } else {
+ return url;
+ }
+ }
- private String vaultAppId;
- private String vaultUserId;
- private String vaultUrl;
- private String vaultSecretEngineName;
+ @Override
+ public VaultProvider create(KeycloakSession session) {
+ VaultService service = new VaultService(session);
+ if (!service.isVaultAvailable(vaultUrl, vaultAppId, vaultUserId)) {
+ logger.error("Vault unavailable : " + vaultUrl);
+ throw new VaultNotFoundException("Vault unavailable : " + vaultUrl);
+ } else {
+ logger.info("Vault available : " + vaultUrl);
+ }
+ return new HashicorpVaultProvider(vaultUrl, vaultAppId, vaultUserId, session.getContext().getRealm().getName(), vaultSecretEngineName, service);
- @Override
- public VaultProvider create(KeycloakSession session) {
- VaultService service = new VaultService(session);
- if (!service.isVaultAvailable(vaultUrl, vaultAppId, vaultUserId)) {
- logger.error("Vault unavailable : " + vaultUrl);
- throw new VaultNotFoundException("Vault unavailable : " + vaultUrl);
- } else {
- logger.info("Vault available : " + vaultUrl);
- }
- return new HashicorpVaultProvider(vaultUrl, vaultAppId, vaultUserId, session.getContext().getRealm().getName(), vaultSecretEngineName, service);
+ }
- }
+ @Override
+ public void init(Scope config) {
+ if (System.getenv("BACKEND_APP") != null) {
+ vaultAppId = System.getenv("BACKEND_APP");
+ } else {
+ vaultAppId = config.get("appId");
+ }
+ if (System.getenv("BACKEND_USER") != null) {
+ vaultUserId = System.getenv("BACKEND_USER");
+ } else {
+ vaultUserId = config.get("userId");
+ }
+ vaultUrl = config.get("url") != null ? format(config.get("url")) : null;
+ vaultSecretEngineName = config.get("engine-name");
+ logger.info("Init Hashicorp: " + vaultUrl);
+ }
- private static String format(String url) {
- if (!(url.charAt(url.length() - 1) == '/')) {
- return url.concat("/");
- } else {
- return url;
- }
- }
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ // TODO Auto-generated method stub
- @Override
- public void init(Scope config) {
- if (System.getenv("BACKEND_APP") != null) {
- vaultAppId = System.getenv("BACKEND_APP");
- } else {
- vaultAppId = config.get("appId");
- }
- if (System.getenv("BACKEND_USER") != null) {
- vaultUserId = System.getenv("BACKEND_USER");
- } else {
- vaultUserId = config.get("userId");
- }
- vaultUrl = config.get("url") != null ? format(config.get("url")) : null;
- vaultSecretEngineName = config.get("engine-name");
- logger.info("Init Hashicorp: " + vaultUrl);
- }
+ }
- @Override
- public void postInit(KeycloakSessionFactory factory) {
- // TODO Auto-generated method stub
+ @Override
+ public void close() {
+ // TODO Auto-generated method stub
- }
+ }
- @Override
- public void close() {
- // TODO Auto-generated method stub
-
- }
-
- @Override
- public String getId() {
- return PROVIDER_ID;
- }
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
}
diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt
index b85d744d6..ba15908ef 100644
--- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt
+++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt
@@ -32,7 +32,6 @@ import org.keycloak.urls.UrlType
import org.keycloak.utils.CredentialHelper
import org.keycloak.utils.TotpUtils
import org.slf4j.LoggerFactory
-import org.springframework.beans.factory.annotation.Value
import org.springframework.kafka.core.KafkaTemplate
import java.util.concurrent.TimeUnit
import java.util.stream.Collectors
@@ -87,7 +86,7 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour
if (!auth.hasScopeAccess("trust")) return ErrorHandler.forbidden()
runCatching {
- validateCaptcha("${request.captchaAnswer}-${session.context.connection.remoteAddr}")
+ validateCaptcha("${request.captchaAnswer}", request.captchaType ?: CaptchaType.INTERNAL)
}.onFailure {
return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.InvalidCaptcha)
}
@@ -140,12 +139,13 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour
@Produces(MediaType.APPLICATION_JSON)
fun forgotPassword(
@QueryParam("email") email: String?,
- @QueryParam("captcha") captcha: String
+ @QueryParam("captcha") captcha: String,
+ @QueryParam("captchaType") captchaType: CaptchaType?,
): Response {
val uri = UriBuilder.fromUri(forgotUrl)
runCatching {
- validateCaptcha("$captcha-${session.context.connection.remoteAddr}")
+ validateCaptcha(captcha, captchaType ?: CaptchaType.INTERNAL)
}.onFailure {
return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.InvalidCaptcha)
}
@@ -216,9 +216,13 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour
@POST
@Path("user/request-verify")
@Produces(MediaType.APPLICATION_JSON)
- fun requestVerifyEmail(@QueryParam("email") email: String?, @QueryParam("captcha") captcha: String): Response {
+ fun requestVerifyEmail(
+ @QueryParam("email") email: String?,
+ @QueryParam("captcha") captcha: String,
+ @QueryParam("captchaType") captchaType: CaptchaType?,
+ ): Response {
runCatching {
- validateCaptcha("${captcha}-${session.context.connection.remoteAddr}")
+ validateCaptcha(captcha, captchaType ?: CaptchaType.INTERNAL)
}.onFailure {
return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.InvalidCaptcha)
}
@@ -430,9 +434,12 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour
return session.userCredentialManager().isConfiguredFor(opexRealm, user, OTPCredentialModel.TYPE)
}
- private fun validateCaptcha(proof: String) {
+ private fun validateCaptcha(proof: String, type: CaptchaType) {
val client: HttpClient = HttpClientBuilder.create().build()
- val post = HttpGet(URIBuilder("http://captcha:8080/verify").addParameter("proof", proof).build())
+ val post = HttpGet(
+ URIBuilder("http://captcha:8080/verify").addParameter("proof", proof).addParameter("type", type.name)
+ .build()
+ )
client.execute(post).let { response ->
logger.info(response.statusLine.statusCode.toString())
check(response.statusLine.statusCode / 500 != 5) { "Could not connect to Opex-Captcha service." }
diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/VaultService.java b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/VaultService.java
index d6fe61a78..7b0f6e732 100644
--- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/VaultService.java
+++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/VaultService.java
@@ -15,22 +15,13 @@
*/
public class VaultService {
- private final KeycloakSession session;
private static final Logger logger = Logger.getLogger(VaultService.class);
+ private final KeycloakSession session;
public VaultService(KeycloakSession session) {
this.session = session;
}
- static class UserId {
- @JsonProperty("user_id")
- public String userId;
-
- public UserId(String userId) {
- this.userId = userId;
- }
- }
-
public ByteBuffer getSecretFromVault(String vaultUrl, String realm, String vaultSecretEngineName, String secretName, String vaultAppId, String vaultUserId, int secretVersion) {
try {
//curl \ --method POST \ --data '{"user_id": ":user_id"}' \ http://127.0.0.1:8200/v1/auth/app-id/login/:app_id
@@ -57,4 +48,13 @@ public boolean isVaultAvailable(String vaultUrl, String vaultAppId, String vault
}
}
+ static class UserId {
+ @JsonProperty("user_id")
+ public String userId;
+
+ public UserId(String userId) {
+ this.userId = userId;
+ }
+ }
+
}
\ No newline at end of file
diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/listener/KycLevelUpdatedListener.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/listener/KycLevelUpdatedListener.kt
index a4425cef6..4a30719e7 100644
--- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/listener/KycLevelUpdatedListener.kt
+++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/listener/KycLevelUpdatedListener.kt
@@ -27,8 +27,10 @@ class KycLevelUpdatedListener : KycLevelUpdatedEventListener {
return "KycLevelUpdatedListener"
}
- override fun onEvent(event: KycLevelUpdatedEvent,
- partition: Int, offset: Long, timestamp: Long, eventId: String) {
+ override fun onEvent(
+ event: KycLevelUpdatedEvent,
+ partition: Int, offset: Long, timestamp: Long, eventId: String
+ ) {
val factory: KeycloakSessionFactory = KeycloakApplication.getSessionFactory()
this.kcSession = factory.create()
kcSession!!.transactionManager.begin()
diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/model/UserCreatedEvent.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/model/UserCreatedEvent.kt
index 76a201345..67ea792ee 100644
--- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/model/UserCreatedEvent.kt
+++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/model/UserCreatedEvent.kt
@@ -13,6 +13,7 @@ class UserCreatedEvent : AuthEvent {
this.lastName = lastName
this.email = email
}
+
constructor() : super()
override fun toString(): String {
diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/model/WhiteListModel.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/model/WhiteListModel.kt
index 5d8a25fb9..493eccb70 100644
--- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/model/WhiteListModel.kt
+++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/model/WhiteListModel.kt
@@ -1,7 +1,9 @@
package co.nilin.opex.auth.gateway.model
-import javax.persistence.*
+import javax.persistence.Entity
+import javax.persistence.Id
+import javax.persistence.Table
@Entity(name = "whitelist")
diff --git a/user-management/keycloak-gateway/src/main/resources/META-INF/whitelisttt-changelog.xml b/user-management/keycloak-gateway/src/main/resources/META-INF/whitelisttt-changelog.xml
index 7b069bcd5..cc0ede79b 100644
--- a/user-management/keycloak-gateway/src/main/resources/META-INF/whitelisttt-changelog.xml
+++ b/user-management/keycloak-gateway/src/main/resources/META-INF/whitelisttt-changelog.xml
@@ -1,5 +1,7 @@
-
+
diff --git a/user-management/keycloak-gateway/src/main/resources/application.yml b/user-management/keycloak-gateway/src/main/resources/application.yml
index 07e9bca92..24c57609b 100644
--- a/user-management/keycloak-gateway/src/main/resources/application.yml
+++ b/user-management/keycloak-gateway/src/main/resources/application.yml
@@ -61,7 +61,7 @@ management:
web:
base-path: /actuator
exposure:
- include: ["health", "prometheus", "metrics"]
+ include: [ "health", "prometheus", "metrics" ]
endpoint:
health:
show-details: when_authorized
@@ -94,6 +94,6 @@ app:
forgot-redirect-url: ${FORGOT_REDIRECT_URL}
whitelist:
register:
- enabled: ${WHITELIST_REGISTER_ENABLED:true}
+ enabled: ${WHITELIST_REGISTER_ENABLED:true}
login:
- enabled: ${WHITELIST_LOGIN_ENABLED:true}
+ enabled: ${WHITELIST_LOGIN_ENABLED:true}
diff --git a/user-management/keycloak-gateway/src/main/resources/email-templates/execute-action.html b/user-management/keycloak-gateway/src/main/resources/email-templates/execute-action.html
index 939b8a962..e964e1e3f 100644
--- a/user-management/keycloak-gateway/src/main/resources/email-templates/execute-action.html
+++ b/user-management/keycloak-gateway/src/main/resources/email-templates/execute-action.html
@@ -1,16 +1,17 @@
-
-
+
+
Email Verification
-
-
+
+