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 list) { + var attributes = role.getAttributes(); + if (attributes.containsKey(attributeNameToInclude)) { + var att = attributes.get(attributeNameToInclude); + if (att.isEmpty()) + return; + + var value = att.get(0); + var splt = value.split(","); + for (var v : splt) { + if (!v.isBlank()) + list.add(v); + } + } + } + +} diff --git a/auth-gateway/keycloak-setup/spi/src/main/java/co/nilin/opex/keycloak/spi/endpoints/PasswordEndpointResourceProvider.java b/auth-gateway/keycloak-setup/spi/src/main/java/co/nilin/opex/keycloak/spi/endpoints/PasswordEndpointResourceProvider.java new file mode 100644 index 000000000..20e037d05 --- /dev/null +++ b/auth-gateway/keycloak-setup/spi/src/main/java/co/nilin/opex/keycloak/spi/endpoints/PasswordEndpointResourceProvider.java @@ -0,0 +1,59 @@ +package co.nilin.opex.keycloak.spi.endpoints; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.credential.PasswordCredentialModel; +import org.keycloak.services.resource.RealmResourceProvider; + +import java.util.Map; + +public class PasswordEndpointResourceProvider implements RealmResourceProvider { + + private final KeycloakSession session; + + public PasswordEndpointResourceProvider(KeycloakSession session) { + this.session = session; + } + + @Override + public Object getResource() { + return this; + } + + @Override + public void close() { + + } + + @POST + @Path("/validate") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response validatePassword(Map payload) { + var userId = payload.get("userId"); + var password = payload.get("password"); + if (userId == null || password == null) + return Response.status(Response.Status.BAD_REQUEST).build(); + + var realm = session.getContext().getRealm(); + var user = session.users().getUserById(realm, userId); + if (user == null) + return Response.status(Response.Status.NOT_FOUND).build(); + + var input = new UserCredentialModel(); + input.setType(PasswordCredentialModel.TYPE); + input.setValue(password); + + var isValid = user.credentialManager().isValid(input); + if (isValid) + return Response.ok(Map.of("valid",true)).build(); + else + return Response.status(Response.Status.UNAUTHORIZED).build(); + } +} \ No newline at end of file diff --git a/auth-gateway/keycloak-setup/spi/src/main/java/co/nilin/opex/keycloak/spi/endpoints/PasswordEndpointResourceProviderFactory.java b/auth-gateway/keycloak-setup/spi/src/main/java/co/nilin/opex/keycloak/spi/endpoints/PasswordEndpointResourceProviderFactory.java new file mode 100644 index 000000000..0e882258e --- /dev/null +++ b/auth-gateway/keycloak-setup/spi/src/main/java/co/nilin/opex/keycloak/spi/endpoints/PasswordEndpointResourceProviderFactory.java @@ -0,0 +1,37 @@ +package co.nilin.opex.keycloak.spi.endpoints; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.services.resource.RealmResourceProvider; +import org.keycloak.services.resource.RealmResourceProviderFactory; + +public class PasswordEndpointResourceProviderFactory implements RealmResourceProviderFactory { + + public static final String ID = "password"; + + @Override + public RealmResourceProvider create(KeycloakSession session) { + return new PasswordEndpointResourceProvider(session); + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return ID; + } +} diff --git a/auth-gateway/keycloak-setup/spi/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/auth-gateway/keycloak-setup/spi/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper new file mode 100644 index 000000000..842ee10be --- /dev/null +++ b/auth-gateway/keycloak-setup/spi/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper @@ -0,0 +1,2 @@ +co.nilin.opex.keycloak.spi.RoleAttributesProtocolMapper +co.nilin.opex.keycloak.spi.ForcedRoleProtocolMapper \ No newline at end of file diff --git a/auth-gateway/keycloak-setup/spi/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory b/auth-gateway/keycloak-setup/spi/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory new file mode 100644 index 000000000..8f62c8527 --- /dev/null +++ b/auth-gateway/keycloak-setup/spi/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory @@ -0,0 +1 @@ +co.nilin.opex.keycloak.spi.endpoints.PasswordEndpointResourceProviderFactory \ No newline at end of file diff --git a/auth-gateway/pom.xml b/auth-gateway/pom.xml new file mode 100644 index 000000000..eb8f5bbe7 --- /dev/null +++ b/auth-gateway/pom.xml @@ -0,0 +1,130 @@ + + + 4.0.0 + + + core + co.nilin.opex + 1.0.1-beta.7 + + + co.nilin.opex.auth + auth-gateway + auth-gateway + pom + + + 21 + 21 + 21 + 2.1.0 + 3.2.3 + 2023.0.0 + 1.1.0 + + + + auth-gateway-app + keycloak-setup/spi + + + + + org.springframework.boot + spring-boot-starter-test + + + co.nilin.opex + common + + + org.zalando + logbook-spring-boot-webflux-autoconfigure + 3.9.0 + + + org.keycloak + keycloak-core + 26.0.0 + compile + + + org.keycloak + keycloak-server-spi + 26.0.0 + compile + + + org.keycloak + keycloak-server-spi-private + 26.0.0 + compile + + + org.keycloak + keycloak-services + 26.0.0 + compile + + + org.keycloak + keycloak-services + 26.0.0 + compile + + + org.keycloak + keycloak-server-spi-private + 26.0.0 + compile + + + + + + + co.nilin.opex.api.core + api-core + ${project.version} + + + co.nilin.opex.api.ports.proxy + api-proxy-rest + ${project.version} + + + co.nilin.opex.api.ports.binance + api-binance-rest + ${project.version} + + + co.nilin.opex.api.ports.postgres + api-persister-postgres + ${project.version} + + + co.nilin.opex.utility + error-handler + ${error-hanlder.version} + + + co.nilin.opex.utility + interceptors + ${interceptor.version} + + + co.nilin.opex.utility + preferences + ${preferences.version} + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + \ No newline at end of file diff --git a/bc-gateway/bc-gateway-app/pom.xml b/bc-gateway/bc-gateway-app/pom.xml index 86f6888d3..2bdccb667 100644 --- a/bc-gateway/bc-gateway-app/pom.xml +++ b/bc-gateway/bc-gateway-app/pom.xml @@ -1,5 +1,5 @@ - 4.0.0 @@ -112,6 +112,10 @@ co.nilin.opex.bcgateway.ports.walletproxy bc-gateway-wallet-proxy + + 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 - - + +