diff --git a/.gitignore b/.gitignore
index 0e7dc419c..e27020d7c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,7 +3,7 @@ target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
-
+./api.csv
### STS ###
.apt_generated
.classpath
diff --git a/accountant/accountant-app/src/main/resources/application.yml b/accountant/accountant-app/src/main/resources/application.yml
index 48b7e0347..1f08d8860 100644
--- a/accountant/accountant-app/src/main/resources/application.yml
+++ b/accountant/accountant-app/src/main/resources/application.yml
@@ -113,4 +113,13 @@ app:
enabled: ${CUSTOM_MESSAGE_ENABLED:false}
base-url: ${CUSTOM_MESSAGE_URL}
custom-user-language:
- enabled: ${CUSTOM_USER_LANGUAGE_ENABLED:false}
\ No newline at end of file
+ enabled: ${CUSTOM_USER_LANGUAGE_ENABLED:false}
+
+
+# --- Swagger / SpringDoc (env-driven) ---
+springdoc:
+ api-docs:
+ enabled: ${SWAGGER_API_DOCS_ENABLED:false}
+ swagger-ui:
+ enabled: ${SWAGGER_UI_ENABLED:false}
+ path: ${SWAGGER_UI_PATH:/swagger-ui.html}
diff --git a/api/api-app/pom.xml b/api/api-app/pom.xml
index 164918767..07c13842b 100644
--- a/api/api-app/pom.xml
+++ b/api/api-app/pom.xml
@@ -60,11 +60,7 @@
co.nilin.opex.api.ports.postgres
api-persister-postgres
-
- io.springfox
- springfox-boot-starter
- 3.0.0
-
+
org.springframework.cloud
spring-cloud-starter-vault-config
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/OpenApiConfig.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/OpenApiConfig.kt
new file mode 100644
index 000000000..ac4597ed7
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/OpenApiConfig.kt
@@ -0,0 +1,38 @@
+package co.nilin.opex.api.app.config
+
+import io.swagger.v3.oas.models.Components
+import io.swagger.v3.oas.models.OpenAPI
+import io.swagger.v3.oas.models.info.Info
+import io.swagger.v3.oas.models.security.SecurityScheme
+import io.swagger.v3.oas.models.servers.Server
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+
+@Configuration
+class OpenApiConfig() {
+
+ @Bean
+ fun opexOpenApi(): OpenAPI {
+ return OpenAPI()
+ .info(
+ Info()
+ .title("Opex API")
+ .description("OpenAPI documentation for Opex REST APIs.")
+ .version("1.0.1-beta.7")
+ .description("Backend for opex exchange.")
+ )
+ .components(
+ Components()
+ .addSecuritySchemes(
+ "bearerAuth",
+ SecurityScheme()
+ .type(SecurityScheme.Type.HTTP)
+ .scheme("bearer")
+ .bearerFormat("JWT")
+ .description("JWT Bearer token")
+ )
+ )
+ }
+}
+
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/OpenApiCorsConfig.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/OpenApiCorsConfig.kt
new file mode 100644
index 000000000..02150efcf
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/OpenApiCorsConfig.kt
@@ -0,0 +1,46 @@
+package co.nilin.opex.api.app.config
+
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.Ordered
+import org.springframework.core.annotation.Order
+import org.springframework.web.cors.CorsConfiguration
+import org.springframework.web.cors.reactive.CorsWebFilter
+import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource
+
+@Configuration(proxyBeanMethods = false)
+class OpenApiCorsConfig(
+ @Value("\${app.swagger.cors.enabled:false}")
+ private val enabled: Boolean,
+
+ @Value("\${app.swagger.cors.allowed-origins:http://localhost:8110}")
+ private val allowedOrigins: String
+) {
+
+ @Bean
+ @Order(Ordered.HIGHEST_PRECEDENCE)
+ fun swaggerCorsWebFilter(): CorsWebFilter {
+ val config = CorsConfiguration().apply {
+ allowedOrigins = if (enabled) {
+ this@OpenApiCorsConfig.allowedOrigins
+ .split(",")
+ .map { it.trim() }
+ .filter { it.isNotBlank() }
+ } else {
+ emptyList()
+ }
+
+ allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
+ allowedHeaders = listOf("*")
+ exposedHeaders = listOf("Location", "Content-Disposition")
+ allowCredentials = false
+ maxAge = 3600
+ }
+
+ val source = UrlBasedCorsConfigurationSource()
+ source.registerCorsConfiguration("/**", config)
+
+ return CorsWebFilter(source)
+ }
+}
\ No newline at end of file
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/SwaggerConfig.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/SwaggerConfig.kt
deleted file mode 100644
index 68723c02a..000000000
--- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/SwaggerConfig.kt
+++ /dev/null
@@ -1,96 +0,0 @@
-package co.nilin.opex.api.app.config
-
-import org.springframework.beans.factory.annotation.Value
-import org.springframework.context.annotation.Bean
-import org.springframework.context.annotation.Configuration
-import org.springframework.security.core.annotation.AuthenticationPrincipal
-import springfox.documentation.builders.ApiInfoBuilder
-import springfox.documentation.builders.OAuthBuilder
-import springfox.documentation.builders.PathSelectors
-import springfox.documentation.builders.RequestParameterBuilder
-import springfox.documentation.service.*
-import springfox.documentation.spi.DocumentationType
-import springfox.documentation.spi.service.contexts.SecurityContext
-import springfox.documentation.spring.web.plugins.Docket
-import springfox.documentation.swagger.web.SecurityConfiguration
-import springfox.documentation.swagger.web.SecurityConfigurationBuilder
-import java.security.Principal
-import java.util.*
-
-@Configuration
-class SwaggerConfig {
- @Value("\${swagger.authUrl}")
- private lateinit var authUrl: String
-
- @Bean
- fun opexApi(): Docket {
- return Docket(DocumentationType.SWAGGER_2)
- .groupName("opex-api")
- .apiInfo(apiInfo())
- .select()
- .paths(PathSelectors.regex("^/api/v3.*"))
- .build()
- .globalRequestParameters(
- Collections.singletonList(
- RequestParameterBuilder()
- .name("content-type")
- .description("content-type")
- .`in`(ParameterType.HEADER)
- .required(true)
- .build()
- )
- )
- .ignoredParameterTypes(AuthenticationPrincipal::class.java, Principal::class.java)
- .useDefaultResponseMessages(false)
- .securitySchemes(Collections.singletonList(oauth()))
- .securityContexts(Collections.singletonList(securityContext()))
- }
-
- private fun apiInfo(): ApiInfo {
- return ApiInfoBuilder()
- .title("OPEX API")
- .description("Backend for opex exchange.")
- .license("MIT License")
- .licenseUrl("https://github.com/opexdev/Back-end/blob/feature/1-MVP/LICENSE")
- .version("0.1")
- .build()
- }
-
- private fun oauth(): SecurityScheme {
- return OAuthBuilder()
- .name("opex")
- .grantTypes(grantTypes())
- .scopes(scopes())
- .build()
- }
-
- private fun scopes(): List {
- return listOf(AuthorizationScope("openid", "OpenId"))
- }
-
- private fun grantTypes(): List {
- val grantType = ResourceOwnerPasswordCredentialsGrant(authUrl)
- return Collections.singletonList(grantType)
- }
-
- private fun securityContext(): SecurityContext {
- val securityReference = SecurityReference.builder()
- .reference("opex")
- .scopes(emptyArray())
- .build()
- return SecurityContext.builder()
- .securityReferences(Collections.singletonList(securityReference))
- .operationSelector { true }
- .build()
- }
-
- @Bean
- fun securityInfo(): SecurityConfiguration {
- return SecurityConfigurationBuilder.builder()
- .clientId("admin-cli")
- .realm("opex")
- .appName("opex")
- .scopeSeparator(",")
- .build()
- }
-}
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 04e566341..5bf8beadf 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
@@ -4,16 +4,28 @@ import co.nilin.opex.api.app.data.ApiKeyResponse
import co.nilin.opex.api.app.data.CreateApiKeyRequest
import co.nilin.opex.api.app.data.UpdateApiKeyRequest
import co.nilin.opex.common.security.JwtUtils
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.enums.ParameterIn
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.web.bind.annotation.*
import java.security.SecureRandom
import java.util.*
@RestController
@RequestMapping("/v1/api-key")
+@Tag(
+ name = "API App - API Keys",
+ description = "API key management operations."
+)
class APIKeyController(
private val apiKeyService: co.nilin.opex.api.core.spi.APIKeyService
) {
-
private val rng = SecureRandom()
private fun generateSecretBase64(bytes: Int = 48): String {
@@ -31,16 +43,40 @@ class APIKeyController(
"X-API-BODY-SHA256" to " (optional)"
)
- // Create a new API key. Caller must provide a user access token; we bind the key to that user. Returns one-time secret and usage hints.
@PostMapping
+ @Operation(
+ summary = "Create API key",
+ description = """POST /v1/api-key.
+Security: Bearer user-token required.
+
+Behavior: Creates a new API key for the authenticated user. The generated secret is returned only once in this response.
+Source of values: Use the Bearer token of the user who should own the API key.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = ApiKeyResponse::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun create(
- @RequestHeader(name = "Authorization", required = false) authorization: String?,
+ @Parameter(hidden = true)
+ @RequestHeader(name = "Authorization", required = false)
+ authorization: String?,
@RequestBody req: CreateApiKeyRequest
): ApiKeyResponse {
require(!authorization.isNullOrBlank() && authorization.startsWith("Bearer ")) { "Authorization Bearer user token is required" }
val userToken = authorization.substringAfter("Bearer ").trim()
val (userId, preferredUsername) = parseJwtUser(userToken)
-
val apiKeyId = req.apiKeyId?.takeIf { it.isNotBlank() } ?: UUID.randomUUID().toString()
val secret = generateSecretBase64()
val stored = apiKeyService.createApiKeyRecord(
@@ -65,7 +101,6 @@ class APIKeyController(
}
private fun parseJwtUser(token: String): Pair {
- // Decode JWT payload using common JwtUtils (no signature verification here).
val payload = JwtUtils.decodePayload(token)
val sub = payload["sub"] as? String
val preferred = payload["username"] as? String
@@ -73,8 +108,35 @@ class APIKeyController(
return Pair(sub!!, preferred)
}
- // List all API keys (admin-only) — secret is not returned
@GetMapping
+ @Operation(
+ summary = "List API keys",
+ description = """GET /v1/api-key.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Behavior: Returns API key metadata. Secrets are not returned.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = ApiKeyResponse::class))
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun list(): List = apiKeyService.listApiKeyRecords().stream().map {
ApiKeyResponse(
apiKeyId = it.apiKeyId,
@@ -87,9 +149,41 @@ class APIKeyController(
)
}.toList()
-
- // Get one API key (admin-only) — secret is not returned
@GetMapping("/{apiKeyId}")
+ @Operation(
+ summary = "Get API key",
+ description = """GET /v1/api-key/{apiKeyId}.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Behavior: Returns API key metadata. Secret is not returned.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ parameters = [Parameter(
+ name = "apiKeyId",
+ `in` = ParameterIn.PATH,
+ required = true,
+ description = "API key id."
+ )],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = ApiKeyResponse::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun get(@PathVariable apiKeyId: String): ApiKeyResponse {
val it = apiKeyService.getApiKeyRecord(apiKeyId) ?: throw NoSuchElementException("API key not found: $apiKeyId")
return ApiKeyResponse(
@@ -103,8 +197,41 @@ class APIKeyController(
)
}
- // Rotate secret (admin-only). Returns new one-time secret
@PostMapping("/{apiKeyId}/rotate")
+ @Operation(
+ summary = "Rotate API key secret",
+ description = """POST /v1/api-key/{apiKeyId}/rotate.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Behavior: Rotates the API key secret. The new secret is returned only once in this response.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ parameters = [Parameter(
+ name = "apiKeyId",
+ `in` = ParameterIn.PATH,
+ required = true,
+ description = "API key id."
+ )],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = ApiKeyResponse::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun rotate(@PathVariable apiKeyId: String): ApiKeyResponse {
val newSecret = generateSecretBase64()
val stored = apiKeyService.rotateApiKeySecret(apiKeyId, newSecret)
@@ -119,8 +246,41 @@ class APIKeyController(
)
}
- // Update metadata or enable/disable (admin-only)
@PutMapping("/{apiKeyId}")
+ @Operation(
+ summary = "Update API key",
+ description = """PUT /v1/api-key/{apiKeyId}.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Behavior: Updates API key metadata and enabled status. Secret is not returned.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ parameters = [Parameter(
+ name = "apiKeyId",
+ `in` = ParameterIn.PATH,
+ required = true,
+ description = "API key id."
+ )],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = ApiKeyResponse::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun update(@PathVariable apiKeyId: String, @RequestBody req: UpdateApiKeyRequest): ApiKeyResponse {
val s = apiKeyService.updateApiKeyRecord(
apiKeyId = apiKeyId,
@@ -140,9 +300,40 @@ class APIKeyController(
)
}
- // Delete/revoke (admin-only)
@DeleteMapping("/{apiKeyId}")
+ @Operation(
+ summary = "Delete API key",
+ description = """DELETE /v1/api-key/{apiKeyId}.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Behavior: Deletes or revokes the API key.
+Response body: No response body.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ parameters = [Parameter(
+ name = "apiKeyId",
+ `in` = ParameterIn.PATH,
+ required = true,
+ description = "API key id."
+ )],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun delete(@PathVariable apiKeyId: String) {
apiKeyService.deleteApiKeyRecord(apiKeyId)
}
-}
\ No newline at end of file
+}
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/controller/RateLimitController.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/controller/RateLimitController.kt
index e4a8f679c..b029997e1 100644
--- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/controller/RateLimitController.kt
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/controller/RateLimitController.kt
@@ -1,17 +1,42 @@
package co.nilin.opex.api.app.controller
import co.nilin.opex.api.core.spi.RateLimitConfigService
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
+import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/v1/rate-limit")
+@Tag(
+ name = "API App - Rate Limit",
+ description = "Rate-limit administration operations."
+)
class RateLimitController(
private val rateLimitConfig: RateLimitConfigService,
) {
- @PostMapping
+
+ @GetMapping
+ @Operation(
+ summary = "Reload rate-limit config",
+ description = """POST /v1/rate-limit.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Behavior: Reloads rate-limit configuration from the configured source.
+Response body: No response body.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. Required authority is missing: ROLE_admin. No response body.", content = [Content()])
+ ]
+ )
suspend fun reloadRateLimits() {
rateLimitConfig.loadConfig()
}
-}
\ 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 157d4c5ba..353f13cc1 100644
--- a/api/api-app/src/main/resources/application.yml
+++ b/api/api-app/src/main/resources/application.yml
@@ -149,5 +149,22 @@ app:
api:
crypto:
key: ${api_crypto_key:0e1fd29572ec8c85970d76e3433e96ee}
-swagger:
- authUrl: ${SWAGGER_AUTH_URL:https://api.opex.dev/auth}/realms/opex/protocol/openid-connect/token
+ swagger:
+ cors:
+ enabled: true
+ allowed-origins: "http://localhost:8110"
+
+# --- Swagger / SpringDoc (env-driven) ---
+springdoc:
+ api-docs:
+ enabled: ${SWAGGER_API_DOCS_ENABLED:false}
+ path: ${SWAGGER_API_DOCS_PATH:/v3/api-docs}
+ swagger-ui:
+ enabled: ${SWAGGER_UI_ENABLED:false}
+ path: ${SWAGGER_UI_PATH:/swagger-ui.html}
+ try-it-out-enabled: ${SWAGGER_TRY_IT_OUT_ENABLED:true}
+ persist-authorization: ${SWAGGER_PERSIST_AUTHORIZATION:true}
+ display-request-duration: true
+ operations-sorter: method
+ tags-sorter: alpha
+
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
index 65b52c913..457deb99a 100644
--- 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
@@ -5,9 +5,6 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo
import java.math.BigDecimal
import java.util.*
-enum class GatewayType() {
- OnChain, OffChain
-}
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/GatewayUpdateCommand.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/GatewayUpdateCommand.kt
new file mode 100644
index 000000000..6378b2c25
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/GatewayUpdateCommand.kt
@@ -0,0 +1,105 @@
+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 CurrencyGatewayUpdateCommand(
+ open var currencySymbol: String? = null,
+ open var gatewayUuid: String? = UUID.randomUUID().toString(),
+ open var isDepositActive: Boolean?,
+ open var isWithdrawActive: 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,
+ open var displayOrder: Int? = null,
+)
+
+data class OffChainGatewayUpdateCommand(
+ var transferMethod: TransferMethod,
+ override var currencySymbol: String? = null,
+ override var gatewayUuid: String? = UUID.randomUUID().toString(),
+ override var isDepositActive: Boolean? = true,
+ override var isWithdrawActive: 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,
+ override var displayOrder: Int? = null,
+
+ ) : CurrencyGatewayCommand(
+ currencySymbol,
+ gatewayUuid,
+ isDepositActive,
+ isWithdrawActive,
+ withdrawFee,
+ withdrawAllowed,
+ depositAllowed,
+ depositMin,
+ depositMax,
+ withdrawMin,
+ withdrawMax,
+ null,
+ null,
+ displayOrder
+)
+
+data class OnChainGatewayUpdateCommand(
+
+ 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 isDepositActive: Boolean? = true,
+ override var isWithdrawActive: 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,
+ override var displayOrder: Int? = null,
+) : CurrencyGatewayCommand(
+ currencySymbol,
+ gatewayUuid,
+ isDepositActive,
+ isWithdrawActive,
+ withdrawFee,
+ withdrawAllowed,
+ depositAllowed,
+ depositMin,
+ depositMax,
+ withdrawMin,
+ withdrawMax,
+ null,
+ null,
+ displayOrder
+)
+
+
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TerminalCommand.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TerminalCommand.kt
index a7c919deb..5439e9feb 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TerminalCommand.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TerminalCommand.kt
@@ -1,12 +1,11 @@
package co.nilin.opex.api.core.inout
-data class TerminalCommand(
+data class TerminalCommand(
var uuid: String?,
- var owner: String?=null,
+ var owner: String? = null,
var identifier: String,
var active: Boolean? = true,
var metaData: String,
- var description : String?=null,
+ var description: String? = null,
var displayOrder: Int? = null,
-
-)
+ )
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TerminalUpdateCommand.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TerminalUpdateCommand.kt
new file mode 100644
index 000000000..c77b1a668
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TerminalUpdateCommand.kt
@@ -0,0 +1,9 @@
+package co.nilin.opex.api.core.inout
+
+data class TerminalUpdateCommand(
+ var uuid: String?,
+ var identifier: String,
+ var active: Boolean? = true,
+ var metaData: String,
+ var displayOrder: Int? = null,
+ )
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserSwapTransactionRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserSwapTransactionRequest.kt
new file mode 100644
index 000000000..02504e5e0
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserSwapTransactionRequest.kt
@@ -0,0 +1,17 @@
+package co.nilin.opex.api.core.inout
+
+data class UserSwapTransactionRequest(
+ val userId: String? = null,
+ val sourceSymbol: String?,
+ val destSymbol: String?,
+ 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/UserTransactionRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionRequest.kt
index 814642140..898d4c420 100644
--- 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
@@ -11,9 +11,5 @@ data class UserTransactionRequest(
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/WithdrawResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawResponse.kt
index ced56abf9..9e9634efa 100644
--- 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
@@ -26,5 +26,5 @@ class WithdrawResponse(
val otpRequired: Int? = 0,
)
enum class WithdrawType {
- CARD_TO_CARD, SHEBA, ON_CHAIN, OFF_CHAIN
+ 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/analytics/ActivityTotals.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/analytics/ActivityTotals.kt
index 43203da9d..11382715b 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/analytics/ActivityTotals.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/analytics/ActivityTotals.kt
@@ -2,9 +2,7 @@ package co.nilin.opex.api.core.inout.analytics
import java.math.BigDecimal
-/**
- * Totals for a single day of user activity (mock values for now).
- */
+
data class ActivityTotals(
val totalBalance: BigDecimal,
val totalWithdraw: BigDecimal,
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 402f8bd21..a16754629 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
@@ -104,9 +104,6 @@ interface WalletProxy {
limit: Int?,
): List
- suspend fun deposit(
- request: RequestDepositBody
- ): TransferResult?
suspend fun requestWithdraw(
token: String,
@@ -127,8 +124,8 @@ interface WalletProxy {
suspend fun getQuoteCurrencies(): List
- suspend fun getSwapTransactions(token: String, request: UserTransactionRequest): List
- suspend fun getSwapTransactionsCount(token: String, request: UserTransactionRequest): Long
+ suspend fun getSwapTransactions(token: String, request: UserSwapTransactionRequest): List
+ suspend fun getSwapTransactionsCount(token: String, request: UserSwapTransactionRequest): Long
suspend fun requestWithdrawOTP(token: String, withdrawUuid: String, otpType: OTPType): TempOtpResponse
suspend fun verifyWithdrawOTP(
@@ -150,7 +147,7 @@ interface WalletProxy {
suspend fun getSwapTransactionsForAdmin(
token: String,
- request: UserTransactionRequest
+ request: UserSwapTransactionRequest
): List
suspend fun getTradeHistoryForAdmin(
@@ -237,14 +234,14 @@ interface WalletProxy {
suspend fun deleteOffChainGatewayLocalization(token: String, id: Long)
suspend fun saveTerminal(token: String, terminal: TerminalCommand): TerminalCommand?
- suspend fun updateTerminal(token: String, terminalUuid: String, terminal: TerminalCommand): TerminalCommand?
+ suspend fun updateTerminal(token: String, terminalUuid: String, terminal: TerminalUpdateCommand): TerminalCommand?
suspend fun deleteTerminal(token: String, terminalUuid: String)
suspend fun getTerminals(token: String): List?
suspend fun getTerminal(token: String, terminalUuid: String): TerminalCommand?
suspend fun getAssignedGatewayToTerminal(token: String, terminalUuid: String): List?
suspend fun assignTerminalsToGateway(token: String, gatewayUuid: String, terminal: List)
suspend fun revokeTerminalsToGateway(token: String, gatewayUuid: String, terminal: List)
- suspend fun addCurrencyToGateway(
+ suspend fun addGatewayToCurrency(
token: String,
currencySymbol: String,
gatewayCommand: CurrencyGatewayCommand
@@ -254,7 +251,7 @@ interface WalletProxy {
token: String,
gatewayUuid: String,
currencySymbol: String,
- gatewayCommand: CurrencyGatewayCommand
+ gatewayCommand: CurrencyGatewayUpdateCommand
): CurrencyGatewayCommand?
suspend fun getGateway(token: String, gatewayUuid: String, currencySymbol: String): CurrencyGatewayCommand?
diff --git a/api/api-ports/api-binance-rest/pom.xml b/api/api-ports/api-binance-rest/pom.xml
index fd5fdffe3..bff6d12c2 100644
--- a/api/api-ports/api-binance-rest/pom.xml
+++ b/api/api-ports/api-binance-rest/pom.xml
@@ -98,5 +98,10 @@
swagger-annotations
1.5.20
+
+ org.springdoc
+ springdoc-openapi-starter-webflux-ui
+ 2.8.14
+
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 f9bd55dc6..3b3996a19 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
@@ -6,6 +6,7 @@ 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.core.annotation.Order
import org.springframework.http.HttpMethod
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.SecurityWebFiltersOrder
@@ -15,6 +16,7 @@ import org.springframework.security.oauth2.jwt.JwtValidators
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder
import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.server.WebFilter
@@ -25,16 +27,51 @@ class SecurityConfig(
@Value("\${app.auth.cert-url}")
private val certUrl: String,
@Value("\${app.auth.iss-url}")
- private val issUrl: String
+ private val issUrl: String,
) {
+ @Value("\${swagger.auth.enabled:false}")
+ private var swaggerAuthEnabled: Boolean = false
+
+ @Value("\${swagger.auth.authority:ROLE_admin}")
+ private lateinit var swaggerAuthority: String
+
+ @Bean
+ @Order(0)
+ fun swaggerSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+ val swaggerPaths = arrayOf(
+ "/swagger-ui.html",
+ "/swagger-ui/**",
+ "/v3/api-docs",
+ "/v3/api-docs/**",
+ "/webjars/**"
+ )
+
+ return http
+ .securityMatcher(ServerWebExchangeMatchers.pathMatchers(*swaggerPaths))
+ .csrf { it.disable() }
+ .authorizeExchange {
+ if (swaggerAuthEnabled) {
+ it.anyExchange().hasAuthority(swaggerAuthority)
+ } else {
+ it.anyExchange().permitAll()
+ }
+ }
+ .oauth2ResourceServer {
+ it.jwt { jwt ->
+ jwt.jwtAuthenticationConverter(ReactiveCustomJwtConverter())
+ }
+ }
+ .build()
+ }
@Bean
- fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+ @Order(1)
+ fun apiSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+
return http.csrf { it.disable() }
.authorizeExchange {
it.pathMatchers("/actuator/**").permitAll()
- .pathMatchers("/swagger-ui/**").permitAll()
- .pathMatchers("/swagger-resources/**").permitAll()
+ .pathMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.pathMatchers("/v1/rate-limit").hasAuthority("ROLE_admin")
.pathMatchers("/v2/api-docs").permitAll()
.pathMatchers("/v3/depth").permitAll()
@@ -69,7 +106,7 @@ class SecurityConfig(
.pathMatchers(HttpMethod.PUT, "/opex/v1/otc/rate").hasAnyAuthority("ROLE_admin", "ROLE_rate_bot")
.pathMatchers(HttpMethod.GET, "/opex/v1/otc/**").permitAll()
.pathMatchers("/opex/v1/otc/**").hasAuthority("ROLE_admin")
- .pathMatchers(HttpMethod.GET, "/opex/v1/bank-account").permitAll()
+ .pathMatchers(HttpMethod.GET, "/opex/v1/bank-account").authenticated()
.pathMatchers("/opex/v1/bank-account/**").hasAuthority("PERM_bank_account:write")
.anyExchange().authenticated()
}
@@ -102,4 +139,5 @@ class SecurityConfig(
return decoder
}
+
}
diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/AccountController.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/AccountController.kt
index 1d146d47b..12c7af9c1 100644
--- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/AccountController.kt
+++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/AccountController.kt
@@ -14,6 +14,7 @@ import io.swagger.annotations.ApiParam
import io.swagger.annotations.ApiResponse
import io.swagger.annotations.Example
import io.swagger.annotations.ExampleProperty
+import io.swagger.v3.oas.annotations.Hidden
import org.springframework.http.MediaType
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
@@ -24,6 +25,8 @@ import java.time.ZoneId
import java.util.*
@RestController
+@Hidden
+@Deprecated("")
class AccountController(
val queryHandler: MarketUserDataProxy,
val matchingGatewayProxy: MatchingGatewayProxy,
diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/LandingController.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/LandingController.kt
index b6524c586..9666453e1 100644
--- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/LandingController.kt
+++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/LandingController.kt
@@ -10,6 +10,7 @@ import co.nilin.opex.api.ports.binance.data.GlobalPriceResponse
import co.nilin.opex.api.ports.binance.data.MarketInfoResponse
import co.nilin.opex.api.ports.binance.data.MarketStatResponse
import co.nilin.opex.common.utils.Interval
+import io.swagger.v3.oas.annotations.Hidden
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.slf4j.LoggerFactory
@@ -19,7 +20,9 @@ import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.math.BigDecimal
-@RestController // Custom service
+@RestController
+@Hidden
+@Deprecated("")// Custom service
@RequestMapping("/v1/landing")
class LandingController(
private val marketStatProxy: MarketStatProxy,
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 c83f41bac..f93715b5b 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
@@ -9,6 +9,8 @@ import co.nilin.opex.api.core.spi.SymbolMapper
import co.nilin.opex.api.ports.binance.data.*
import co.nilin.opex.common.OpexError
import co.nilin.opex.common.utils.Interval
+import io.swagger.v3.oas.annotations.Hidden
+import io.swagger.v3.oas.annotations.tags.Tag
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.springframework.web.bind.annotation.GetMapping
@@ -18,7 +20,6 @@ import org.springframework.web.bind.annotation.RestController
import java.math.BigDecimal
import java.security.Principal
import java.time.ZoneId
-
@RestController("binanceMarketController")
class MarketController(
private val accountantProxy: AccountantProxy,
@@ -35,6 +36,8 @@ class MarketController(
// 500 - 5
// 1000 - 10
// 5000 - 50
+ @Hidden
+ @Deprecated("Deprecated")
@GetMapping("/v3/depth")
suspend fun orderBook(
@RequestParam
@@ -72,7 +75,8 @@ class MarketController(
val lastOrder = marketDataProxy.lastOrder(localSymbol)
return OrderBookResponse(lastOrder?.orderId ?: -1, mappedBidOrders, mappedAskOrders)
}
-
+ @Hidden
+ @Deprecated("Deprecated")
@GetMapping("/v3/trades")
suspend fun recentTrades(
principal: Principal,
@@ -99,7 +103,8 @@ class MarketController(
)
}
}
-
+ @Hidden
+ @Deprecated("Deprecated")
@GetMapping("/v3/ticker/{duration:24h|7d|1M}")
suspend fun priceChange(
@PathVariable duration: String,
@@ -137,7 +142,8 @@ class MarketController(
return if (quote.isNullOrEmpty()) result else result.filter { it.quote.equals(quote, true) }
}
-
+ @Hidden
+ @Deprecated("Deprecated")
// Weight
// 1 for a single symbol
// 2 when the symbol parameter is omitted
@@ -150,7 +156,8 @@ class MarketController(
symbolMapper.toInternalSymbol(symbol) ?: throw OpexError.SymbolNotFound.exception()
return marketDataProxy.lastPrice(localSymbol).onEach { symbols[it.symbol]?.let { s -> it.symbol = s } }
}
-
+ @Hidden
+ @Deprecated("Deprecated")
@GetMapping("/v3/exchangeInfo")
suspend fun pairInfo(
@RequestParam(required = false)
@@ -198,7 +205,8 @@ class MarketController(
// )
// }
// }
-
+ @Hidden
+ @Deprecated("Deprecated")
// Custom service
@GetMapping("/v3/currencyInfo/quotes")
suspend fun getQuoteCurrencies(): List {
@@ -206,8 +214,8 @@ class MarketController(
.map { it.rightSideWalletSymbol }
.distinct()
}
-
// Weight(IP): 1
+ @Tag(name = "Internal Charts")
@GetMapping("/v3/klines")
suspend fun klines(
@RequestParam
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 9b389bdf5..e21f99536 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
@@ -6,6 +6,7 @@ import co.nilin.opex.api.ports.binance.data.AssetResponse
import co.nilin.opex.api.ports.binance.data.AssetsEstimatedValue
import co.nilin.opex.common.security.jwtAuthentication
import co.nilin.opex.common.security.tokenValue
+import jdk.internal.vm.annotation.Hidden
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
import org.springframework.web.bind.annotation.GetMapping
@@ -14,6 +15,8 @@ import org.springframework.web.bind.annotation.RestController
import java.math.BigDecimal
@RestController("walletBinanceController")
+@io.swagger.v3.oas.annotations.Hidden
+@Deprecated("")
class WalletController(
private val walletProxy: WalletProxy,
private val marketDataProxy: MarketDataProxy,
diff --git a/api/api-ports/api-opex-rest/pom.xml b/api/api-ports/api-opex-rest/pom.xml
index 0e857f35c..22b37a60d 100644
--- a/api/api-ports/api-opex-rest/pom.xml
+++ b/api/api-ports/api-opex-rest/pom.xml
@@ -67,9 +67,15 @@
test
- io.swagger
- swagger-annotations
- 1.5.20
+ org.springdoc
+ springdoc-openapi-starter-webflux-ui
+ 2.8.14
+
+
+ central
+ https://repo1.maven.org/maven2/
+
+
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/AddressBookController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/AddressBookController.kt
index 369111929..2455d3ca0 100644
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/AddressBookController.kt
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/AddressBookController.kt
@@ -5,44 +5,200 @@ import co.nilin.opex.api.core.inout.AddressBookResponse
import co.nilin.opex.api.core.spi.ProfileProxy
import co.nilin.opex.api.ports.opex.util.jwtAuthentication
import co.nilin.opex.api.ports.opex.util.tokenValue
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.enums.ParameterIn
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
-import org.springframework.web.bind.annotation.*
+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
@RestController
@RequestMapping("/opex/v1/address-book")
+@Tag(
+ name = "Address Book",
+ description = "Manage authenticated user's saved destination addresses."
+)
class AddressBookController(
- val profileProxy: ProfileProxy,
+ val profileProxy: ProfileProxy
) {
@PostMapping
+ @Operation(
+ summary = "Create address book entry",
+ description = """Creates a new address book entry for the authenticated user.
+Security: Bearer user-token is required.
+
+Behavior: `addressType` is a server-provided string. Clients should use the value returned by the related chain/address-type service and must not hardcode a fixed enum list.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ requestBody = io.swagger.v3.oas.annotations.parameters.RequestBody(
+ required = true,
+ description = "Address book entry creation payload.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = AddAddressBookItemRequest::class)
+ )
+ ]
+ ),
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Address book entry created successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = AddressBookResponse::class)
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun addAddressBook(
@RequestBody request: AddAddressBookItemRequest,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext
): AddressBookResponse {
return profileProxy.addAddressBook(securityContext.jwtAuthentication().tokenValue(), request)
}
@GetMapping
- suspend fun getAddressBook(@CurrentSecurityContext securityContext: SecurityContext): List {
+ @Operation(
+ summary = "List address book entries",
+ description = """Returns all address book entries for the authenticated user.
+Security: Bearer user-token is required.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Address book entries returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = AddressBookResponse::class))
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
+ suspend fun getAddressBook(
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext
+ ): List {
return profileProxy.getAllAddressBooks(securityContext.jwtAuthentication().tokenValue())
}
@DeleteMapping("/{id}")
+ @Operation(
+ summary = "Delete address book entry",
+ description = """Deletes one address book entry by ID for the authenticated user.
+Security: Bearer user-token is required.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ parameters = [
+ Parameter(
+ name = "id",
+ `in` = ParameterIn.PATH,
+ required = true,
+ description = "Address book entry ID.",
+ schema = Schema(type = "integer", format = "int64"),
+ example = "1"
+ )
+ ],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Address book entry deleted successfully. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun deleteAddressBook(
@PathVariable("id") id: Long,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext
) {
profileProxy.deleteAddressBook(securityContext.jwtAuthentication().tokenValue(), id)
}
@PutMapping("/{id}")
+ @Operation(
+ summary = "Update address book entry",
+ description = """Updates one address book entry by ID for the authenticated user.
+Security: Bearer user-token is required.
+
+Behavior: `addressType` is a server-provided string. Clients should use the value returned by the related chain/address-type service and must not hardcode a fixed enum list.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ parameters = [
+ Parameter(
+ name = "id",
+ `in` = ParameterIn.PATH,
+ required = true,
+ description = "Address book entry ID.",
+ schema = Schema(type = "integer", format = "int64"),
+ example = "1"
+ )
+ ],
+ requestBody = io.swagger.v3.oas.annotations.parameters.RequestBody(
+ required = true,
+ description = "Address book entry update payload.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = AddAddressBookItemRequest::class)
+ )
+ ]
+ ),
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Address book entry updated successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = AddressBookResponse::class)
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun updateAddressBook(
@PathVariable("id") id: Long,
@RequestBody request: AddAddressBookItemRequest,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext
): AddressBookResponse {
return profileProxy.updateAddressBook(securityContext.jwtAuthentication().tokenValue(), id, 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/BankAccountController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/BankAccountController.kt
index e704894c7..0d9b8140d 100644
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/BankAccountController.kt
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/BankAccountController.kt
@@ -8,33 +8,84 @@ 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.*
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
@RestController
@RequestMapping("/opex/v1/bank-account")
+@Tag(name = "Bank Account", description = "Manage authenticated user's bank accounts.")
class BankAccountController(
- val profileProxy: ProfileProxy,
+ val profileProxy: ProfileProxy
) {
@PostMapping
+ @Operation(
+ summary = "Add bank account",
+ description = """POST /opex/v1/bank-account.
+Security: Bearer user-token required. Requires authenticated user JWT.
+
+Validation: Exactly one of `cardNumber` or `iban` must be provided. Providing both or neither is invalid.
+Allowed values:
+- status: WAITING, VERIFIED, REJECTED.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response.", content = [Content(mediaType = "application/json", schema = Schema(implementation = BankAccountResponse::class))]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()])
+ ]
+ )
suspend fun addBankAccount(
@RequestBody request: AddBankAccountRequest,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext
): BankAccountResponse {
return profileProxy.addBankAccount(securityContext.jwtAuthentication().tokenValue(), request)
}
@GetMapping
- suspend fun getBankAccounts(@CurrentSecurityContext securityContext: SecurityContext): List {
+ @Operation(
+ summary = "Get bank accounts",
+ description = """GET /opex/v1/bank-account.
+Security: Bearer user-token required. Requires authenticated user JWT.
+Allowed values:
+- status: WAITING, VERIFIED, REJECTED.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response.", content = [Content(mediaType = "application/json", array = ArraySchema(schema = Schema(implementation = BankAccountResponse::class)))]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()])
+ ]
+ )
+ suspend fun getBankAccounts(
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext
+ ): List {
return profileProxy.getBankAccounts(securityContext.jwtAuthentication().tokenValue())
}
@DeleteMapping("/{id}")
+ @Operation(
+ summary = "Delete bank account",
+ description = """DELETE /opex/v1/bank-account/{id}.
+Security: Bearer user-token required. Required authority: PERM_bank_account:write.
+Allowed values:
+- status: WAITING, VERIFIED, REJECTED.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "No response body.", content = [Content()]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. Required authority is missing: PERM_bank_account:write. No response body.", content = [Content()])
+ ]
+ )
suspend fun deleteBankAccount(
@PathVariable("id") id: Long,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext
) {
profileProxy.deleteBankAccount(securityContext.jwtAuthentication().tokenValue(), id)
}
-
-
-}
\ 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/ConfigAdminController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/ConfigAdminController.kt
new file mode 100644
index 000000000..31a2ee981
--- /dev/null
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/ConfigAdminController.kt
@@ -0,0 +1,67 @@
+package co.nilin.opex.api.ports.opex.controller
+
+import co.nilin.opex.api.core.inout.UpdateUserConfigRequest
+import co.nilin.opex.api.core.inout.UpdateWebConfigRequest
+import co.nilin.opex.api.core.inout.UserWebConfig
+import co.nilin.opex.api.core.spi.ConfigProxy
+import co.nilin.opex.api.ports.opex.util.jwtAuthentication
+import co.nilin.opex.api.ports.opex.util.tokenValue
+import co.nilin.opex.common.data.WebConfig
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
+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.PutMapping
+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")
+@Tag(
+ name = "Admin Config",
+ description = "Default web and user configuration operations."
+)
+class ConfigAdminController(private val configProxy: ConfigProxy) {
+
+
+ @PutMapping("/admin/web/config")
+ @Operation(
+ summary = "Update web config",
+ description = """PUT /opex/v1/admin/web/config.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Allowed values:
+- defaultLanguage/supportedLanguages: EN, FA, AR, UZ.
+- supportedCalenders: JALALI, HIJRI, GREGORIAN.
+- defaultTheme/supportedThemes: DARK, LIGHT, SYSTEM.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ requestBody = io.swagger.v3.oas.annotations.parameters.RequestBody(
+ required = true,
+ content = [Content(mediaType = "application/json", schema = Schema(implementation = UpdateWebConfigRequest::class))]
+ ),
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response.", content = [Content(mediaType = "application/json", schema = Schema(implementation = WebConfig::class))]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. No response body.", content = [Content()])
+ ]
+ )
+ suspend fun updateWebConfig(
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext,
+ @RequestBody request: UpdateWebConfigRequest
+ ): WebConfig {
+ return configProxy.updateWebConfig(securityContext.jwtAuthentication().tokenValue(), request)
+ }
+
+}
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/ConfigController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/ConfigController.kt
deleted file mode 100644
index c61b07fd2..000000000
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/ConfigController.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-package co.nilin.opex.api.ports.opex.controller
-
-import co.nilin.opex.api.core.inout.UpdateUserConfigRequest
-import co.nilin.opex.api.core.inout.UpdateWebConfigRequest
-import co.nilin.opex.api.core.inout.UserLevelConfig
-import co.nilin.opex.api.core.inout.UserWebConfig
-import co.nilin.opex.api.core.spi.ConfigProxy
-import co.nilin.opex.api.ports.opex.util.jwtAuthentication
-import co.nilin.opex.api.ports.opex.util.tokenValue
-import co.nilin.opex.common.data.WebConfig
-import org.springframework.security.core.annotation.CurrentSecurityContext
-import org.springframework.security.core.context.SecurityContext
-import org.springframework.web.bind.annotation.*
-
-@RestController
-@RequestMapping("/opex/v1")
-class ConfigController(private val configProxy: ConfigProxy) {
-
- @GetMapping("/web/config")
- suspend fun getWebConfig(): WebConfig {
- return configProxy.getWebConfig()
- }
-
- @PutMapping("/admin/web/config")
- suspend fun updateWebConfig(
- @CurrentSecurityContext securityContext: SecurityContext,
- @RequestBody request: UpdateWebConfigRequest
- ): WebConfig {
- return configProxy.updateWebConfig(securityContext.jwtAuthentication().tokenValue(), request)
- }
-
- @GetMapping("/user-level/config")
- suspend fun getUserLevelConfig(): List {
- return configProxy.getUserLevelConfig()
- }
-
- @PutMapping("/admin/user-level/config")
- suspend fun updateUserLevelConfig(
- @CurrentSecurityContext securityContext: SecurityContext,
- @RequestBody userLevelConfig: UserLevelConfig
- ): UserLevelConfig {
- return configProxy.updateUserLevelConfig(securityContext.jwtAuthentication().tokenValue(), userLevelConfig)
- }
-
- @DeleteMapping("/admin/user-level/config/{userLevel}/{language}")
- suspend fun updateUserLevelConfig(
- @CurrentSecurityContext securityContext: SecurityContext,
- @PathVariable userLevel: String,
- @PathVariable language: String
- ) {
- configProxy.deleteUserLevelConfig(securityContext.jwtAuthentication().tokenValue(), userLevel, language)
- }
-
- @GetMapping("/user/config")
- suspend fun getUserConfig(@CurrentSecurityContext securityContext: SecurityContext): UserWebConfig {
- return configProxy.getUserConfig(securityContext.jwtAuthentication().tokenValue())
- }
-
- @PutMapping("/user/config")
- suspend fun updateConfig(
- @CurrentSecurityContext securityContext: SecurityContext,
- @RequestBody request: UpdateUserConfigRequest
- ): UserWebConfig {
- return configProxy.updateUserConfig(securityContext.jwtAuthentication().tokenValue(), request)
- }
-
- @GetMapping("/user/config/pair")
- suspend fun getUserFavoritePair(@CurrentSecurityContext securityContext: SecurityContext): Set {
- return configProxy.getUserFavoritePair(securityContext.jwtAuthentication().tokenValue())
- }
-
- @PostMapping("/user/config/pair/{pair}")
- suspend fun addUserFavoritePair(
- @CurrentSecurityContext securityContext: SecurityContext,
- @PathVariable pair: String
- ): Set {
- return configProxy.addUserFavoritePair(securityContext.jwtAuthentication().tokenValue(), pair)
- }
-
- @DeleteMapping("/user/config/pair/{pair}")
- suspend fun removeUserFavoritePair(
- @CurrentSecurityContext securityContext: SecurityContext,
- @PathVariable pair: String
- ): Set {
- return configProxy.removeUserFavoritePair(securityContext.jwtAuthentication().tokenValue(), pair)
- }
-}
\ 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/CurrencyRatesController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/CurrencyRatesController.kt
index 717bfdd61..8a5aaeb3d 100644
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/CurrencyRatesController.kt
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/CurrencyRatesController.kt
@@ -1,22 +1,89 @@
package co.nilin.opex.api.ports.opex.controller
-import co.nilin.opex.api.core.inout.otc.*
+import co.nilin.opex.api.core.inout.otc.CurrencyExchangeRatesResponse
+import co.nilin.opex.api.core.inout.otc.CurrencyPair
+import co.nilin.opex.api.core.inout.otc.CurrencyPrice
+import co.nilin.opex.api.core.inout.otc.ForbiddenPairs
+import co.nilin.opex.api.core.inout.otc.Rate
+import co.nilin.opex.api.core.inout.otc.Rates
+import co.nilin.opex.api.core.inout.otc.SetCurrencyExchangeRateRequest
+import co.nilin.opex.api.core.inout.otc.Symbols
import co.nilin.opex.api.core.spi.RateProxy
import co.nilin.opex.api.ports.opex.util.jwtAuthentication
import co.nilin.opex.api.ports.opex.util.tokenValue
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.enums.ParameterIn
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
-import org.springframework.web.bind.annotation.*
+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.RequestParam
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
@RestController
-@RequestMapping( "/opex/v1/otc")
+@RequestMapping("/opex/v1/otc")
class CurrencyRatesController(
private val rateProxy: RateProxy
) {
// Rates
@PostMapping("/rate")
+ @Operation(
+ tags = ["OTC Rates"],
+ summary = "Create OTC exchange rate",
+ description = """Creates a new OTC exchange rate.
+
+Required authentication:
+- Bearer admin-token is required.
+- Required role: ROLE_admin.
+
+Validation:
+- `rate` must be greater than zero.
+- `sourceSymbol` and `destSymbol` must be different.
+
+Request body: SetCurrencyExchangeRateRequest.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ requestBody = io.swagger.v3.oas.annotations.parameters.RequestBody(
+ required = true,
+ description = "OTC exchange rate creation payload.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = SetCurrencyExchangeRateRequest::class)
+ )
+ ]
+ ),
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "OTC exchange rate created successfully. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required role is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun createRate(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@RequestBody request: SetCurrencyExchangeRateRequest
) {
@@ -25,7 +92,58 @@ class CurrencyRatesController(
}
@PutMapping("/rate")
+ @Operation(
+ tags = ["OTC Rates"],
+ summary = "Update OTC exchange rate",
+ description = """Updates an existing OTC exchange rate.
+
+Required authentication:
+- Bearer admin-token or service token is required.
+- Required role: ROLE_admin or ROLE_rate_bot.
+
+Validation:
+- `rate` must be greater than zero.
+- `sourceSymbol` and `destSymbol` must be different.
+
+Request body: SetCurrencyExchangeRateRequest.
+
+Response body: Rates.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ requestBody = io.swagger.v3.oas.annotations.parameters.RequestBody(
+ required = true,
+ description = "OTC exchange rate update payload.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = SetCurrencyExchangeRateRequest::class)
+ )
+ ]
+ ),
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "OTC exchange rate updated successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = Rates::class)
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required role is missing: ROLE_admin or ROLE_rate_bot. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun updateRate(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@RequestBody request: SetCurrencyExchangeRateRequest
): Rates {
@@ -34,7 +152,62 @@ class CurrencyRatesController(
}
@DeleteMapping("/rate/{sourceSymbol}/{destSymbol}")
+ @Operation(
+ tags = ["OTC Rates"],
+ summary = "Delete OTC exchange rate",
+ description = """Deletes an OTC exchange rate by source and destination symbols.
+
+Required authentication:
+- Bearer admin-token is required.
+- Required role: ROLE_admin.
+
+Path parameters:
+
+Response body: Rates.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ parameters = [
+ Parameter(
+ name = "sourceSymbol",
+ `in` = ParameterIn.PATH,
+ required = true,
+ description = "Source currency symbol.",
+ example = "BTC",
+ schema = Schema(type = "string")
+ ),
+ Parameter(
+ name = "destSymbol",
+ `in` = ParameterIn.PATH,
+ required = true,
+ description = "Destination currency symbol.",
+ example = "USDT",
+ schema = Schema(type = "string")
+ )
+ ],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "OTC exchange rate deleted successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = Rates::class)
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required role is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun deleteRate(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@PathVariable sourceSymbol: String,
@PathVariable destSymbol: String
@@ -43,11 +216,75 @@ class CurrencyRatesController(
}
@GetMapping("/rate")
+ @Operation(
+ tags = ["OTC Rates"],
+ summary = "List OTC exchange rates",
+ description = """Returns configured OTC exchange rates.
+
+Authentication:
+- Public endpoint. No Bearer token is required.
+
+Response body: Rates.""",
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "OTC exchange rates returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = Rates::class)
+ )
+ ]
+ )
+ ]
+ )
suspend fun fetchRates(): Rates {
return rateProxy.fetchRates()
}
@GetMapping("/rate/{sourceSymbol}/{destSymbol}")
+ @Operation(
+ tags = ["OTC Rates"],
+ summary = "Get OTC exchange rate",
+ description = """Returns one OTC exchange rate by source and destination symbols.
+
+Authentication:
+- Public endpoint. No Bearer token is required.
+
+Path parameters:
+
+Response body: Rate, nullable.""",
+ parameters = [
+ Parameter(
+ name = "sourceSymbol",
+ `in` = ParameterIn.PATH,
+ required = true,
+ description = "Source currency symbol.",
+ example = "BTC",
+ schema = Schema(type = "string")
+ ),
+ Parameter(
+ name = "destSymbol",
+ `in` = ParameterIn.PATH,
+ required = true,
+ description = "Destination currency symbol.",
+ example = "USDT",
+ schema = Schema(type = "string")
+ )
+ ],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "OTC exchange rate returned successfully. Response may be null if no rate exists.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = Rate::class)
+ )
+ ]
+ )
+ ]
+ )
suspend fun fetchRate(
@PathVariable sourceSymbol: String,
@PathVariable destSymbol: String
@@ -57,7 +294,50 @@ class CurrencyRatesController(
// Forbidden pairs
@PostMapping("/forbidden-pairs")
+ @Operation(
+ tags = ["OTC Forbidden Pairs"],
+ summary = "Add forbidden OTC pair",
+ description = """Adds a forbidden OTC currency pair.
+
+Required authentication:
+- Bearer admin-token is required.
+- Required role: ROLE_admin.
+
+Validation:
+- `sourceSymbol` and `destSymbol` must be different.
+
+Request body: CurrencyPair.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ requestBody = io.swagger.v3.oas.annotations.parameters.RequestBody(
+ required = true,
+ description = "Forbidden OTC pair payload.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = CurrencyPair::class)
+ )
+ ]
+ ),
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Forbidden pair added successfully. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required role is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun addForbiddenPair(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@RequestBody request: CurrencyPair
) {
@@ -66,7 +346,62 @@ class CurrencyRatesController(
}
@DeleteMapping("/forbidden-pairs/{sourceSymbol}/{destSymbol}")
+ @Operation(
+ tags = ["OTC Forbidden Pairs"],
+ summary = "Delete forbidden OTC pair",
+ description = """Deletes one forbidden OTC currency pair.
+
+Required authentication:
+- Bearer admin-token is required.
+- Required role: ROLE_admin.
+
+Path parameters:
+
+Response body: ForbiddenPairs.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ parameters = [
+ Parameter(
+ name = "sourceSymbol",
+ `in` = ParameterIn.PATH,
+ required = true,
+ description = "Source currency symbol.",
+ example = "BTC",
+ schema = Schema(type = "string")
+ ),
+ Parameter(
+ name = "destSymbol",
+ `in` = ParameterIn.PATH,
+ required = true,
+ description = "Destination currency symbol.",
+ example = "IRR",
+ schema = Schema(type = "string")
+ )
+ ],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Forbidden pair deleted successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = ForbiddenPairs::class)
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required role is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun deleteForbiddenPair(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@PathVariable sourceSymbol: String,
@PathVariable destSymbol: String
@@ -75,13 +410,75 @@ class CurrencyRatesController(
}
@GetMapping("/forbidden-pairs")
+ @Operation(
+ tags = ["OTC Forbidden Pairs"],
+ summary = "List forbidden OTC pairs",
+ description = """Returns forbidden OTC currency pairs.
+
+Authentication:
+- Public endpoint. No Bearer token is required.
+
+Response body: ForbiddenPairs.""",
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Forbidden OTC pairs returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = ForbiddenPairs::class)
+ )
+ ]
+ )
+ ]
+ )
suspend fun fetchForbiddenPairs(): ForbiddenPairs {
return rateProxy.fetchForbiddenPairs()
}
// Transitive symbols
@PostMapping("/transitive-symbols")
+ @Operation(
+ tags = ["OTC Transitive Symbols"],
+ summary = "Add transitive OTC symbols",
+ description = """Adds transitive symbols used for OTC route calculation.
+
+Required authentication:
+- Bearer admin-token is required.
+- Required role: ROLE_admin.
+
+Request body: Symbols.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ requestBody = io.swagger.v3.oas.annotations.parameters.RequestBody(
+ required = true,
+ description = "Transitive symbols payload.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = Symbols::class)
+ )
+ ]
+ ),
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Transitive symbols added successfully. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required role is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun addTransitiveSymbols(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@RequestBody symbols: Symbols
) {
@@ -89,7 +486,54 @@ class CurrencyRatesController(
}
@DeleteMapping("/transitive-symbols/{symbol}")
+ @Operation(
+ tags = ["OTC Transitive Symbols"],
+ summary = "Delete one transitive OTC symbol",
+ description = """Deletes one transitive symbol used for OTC route calculation.
+
+Required authentication:
+- Bearer admin-token is required.
+- Required role: ROLE_admin.
+
+Path parameters:
+
+Response body: Symbols.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ parameters = [
+ Parameter(
+ name = "symbol",
+ `in` = ParameterIn.PATH,
+ required = true,
+ description = "Transitive symbol to delete.",
+ example = "USDT",
+ schema = Schema(type = "string")
+ )
+ ],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Transitive symbol deleted successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = Symbols::class)
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required role is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun deleteTransitiveSymbols(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@PathVariable symbol: String
): Symbols {
@@ -97,7 +541,54 @@ class CurrencyRatesController(
}
@DeleteMapping("/transitive-symbols")
+ @Operation(
+ tags = ["OTC Transitive Symbols"],
+ summary = "Delete multiple transitive OTC symbols",
+ description = """Deletes multiple transitive symbols used for OTC route calculation.
+
+Required authentication:
+- Bearer admin-token is required.
+- Required role: ROLE_admin.
+
+Request body: Symbols.
+
+Response body: Symbols.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ requestBody = io.swagger.v3.oas.annotations.parameters.RequestBody(
+ required = true,
+ description = "Transitive symbols delete payload.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = Symbols::class)
+ )
+ ]
+ ),
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Transitive symbols deleted successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = Symbols::class)
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required role is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun deleteTransitiveSymbols(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@RequestBody symbols: Symbols
): Symbols {
@@ -105,20 +596,194 @@ class CurrencyRatesController(
}
@GetMapping("/transitive-symbols")
+ @Operation(
+ tags = ["OTC Transitive Symbols"],
+ summary = "List transitive OTC symbols",
+ description = """Returns transitive symbols used for OTC route calculation.
+
+Authentication:
+- Public endpoint. No Bearer token is required.
+
+Response body: Symbols.""",
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Transitive symbols returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = Symbols::class)
+ )
+ ]
+ )
+ ]
+ )
suspend fun fetchTransitiveSymbols(): Symbols {
return rateProxy.fetchTransitiveSymbols()
}
// Routes and prices
- @RequestMapping("/route", method = [RequestMethod.POST, RequestMethod.GET])
+ @GetMapping("/route")
+ @Operation(
+ tags = ["OTC Routes & Prices"],
+ summary = "Calculate OTC exchange routes",
+ description = """Returns calculated OTC exchange routes.
+
+Authentication:
+- Public endpoint. No Bearer token is required.
+
+Query parameters:
+
+Route calculation behavior:
+- If both sourceSymbol and destSymbol are provided, routes are calculated for that specific pair.
+- If sourceSymbol is omitted, routes are calculated from all possible source symbols.
+- If destSymbol is omitted, routes are calculated to all possible destination symbols.
+- If both are omitted, routes are calculated for all possible symbol combinations.
+- Do not send the literal string "null". Omit the query parameter when it should be treated as null.
+
+Response body: CurrencyExchangeRatesResponse.""",
+ parameters = [
+ Parameter(
+ name = "sourceSymbol",
+ `in` = ParameterIn.QUERY,
+ required = false,
+ description = "Optional source currency symbol. If omitted, all possible source symbols are considered. Do not send the literal string \"null\".",
+ example = "BTC",
+ schema = Schema(type = "string")
+ ),
+ Parameter(
+ name = "destSymbol",
+ `in` = ParameterIn.QUERY,
+ required = false,
+ description = "Optional destination currency symbol. If omitted, all possible destination symbols are considered. Do not send the literal string \"null\".",
+ example = "USDT",
+ schema = Schema(type = "string")
+ )
+ ],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "OTC exchange routes returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = CurrencyExchangeRatesResponse::class)
+ )
+ ]
+ )
+ ]
+ )
suspend fun fetchRoutes(
- @RequestParam("sourceSymbol") sourceSymbol: String? = null,
- @RequestParam("destSymbol") destSymbol: String? = null
+ @RequestParam("sourceSymbol", required = false) sourceSymbol: String? = null,
+ @RequestParam("destSymbol", required = false) destSymbol: String? = null
+ ): CurrencyExchangeRatesResponse {
+ return rateProxy.fetchRoutes(sourceSymbol, destSymbol)
+ }
+
+ @PostMapping("/route")
+ @Operation(
+ tags = ["OTC Routes & Prices"],
+ summary = "Calculate OTC exchange routes as admin",
+ description = """Returns calculated OTC exchange routes.
+
+Required authentication:
+- Bearer admin-token is required.
+- Required role: ROLE_admin.
+
+Query parameters:
+
+Route calculation behavior:
+- If both sourceSymbol and destSymbol are provided, routes are calculated for that specific pair.
+- If sourceSymbol is omitted, routes are calculated from all possible source symbols.
+- If destSymbol is omitted, routes are calculated to all possible destination symbols.
+- If both are omitted, routes are calculated for all possible symbol combinations.
+- Do not send the literal string "null". Omit the query parameter when it should be treated as null.
+
+Response body: CurrencyExchangeRatesResponse.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ parameters = [
+ Parameter(
+ name = "sourceSymbol",
+ `in` = ParameterIn.QUERY,
+ required = false,
+ description = "Optional source currency symbol. If omitted, all possible source symbols are considered. Do not send the literal string \"null\".",
+ example = "BTC",
+ schema = Schema(type = "string")
+ ),
+ Parameter(
+ name = "destSymbol",
+ `in` = ParameterIn.QUERY,
+ required = false,
+ description = "Optional destination currency symbol. If omitted, all possible destination symbols are considered. Do not send the literal string \"null\".",
+ example = "USDT",
+ schema = Schema(type = "string")
+ )
+ ],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "OTC exchange routes returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = CurrencyExchangeRatesResponse::class)
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required role is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
+ suspend fun fetchRoutesAsAdmin(
+ @RequestParam("sourceSymbol", required = false) sourceSymbol: String? = null,
+ @RequestParam("destSymbol", required = false) destSymbol: String? = null
): CurrencyExchangeRatesResponse {
return rateProxy.fetchRoutes(sourceSymbol, destSymbol)
}
@GetMapping("/currency/price")
+ @Operation(
+ tags = ["OTC Routes & Prices"],
+ summary = "Get OTC currency prices",
+ description = """Returns OTC currency prices for the requested unit.
+
+Authentication:
+- Public endpoint. No Bearer token is required.
+
+Query parameters:
+
+Response body: Array.""",
+ parameters = [
+ Parameter(
+ name = "unit",
+ `in` = ParameterIn.QUERY,
+ required = true,
+ description = "Pricing unit.",
+ example = "USDT",
+ schema = Schema(type = "string")
+ )
+ ],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Currency prices returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = CurrencyPrice::class))
+ )
+ ]
+ )
+ ]
+ )
suspend fun getPrice(
@RequestParam("unit") unit: String
): List {
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/DepositAdminController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/DepositAdminController.kt
index 9ae00782d..92628de81 100644
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/DepositAdminController.kt
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/DepositAdminController.kt
@@ -9,18 +9,41 @@ 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 io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
@RestController
@RequestMapping("/opex/v1/admin/deposit")
+@Tag(name = "Deposit Admin", description = "Admin manual deposit operations.")
class DepositAdminController(
- private val walletProxy: WalletProxy,
+ private val walletProxy: WalletProxy
) {
@PostMapping("/manually/{amount}_{symbol}/{receiverUuid}")
+ @Operation(
+ summary = "Deposit manually",
+ description = """POST /opex/v1/admin/deposit/manually/{amount}_{symbol}/{receiverUuid}.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+Allowed values:
+- receiverWalletType/sourceWalletType where used: MAIN, EXCHANGE, CASHOUT.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response.", content = [Content(mediaType = "application/json", schema = Schema(implementation = TransferResult::class))]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. Required authority is missing: ROLE_admin. No response body.", content = [Content()])
+ ]
+ )
suspend fun depositManually(
@PathVariable("symbol") symbol: String,
@PathVariable("receiverUuid") receiverUuid: String,
@PathVariable("amount") amount: BigDecimal,
@RequestBody request: ManualTransferRequest,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext
): TransferResult {
return walletProxy.depositManually(
@@ -32,4 +55,4 @@ class DepositAdminController(
)
}
-}
\ 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
deleted file mode 100644
index fd055219b..000000000
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/DepositController.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-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/GatewayAdminController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/GatewayAdminController.kt
index 85889788f..ddf72c5b9 100644
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/GatewayAdminController.kt
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/GatewayAdminController.kt
@@ -1,34 +1,391 @@
package co.nilin.opex.api.ports.opex.controller
import co.nilin.opex.api.core.inout.CurrencyGatewayCommand
+import co.nilin.opex.api.core.inout.CurrencyGatewayUpdateCommand
+import co.nilin.opex.api.core.inout.OffChainGatewayCommand
+import co.nilin.opex.api.core.inout.OnChainGatewayCommand
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 io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.ExampleObject
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
import org.springframework.web.bind.annotation.*
+import io.swagger.v3.oas.annotations.parameters.RequestBody as SwaggerRequestBody
@RestController
@RequestMapping("/opex/v1/admin")
+@Tag(name = "Gateway Admin", description = "Admin gateway management for currencies.")
class GatewayAdminController(
private val walletProxy: WalletProxy,
) {
+
@PostMapping("/{currencySymbol}/gateway")
+ @Operation(
+ summary = "Add currency gateway",
+ description = """
+Security:
+- Bearer admin-token is required.
+- Required authority: ROLE_admin.
+
+Behavior:
+- OffChain gateways use type=OffChain and transferMethod.
+- OnChain native gateways use type=OnChain and isToken=false.
+- Token gateways use type=OnChain and isToken=true with token fields.
+
+Source of values:
+- transferMethod: CARD, SHEBA, IPG, EXCHANGE, MANUALLY, VOUCHER, MPG, REWARD.
+- chain values should come from the server-provided chain list.
+
+Response body: CurrencyGatewayCommand.
+ """,
+ security = [SecurityRequirement(name = "bearerAuth")],
+ requestBody = SwaggerRequestBody(
+ required = true,
+ description = "CurrencyGatewayCommand. Use one of: OffChainGatewayCommand, OnChainGatewayCommand, or OnChainGatewayCommand with isToken=true for token gateways.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(
+ oneOf = [OffChainGatewayCommand::class, OnChainGatewayCommand::class],
+ discriminatorProperty = "type"
+ ),
+ examples = [
+ ExampleObject(
+ name = "OffChain gateway",
+ summary = "OffChainGatewayCommand",
+ value = """
+{
+ "type": "OffChain",
+ "currencySymbol": "IRT",
+ "gatewayUuid": "ofg-card-irt",
+ "isDepositActive": true,
+ "isWithdrawActive": true,
+ "withdrawFee": 0,
+ "withdrawAllowed": true,
+ "depositAllowed": true,
+ "depositMin": 100000,
+ "depositMax": 500000000,
+ "withdrawMin": 100000,
+ "withdrawMax": 500000000,
+ "depositDescription": "Card deposit",
+ "withdrawDescription": "Card withdraw",
+ "displayOrder": 1,
+ "transferMethod": "CARD"
+}
+ """
+ ),
+ ExampleObject(
+ name = "OnChain native gateway",
+ summary = "OnChainGatewayCommand with isToken=false",
+ value = """
+{
+ "type": "OnChain",
+ "currencySymbol": "BTC",
+ "gatewayUuid": "ong-bitcoin-btc",
+ "isDepositActive": true,
+ "isWithdrawActive": true,
+ "withdrawFee": 0.0005,
+ "withdrawAllowed": true,
+ "depositAllowed": true,
+ "depositMin": 0.0001,
+ "depositMax": 10,
+ "withdrawMin": 0.0001,
+ "withdrawMax": 5,
+ "depositDescription": "Bitcoin deposit",
+ "withdrawDescription": "Bitcoin withdraw",
+ "displayOrder": 1,
+ "implementationSymbol": "BTC",
+ "tokenName": null,
+ "tokenAddress": null,
+ "isToken": false,
+ "decimal": 8,
+ "chain": "bitcoin"
+}
+ """
+ ),
+ ExampleObject(
+ name = "OnChain token gateway",
+ summary = "OnChainGatewayCommand with isToken=true",
+ value = """
+{
+ "type": "OnChain",
+ "currencySymbol": "USDT",
+ "gatewayUuid": "ong-tron-usdt",
+ "isDepositActive": true,
+ "isWithdrawActive": true,
+ "withdrawFee": 1,
+ "withdrawAllowed": true,
+ "depositAllowed": true,
+ "depositMin": 1,
+ "depositMax": 100000,
+ "withdrawMin": 1,
+ "withdrawMax": 50000,
+ "depositDescription": "USDT TRC20 deposit",
+ "withdrawDescription": "USDT TRC20 withdraw",
+ "displayOrder": 2,
+ "implementationSymbol": "USDT",
+ "tokenName": "Tether USD",
+ "tokenAddress": "TX0000000000000000000000000000000000",
+ "isToken": true,
+ "decimal": 6,
+ "chain": "tron"
+}
+ """
+ )
+ ]
+ )
+ ]
+ ),
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Gateway created successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(
+ oneOf = [OffChainGatewayCommand::class, OnChainGatewayCommand::class],
+ discriminatorProperty = "type"
+ ),
+ examples = [
+ ExampleObject(
+ name = "OffChain gateway response", value = """
+{
+ "type": "OffChain",
+ "currencySymbol": "IRT",
+ "gatewayUuid": "ofg-card-irt",
+ "isDepositActive": true,
+ "isWithdrawActive": true,
+ "withdrawFee": 0,
+ "withdrawAllowed": true,
+ "depositAllowed": true,
+ "depositMin": 100000,
+ "depositMax": 500000000,
+ "withdrawMin": 100000,
+ "withdrawMax": 500000000,
+ "depositDescription": "Card deposit",
+ "withdrawDescription": "Card withdraw",
+ "displayOrder": 1,
+ "transferMethod": "CARD"
+}
+ """
+ ),
+ ExampleObject(
+ name = "OnChain native gateway response", value = """
+{
+ "type": "OnChain",
+ "currencySymbol": "BTC",
+ "gatewayUuid": "ong-bitcoin-btc",
+ "isDepositActive": true,
+ "isWithdrawActive": true,
+ "withdrawFee": 0.0005,
+ "withdrawAllowed": true,
+ "depositAllowed": true,
+ "depositMin": 0.0001,
+ "depositMax": 10,
+ "withdrawMin": 0.0001,
+ "withdrawMax": 5,
+ "depositDescription": "Bitcoin deposit",
+ "withdrawDescription": "Bitcoin withdraw",
+ "displayOrder": 1,
+ "implementationSymbol": "BTC",
+ "tokenName": null,
+ "tokenAddress": null,
+ "isToken": false,
+ "decimal": 8,
+ "chain": "bitcoin"
+}
+ """
+ ),
+ ExampleObject(
+ name = "OnChain token gateway response", value = """
+{
+ "type": "OnChain",
+ "currencySymbol": "USDT",
+ "gatewayUuid": "ong-tron-usdt",
+ "isDepositActive": true,
+ "isWithdrawActive": true,
+ "withdrawFee": 1,
+ "withdrawAllowed": true,
+ "depositAllowed": true,
+ "depositMin": 1,
+ "depositMax": 100000,
+ "withdrawMin": 1,
+ "withdrawMax": 50000,
+ "depositDescription": "USDT TRC20 deposit",
+ "withdrawDescription": "USDT TRC20 withdraw",
+ "displayOrder": 2,
+ "implementationSymbol": "USDT",
+ "tokenName": "Tether USD",
+ "tokenAddress": "TX0000000000000000000000000000000000",
+ "isToken": true,
+ "decimal": 6,
+ "chain": "tron"
+}
+ """
+ )
+ ]
+ )
+ ]
+ ),
+ ApiResponse(responseCode = "401", description = "Unauthorized. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. No response body.", content = [Content()])
+ ]
+ )
suspend fun addGatewayToCurrency(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@PathVariable("currencySymbol") currencySymbol: String,
@RequestBody body: CurrencyGatewayCommand,
): CurrencyGatewayCommand? {
- return walletProxy.addCurrencyToGateway(securityContext.jwtAuthentication().tokenValue(), currencySymbol, body)
+ return walletProxy.addGatewayToCurrency(securityContext.jwtAuthentication().tokenValue(), currencySymbol, body)
}
-
@PutMapping("/{currencySymbol}/gateway/{uuid}")
+ @Operation(
+ summary = "Update currency gateway",
+ description = """
+Security:
+- Bearer admin-token is required.
+- Required authority: ROLE_admin.
+
+Behavior:
+- OffChain gateways use type=OffChain and transferMethod.
+- OnChain native gateways use type=OnChain and isToken=false.
+- Token gateways use type=OnChain and isToken=true with token fields.
+
+Source of values:
+- transferMethod: CARD, SHEBA, IPG, EXCHANGE, MANUALLY, VOUCHER, MPG, REWARD.
+- chain values should come from the server-provided chain list.
+
+Response body: CurrencyGatewayCommand.
+ """,
+ security = [SecurityRequirement(name = "bearerAuth")],
+ requestBody = SwaggerRequestBody(
+ required = true,
+ description = "CurrencyGatewayCommand. Use one of: OffChainGatewayCommand, OnChainGatewayCommand, or OnChainGatewayCommand with isToken=true for token gateways.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(
+ oneOf = [OffChainGatewayCommand::class, OnChainGatewayCommand::class],
+ discriminatorProperty = "type"
+ ),
+ examples = [
+ ExampleObject(
+ name = "OffChain gateway", summary = "OffChainGatewayCommand", value = """
+{
+ "type": "OffChain",
+ "currencySymbol": "IRT",
+ "gatewayUuid": "ofg-card-irt",
+ "isDepositActive": true,
+ "isWithdrawActive": true,
+ "withdrawFee": 0,
+ "withdrawAllowed": true,
+ "depositAllowed": true,
+ "depositMin": 100000,
+ "depositMax": 500000000,
+ "withdrawMin": 100000,
+ "withdrawMax": 500000000,
+ "depositDescription": "Card deposit",
+ "withdrawDescription": "Card withdraw",
+ "displayOrder": 1,
+ "transferMethod": "CARD"
+}
+ """
+ ),
+ ExampleObject(
+ name = "OnChain native gateway",
+ summary = "OnChainGatewayCommand with isToken=false",
+ value = """
+{
+ "type": "OnChain",
+ "currencySymbol": "BTC",
+ "gatewayUuid": "ong-bitcoin-btc",
+ "isDepositActive": true,
+ "isWithdrawActive": true,
+ "withdrawFee": 0.0005,
+ "withdrawAllowed": true,
+ "depositAllowed": true,
+ "depositMin": 0.0001,
+ "depositMax": 10,
+ "withdrawMin": 0.0001,
+ "withdrawMax": 5,
+ "depositDescription": "Bitcoin deposit",
+ "withdrawDescription": "Bitcoin withdraw",
+ "displayOrder": 1,
+ "implementationSymbol": "BTC",
+ "tokenName": null,
+ "tokenAddress": null,
+ "isToken": false,
+ "decimal": 8,
+ "chain": "bitcoin"
+}
+ """
+ ),
+ ExampleObject(
+ name = "OnChain token gateway",
+ summary = "OnChainGatewayCommand with isToken=true",
+ value = """
+{
+ "type": "OnChain",
+ "currencySymbol": "USDT",
+ "gatewayUuid": "ong-tron-usdt",
+ "isDepositActive": true,
+ "isWithdrawActive": true,
+ "withdrawFee": 1,
+ "withdrawAllowed": true,
+ "depositAllowed": true,
+ "depositMin": 1,
+ "depositMax": 100000,
+ "withdrawMin": 1,
+ "withdrawMax": 50000,
+ "depositDescription": "USDT TRC20 deposit",
+ "withdrawDescription": "USDT TRC20 withdraw",
+ "displayOrder": 2,
+ "implementationSymbol": "USDT",
+ "tokenName": "Tether USD",
+ "tokenAddress": "TX0000000000000000000000000000000000",
+ "isToken": true,
+ "decimal": 6,
+ "chain": "tron"
+}
+ """
+ )
+ ]
+ )
+ ]
+ ),
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Gateway updated successfully.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(
+ oneOf = [OffChainGatewayCommand::class, OnChainGatewayCommand::class],
+ discriminatorProperty = "type"
+ )
+ )]
+ ),
+ ApiResponse(responseCode = "401", description = "Unauthorized. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. No response body.", content = [Content()])
+ ]
+ )
suspend fun updateGateway(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@PathVariable("uuid") gatewayUuid: String,
@PathVariable("currencySymbol") currencySymbol: String,
- @RequestBody body: CurrencyGatewayCommand
+ @RequestBody body: CurrencyGatewayUpdateCommand,
): CurrencyGatewayCommand? {
return walletProxy.updateGateway(
securityContext.jwtAuthentication().tokenValue(),
@@ -39,7 +396,117 @@ class GatewayAdminController(
}
@GetMapping("/{currencySymbol}/gateway/{uuid}")
+ @Operation(
+ summary = "Get currency gateway",
+ description = """
+Security:
+- Bearer admin-token is required.
+- Required authority: ROLE_admin.
+
+Behavior:
+- Response can be OffChainGatewayCommand, OnChainGatewayCommand, or OnChainGatewayCommand with isToken=true for token gateways.
+
+Response body: CurrencyGatewayCommand.
+ """,
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Gateway returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(
+ oneOf = [OffChainGatewayCommand::class, OnChainGatewayCommand::class],
+ discriminatorProperty = "type"
+ ),
+ examples = [
+ ExampleObject(
+ name = "OffChain gateway response", value = """
+{
+ "type": "OffChain",
+ "currencySymbol": "IRT",
+ "gatewayUuid": "ofg-card-irt",
+ "isDepositActive": true,
+ "isWithdrawActive": true,
+ "withdrawFee": 0,
+ "withdrawAllowed": true,
+ "depositAllowed": true,
+ "depositMin": 100000,
+ "depositMax": 500000000,
+ "withdrawMin": 100000,
+ "withdrawMax": 500000000,
+ "depositDescription": "Card deposit",
+ "withdrawDescription": "Card withdraw",
+ "displayOrder": 1,
+ "transferMethod": "CARD"
+}
+ """
+ ),
+ ExampleObject(
+ name = "OnChain native gateway response", value = """
+{
+ "type": "OnChain",
+ "currencySymbol": "BTC",
+ "gatewayUuid": "ong-bitcoin-btc",
+ "isDepositActive": true,
+ "isWithdrawActive": true,
+ "withdrawFee": 0.0005,
+ "withdrawAllowed": true,
+ "depositAllowed": true,
+ "depositMin": 0.0001,
+ "depositMax": 10,
+ "withdrawMin": 0.0001,
+ "withdrawMax": 5,
+ "depositDescription": "Bitcoin deposit",
+ "withdrawDescription": "Bitcoin withdraw",
+ "displayOrder": 1,
+ "implementationSymbol": "BTC",
+ "tokenName": null,
+ "tokenAddress": null,
+ "isToken": false,
+ "decimal": 8,
+ "chain": "bitcoin"
+}
+ """
+ ),
+ ExampleObject(
+ name = "OnChain token gateway response", value = """
+{
+ "type": "OnChain",
+ "currencySymbol": "USDT",
+ "gatewayUuid": "ong-tron-usdt",
+ "isDepositActive": true,
+ "isWithdrawActive": true,
+ "withdrawFee": 1,
+ "withdrawAllowed": true,
+ "depositAllowed": true,
+ "depositMin": 1,
+ "depositMax": 100000,
+ "withdrawMin": 1,
+ "withdrawMax": 50000,
+ "depositDescription": "USDT TRC20 deposit",
+ "withdrawDescription": "USDT TRC20 withdraw",
+ "displayOrder": 2,
+ "implementationSymbol": "USDT",
+ "tokenName": "Tether USD",
+ "tokenAddress": "TX0000000000000000000000000000000000",
+ "isToken": true,
+ "decimal": 6,
+ "chain": "tron"
+}
+ """
+ )
+ ]
+ )
+ ]
+ ),
+ ApiResponse(responseCode = "401", description = "Unauthorized. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. No response body.", content = [Content()])
+ ]
+ )
suspend fun getGateway(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@PathVariable("uuid") gatewayUuid: String,
@PathVariable("currencySymbol") currencySymbol: String,
@@ -52,12 +519,32 @@ class GatewayAdminController(
}
@DeleteMapping("/{currencySymbol}/gateway/{uuid}")
+ @Operation(
+ summary = "Delete currency gateway",
+ description = """
+Security:
+- Bearer admin-token is required.
+- Required authority: ROLE_admin.
+
+Response body: No response body.
+ """,
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Gateway deleted successfully. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(responseCode = "401", description = "Unauthorized. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. No response body.", content = [Content()])
+ ]
+ )
suspend fun deleteGateway(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@PathVariable("uuid") gatewayUuid: String,
@PathVariable("currencySymbol") currencySymbol: String,
) {
walletProxy.deleteGateway(securityContext.jwtAuthentication().tokenValue(), gatewayUuid, currencySymbol)
}
-
}
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/LocalizationAdminController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/LocalizationAdminController.kt
index dcb038f74..e84ced5fc 100644
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/LocalizationAdminController.kt
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/LocalizationAdminController.kt
@@ -9,15 +9,38 @@ import co.nilin.opex.common.OpexError
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
import org.springframework.web.bind.annotation.*
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
@RestController
@RequestMapping("/opex/v1/admin")
+@Tag(name = "Localization Admin", description = "Admin localization management for currencies, terminals, and gateways.")
class LocalizationAdminController(
private val walletProxy: WalletProxy,
private val blockchainGatewayProxy: BlockchainGatewayProxy
) {
@GetMapping("/currency/{currency}/localization")
+ @Operation(
+ summary = "Get currency localizations",
+ description = """GET /opex/v1/admin/currency/{currency}/localization.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Behavior: Send one localization object per language. Existing items can include `id`; new items should use null or omit `id`.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response.", content = [Content(mediaType = "application/json", schema = Schema(implementation = CurrencyLocalizationResponse::class))]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. Required authority is missing: ROLE_admin. No response body.", content = [Content()])
+ ]
+ )
suspend fun getCurrencyLocalizations(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@PathVariable("currency") currency: String
): CurrencyLocalizationResponse {
@@ -25,7 +48,21 @@ class LocalizationAdminController(
}
@PostMapping("/currency/{currency}/localization")
+ @Operation(
+ summary = "Save or update currency localizations",
+ description = """POST /opex/v1/admin/currency/{currency}/localization.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Behavior: Send one localization object per language. Existing items can include `id`; new items should use null or omit `id`.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response.", content = [Content(mediaType = "application/json", schema = Schema(implementation = CurrencyLocalizationResponse::class))]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. Required authority is missing: ROLE_admin. No response body.", content = [Content()])
+ ]
+ )
suspend fun saveCurrencyLocalizations(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@PathVariable("currency") currency: String,
@RequestBody currencyLocalizations: List
@@ -38,7 +75,21 @@ class LocalizationAdminController(
}
@DeleteMapping("/currency/localization/{id}")
+ @Operation(
+ summary = "Delete currency localization",
+ description = """DELETE /opex/v1/admin/currency/localization/{id}.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Behavior: Send one localization object per language. Existing items can include `id`; new items should use null or omit `id`.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "No response body.", content = [Content()]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. Required authority is missing: ROLE_admin. No response body.", content = [Content()])
+ ]
+ )
suspend fun deleteCurrencyLocalization(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@PathVariable("id") id: Long
) {
@@ -46,7 +97,21 @@ class LocalizationAdminController(
}
@GetMapping("/terminal/{terminalUuid}/localization")
+ @Operation(
+ summary = "Get terminal localizations",
+ description = """GET /opex/v1/admin/terminal/{terminalUuid}/localization.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Behavior: Send one localization object per language. Existing items can include `id`; new items should use null or omit `id`.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response.", content = [Content(mediaType = "application/json", schema = Schema(implementation = TerminalLocalizationResponse::class))]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. Required authority is missing: ROLE_admin. No response body.", content = [Content()])
+ ]
+ )
suspend fun getTerminalLocalizations(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@PathVariable("terminalUuid") terminalUuid: String
): TerminalLocalizationResponse {
@@ -54,7 +119,21 @@ class LocalizationAdminController(
}
@PostMapping("/terminal/{terminalUuid}/localization")
+ @Operation(
+ summary = "Save or update terminal localizations",
+ description = """POST /opex/v1/admin/terminal/{terminalUuid}/localization.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Behavior: Send one localization object per language. Existing items can include `id`; new items should use null or omit `id`.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response.", content = [Content(mediaType = "application/json", schema = Schema(implementation = TerminalLocalizationResponse::class))]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. Required authority is missing: ROLE_admin. No response body.", content = [Content()])
+ ]
+ )
suspend fun saveTerminalLocalizations(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@PathVariable("terminalUuid") terminalUuid: String,
@RequestBody terminalLocalizations: List
@@ -67,7 +146,21 @@ class LocalizationAdminController(
}
@DeleteMapping("/terminal/localization/{id}")
+ @Operation(
+ summary = "Delete terminal localization",
+ description = """DELETE /opex/v1/admin/terminal/localization/{id}.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Behavior: Send one localization object per language. Existing items can include `id`; new items should use null or omit `id`.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "No response body.", content = [Content()]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. Required authority is missing: ROLE_admin. No response body.", content = [Content()])
+ ]
+ )
suspend fun deleteTerminalLocalization(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@PathVariable("id") id: Long
) {
@@ -75,7 +168,21 @@ class LocalizationAdminController(
}
@GetMapping("/gateway/{gatewayUuid}/localization")
+ @Operation(
+ summary = "Get gateway localization",
+ description = """GET /opex/v1/admin/gateway/{gatewayUuid}/localization.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Behavior: Send one localization object per language. Existing items can include `id`; new items should use null or omit `id`.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response.", content = [Content(mediaType = "application/json", schema = Schema(implementation = GatewayLocalizationResponse::class))]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. Required authority is missing: ROLE_admin. No response body.", content = [Content()])
+ ]
+ )
suspend fun getGatewayLocalization(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@PathVariable("gatewayUuid") gatewayUuid: String
): GatewayLocalizationResponse {
@@ -92,7 +199,21 @@ class LocalizationAdminController(
}
@PostMapping("/gateway/{gatewayUuid}/localization")
+ @Operation(
+ summary = "Save or update gateway localizations",
+ description = """POST /opex/v1/admin/gateway/{gatewayUuid}/localization.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Behavior: Send one localization object per language. Existing items can include `id`; new items should use null or omit `id`.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response.", content = [Content(mediaType = "application/json", schema = Schema(implementation = GatewayLocalizationResponse::class))]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. Required authority is missing: ROLE_admin. No response body.", content = [Content()])
+ ]
+ )
suspend fun saveGatewayLocalizations(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@PathVariable("gatewayUuid") gatewayUuid: String,
@RequestBody gatewayLocalizations: List
@@ -113,7 +234,21 @@ class LocalizationAdminController(
}
@DeleteMapping("/gateway/{gatewayUuid}/localization/{id}")
+ @Operation(
+ summary = "Delete gateway localization",
+ description = """DELETE /opex/v1/admin/gateway/{gatewayUuid}/localization/{id}.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Behavior: Send one localization object per language. Existing items can include `id`; new items should use null or omit `id`.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "No response body.", content = [Content()]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. Required authority is missing: ROLE_admin. No response body.", content = [Content()])
+ ]
+ )
suspend fun deleteGatewayLocalization(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@PathVariable("id") id: Long,
@PathVariable("gatewayUuid") gatewayUuid: String
@@ -130,4 +265,4 @@ class LocalizationAdminController(
)
} else throw OpexError.GatewayNotFount.exception()
}
-}
\ 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
index 5ea8c718d..290c55827 100644
--- 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
@@ -8,16 +8,27 @@ 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 io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.tags.Tag
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.springframework.beans.factory.annotation.Value
-import org.springframework.web.bind.annotation.*
+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.math.BigDecimal
import java.time.ZoneId
-import kotlin.collections.mapNotNull
@RestController("opexMarketController")
@RequestMapping("/opex/v1/market")
+@Tag(name = "Market", description = "Public market information, order book, trades, tickers, and basic market data.")
class MarketController(
private val accountantProxy: AccountantProxy,
private val marketStatProxy: MarketStatProxy,
@@ -26,17 +37,61 @@ class MarketController(
private val matchingGatewayProxy: MatchingGatewayProxy,
private val blockChainGatewayProxy: BlockchainGatewayProxy,
@Value("\${app.user-activity-reference-currency}")
- private val userActivityReferenceCurrency: String,
+ private val userActivityReferenceCurrency: String
) {
private val orderBookValidLimits = arrayListOf(5, 10, 20, 50, 100, 500, 1000, 5000)
private val validDurations = arrayListOf("24h", "7d", "1M")
@GetMapping("/currency")
+ @Operation(
+ summary = "Get currencies",
+ description = """
+Security:
+- Public endpoint. No Bearer token is required.
+
+Response body:
+- Array of currency data.
+ """,
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Currencies returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = CurrencyData::class))
+ )
+ ]
+ )
+ ]
+ )
suspend fun getCurrencies(): List {
return walletProxy.getCurrencies()
}
@GetMapping("/pair")
+ @Operation(
+ summary = "Get trading pairs",
+ description = """
+Security:
+- Public endpoint. No Bearer token is required.
+
+Response body:
+- Array of pair info.
+ """,
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Trading pairs returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = PairInfoResponse::class))
+ )
+ ]
+ )
+ ]
+ )
suspend fun getPairs(): List {
val pairSettings = matchingGatewayProxy.getPairSettings().associateBy { it.pair }
@@ -56,27 +111,167 @@ class MarketController(
}
@GetMapping("/chain")
+ @Operation(
+ summary = "Get chains",
+ description = """
+Security:
+- Public endpoint. No Bearer token is required.
+
+Source of values:
+- Chain names returned here should be used by clients when selecting chain-based fields in other APIs.
+
+Response body:
+- Array of chain info.
+ """,
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Chains returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = ChainInfo::class))
+ )
+ ]
+ )
+ ]
+ )
suspend fun getChains(): List {
- return blockChainGatewayProxy.getChainInfo()
+ return blockChainGatewayProxy.getChainInfo()
}
@GetMapping("/currency/gateway")
+ @Operation(
+ summary = "Get currency gateways",
+ description = """
+Security:
+- Public endpoint. No Bearer token is required.
+
+Behavior:
+- `includeOffChainGateways` defaults to `true`.
+- `includeOnChainGateways` defaults to `true`.
+- Set either flag to `false` to exclude that gateway type.
+
+Response body:
+- Array of currency gateway commands.
+ """,
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Currency gateways returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = CurrencyGatewayCommand::class))
+ )
+ ]
+ )
+ ]
+ )
suspend fun getCurrencyGateways(
- @RequestParam(defaultValue = "true") includeOffChainGateways: Boolean,
- @RequestParam(defaultValue = "true") includeOnChainGateways: Boolean,
+ @Parameter(
+ name = "includeOffChainGateways",
+ description = "Whether OffChain gateways should be included. Defaults to true.",
+ required = false,
+ schema = Schema(type = "boolean", defaultValue = "true")
+ )
+ @RequestParam(name = "includeOffChainGateways", defaultValue = "true")
+ includeOffChainGateways: Boolean,
+
+ @Parameter(
+ name = "includeOnChainGateways",
+ description = "Whether OnChain gateways should be included. Defaults to true.",
+ required = false,
+ schema = Schema(type = "boolean", defaultValue = "true")
+ )
+ @RequestParam(name = "includeOnChainGateways", defaultValue = "true")
+ includeOnChainGateways: Boolean
): List {
return walletProxy.getGateWays(includeOffChainGateways, includeOnChainGateways)
}
@GetMapping("/fee")
+ @Operation(
+ summary = "Get fee configs",
+ description = """
+Security:
+- Public endpoint. No Bearer token is required.
+
+Response body:
+- Array of fee config data.
+ """,
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Fee configs returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = FeeConfig::class))
+ )
+ ]
+ )
+ ]
+ )
suspend fun getFeeConfigs(): List {
return accountantProxy.getFeeConfigs()
}
@GetMapping("/stats")
+ @Operation(
+ summary = "Get market stats",
+ description = """
+Security:
+- Public endpoint. No Bearer token is required.
+
+Behavior:
+- `interval` uses interval labels such as `1d`, `1w`, `1M`, or `3M`.
+- If `interval` is invalid, the service falls back to `1w`.
+- `limit` defaults to `100`.
+- `limit` is clamped to the range `1..1000`.
+
+Response body:
+- Market statistics response.
+ """,
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Market stats returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = MarketStatResponse::class)
+ )
+ ]
+ )
+ ]
+ )
suspend fun getMarketStats(
- @RequestParam interval: String,
- @RequestParam(required = false) limit: Int?
+ @Parameter(
+ name = "interval",
+ description = "Interval label. Invalid values fall back to 1w.",
+ required = true,
+ schema = Schema(
+ type = "string",
+ allowableValues = [
+ "1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h",
+ "24h", "1d", "3d", "7d", "1w", "1M", "3M", "1Y"
+ ]
+ ),
+ example = "1w"
+ )
+ @RequestParam(name = "interval")
+ interval: String,
+
+ @Parameter(
+ name = "limit",
+ description = "Optional result limit. Defaults to 100 and is clamped to 1..1000.",
+ required = false,
+ schema = Schema(type = "integer", format = "int32", defaultValue = "100"),
+ example = "100"
+ )
+ @RequestParam(name = "limit", required = false)
+ limit: Int?
): MarketStatResponse = coroutineScope {
val intervalEnum = Interval.findByLabel(interval) ?: Interval.Week
val validLimit = getValidLimit(limit)
@@ -106,21 +301,110 @@ class MarketController(
}
@GetMapping("/info")
- suspend fun getMarketInfo(@RequestParam interval: String): MarketInfoResponse {
+ @Operation(
+ summary = "Get market info",
+ description = """
+Security:
+- Public endpoint. No Bearer token is required.
+
+Behavior:
+- `interval` uses interval labels such as `1d`, `1w`, `1M`, or `3M`.
+- If `interval` is invalid, the service falls back to `3M`.
+
+Response body:
+- Market info response.
+ """,
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Market info returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = MarketInfoResponse::class)
+ )
+ ]
+ )
+ ]
+ )
+ suspend fun getMarketInfo(
+ @Parameter(
+ name = "interval",
+ description = "Interval label. Invalid values fall back to 3M.",
+ required = true,
+ schema = Schema(
+ type = "string",
+ allowableValues = [
+ "1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h",
+ "24h", "1d", "3d", "7d", "1w", "1M", "3M", "1Y"
+ ]
+ ),
+ example = "3M"
+ )
+ @RequestParam(name = "interval")
+ interval: String
+ ): MarketInfoResponse {
val intervalEnum = Interval.findByLabel(interval) ?: Interval.ThreeMonth
return MarketInfoResponse(
marketDataProxy.countActiveUsers(intervalEnum),
marketDataProxy.countTotalOrders(intervalEnum),
- marketDataProxy.countTotalTrades(intervalEnum),
+ marketDataProxy.countTotalTrades(intervalEnum)
)
}
@GetMapping("/depth")
+ @Operation(
+ summary = "Get order book",
+ description = """
+Security:
+- Public endpoint. No Bearer token is required.
+
+Validation:
+- `limit` must be one of: `5`, `10`, `20`, `50`, `100`, `500`, `1000`, `5000`.
+
+Behavior:
+- `limit` defaults to `100` when omitted.
+
+Response body:
+- Order book response.
+ """,
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Order book returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = OrderBookResponse::class)
+ )
+ ]
+ )
+ ]
+ )
suspend fun orderBook(
- @RequestParam
+ @Parameter(
+ name = "symbol",
+ description = "Market symbol.",
+ required = true,
+ example = "BTCUSDT"
+ )
+ @RequestParam(name = "symbol")
symbol: String,
- @RequestParam(required = false)
- limit: Int? // Default 100; max 5000. Valid limits:[5, 10, 20, 50, 100, 500, 1000, 5000]
+
+ @Parameter(
+ name = "limit",
+ description = "Optional order book limit. Defaults to 100.",
+ required = false,
+ schema = Schema(
+ type = "integer",
+ format = "int32",
+ defaultValue = "100",
+ allowableValues = ["5", "10", "20", "50", "100", "500", "1000", "5000"]
+ ),
+ example = "100"
+ )
+ @RequestParam(name = "limit", required = false)
+ limit: Int?
): OrderBookResponse {
val validLimit = limit ?: 100
if (!orderBookValidLimits.contains(validLimit))
@@ -153,11 +437,53 @@ class MarketController(
}
@GetMapping("/trades")
+ @Operation(
+ summary = "Get recent trades",
+ description = """
+Security:
+- Public endpoint. No Bearer token is required.
+
+Validation:
+- `limit` must be between `1` and `1000`.
+
+Behavior:
+- `limit` defaults to `500` when omitted.
+
+Response body:
+- Array of recent trades.
+ """,
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Recent trades returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = RecentTradeResponse::class))
+ )
+ ]
+ )
+ ]
+ )
suspend fun recentTrades(
- @RequestParam
+ @Parameter(
+ name = "symbol",
+ description = "Market symbol.",
+ required = true,
+ example = "BTCUSDT"
+ )
+ @RequestParam(name = "symbol")
symbol: String,
- @RequestParam(required = false)
- limit: Int? // Default 500; max 1000.
+
+ @Parameter(
+ name = "limit",
+ description = "Optional recent-trade limit. Defaults to 500. Valid range: 1..1000.",
+ required = false,
+ schema = Schema(type = "integer", format = "int32", defaultValue = "500", minimum = "1", maximum = "1000"),
+ example = "500"
+ )
+ @RequestParam(name = "limit", required = false)
+ limit: Int?
): List {
val validLimit = limit ?: 500
if (validLimit !in 1..1000)
@@ -178,10 +504,66 @@ class MarketController(
}
@GetMapping("/ticker/{duration:24h|7d|1M}")
+ @Operation(
+ summary = "Get price change tickers",
+ description = """
+Security:
+- Public endpoint. No Bearer token is required.
+
+Validation:
+- `duration` must be one of: `24h`, `7d`, `1M`.
+
+Behavior:
+- If `symbol` is omitted, all symbols are returned.
+- If `quote` is provided, results are filtered by quote currency.
+
+Source of values:
+- Quote values should come from `/opex/v1/market/currencyInfo/quotes`.
+
+Response body:
+- Array of price change data.
+ """,
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Price changes returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = PriceChange::class))
+ )
+ ]
+ )
+ ]
+ )
suspend fun priceChange(
- @PathVariable duration: String,
- @RequestParam(required = false) symbol: String?,
- @RequestParam(required = false) quote: String?
+ @Parameter(
+ name = "duration",
+ description = "Ticker duration.",
+ required = true,
+ schema = Schema(type = "string", allowableValues = ["24h", "7d", "1M"]),
+ example = "24h"
+ )
+ @PathVariable("duration")
+ duration: String,
+
+ @Parameter(
+ name = "symbol",
+ description = "Optional market symbol. If omitted, all symbols are returned.",
+ required = false,
+ example = "BTCUSDT"
+ )
+ @RequestParam(name = "symbol", required = false)
+ symbol: String?,
+
+ @Parameter(
+ name = "quote",
+ description = "Optional quote currency filter.",
+ required = false,
+ example = "USDT"
+ )
+ @RequestParam(name = "quote", required = false)
+ quote: String?
): List {
if (!validDurations.contains(duration))
OpexError.InvalidPriceChangeDuration.exception()
@@ -205,27 +587,159 @@ class MarketController(
}
@GetMapping("/ticker/price")
- suspend fun priceTicker(@RequestParam(required = false) symbol: String?): List {
+ @Operation(
+ summary = "Get price ticker",
+ description = """
+Security:
+- Public endpoint. No Bearer token is required.
+
+Behavior:
+- If `symbol` is omitted, prices for all available symbols are returned.
+
+Response body:
+- Array of price ticker data.
+ """,
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Price ticker returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = PriceTicker::class))
+ )
+ ]
+ )
+ ]
+ )
+ suspend fun priceTicker(
+ @Parameter(
+ name = "symbol",
+ description = "Optional market symbol. If omitted, all available symbols are returned.",
+ required = false,
+ example = "BTCUSDT"
+ )
+ @RequestParam(name = "symbol", required = false)
+ symbol: String?
+ ): List {
return marketDataProxy.lastPrice(symbol)
}
@GetMapping("/currencyInfo/quotes")
+ @Operation(
+ summary = "Get quote currencies",
+ description = """
+Security:
+- Public endpoint. No Bearer token is required.
+
+Source of values:
+- Returned values can be used as the `quote` filter in ticker endpoints.
+
+Response body:
+- Array of quote currency strings.
+ """,
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Quote currencies returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(type = "string"))
+ )
+ ]
+ )
+ ]
+ )
suspend fun getQuoteCurrencies(): List {
return walletProxy.getQuoteCurrencies().map { it.currency }
}
@GetMapping("/klines")
+ @Operation(
+ summary = "Get klines",
+ description = """
+Security:
+- Public endpoint. No Bearer token is required.
+
+Validation:
+- `interval` must match one of the supported interval labels.
+- `limit` must be between `1` and `1000`.
+
+Behavior:
+- `limit` defaults to `500` when omitted.
+- `startTime` and `endTime` are Unix timestamps in milliseconds.
+- Response rows are nested arrays in candlestick order.
+
+Response body:
+- Nested array of kline rows.
+ """,
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Klines returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(type = "array"))
+ )
+ ]
+ )
+ ]
+ )
suspend fun klines(
- @RequestParam
+ @Parameter(
+ name = "symbol",
+ description = "Market symbol.",
+ required = true,
+ example = "BTCUSDT"
+ )
+ @RequestParam(name = "symbol")
symbol: String,
- @RequestParam
+
+ @Parameter(
+ name = "interval",
+ description = "Candlestick interval label.",
+ required = true,
+ schema = Schema(
+ type = "string",
+ allowableValues = [
+ "1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h",
+ "24h", "1d", "3d", "7d", "1w", "1M", "3M", "1Y"
+ ]
+ ),
+ example = "1h"
+ )
+ @RequestParam(name = "interval")
interval: String,
- @RequestParam(required = false)
+
+ @Parameter(
+ name = "startTime",
+ description = "Optional start time as Unix timestamp in milliseconds.",
+ required = false,
+ schema = Schema(type = "integer", format = "int64")
+ )
+ @RequestParam(name = "startTime", required = false)
startTime: Long?,
- @RequestParam(required = false)
+
+ @Parameter(
+ name = "endTime",
+ description = "Optional end time as Unix timestamp in milliseconds.",
+ required = false,
+ schema = Schema(type = "integer", format = "int64")
+ )
+ @RequestParam(name = "endTime", required = false)
endTime: Long?,
- @RequestParam(required = false)
- limit: Int? // Default 500; max 1000.
+
+ @Parameter(
+ name = "limit",
+ description = "Optional kline limit. Defaults to 500. Valid range: 1..1000.",
+ required = false,
+ schema = Schema(type = "integer", format = "int32", defaultValue = "500", minimum = "1", maximum = "1000"),
+ example = "500"
+ )
+ @RequestParam(name = "limit", required = false)
+ limit: Int?
): List> {
val validLimit = limit ?: 500
if (validLimit !in 1..1000)
@@ -257,24 +771,99 @@ class MarketController(
}
@GetMapping("/basic-data")
+ @Operation(
+ summary = "Get basic market data",
+ description = """
+Security:
+- Public endpoint. No Bearer token is required.
+
+Response body:
+- Basic market data.
+ """,
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Basic market data returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = MarketBasicData::class)
+ )
+ ]
+ )
+ ]
+ )
suspend fun getBasicData(): MarketBasicData {
val quoteCurrencies = walletProxy.getQuoteCurrencies()
return MarketBasicData(
- (quoteCurrencies.map { it.currency }),
- (quoteCurrencies.filter { it.isReference }.map { it.currency }),
+ quoteCurrencies.map { it.currency },
+ quoteCurrencies.filter { it.isReference }.map { it.currency },
userActivityReferenceCurrency
-
)
}
@GetMapping("/withdraw-limits")
+ @Operation(
+ summary = "Get withdraw limits",
+ description = """
+Security:
+- Public endpoint. No Bearer token is required.
+
+Response body:
+- Array of withdraw limit configs.
+ """,
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Withdraw limits returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = WithdrawLimitConfig::class))
+ )
+ ]
+ )
+ ]
+ )
suspend fun getWithdrawLimits(): List {
return accountantProxy.getWithdrawLimitConfigs()
}
@GetMapping("/gateway/{gatewayUuid}/terminal")
+ @Operation(
+ summary = "Get gateway terminals",
+ description = """
+Security:
+- Public endpoint. No Bearer token is required.
+
+Source of values:
+- `gatewayUuid` should come from currency gateway data.
+
+Response body:
+- Array of terminal commands, or null if no data is returned.
+ """,
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Gateway terminals returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = TerminalCommand::class))
+ )
+ ]
+ )
+ ]
+ )
suspend fun getGatewayTerminal(
- @PathVariable("gatewayUuid") gatewayUuid: String,
+ @Parameter(
+ name = "gatewayUuid",
+ description = "Gateway UUID.",
+ required = true,
+ example = "ofg-uuid-sample"
+ )
+ @PathVariable("gatewayUuid")
+ gatewayUuid: String
): List? {
return walletProxy.getGatewayTerminal(gatewayUuid)
}
@@ -285,4 +874,4 @@ class MarketController(
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
index db9cf0c90..fc80f91ed 100644
--- 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
@@ -10,7 +10,15 @@ import co.nilin.opex.api.ports.opex.util.*
import co.nilin.opex.common.OpexError
import co.nilin.opex.common.security.jwtAuthentication
import co.nilin.opex.common.security.tokenValue
-import io.swagger.annotations.ApiParam
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.enums.ParameterIn
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
import org.springframework.web.bind.annotation.*
@@ -21,33 +29,144 @@ import java.util.*
@RestController
@RequestMapping("/opex/v1/order")
+@Tag(
+ name = "Order",
+ description = "Create, cancel, query, and list authenticated user's orders."
+)
class OrderController(
val queryHandler: MarketUserDataProxy,
val matchingGatewayProxy: MatchingGatewayProxy,
) {
+
@PostMapping
+ @Operation(
+ summary = "Create order",
+ description = """
+Security:
+- Bearer user-token is required.
+- Required permission: PERM_order:write.
+
+Validation:
+- symbol, side, and type are required.
+- side values: BUY, SELL.
+- type values: LIMIT, MARKET, STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, TAKE_PROFIT_LIMIT, LIMIT_MAKER.
+- timeInForce values: GTC, IOC, FOK.
+- LIMIT(*) requires price, quantity, and timeInForce.
+- MARKET requires quantity or quoteOrderQty.
+- STOP_LOSS requires quantity and stopPrice.
+- STOP_LOSS_LIMIT requires price, quantity, stopPrice, and timeInForce.
+- TAKE_PROFIT requires quantity and stopPrice.
+- TAKE_PROFIT_LIMIT requires price, quantity, stopPrice, and timeInForce.
+- LIMIT_MAKER requires price and quantity.
+
+Behavior:
+- Optional parameters that are not applicable to the selected order type should be omitted.
+- Do not send the literal string "null" for optional numeric parameters.
+
+Response body:
+- NewOrderResponse.
+ """,
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Order created successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = NewOrderResponse::class)
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
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?,
+ @Parameter(
+ name = "symbol",
+ description = "Trading pair symbol.",
+ required = true,
+ `in` = ParameterIn.QUERY,
+ example = "BTC_USDT"
+ )
+ @RequestParam symbol: String,
+
+ @Parameter(
+ name = "side",
+ description = "Order side. Values: BUY, SELL.",
+ required = true,
+ `in` = ParameterIn.QUERY,
+ schema = Schema(implementation = OrderSide::class)
+ )
+ @RequestParam side: OrderSide,
+
+ @Parameter(
+ name = "type",
+ description = "Order type. Values: LIMIT, MARKET, STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, TAKE_PROFIT_LIMIT, LIMIT_MAKER.",
+ required = true,
+ `in` = ParameterIn.QUERY,
+ schema = Schema(implementation = OrderType::class)
+ )
+ @RequestParam type: OrderType,
+
+ @Parameter(
+ name = "timeInForce",
+ description = "Optional time-in-force. Values: GTC, IOC, FOK. Required for LIMIT, STOP_LOSS_LIMIT, and TAKE_PROFIT_LIMIT orders.",
+ required = false,
+ `in` = ParameterIn.QUERY,
+ schema = Schema(implementation = TimeInForce::class)
+ )
+ @RequestParam(required = false) timeInForce: TimeInForce?,
+
+ @Parameter(
+ name = "quantity",
+ description = "Optional base quantity. Required for LIMIT, STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, TAKE_PROFIT_LIMIT, and LIMIT_MAKER orders. For MARKET orders, quantity or quoteOrderQty must be provided.",
+ required = false,
+ `in` = ParameterIn.QUERY,
+ example = "0.001"
+ )
+ @RequestParam(required = false) quantity: BigDecimal?,
+
+ @Parameter(
+ name = "quoteOrderQty",
+ description = "Optional quote quantity for MARKET orders when quantity is not provided.",
+ required = false,
+ `in` = ParameterIn.QUERY,
+ example = "10.0"
+ )
+ @RequestParam(required = false) quoteOrderQty: BigDecimal?,
+
+ @Parameter(
+ name = "price",
+ description = "Optional order price. Required for LIMIT, STOP_LOSS_LIMIT, TAKE_PROFIT_LIMIT, and LIMIT_MAKER orders.",
+ required = false,
+ `in` = ParameterIn.QUERY,
+ example = "10.0"
+ )
+ @RequestParam(required = false) price: BigDecimal?,
+
+ @Parameter(
+ name = "stopPrice",
+ description = "Optional stop price. Required for STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, and TAKE_PROFIT_LIMIT orders.",
+ required = false,
+ `in` = ParameterIn.QUERY,
+ example = "9.5"
+ )
+ @RequestParam(required = false) stopPrice: BigDecimal?,
+
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext
): NewOrderResponse {
validateNewOrderParams(type, price, quantity, timeInForce, stopPrice, quoteOrderQty)
-
matchingGatewayProxy.createNewOrder(
securityContext.jwtAuthentication().name,
symbol,
@@ -63,22 +182,85 @@ class OrderController(
}
@PutMapping
+ @Operation(
+ summary = "Cancel order",
+ description = """
+Security:
+- Bearer user-token is required.
+- Required permission: PERM_order:write.
+
+Validation:
+- symbol is required.
+- At least one lookup identifier is required: orderId or origClientOrderId.
+
+Behavior:
+- Already canceled orders return a canceled response.
+- Rejected, expired, or filled orders cannot be canceled.
+
+Response body:
+- CancelOrderResponse.
+ """,
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Order canceled successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = CancelOrderResponse::class)
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun cancelOrder(
+ @Parameter(hidden = true)
principal: Principal,
- @RequestParam
- symbol: String,
- @RequestParam(required = false)
- orderId: Long?,
- @RequestParam(required = false)
- origClientOrderId: String?,
+
+ @Parameter(
+ name = "symbol",
+ description = "Trading pair symbol.",
+ required = true,
+ `in` = ParameterIn.QUERY,
+ example = "BTC_USDT"
+ )
+ @RequestParam symbol: String,
+
+ @Parameter(
+ name = "orderId",
+ description = "Optional numeric order ID. Required when origClientOrderId is not provided.",
+ required = false,
+ `in` = ParameterIn.QUERY,
+ example = "1"
+ )
+ @RequestParam(required = false) orderId: Long?,
+
+ @Parameter(
+ name = "origClientOrderId",
+ description = "Optional original client order ID. Required when orderId is not provided.",
+ required = false,
+ `in` = ParameterIn.QUERY,
+ example = "client-order-id-sample"
+ )
+ @RequestParam(required = false) origClientOrderId: String?,
+
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext
): CancelOrderResponse {
- if (orderId == null && origClientOrderId == null)
- throw OpexError.BadRequest.exception("'orderId' or 'origClientOrderId' must be sent")
-
+ 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,
@@ -94,13 +276,9 @@ class OrderController(
order.type.asOrderType(),
order.direction.asOrderSide()
)
-
- if (order.status == OrderStatus.CANCELED)
- return response
-
+ 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,
@@ -112,14 +290,68 @@ class OrderController(
}
@GetMapping
+ @Operation(
+ summary = "Get order",
+ description = """
+Security:
+- Bearer user-token is required.
+
+Validation:
+- symbol is required.
+- At least one lookup identifier should be provided: orderId or origClientOrderId.
+
+Response body:
+- QueryOrderResponse.
+ """,
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Order returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = QueryOrderResponse::class)
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun queryOrder(
+ @Parameter(hidden = true)
principal: Principal,
- @RequestParam
- symbol: String,
- @RequestParam(required = false)
- orderId: Long?,
- @RequestParam(required = false)
- origClientOrderId: String?,
+
+ @Parameter(
+ name = "symbol",
+ description = "Trading pair symbol.",
+ required = true,
+ `in` = ParameterIn.QUERY,
+ example = "BTC_USDT"
+ )
+ @RequestParam symbol: String,
+
+ @Parameter(
+ name = "orderId",
+ description = "Optional numeric order ID.",
+ required = false,
+ `in` = ParameterIn.QUERY,
+ example = "1"
+ )
+ @RequestParam(required = false) orderId: Long?,
+
+ @Parameter(
+ name = "origClientOrderId",
+ description = "Optional original client order ID.",
+ required = false,
+ `in` = ParameterIn.QUERY,
+ example = "client-order-id-sample"
+ )
+ @RequestParam(required = false) origClientOrderId: String?,
): QueryOrderResponse {
return queryHandler.queryOrder(principal, symbol, orderId, origClientOrderId)
?.asQueryOrderResponse()
@@ -128,12 +360,59 @@ class OrderController(
}
@GetMapping("/open")
+ @Operation(
+ summary = "List open orders",
+ description = """
+Security:
+- Bearer user-token is required.
+
+Behavior:
+- symbol is optional. When omitted, open orders are returned without symbol filtering.
+- limit is optional.
+
+Response body:
+- Array of QueryOrderResponse.
+ """,
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Open orders returned successfully.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = QueryOrderResponse::class))
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun fetchOpenOrders(
+ @Parameter(hidden = true)
principal: Principal,
- @RequestParam(required = false)
- symbol: String?,
- @RequestParam(required = false)
- limit: Int?
+
+ @Parameter(
+ name = "symbol",
+ description = "Optional trading pair symbol filter.",
+ required = false,
+ `in` = ParameterIn.QUERY,
+ example = "BTC_USDT"
+ )
+ @RequestParam(required = false) symbol: String?,
+
+ @Parameter(
+ name = "limit",
+ description = "Optional maximum number of open orders to return.",
+ required = false,
+ `in` = ParameterIn.QUERY,
+ example = "10"
+ )
+ @RequestParam(required = false) limit: Int?
): List {
return queryHandler.openOrders(principal, symbol, limit).map {
it.asQueryOrderResponse().apply { symbol?.let { s -> this.symbol = s } }
@@ -156,10 +435,8 @@ class OrderController(
}
OrderType.MARKET -> {
- if (quantity == null)
- checkDecimal(quoteOrderQty, "quoteOrderQty")
- else
- checkDecimal(quantity, "quantity")
+ if (quantity == null) checkDecimal(quoteOrderQty, "quoteOrderQty")
+ else checkDecimal(quantity, "quantity")
}
OrderType.STOP_LOSS -> {
@@ -224,5 +501,4 @@ class OrderController(
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/ProfileAdminController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/ProfileAdminController.kt
index 4606f77dc..956bf60ba 100644
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/ProfileAdminController.kt
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/ProfileAdminController.kt
@@ -4,18 +4,60 @@ import co.nilin.opex.api.core.inout.*
import co.nilin.opex.api.core.spi.ProfileProxy
import co.nilin.opex.api.ports.opex.util.jwtAuthentication
import co.nilin.opex.api.ports.opex.util.tokenValue
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/opex/v1/admin/profile")
+@Tag(name = "Profile Admin", description = "Admin profile and approval request operations.")
class ProfileAdminController(private val profileProxy: ProfileProxy) {
@PostMapping
+ @Operation(
+ summary = "Get profiles",
+ description = """POST /opex/v1/admin/profile.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+Allowed values:
+- nationality: IRANIAN, NON_IRANIAN.
+- gender: FEMALE, MALE.
+- status: CREATED, CONTACT_INFO_COMPLETED, PROFILE_COMPLETED, SYSTEM_APPROVED, PENDING_ADMIN_APPROVAL, ADMIN_REJECTED, ADMIN_APPROVED.
+- kycLevel: LEVEL_1, LEVEL_2, LEVEL_3.
+- approval request status: PENDING, APPROVED, REJECTED.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = Profile::class))
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getProfiles(
@RequestBody profileRequest: ProfileRequest,
- @CurrentSecurityContext securityContext: SecurityContext,
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext
): List {
return profileProxy.getProfiles(
securityContext.jwtAuthentication().tokenValue(),
@@ -24,18 +66,91 @@ class ProfileAdminController(private val profileProxy: ProfileProxy) {
}
@GetMapping("/{uuid}")
+ @Operation(
+ summary = "Get profile",
+ description = """GET /opex/v1/admin/profile/{uuid}.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+Allowed values:
+- nationality: IRANIAN, NON_IRANIAN.
+- gender: FEMALE, MALE.
+- status: CREATED, CONTACT_INFO_COMPLETED, PROFILE_COMPLETED, SYSTEM_APPROVED, PENDING_ADMIN_APPROVAL, ADMIN_REJECTED, ADMIN_APPROVED.
+- kycLevel: LEVEL_1, LEVEL_2, LEVEL_3.
+- approval request status: PENDING, APPROVED, REJECTED.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(mediaType = "application/json", schema = Schema(implementation = Profile::class))]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getProfile(
+ @Parameter(
+ name = "uuid",
+ description = "User/profile/terminal UUID depending on the endpoint context.",
+ required = true
+ )
@PathVariable uuid: String,
- @CurrentSecurityContext securityContext: SecurityContext,
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext
): Profile {
return profileProxy.getProfileAdmin(securityContext.jwtAuthentication().tokenValue(), uuid)
}
@GetMapping("/history/{uuid}")
+ @Operation(
+ summary = "Get profile history",
+ description = """GET /opex/v1/admin/profile/history/{uuid}.
+Behavior: `limit` defaults to 10 and `offset` defaults to 0 when omitted.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+Allowed values:
+- nationality: IRANIAN, NON_IRANIAN.
+- gender: FEMALE, MALE.
+- status: CREATED, CONTACT_INFO_COMPLETED, PROFILE_COMPLETED, SYSTEM_APPROVED, PENDING_ADMIN_APPROVAL, ADMIN_REJECTED, ADMIN_APPROVED.
+- kycLevel: LEVEL_1, LEVEL_2, LEVEL_3.
+- approval request status: PENDING, APPROVED, REJECTED.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = ProfileHistory::class))
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getProfileHistory(
+ @Parameter(name = "uuid", description = "User UUID depending on the endpoint context.", required = true)
@PathVariable uuid: String,
- @RequestParam offset: Int?, @RequestParam limit: Int?,
- @CurrentSecurityContext securityContext: SecurityContext,
+ @Parameter(name = "offset", description = "Optional page offset.", required = false)
+ @RequestParam offset: Int?,
+ @Parameter(name = "limit", description = "Optional page size.", required = false)
+ @RequestParam limit: Int?,
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext
): List {
return profileProxy.getProfileHistory(
securityContext.jwtAuthentication().tokenValue(),
@@ -46,24 +161,124 @@ class ProfileAdminController(private val profileProxy: ProfileProxy) {
}
@PostMapping("/approval-requests")
+ @Operation(
+ summary = "Get approval requests",
+ description = """POST /opex/v1/admin/profile/approval-requests.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+Allowed values:
+- nationality: IRANIAN, NON_IRANIAN.
+- gender: FEMALE, MALE.
+- status: CREATED, CONTACT_INFO_COMPLETED, PROFILE_COMPLETED, SYSTEM_APPROVED, PENDING_ADMIN_APPROVAL, ADMIN_REJECTED, ADMIN_APPROVED.
+- kycLevel: LEVEL_1, LEVEL_2, LEVEL_3.
+- approval request status: PENDING, APPROVED, REJECTED.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = ProfileApprovalAdminResponse::class))
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getApprovalRequests(
@RequestBody request: ProfileApprovalRequestFilter,
- @CurrentSecurityContext securityContext: SecurityContext,
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext
): List {
return profileProxy.getProfileApprovalRequests(securityContext.jwtAuthentication().tokenValue(), request)
}
@GetMapping("/approval-request/{id}")
+ @Operation(
+ summary = "Get approval request",
+ description = """GET /opex/v1/admin/profile/approval-request/{id}.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+Allowed values:
+- nationality: IRANIAN, NON_IRANIAN.
+- gender: FEMALE, MALE.
+- status: CREATED, CONTACT_INFO_COMPLETED, PROFILE_COMPLETED, SYSTEM_APPROVED, PENDING_ADMIN_APPROVAL, ADMIN_REJECTED, ADMIN_APPROVED.
+- kycLevel: LEVEL_1, LEVEL_2, LEVEL_3.
+- approval request status: PENDING, APPROVED, REJECTED.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = ProfileApprovalAdminResponse::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getApprovalRequest(
+ @Parameter(name = "id", description = "Numeric resource ID.", required = true)
@PathVariable("id") id: Long,
- @CurrentSecurityContext securityContext: SecurityContext,
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext
): ProfileApprovalAdminResponse {
return profileProxy.getProfileApprovalRequest(securityContext.jwtAuthentication().tokenValue(), id)
}
@PutMapping("/approval-request")
+ @Operation(
+ summary = "Update approval request status",
+ description = """PUT /opex/v1/admin/profile/approval-request.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+Allowed values:
+- nationality: IRANIAN, NON_IRANIAN.
+- gender: FEMALE, MALE.
+- status: CREATED, CONTACT_INFO_COMPLETED, PROFILE_COMPLETED, SYSTEM_APPROVED, PENDING_ADMIN_APPROVAL, ADMIN_REJECTED, ADMIN_APPROVED.
+- kycLevel: LEVEL_1, LEVEL_2, LEVEL_3.
+- approval request status: PENDING, APPROVED, REJECTED.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = ProfileApprovalAdminResponse::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun updateApprovalRequestStatus(
@RequestBody changeRequestStatusBody: UpdateApprovalRequestBody,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext
): ProfileApprovalAdminResponse {
return profileProxy.updateProfileApprovalRequest(
@@ -71,4 +286,4 @@ class ProfileAdminController(private val profileProxy: ProfileProxy) {
changeRequestStatusBody
)
}
-}
\ 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/ProfileController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/ProfileController.kt
index 9679d60e4..6ee993593 100644
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/ProfileController.kt
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/ProfileController.kt
@@ -6,6 +6,13 @@ import co.nilin.opex.api.ports.opex.util.jwtAuthentication
import co.nilin.opex.api.ports.opex.util.toProfileApprovalRequestUserResponse
import co.nilin.opex.api.ports.opex.util.toProfileResponse
import co.nilin.opex.api.ports.opex.util.tokenValue
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
import org.springframework.web.bind.annotation.*
@@ -13,18 +20,76 @@ import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/opex/v1/profile")
+@Tag(
+ name = "Profile",
+ description = "Authenticated user profile operations."
+)
class ProfileController(
- val profileProxy: ProfileProxy,
+ val profileProxy: ProfileProxy
) {
@GetMapping("/personal-data")
- suspend fun getProfile(@CurrentSecurityContext securityContext: SecurityContext): ProfileResponse {
+ @Operation(
+ summary = "Get profile",
+ description = """GET /opex/v1/profile/personal-data.
+Security: Bearer user-token required. Requires authenticated user JWT.
+Allowed values:
+- nationality: IRANIAN, NON_IRANIAN.
+- gender: FEMALE, MALE.
+- approval request status: PENDING, APPROVED, REJECTED.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = ProfileResponse::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
+ suspend fun getProfile(
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext
+ ): ProfileResponse {
return profileProxy.getProfile(securityContext.jwtAuthentication().tokenValue()).toProfileResponse()
}
@PutMapping("/completion")
+ @Operation(
+ summary = "Complete profile",
+ description = """PUT /opex/v1/profile/completion.
+Security: Bearer user-token required. Requires authenticated user JWT.
+Allowed values:
+- nationality: IRANIAN, NON_IRANIAN.
+- gender: FEMALE, MALE.
+- approval request status: PENDING, APPROVED, REJECTED.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = ProfileResponse::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun completeProfile(
@RequestBody completeProfileRequest: CompleteProfileRequest,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext
): ProfileResponse? {
return profileProxy.completeProfile(securityContext.jwtAuthentication().tokenValue(), completeProfileRequest)
@@ -32,16 +97,57 @@ class ProfileController(
}
@PostMapping("/contact/update/otp-request")
+ @Operation(
+ summary = "Request contact update",
+ description = """POST /opex/v1/profile/contact/update/otp-request.
+Behavior: Starts contact update flow and returns OTP delivery information.
+Security: Bearer user-token required. Requires authenticated user JWT.
+""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = TempOtpResponse::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun requestContactUpdate(
@RequestBody request: ContactUpdateRequest,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext
): TempOtpResponse {
return profileProxy.requestContactUpdate(securityContext.jwtAuthentication().tokenValue(), request)
}
@PatchMapping("/contact/update/otp-verification")
+ @Operation(
+ summary = "Confirm contact update",
+ description = """PATCH /opex/v1/profile/contact/update/otp-verification.
+Validation: Verification request must contain the OTP value and target contact details required by the request schema.
+Security: Bearer user-token required. Requires authenticated user JWT.
+""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "No response body.", content = [Content()]),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun confirmContactUpdate(
@RequestBody request: ContactUpdateConfirmRequest,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext
) {
profileProxy.confirmContactUpdate(securityContext.jwtAuthentication().tokenValue(), request)
@@ -49,8 +155,36 @@ class ProfileController(
}
@GetMapping("/approval-request")
- suspend fun getApprovalRequest(@CurrentSecurityContext securityContext: SecurityContext): ProfileApprovalRequestUserResponse {
+ @Operation(
+ summary = "Get approval request",
+ description = """GET /opex/v1/profile/approval-request.
+Security: Bearer user-token required. Requires authenticated user JWT.
+Allowed values:
+- nationality: IRANIAN, NON_IRANIAN.
+- gender: FEMALE, MALE.
+- approval request status: PENDING, APPROVED, REJECTED.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = ProfileApprovalRequestUserResponse::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
+ suspend fun getApprovalRequest(
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext
+ ): ProfileApprovalRequestUserResponse {
return profileProxy.getUserProfileApprovalRequest(securityContext.jwtAuthentication().tokenValue())
.toProfileApprovalRequestUserResponse()
}
-}
\ 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/StorageAdminController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/StorageAdminController.kt
index 3481f3ac5..bd0484223 100644
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/StorageAdminController.kt
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/StorageAdminController.kt
@@ -3,47 +3,205 @@ package co.nilin.opex.api.ports.opex.controller
import co.nilin.opex.api.core.spi.StorageProxy
import co.nilin.opex.api.ports.opex.util.jwtAuthentication
import co.nilin.opex.api.ports.opex.util.tokenValue
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.beans.factory.annotation.Value
+import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.http.codec.multipart.FilePart
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
import org.springframework.web.bind.annotation.*
+
+@Schema(name = "StorageUploadMultipartRequest")
+data class StorageUploadMultipartRequest(
+
+ @field:Schema(
+ description = "File to upload.",
+ type = "string",
+ format = "binary"
+ )
+ val file: String? = null
+)
+
@RestController
@RequestMapping("/opex/v1/admin/storage")
+@Tag(name = "Storage Admin", description = "Admin storage upload, download, and delete operations.")
class StorageAdminController(
private val storageProxy: StorageProxy,
@Value("\${app.base.url}")
private val appBaseUrl: String
) {
@GetMapping
+ @Operation(
+ summary = "Download",
+ description = """GET /opex/v1/admin/storage.
+Behavior: Returns binary file bytes for the requested bucket/key.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+Allowed values:
+- isPublic: true, false.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/octet-stream",
+ schema = Schema(type = "string", format = "binary")
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun download(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
+ @Parameter(name = "bucket", description = "Storage bucket name.", required = true)
@RequestParam("bucket") bucket: String,
- @RequestParam("key") key: String,
+ @Parameter(name = "key", description = "Storage object key.", required = true)
+ @RequestParam("key") key: String
): ResponseEntity {
return storageProxy.adminDownload(securityContext.jwtAuthentication().tokenValue(), bucket, key)
}
- @PostMapping
+ @PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
+ @Operation(
+ summary = "Upload",
+ description = """POST /opex/v1/admin/storage.
+Behavior: Multipart upload. `file` is required. `isPublic` defaults to false when omitted.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Validation: Send multipart/form-data with a `file` part. `bucket` and `key` identify the target object.
+Allowed values:
+- isPublic: true, false.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ requestBody = io.swagger.v3.oas.annotations.parameters.RequestBody(
+ required = true,
+ content = [
+ Content(
+ mediaType = MediaType.MULTIPART_FORM_DATA_VALUE,
+ schema = Schema(implementation = StorageUploadMultipartRequest::class)
+ )
+ ]
+ ),
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [
+ Content(
+ mediaType = "text/plain",
+ schema = Schema(type = "string")
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
+
suspend fun upload(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
- @RequestParam("bucket") bucket: String,
- @RequestParam("key") key: String,
- @RequestPart("file") file: FilePart,
- @RequestParam("isPublic") isPublic: Boolean? = false,
+
+ @Parameter(
+ name = "bucket",
+ description = "Storage bucket name.",
+ required = true
+ )
+ @RequestParam("bucket")
+ bucket: String,
+
+ @Parameter(
+ name = "key",
+ description = "Storage object key.",
+ required = true
+ )
+ @RequestParam("key")
+ key: String,
+
+ @Parameter(
+ name = "file",
+ description = "File to upload.",
+ required = true,
+ schema = Schema(type = "string", format = "binary")
+ )
+ @RequestPart("file")
+ file: FilePart,
+
+ @Parameter(
+ name = "isPublic",
+ description = "Whether the uploaded object should be publicly accessible.",
+ required = false,
+ schema = Schema(type = "boolean", defaultValue = "false")
+ )
+ @RequestParam("isPublic", required = false)
+ isPublic: Boolean? = false
): String {
- storageProxy.adminUpload(securityContext.jwtAuthentication().tokenValue(), bucket, key, file, isPublic)
+ storageProxy.adminUpload(
+ securityContext.jwtAuthentication().tokenValue(),
+ bucket,
+ key,
+ file,
+ isPublic
+ )
return "$appBaseUrl/opex/v1/storage?bucket=$bucket&key=$key"
}
@DeleteMapping
+ @Operation(
+ summary = "Delete",
+ description = """DELETE /opex/v1/admin/storage.
+Behavior: Deletes the object identified by bucket/key. Response has no body.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+Allowed values:
+- isPublic: true, false.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "No response body.", content = [Content()]),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun delete(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
+ @Parameter(name = "bucket", description = "Storage bucket name.", required = true)
@RequestParam("bucket") bucket: String,
- @RequestParam("key") key: String,
+ @Parameter(name = "key", description = "Storage object key.", required = true)
+ @RequestParam("key") key: String
) {
storageProxy.adminDelete(securityContext.jwtAuthentication().tokenValue(), bucket, key)
}
-}
\ 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/StorageController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/StorageController.kt
index dd532ded9..04d4e8c54 100644
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/StorageController.kt
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/StorageController.kt
@@ -1,6 +1,12 @@
package co.nilin.opex.api.ports.opex.controller
import co.nilin.opex.api.core.spi.StorageProxy
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
@@ -9,15 +15,35 @@ import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/opex/v1/storage")
+@Tag(name = "Storage", description = "Public storage download operations.")
class StorageController(
- private val storageProxy: StorageProxy,
+ private val storageProxy: StorageProxy
) {
@GetMapping
+ @Operation(
+ summary = "Download",
+ description = """GET /opex/v1/storage.
+Behavior: Public file download for the requested bucket/key. Returns binary file bytes.
+Security: Public endpoint. No Bearer token is required.
+""",
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/octet-stream",
+ schema = Schema(type = "string", format = "binary")
+ )]
+ )
+ ]
+ )
suspend fun download(
+ @Parameter(name = "bucket", description = "Storage bucket name.", required = true)
@RequestParam("bucket") bucket: String,
- @RequestParam("key") key: String,
+ @Parameter(name = "key", description = "Storage object key.", required = true)
+ @RequestParam("key") key: String
): ResponseEntity {
return storageProxy.publicDownload(bucket, key)
}
-}
\ 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/SwapController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/SwapController.kt
index 29330c9dd..52b2779fc 100644
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/SwapController.kt
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/SwapController.kt
@@ -6,28 +6,96 @@ import co.nilin.opex.api.core.inout.TransferResult
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 io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/opex/v1/swap")
+@Tag(name = "Swap", description = "Authenticated swap reserve and finalize operations.")
class SwapController(
- val walletProxy: WalletProxy,
+ val walletProxy: WalletProxy
) {
@PostMapping("/reserve")
+ @Operation(
+ summary = "Reserve",
+ description = """POST /opex/v1/swap/reserve.
+Security: Bearer user-token required. Requires authenticated user JWT.
+Allowed values:
+- sourceWalletType and destWalletType where used: MAIN, EXCHANGE, CASHOUT.
+Source of values:
+- sourceSymbol and destSymbol are server-provided currency symbols.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = ReservedTransferResponse::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun reserve(
@RequestBody request: TransferReserveRequest,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext
): ReservedTransferResponse {
return walletProxy.reserveSwap(securityContext.jwtAuthentication().tokenValue(), request)
}
@PostMapping("/finalize/{reserveUuid}")
+ @Operation(
+ summary = "Finalize transfer",
+ description = """POST /opex/v1/swap/finalize/{reserveUuid}.
+Behavior: `description` and `transferRef` are optional metadata for finalizing a reserved swap.
+Security: Bearer user-token required. Requires authenticated user JWT.
+Allowed values:
+- sourceWalletType and destWalletType where used: MAIN, EXCHANGE, CASHOUT.
+Source of values:
+- sourceSymbol and destSymbol are server-provided currency symbols.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = TransferResult::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun finalizeTransfer(
+ @Parameter(
+ name = "reserveUuid",
+ description = "Swap reserve UUID returned by reserve endpoint.",
+ required = true
+ )
@PathVariable reserveUuid: String,
+ @Parameter(name = "description", description = "Optional transfer description.", required = false)
@RequestParam description: String?,
+ @Parameter(name = "transferRef", description = "Optional external transfer reference.", required = false)
@RequestParam transferRef: String?,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext
): TransferResult {
return walletProxy.finalizeSwap(
@@ -37,4 +105,4 @@ class SwapController(
transferRef
)
}
-}
\ 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/TerminalAdminController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/TerminalAdminController.kt
index 932733f64..09cc709e7 100644
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/TerminalAdminController.kt
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/TerminalAdminController.kt
@@ -2,20 +2,58 @@ package co.nilin.opex.api.ports.opex.controller
import co.nilin.opex.api.core.inout.CurrencyGatewayCommand
import co.nilin.opex.api.core.inout.TerminalCommand
+import co.nilin.opex.api.core.inout.TerminalUpdateCommand
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 io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/opex/v1/admin/terminal")
+@Tag(name = "Terminal Admin", description = "Admin terminal and terminal-gateway assignment operations.")
class TerminalAdminController(
- private val walletProxy: WalletProxy,
+ private val walletProxy: WalletProxy
) {
@PostMapping
+ @Operation(
+ summary = "Register terminal",
+ description = """POST /opex/v1/admin/terminal.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = TerminalCommand::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun registerTerminal(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext, @RequestBody body: TerminalCommand
): TerminalCommand? {
return walletProxy.saveTerminal(securityContext.jwtAuthentication().tokenValue(), body)
@@ -23,50 +61,211 @@ class TerminalAdminController(
@PutMapping("/{uuid}")
+ @Operation(
+ summary = "Update terminal",
+ description = """PUT /opex/v1/admin/terminal/{terminalUuid}.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = TerminalCommand::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun updateTerminal(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
+ @Parameter(name = "uuid", description = "Terminal UUID.", required = true)
@PathVariable("uuid") terminalUuid: String,
- @RequestBody body: TerminalCommand
+ @RequestBody body: TerminalUpdateCommand
): TerminalCommand? {
return walletProxy.updateTerminal(securityContext.jwtAuthentication().tokenValue(), terminalUuid, body)
}
@DeleteMapping("/{uuid}")
+ @Operation(
+ summary = "Delete terminal",
+ description = """DELETE /opex/v1/admin/terminal/{terminalUuid}.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "No response body.", content = [Content()]),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun deleteTerminal(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
- @PathVariable("uuid") terminalUuid: String,
+ @Parameter(name = "uuid", description = "Terminal UUID.", required = true)
+ @PathVariable("uuid") terminalUuid: String
) {
walletProxy.deleteTerminal(securityContext.jwtAuthentication().tokenValue(), terminalUuid)
}
@GetMapping
+ @Operation(
+ summary = "Get terminal",
+ description = """GET /opex/v1/admin/terminal.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = TerminalCommand::class))
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getTerminal(
- @CurrentSecurityContext securityContext: SecurityContext,
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext
): List? {
return walletProxy.getTerminals(securityContext.jwtAuthentication().tokenValue())
}
@GetMapping("/{uuid}")
+ @Operation(
+ summary = "Get terminal",
+ description = """GET /opex/v1/admin/terminal/{terminalUuid}.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = TerminalCommand::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getTerminal(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
- @PathVariable("uuid") terminalUuid: String,
+ @Parameter(name = "uuid", description = "Terminal UUID depending on the endpoint context.", required = true)
+ @PathVariable("uuid") terminalUuid: String
): TerminalCommand? {
return walletProxy.getTerminal(securityContext.jwtAuthentication().tokenValue(), terminalUuid)
}
@GetMapping("/{uuid}/gateway")
+ @Operation(
+ summary = "Get gateway(s) which the terminal is assigned to",
+ description = """GET /opex/v1/admin/terminal/{gatewayUuid}/gateway.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = CurrencyGatewayCommand::class))
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getGatewayTerminal(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
- @PathVariable("uuid") terminalUuid: String,
+ @Parameter(name = "uuid", description = "Terminal UUID.", required = true)
+ @PathVariable("uuid") terminalUuid: String
): List? {
return walletProxy.getAssignedGatewayToTerminal(securityContext.jwtAuthentication().tokenValue(), terminalUuid)
}
@PostMapping("/gateway/{uuid}")
+ @Operation(
+ summary = "Assign terminal to gateway",
+ description = """POST /opex/v1/admin/terminal/gateway/{gatewayUuid}.
+Validation: Request body is a raw JSON array of terminal UUID strings.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "No response body.", content = [Content()]),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun assignTerminalToGateway(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
+ @Parameter(name = "uuid", description = "Gateway UUID.", required = true)
@PathVariable("uuid") gatewayUuid: String,
- @RequestBody terminal: List,
+ @RequestBody terminal: List
) {
return walletProxy.assignTerminalsToGateway(
securityContext.jwtAuthentication().tokenValue(),
@@ -76,10 +275,33 @@ class TerminalAdminController(
}
@DeleteMapping("/gateway/{uuid}")
+ @Operation(
+ summary = "Revoke terminal from gateway",
+ description = """DELETE /opex/v1/admin/terminal/gateway/{gatewayUuid}.
+Validation: Request body is a raw JSON array of terminal UUID strings to revoke from the gateway.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "No response body.", content = [Content()]),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun revokeTerminalFromGateway(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
+ @Parameter(name = "uuid", description = "Gateway UUID.", required = true)
@PathVariable("uuid") gatewayUuid: String,
- @RequestBody terminal: List,
+ @RequestBody terminal: List
) {
return walletProxy.revokeTerminalsToGateway(
securityContext.jwtAuthentication().tokenValue(),
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/TransactionAdminController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/TransactionAdminController.kt
index c55f72079..d28fb924a 100644
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/TransactionAdminController.kt
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/TransactionAdminController.kt
@@ -4,6 +4,14 @@ import co.nilin.opex.api.core.inout.*
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 io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
import org.springframework.web.bind.annotation.PostMapping
@@ -14,20 +22,83 @@ import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/opex/v1/admin/transactions")
+@Tag(name = "Transactions Admin", description = "Admin transaction history and summary operations.")
class TransactionAdminController(
- private val walletProxy: WalletProxy,
+ private val walletProxy: WalletProxy
) {
@PostMapping("/summary")
+ @Operation(
+ summary = "Get user transaction history",
+ description = """POST /opex/v1/admin/transactions/summary.
+Security: Bearer admin-token required. Required authority: ROLE_monitoring or ROLE_admin.
+Allowed values:
+- category: TRADE, DEPOSIT, DEPOSIT_TO, WITHDRAW_FROM, WITHDRAW, FEE, SWAP, REFERRAL_COMMISSION, REFERRAL_KYC_REWARD, REFERENT_COMMISSION, KYC_ACCEPTED_REWARD, SYSTEM.
+
+""",
+
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = UserTransactionHistory::class))
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_monitoring or ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getUserTransactionHistory(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
- @RequestBody request: UserTransactionRequest,
+ @RequestBody request: UserTransactionRequest
): List {
return walletProxy.getUserTransactionHistoryForAdmin(securityContext.jwtAuthentication().tokenValue(), request)
}
@PostMapping("/deposits")
+ @Operation(
+ summary = "Get deposit transactions",
+ description = """POST /opex/v1/admin/transactions/deposits.
+Security: Bearer admin-token required. Required authority: ROLE_monitoring or ROLE_admin.
+Allowed values:
+- DepositStatus: PROCESSING, DONE, INVALID
+""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = DepositAdminResponse::class))
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_monitoring or ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getDepositTransactions(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@RequestBody request: AdminDepositHistoryRequest
): List {
@@ -38,7 +109,35 @@ class TransactionAdminController(
}
@PostMapping("/withdraws")
+ @Operation(
+ summary = "Get withdraw transactions",
+ description = """POST /opex/v1/admin/transactions/withdraws.
+Security: Bearer admin-token required. Required authority: ROLE_monitoring or ROLE_admin.
+""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = WithdrawAdminResponse::class))
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_monitoring or ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getWithdrawTransactions(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@RequestBody request: AdminWithdrawHistoryRequest
): List {
@@ -49,9 +148,39 @@ class TransactionAdminController(
}
@PostMapping("/swaps")
+ @Operation(
+ summary = "Get swap transactions",
+ description = """POST /opex/v1/admin/transactions/swaps.
+Security: Bearer admin-token required. Required authority: ROLE_monitoring or ROLE_admin.
+Allowed values:
+- ReservedStatus: Created, Expired, Committed,
+""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = SwapAdminResponse::class))
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_monitoring or ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getSwapTransactions(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
- @RequestBody request: UserTransactionRequest,
+ @RequestBody request: UserSwapTransactionRequest
): List {
return walletProxy.getSwapTransactionsForAdmin(
securityContext.jwtAuthentication().tokenValue(),
@@ -61,10 +190,38 @@ class TransactionAdminController(
// This part is temporary and the structure of fetching trades needs to be fixed.
@PostMapping("/trades")
+ @Operation(
+ summary = "Get transaction history",
+ description = """POST /opex/v1/admin/transactions/trades.
+Security: Bearer admin-token required. Required authority: ROLE_monitoring or ROLE_admin.
+""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = TradeAdminResponse::class))
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_monitoring or ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getTransactionHistory(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
- @RequestBody request: AdminTradeHistoryRequest,
+ @RequestBody request: AdminTradeHistoryRequest
): List {
return walletProxy.getTradeHistoryForAdmin(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/UserAnalyticsController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserAnalyticsController.kt
index af11c880f..5a0fe2e95 100644
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserAnalyticsController.kt
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserAnalyticsController.kt
@@ -6,6 +6,15 @@ import co.nilin.opex.api.core.spi.WalletProxy
import co.nilin.opex.api.ports.opex.service.UserActivityAggregationService
import co.nilin.opex.api.ports.opex.util.jwtAuthentication
import co.nilin.opex.api.ports.opex.util.tokenValue
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.ExampleObject
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
import org.springframework.web.bind.annotation.GetMapping
@@ -15,21 +24,91 @@ import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/opex/v1/analytics")
+@Tag(
+ name = "User Analytics",
+ description = "User analytics and user-asset analytics operations."
+)
class UserAnalyticsController(
private val userActivityAggregationService: UserActivityAggregationService,
- val walletProxy: WalletProxy,
+ val walletProxy: WalletProxy
) {
@GetMapping("/user-activity")
- suspend fun userActivity(@CurrentSecurityContext securityContext: SecurityContext): Map {
+ @Operation(
+ summary = "User activity",
+ description = """GET /opex/v1/analytics/user-activity.
+Security: Bearer user-token required. Requires authenticated user JWT.
+
+Behavior:
+- Returns activity totals for the last 31 days.
+- Response object keys are epoch timestamps in milliseconds.
+- All date/time values exposed by the API layer are timestamps.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response. Map key is epoch timestamp in milliseconds. Map value is daily activity totals.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(type = "object", additionalPropertiesSchema = ActivityTotals::class),
+ examples = [ExampleObject(
+ name = "User activity response",
+ value = """
+{
+ "1715731200000": {
+ "totalBalance": 1000.50,
+ "totalWithdraw": 20.00,
+ "totalDeposit": 200.00,
+ "totalTrade": 150.00,
+ "totalOrder": 3
+ }
+}
+ """
+ )]
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
+ suspend fun userActivity(
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext
+ ): Map {
val auth = securityContext.jwtAuthentication()
return userActivityAggregationService.getLast31DaysUserStats(auth.tokenValue(), auth.name)
}
@GetMapping("/users-detail-assets")
+ @Operation(
+ summary = "Get user details assets",
+ description = """GET /opex/v1/analytics/users-detail-assets.
+Security: Public endpoint. No Bearer token is required.
+
+Behavior:
+- limit defaults to 10 when omitted.
+- offset defaults to 0 when omitted.
+
+""",
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = UserDetailAssetsSnapshot::class))
+ )]
+ )
+ ]
+ )
suspend fun getUserDetailsAssets(
+ @Parameter(name = "limit", description = "Optional page size.", required = false)
@RequestParam limit: Int?,
- @RequestParam offset: Int?,
+ @Parameter(name = "offset", description = "Optional page offset.", required = false)
+ @RequestParam offset: Int?
): List {
return walletProxy.getUsersDetailAssets(limit ?: 10, offset ?: 0)
}
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserConfigController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserConfigController.kt
new file mode 100644
index 000000000..bf3fb1280
--- /dev/null
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserConfigController.kt
@@ -0,0 +1,201 @@
+package co.nilin.opex.api.ports.opex.controller
+
+import co.nilin.opex.api.core.inout.UpdateUserConfigRequest
+import co.nilin.opex.api.core.inout.UserWebConfig
+import co.nilin.opex.api.core.spi.ConfigProxy
+import co.nilin.opex.api.ports.opex.util.jwtAuthentication
+import co.nilin.opex.api.ports.opex.util.tokenValue
+import co.nilin.opex.common.data.WebConfig
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
+import org.springframework.security.core.annotation.CurrentSecurityContext
+import org.springframework.security.core.context.SecurityContext
+import org.springframework.web.bind.annotation.*
+
+@RestController
+@RequestMapping("/opex/v1")
+@Tag(
+ name = "Config",
+ description = "Web and user configuration operations."
+)
+class UserConfigController(private val configProxy: ConfigProxy) {
+
+ @GetMapping("/web/config")
+ @Operation(
+ summary = "Get web config",
+ description = """GET /opex/v1/web/config.
+Security: Public endpoint. No Bearer token is required.
+
+Allowed values:
+- language: EN, FA, AR, UZ.
+- calender: JALALI, HIJRI, GREGORIAN.
+- theme: DARK, LIGHT, SYSTEM.""",
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(mediaType = "application/json", schema = Schema(implementation = WebConfig::class))]
+ ),
+ ApiResponse(responseCode = "401", description = "Unauthorized. No response body.", content = [Content()])
+ ]
+ )
+ suspend fun getWebConfig(): WebConfig {
+ return configProxy.getWebConfig()
+ }
+
+ @GetMapping("/user/config")
+ @Operation(
+ summary = "Get user config",
+ description = """GET /opex/v1/user/config.
+Security: Bearer user-token required. Requires authenticated user JWT.
+
+Allowed values:
+- language: EN, FA, AR, UZ.
+- calender: JALALI, HIJRI, GREGORIAN.
+- theme: DARK, LIGHT, SYSTEM.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = UserWebConfig::class)
+ )]
+ ),
+ ApiResponse(responseCode = "401", description = "Unauthorized. No response body.", content = [Content()])
+ ]
+ )
+ suspend fun getUserConfig(
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext
+ ): UserWebConfig {
+ return configProxy.getUserConfig(securityContext.jwtAuthentication().tokenValue())
+ }
+
+ @PutMapping("/user/config")
+ @Operation(
+ summary = "Update user config",
+ description = """PUT /opex/v1/user/config.
+Security: Bearer user-token required. Requires authenticated user JWT.
+
+Allowed values:
+- language: EN, FA, AR, UZ.
+- calender: JALALI, HIJRI, GREGORIAN.
+- theme: DARK, LIGHT, SYSTEM.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ requestBody = io.swagger.v3.oas.annotations.parameters.RequestBody(
+ required = true,
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = UpdateUserConfigRequest::class)
+ )]
+ ),
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = UserWebConfig::class)
+ )]
+ ),
+ ApiResponse(responseCode = "401", description = "Unauthorized. No response body.", content = [Content()])
+ ]
+ )
+ suspend fun updateConfig(
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext,
+ @RequestBody request: UpdateUserConfigRequest
+ ): UserWebConfig {
+ return configProxy.updateUserConfig(securityContext.jwtAuthentication().tokenValue(), request)
+ }
+
+ @GetMapping("/user/config/pair")
+ @Operation(
+ summary = "Get favorite pairs",
+ description = """GET /opex/v1/user/config/pair.
+Security: Bearer user-token required. Requires authenticated user JWT.
+""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(type = "string"))
+ )]
+ ),
+ ApiResponse(responseCode = "401", description = "Unauthorized. No response body.", content = [Content()])
+ ]
+ )
+ suspend fun getUserFavoritePair(
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext
+ ): Set {
+ return configProxy.getUserFavoritePair(securityContext.jwtAuthentication().tokenValue())
+ }
+
+ @PostMapping("/user/config/pair/{pair}")
+ @Operation(
+ summary = "Add favorite pair",
+ description = """POST /opex/v1/user/config/pair/{pair}.
+Security: Bearer user-token required. Requires authenticated user JWT.
+""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(type = "string"))
+ )]
+ ),
+ ApiResponse(responseCode = "401", description = "Unauthorized. No response body.", content = [Content()])
+ ]
+ )
+ suspend fun addUserFavoritePair(
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext,
+ @Parameter(name = "pair", description = "Market pair symbol.", required = true)
+ @PathVariable pair: String
+ ): Set {
+ return configProxy.addUserFavoritePair(securityContext.jwtAuthentication().tokenValue(), pair)
+ }
+
+ @DeleteMapping("/user/config/pair/{pair}")
+ @Operation(
+ summary = "Remove favorite pair",
+ description = """DELETE /opex/v1/user/config/pair/{pair}.
+Security: Bearer user-token required. Requires authenticated user JWT.
+""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(type = "string"))
+ )]
+ ),
+ ApiResponse(responseCode = "401", description = "Unauthorized. No response body.", content = [Content()])
+ ]
+ )
+ suspend fun removeUserFavoritePair(
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext,
+ @Parameter(name = "pair", description = "Market pair symbol.", required = true)
+ @PathVariable pair: String
+ ): Set {
+ return configProxy.removeUserFavoritePair(securityContext.jwtAuthentication().tokenValue(), pair)
+ }
+}
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserDataController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserDataController.kt
index 4be17e6f7..155570cfc 100644
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserDataController.kt
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserDataController.kt
@@ -2,8 +2,15 @@ package co.nilin.opex.api.ports.opex.controller
import co.nilin.opex.api.core.inout.UserFee
import co.nilin.opex.api.core.spi.AccountantProxy
-import co.nilin.opex.common.OpexError
import co.nilin.opex.common.utils.Interval
+import co.nilin.opex.common.utils.LimitedInterval
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
import org.springframework.web.bind.annotation.GetMapping
@@ -14,50 +21,151 @@ import java.math.BigDecimal
@RestController
@RequestMapping("/opex/v1/user/data")
+@Tag(
+ name = "User Exchange Data",
+ description = "Authenticated user fee, volume, and activity data."
+)
class UserDataController(
- private val accountantProxy: AccountantProxy,
+ private val accountantProxy: AccountantProxy
) {
@GetMapping("/trade/volume")
+ @Operation(
+ summary = "Get trade volume by currency",
+ description = """GET /opex/v1/user/data/trade/volume.
+Validation: `interval` must be one of Day, Week, Month, Year.
+Security: Bearer user-token required. Requires authenticated user JWT.
+Allowed values:
+- interval: Day, Week, Month, Year.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(mediaType = "application/json", schema = Schema(type = "number"))]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getTradeVolumeByCurrency(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
+ @Parameter(
+ name = "symbol",
+ description = "Trading pair or currency symbol, depending on endpoint.",
+ required = true
+ )
@RequestParam symbol: String,
- @RequestParam interval: Interval
+ @Parameter(
+ name = "interval",
+ description = "Interval. For user data endpoints allowed values are Day, Week, Month, Year. ",
+ required = true
+ )
+ @RequestParam interval: LimitedInterval
): BigDecimal {
- checkValidInterval(interval)
+ val interval = Interval.valueOf(interval.name)
val uuid = securityContext.authentication.name
return accountantProxy.getTradeVolumeByCurrency(uuid, symbol, interval)
}
@GetMapping("/trade/volume/total")
+ @Operation(
+ summary = "Get total trade volume value",
+ description = """GET /opex/v1/user/data/trade/volume/total.
+Security: Bearer user-token required. Requires authenticated user JWT.
+Allowed values:
+- interval: Day, Week, Month, Year.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(mediaType = "application/json", schema = Schema(type = "number"))]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getTotalTradeVolumeValue(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
- @RequestParam interval: Interval
+ @Parameter(
+ name = "interval",
+ description = "Interval. For user data endpoints allowed values are Day, Week, Month, Year.",
+ required = true
+ )
+ @RequestParam interval: LimitedInterval
): BigDecimal {
- checkValidInterval(interval)
+ val interval = Interval.valueOf(interval.name)
val uuid = securityContext.authentication.name
return accountantProxy.getTotalTradeVolumeValue(uuid, interval)
}
@GetMapping("/fee")
- suspend fun getUserFee(@CurrentSecurityContext securityContext: SecurityContext): UserFee {
+ @Operation(
+ summary = "Get user fee",
+ description = """GET /opex/v1/user/data/fee.
+Security: Bearer user-token required. Requires authenticated user JWT.
+""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(mediaType = "application/json", schema = Schema(implementation = UserFee::class))]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
+ suspend fun getUserFee(
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext
+ ): UserFee {
return accountantProxy.getUserFee(securityContext.authentication.name)
}
@GetMapping("/withdraw/volume/total")
+ @Operation(
+ summary = "Get total withdraw volume value",
+ description = """GET /opex/v1/user/data/withdraw/volume/total.
+Behavior: `interval` is optional. Accepted labels map to Day, Week, Month, Year where supported.
+Security: Bearer user-token required. Requires authenticated user JWT.
+Allowed values:
+- interval: Day, Week, Month, Year.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "No response body.", content = [Content()]),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getTotalWithdrawVolumeValue(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
- @RequestParam(required = false) interval: String?
+ @Parameter(
+ name = "interval",
+ description = "Interval. For user data endpoints allowed values are Day, Week, Month, Year. ",
+ required = false
+ )
+ @RequestParam(required = false) interval: LimitedInterval?
): BigDecimal =
accountantProxy.getTotalWithdrawVolumeValue(
securityContext.authentication.name,
- interval?.let(Interval::findByLabel)
+ interval?.let { Interval.valueOf(interval.name) }
)
-
- private fun checkValidInterval(interval: Interval) {
- if (interval == Interval.Day || interval == Interval.Week || interval == Interval.Month || interval == Interval.Year)
- return
- throw OpexError.BadRequest.exception()
- }
-}
\ 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
index cb90e5c3c..9b59872c5 100644
--- 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
@@ -7,26 +7,90 @@ 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 io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
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")
+@Tag(
+ name = "User History",
+ description = """Authenticated user history, summaries, and swap-history operations."""
+)
class UserHistoryController(
private val marketUserDataProxy: MarketUserDataProxy,
private val walletProxy: WalletProxy,
) {
@GetMapping("/history/order")
+ @Operation(
+ summary = "Get order history",
+ description = """GET /opex/v1/user/history/order.
+Security: Bearer user-token required. Requires authenticated user JWT.
+
+Behavior / Validation:
+- Optional filters are applied only when provided.
+- `startTime` and `endTime` are epoch milliseconds.
+- `limit` defaults to 10 and `offset` defaults to 0 when omitted.
+
+Allowed values:
+- orderType: LIMIT_ORDER, MARKET_ORDER.
+- direction: ASK, BID.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = OrderDataResponse::class))
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getOrderHistory(
- @RequestParam symbol: String?,
- @RequestParam startTime: Long?,
- @RequestParam endTime: Long?,
- @RequestParam orderType: MatchingOrderType?,
- @RequestParam direction: OrderDirection?,
- @RequestParam limit: Int?,
- @RequestParam offset: Int?,
+ @Parameter(name = "symbol", description = "Optional trading pair symbol.", required = false)
+ @RequestParam(name = "symbol", required = false) symbol: String?,
+ @Parameter(
+ name = "startTime",
+ description = "Optional start timestamp in epoch milliseconds.",
+ required = false
+ )
+ @RequestParam(name = "startTime", required = false) startTime: Long?,
+ @Parameter(name = "endTime", description = "Optional end timestamp in epoch milliseconds.", required = false)
+ @RequestParam(name = "endTime", required = false) endTime: Long?,
+ @Parameter(
+ name = "orderType",
+ description = "Optional order type filter. Allowed values: LIMIT_ORDER, MARKET_ORDER.",
+ required = false
+ )
+ @RequestParam(name = "orderType", required = false) orderType: MatchingOrderType?,
+ @Parameter(
+ name = "direction",
+ description = "Optional order direction filter. Allowed values: ASK, BID.",
+ required = false
+ )
+ @RequestParam(name = "direction", required = false) direction: OrderDirection?,
+ @Parameter(name = "limit", description = "Optional page size. Defaults to 10 when omitted.", required = false)
+ @RequestParam(name = "limit", required = false) limit: Int?,
+ @Parameter(name = "offset", description = "Optional page offset. Defaults to 0 when omitted.", required = false)
+ @RequestParam(name = "offset", required = false) offset: Int?,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
): List {
return marketUserDataProxy.getOrderHistory(
@@ -42,12 +106,56 @@ class UserHistoryController(
}
@GetMapping("/history/order/count")
+ @Operation(
+ summary = "Count order history",
+ description = """GET /opex/v1/user/history/order/count.
+Security: Bearer user-token required. Requires authenticated user JWT.
+
+Behavior / Validation:
+- Optional filters are applied only when provided.
+- `startTime` and `endTime` are epoch milliseconds.
+
+Allowed values:
+- orderType: LIMIT_ORDER, MARKET_ORDER.
+- direction: ASK, BID.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(mediaType = "application/json", schema = Schema(implementation = Long::class))]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getOrderHistoryCount(
- @RequestParam symbol: String?,
- @RequestParam startTime: Long?,
- @RequestParam endTime: Long?,
- @RequestParam orderType: MatchingOrderType?,
- @RequestParam direction: OrderDirection?,
+ @Parameter(name = "symbol", description = "Optional trading pair symbol.", required = false)
+ @RequestParam(name = "symbol", required = false) symbol: String?,
+ @Parameter(
+ name = "startTime",
+ description = "Optional start timestamp in epoch milliseconds.",
+ required = false
+ )
+ @RequestParam(name = "startTime", required = false) startTime: Long?,
+ @Parameter(name = "endTime", description = "Optional end timestamp in epoch milliseconds.", required = false)
+ @RequestParam(name = "endTime", required = false) endTime: Long?,
+ @Parameter(
+ name = "orderType",
+ description = "Optional order type filter. Allowed values: LIMIT_ORDER, MARKET_ORDER.",
+ required = false
+ )
+ @RequestParam(name = "orderType", required = false) orderType: MatchingOrderType?,
+ @Parameter(
+ name = "direction",
+ description = "Optional order direction filter. Allowed values: ASK, BID.",
+ required = false
+ )
+ @RequestParam(name = "direction", required = false) direction: OrderDirection?,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
): Long {
return marketUserDataProxy.getOrderHistoryCount(
@@ -61,42 +169,189 @@ class UserHistoryController(
}
@GetMapping("/history/trade")
+ @Operation(
+ summary = "Get trade history",
+ description = """GET /opex/v1/user/history/trade.
+Security: Bearer user-token required. Requires authenticated user JWT.
+
+Behavior / Validation:
+- Optional filters are applied only when provided.
+- `startTime` and `endTime` are epoch milliseconds.
+- `limit` defaults to 10 and `offset` defaults to 0 when omitted.
+
+Allowed values:
+- direction: ASK, BID.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = Trade::class))
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getTradeHistory(
- @RequestParam symbol: String?,
- @RequestParam startTime: Long?,
- @RequestParam endTime: Long?,
- @RequestParam direction: OrderDirection?,
- @RequestParam limit: Int?,
- @RequestParam offset: Int?,
+ @Parameter(name = "symbol", description = "Optional trading pair symbol.", required = false)
+ @RequestParam(name = "symbol", required = false) symbol: String?,
+ @Parameter(
+ name = "startTime",
+ description = "Optional start timestamp in epoch milliseconds.",
+ required = false
+ )
+ @RequestParam(name = "startTime", required = false) startTime: Long?,
+ @Parameter(name = "endTime", description = "Optional end timestamp in epoch milliseconds.", required = false)
+ @RequestParam(name = "endTime", required = false) endTime: Long?,
+ @Parameter(
+ name = "direction",
+ description = "Optional trade direction filter. Allowed values: ASK, BID.",
+ required = false
+ )
+ @RequestParam(name = "direction", required = false) direction: OrderDirection?,
+ @Parameter(name = "limit", description = "Optional page size. Defaults to 10 when omitted.", required = false)
+ @RequestParam(name = "limit", required = false) limit: Int?,
+ @Parameter(name = "offset", description = "Optional page offset. Defaults to 0 when omitted.", required = false)
+ @RequestParam(name = "offset", required = false) offset: Int?,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
): List {
return marketUserDataProxy.getTradeHistory(
- securityContext.authentication.name, symbol, startTime, endTime, direction, limit ?: 10, offset ?: 0
+ securityContext.authentication.name,
+ symbol,
+ startTime,
+ endTime,
+ direction,
+ limit ?: 10,
+ offset ?: 0
)
}
@GetMapping("/history/trade/count")
+ @Operation(
+ summary = "Count trade history",
+ description = """GET /opex/v1/user/history/trade/count.
+Security: Bearer user-token required. Requires authenticated user JWT.
+
+Behavior / Validation:
+- Optional filters are applied only when provided.
+- `startTime` and `endTime` are epoch milliseconds.
+
+Allowed values:
+- direction: ASK, BID.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(mediaType = "application/json", schema = Schema(implementation = Long::class))]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getTradeHistoryCount(
- @RequestParam symbol: String?,
- @RequestParam startTime: Long?,
- @RequestParam endTime: Long?,
- @RequestParam direction: OrderDirection?,
+ @Parameter(name = "symbol", description = "Optional trading pair symbol.", required = false)
+ @RequestParam(name = "symbol", required = false) symbol: String?,
+ @Parameter(
+ name = "startTime",
+ description = "Optional start timestamp in epoch milliseconds.",
+ required = false
+ )
+ @RequestParam(name = "startTime", required = false) startTime: Long?,
+ @Parameter(name = "endTime", description = "Optional end timestamp in epoch milliseconds.", required = false)
+ @RequestParam(name = "endTime", required = false) endTime: Long?,
+ @Parameter(
+ name = "direction",
+ description = "Optional trade direction filter. Allowed values: ASK, BID.",
+ required = false
+ )
+ @RequestParam(name = "direction", required = false) direction: OrderDirection?,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
): Long {
return marketUserDataProxy.getTradeHistoryCount(
- securityContext.authentication.name, symbol, startTime, endTime, direction
+ securityContext.authentication.name,
+ symbol,
+ startTime,
+ endTime,
+ direction
)
}
@GetMapping("/history/withdraw")
+ @Operation(
+ summary = "Get withdraw history",
+ description = """GET /opex/v1/user/history/withdraw.
+Security: Bearer user-token required. Requires authenticated user JWT.
+
+Behavior / Validation:
+- Optional filters are applied only when provided.
+- `startTime` and `endTime` are epoch milliseconds.
+- `limit` defaults to 10 and `offset` defaults to 0 when omitted.
+- `ascendingByTime` controls time sorting when supported.
+
+Allowed values:
+- status: REQUESTED, CREATED, ACCEPTED, CANCELED, REJECTED, DONE.
+- transferMethod in response: CARD, SHEBA, IPG, EXCHANGE, MANUALLY, VOUCHER, MPG, REWARD.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = WithdrawResponse::class))
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getWithdrawHistory(
- @RequestParam currency: String?,
- @RequestParam status: WithdrawStatus?,
- @RequestParam startTime: Long?,
- @RequestParam endTime: Long?,
- @RequestParam limit: Int?,
- @RequestParam offset: Int?,
- @RequestParam ascendingByTime: Boolean?,
+ @Parameter(name = "currency", description = "Optional currency symbol.", required = false)
+ @RequestParam(name = "currency", required = false) currency: String?,
+ @Parameter(
+ name = "status",
+ description = "Optional withdraw status. Allowed values: REQUESTED, CREATED, ACCEPTED, CANCELED, REJECTED, DONE.",
+ required = false
+ )
+ @RequestParam(name = "status", required = false) status: WithdrawStatus?,
+ @Parameter(
+ name = "startTime",
+ description = "Optional start timestamp in epoch milliseconds.",
+ required = false
+ )
+ @RequestParam(name = "startTime", required = false) startTime: Long?,
+ @Parameter(name = "endTime", description = "Optional end timestamp in epoch milliseconds.", required = false)
+ @RequestParam(name = "endTime", required = false) endTime: Long?,
+ @Parameter(name = "limit", description = "Optional page size. Defaults to 10 when omitted.", required = false)
+ @RequestParam(name = "limit", required = false) limit: Int?,
+ @Parameter(name = "offset", description = "Optional page offset. Defaults to 0 when omitted.", required = false)
+ @RequestParam(name = "offset", required = false) offset: Int?,
+ @Parameter(
+ name = "ascendingByTime",
+ description = "Optional sorting flag. true sorts ascending by time; false sorts descending where supported.",
+ required = false
+ )
+ @RequestParam(name = "ascendingByTime", required = false) ascendingByTime: Boolean?,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
): List {
return walletProxy.getWithdrawTransactions(
@@ -113,11 +368,49 @@ class UserHistoryController(
}
@GetMapping("/history/withdraw/count")
+ @Operation(
+ summary = "Count withdraw history",
+ description = """GET /opex/v1/user/history/withdraw/count.
+Security: Bearer user-token required. Requires authenticated user JWT.
+
+Behavior / Validation:
+- Optional filters are applied only when provided.
+- `startTime` and `endTime` are epoch milliseconds.
+
+Allowed values:
+- status: REQUESTED, CREATED, ACCEPTED, CANCELED, REJECTED, DONE.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(mediaType = "application/json", schema = Schema(implementation = Long::class))]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getWithdrawHistoryCount(
- @RequestParam currency: String?,
- @RequestParam status: WithdrawStatus?,
- @RequestParam startTime: Long?,
- @RequestParam endTime: Long?,
+ @Parameter(name = "currency", description = "Optional currency symbol.", required = false)
+ @RequestParam(name = "currency", required = false) currency: String?,
+ @Parameter(
+ name = "status",
+ description = "Optional withdraw status. Allowed values: REQUESTED, CREATED, ACCEPTED, CANCELED, REJECTED, DONE.",
+ required = false
+ )
+ @RequestParam(name = "status", required = false) status: WithdrawStatus?,
+ @Parameter(
+ name = "startTime",
+ description = "Optional start timestamp in epoch milliseconds.",
+ required = false
+ )
+ @RequestParam(name = "startTime", required = false) startTime: Long?,
+ @Parameter(name = "endTime", description = "Optional end timestamp in epoch milliseconds.", required = false)
+ @RequestParam(name = "endTime", required = false) endTime: Long?,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
): Long {
return walletProxy.getWithdrawTransactionsCount(
@@ -131,13 +424,62 @@ class UserHistoryController(
}
@GetMapping("/history/deposit")
+ @Operation(
+ summary = "Get deposit history",
+ description = """GET /opex/v1/user/history/deposit.
+Security: Bearer user-token required. Requires authenticated user JWT.
+
+Behavior / Validation:
+- Optional filters are applied only when provided.
+- `startTime` and `endTime` are epoch milliseconds.
+- `limit` defaults to 10 and `offset` defaults to 0 when omitted.
+- `ascendingByTime` controls time sorting when supported.
+
+Allowed values in response:
+- status: PROCESSING, DONE, INVALID.
+- type: ON_CHAIN, OFF_CHAIN.
+- transferMethod: CARD, SHEBA, IPG, EXCHANGE, MANUALLY, VOUCHER, MPG, REWARD.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = DepositHistoryResponse::class))
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getDepositHistory(
- @RequestParam currency: String?,
- @RequestParam startTime: Long?,
- @RequestParam endTime: Long?,
- @RequestParam limit: Int?,
- @RequestParam offset: Int?,
- @RequestParam ascendingByTime: Boolean?,
+ @Parameter(name = "currency", description = "Optional currency symbol.", required = false)
+ @RequestParam(name = "currency", required = false) currency: String?,
+ @Parameter(
+ name = "startTime",
+ description = "Optional start timestamp in epoch milliseconds.",
+ required = false
+ )
+ @RequestParam(name = "startTime", required = false) startTime: Long?,
+ @Parameter(name = "endTime", description = "Optional end timestamp in epoch milliseconds.", required = false)
+ @RequestParam(name = "endTime", required = false) endTime: Long?,
+ @Parameter(name = "limit", description = "Optional page size. Defaults to 10 when omitted.", required = false)
+ @RequestParam(name = "limit", required = false) limit: Int?,
+ @Parameter(name = "offset", description = "Optional page offset. Defaults to 0 when omitted.", required = false)
+ @RequestParam(name = "offset", required = false) offset: Int?,
+ @Parameter(
+ name = "ascendingByTime",
+ description = "Optional sorting flag. true sorts ascending by time; false sorts descending where supported.",
+ required = false
+ )
+ @RequestParam(name = "ascendingByTime", required = false) ascendingByTime: Boolean?,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
): List {
return walletProxy.getDepositTransactions(
@@ -153,10 +495,40 @@ class UserHistoryController(
}
@GetMapping("/history/deposit/count")
+ @Operation(
+ summary = "Count deposit history",
+ description = """GET /opex/v1/user/history/deposit/count.
+Security: Bearer user-token required. Requires authenticated user JWT.
+
+Behavior / Validation:
+- Optional filters are applied only when provided.
+- `startTime` and `endTime` are epoch milliseconds.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(mediaType = "application/json", schema = Schema(implementation = Long::class))]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getDepositHistoryCount(
- @RequestParam currency: String?,
- @RequestParam startTime: Long?,
- @RequestParam endTime: Long?,
+ @Parameter(name = "currency", description = "Optional currency symbol.", required = false)
+ @RequestParam(name = "currency", required = false) currency: String?,
+ @Parameter(
+ name = "startTime",
+ description = "Optional start timestamp in epoch milliseconds.",
+ required = false
+ )
+ @RequestParam(name = "startTime", required = false) startTime: Long?,
+ @Parameter(name = "endTime", description = "Optional end timestamp in epoch milliseconds.", required = false)
+ @RequestParam(name = "endTime", required = false) endTime: Long?,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
): Long {
return walletProxy.getDepositTransactionsCount(
@@ -169,14 +541,66 @@ class UserHistoryController(
}
@GetMapping("/history/transaction")
+ @Operation(
+ summary = "Get transaction history",
+ description = """GET /opex/v1/user/history/transaction.
+Security: Bearer user-token required. Requires authenticated user JWT.
+
+Behavior / Validation:
+- Optional filters are applied only when provided.
+- `startTime` and `endTime` are epoch milliseconds.
+- `limit` defaults to 10 and `offset` defaults to 0 when omitted.
+- `ascendingByTime` controls time sorting when supported.
+
+Allowed values:
+- category: TRADE, DEPOSIT, DEPOSIT_TO, WITHDRAW_FROM, WITHDRAW, FEE, SWAP, REFERRAL_COMMISSION, REFERRAL_KYC_REWARD, REFERENT_COMMISSION, KYC_ACCEPTED_REWARD, SYSTEM.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = UserTransactionHistory::class))
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getTransactionHistory(
- @RequestParam currency: String?,
- @RequestParam category: UserTransactionCategory?,
- @RequestParam startTime: Long?,
- @RequestParam endTime: Long?,
- @RequestParam limit: Int?,
- @RequestParam offset: Int?,
- @RequestParam ascendingByTime: Boolean?,
+ @Parameter(name = "currency", description = "Optional currency symbol.", required = false)
+ @RequestParam(name = "currency", required = false) currency: String?,
+ @Parameter(
+ name = "category",
+ description = "Optional transaction category. Allowed values: TRADE, DEPOSIT, DEPOSIT_TO, WITHDRAW_FROM, WITHDRAW, FEE, SWAP, REFERRAL_COMMISSION, REFERRAL_KYC_REWARD, REFERENT_COMMISSION, KYC_ACCEPTED_REWARD, SYSTEM.",
+ required = false
+ )
+ @RequestParam(name = "category", required = false) category: UserTransactionCategory?,
+ @Parameter(
+ name = "startTime",
+ description = "Optional start timestamp in epoch milliseconds.",
+ required = false
+ )
+ @RequestParam(name = "startTime", required = false) startTime: Long?,
+ @Parameter(name = "endTime", description = "Optional end timestamp in epoch milliseconds.", required = false)
+ @RequestParam(name = "endTime", required = false) endTime: Long?,
+ @Parameter(name = "limit", description = "Optional page size. Defaults to 10 when omitted.", required = false)
+ @RequestParam(name = "limit", required = false) limit: Int?,
+ @Parameter(name = "offset", description = "Optional page offset. Defaults to 0 when omitted.", required = false)
+ @RequestParam(name = "offset", required = false) offset: Int?,
+ @Parameter(
+ name = "ascendingByTime",
+ description = "Optional sorting flag. true sorts ascending by time; false sorts descending where supported.",
+ required = false
+ )
+ @RequestParam(name = "ascendingByTime", required = false) ascendingByTime: Boolean?,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
): List {
return walletProxy.getTransactions(
@@ -193,11 +617,49 @@ class UserHistoryController(
}
@GetMapping("/history/transaction/count")
+ @Operation(
+ summary = "Count transaction history",
+ description = """GET /opex/v1/user/history/transaction/count.
+Security: Bearer user-token required. Requires authenticated user JWT.
+
+Behavior / Validation:
+- Optional filters are applied only when provided.
+- `startTime` and `endTime` are epoch milliseconds.
+
+Allowed values:
+- category: TRADE, DEPOSIT, DEPOSIT_TO, WITHDRAW_FROM, WITHDRAW, FEE, SWAP, REFERRAL_COMMISSION, REFERRAL_KYC_REWARD, REFERENT_COMMISSION, KYC_ACCEPTED_REWARD, SYSTEM.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(mediaType = "application/json", schema = Schema(implementation = Long::class))]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getTransactionHistoryCount(
- @RequestParam currency: String?,
- @RequestParam category: UserTransactionCategory?,
- @RequestParam startTime: Long?,
- @RequestParam endTime: Long?,
+ @Parameter(name = "currency", description = "Optional currency symbol.", required = false)
+ @RequestParam(name = "currency", required = false) currency: String?,
+ @Parameter(
+ name = "category",
+ description = "Optional transaction category. Allowed values: TRADE, DEPOSIT, DEPOSIT_TO, WITHDRAW_FROM, WITHDRAW, FEE, SWAP, REFERRAL_COMMISSION, REFERRAL_KYC_REWARD, REFERENT_COMMISSION, KYC_ACCEPTED_REWARD, SYSTEM.",
+ required = false
+ )
+ @RequestParam(name = "category", required = false) category: UserTransactionCategory?,
+ @Parameter(
+ name = "startTime",
+ description = "Optional start timestamp in epoch milliseconds.",
+ required = false
+ )
+ @RequestParam(name = "startTime", required = false) startTime: Long?,
+ @Parameter(name = "endTime", description = "Optional end timestamp in epoch milliseconds.", required = false)
+ @RequestParam(name = "endTime", required = false) endTime: Long?,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
): Long {
return walletProxy.getTransactionsCount(
@@ -211,10 +673,46 @@ class UserHistoryController(
}
@GetMapping("/summary/trade")
+ @Operation(
+ summary = "Get trade summary",
+ description = """GET /opex/v1/user/summary/trade.
+Security: Bearer user-token required. Requires authenticated user JWT.
+
+Behavior / Validation:
+- Optional filters are applied only when provided.
+- `startTime` and `endTime` are epoch milliseconds.
+- `limit` limits the number of summary rows when supported.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = TransactionSummary::class))
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getTradeTransactionSummary(
- @RequestParam startTime: Long?,
- @RequestParam endTime: Long?,
- @RequestParam limit: Int?,
+ @Parameter(
+ name = "startTime",
+ description = "Optional start timestamp in epoch milliseconds.",
+ required = false
+ )
+ @RequestParam(name = "startTime", required = false) startTime: Long?,
+ @Parameter(name = "endTime", description = "Optional end timestamp in epoch milliseconds.", required = false)
+ @RequestParam(name = "endTime", required = false) endTime: Long?,
+ @Parameter(name = "limit", description = "Optional number of summary rows.", required = false)
+ @RequestParam(name = "limit", required = false) limit: Int?,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
): List {
return walletProxy.getUserTradeTransactionSummary(
@@ -227,10 +725,46 @@ class UserHistoryController(
}
@GetMapping("/summary/deposit")
+ @Operation(
+ summary = "Get deposit summary",
+ description = """GET /opex/v1/user/summary/deposit.
+Security: Bearer user-token required. Requires authenticated user JWT.
+
+Behavior / Validation:
+- Optional filters are applied only when provided.
+- `startTime` and `endTime` are epoch milliseconds.
+- `limit` limits the number of summary rows when supported.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = TransactionSummary::class))
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getDepositSummary(
- @RequestParam startTime: Long?,
- @RequestParam endTime: Long?,
- @RequestParam limit: Int?,
+ @Parameter(
+ name = "startTime",
+ description = "Optional start timestamp in epoch milliseconds.",
+ required = false
+ )
+ @RequestParam(name = "startTime", required = false) startTime: Long?,
+ @Parameter(name = "endTime", description = "Optional end timestamp in epoch milliseconds.", required = false)
+ @RequestParam(name = "endTime", required = false) endTime: Long?,
+ @Parameter(name = "limit", description = "Optional number of summary rows.", required = false)
+ @RequestParam(name = "limit", required = false) limit: Int?,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
): List {
return walletProxy.getUserDepositSummary(
@@ -243,10 +777,46 @@ class UserHistoryController(
}
@GetMapping("/summary/withdraw")
+ @Operation(
+ summary = "Get withdraw summary",
+ description = """GET /opex/v1/user/summary/withdraw.
+Security: Bearer user-token required. Requires authenticated user JWT.
+
+Behavior / Validation:
+- Optional filters are applied only when provided.
+- `startTime` and `endTime` are epoch milliseconds.
+- `limit` limits the number of summary rows when supported.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = TransactionSummary::class))
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getWithdrawSummary(
- @RequestParam startTime: Long?,
- @RequestParam endTime: Long?,
- @RequestParam limit: Int?,
+ @Parameter(
+ name = "startTime",
+ description = "Optional start timestamp in epoch milliseconds.",
+ required = false
+ )
+ @RequestParam(name = "startTime", required = false) startTime: Long?,
+ @Parameter(name = "endTime", description = "Optional end timestamp in epoch milliseconds.", required = false)
+ @RequestParam(name = "endTime", required = false) endTime: Long?,
+ @Parameter(name = "limit", description = "Optional number of summary rows.", required = false)
+ @RequestParam(name = "limit", required = false) limit: Int?,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
): List {
return walletProxy.getUserWithdrawSummary(
@@ -259,18 +829,90 @@ class UserHistoryController(
}
@PostMapping("/history/swap")
+ @Operation(
+ summary = "Get swap history",
+ description = """POST /opex/v1/user/history/swap.
+Security: Bearer user-token required. Requires authenticated user JWT.
+
+Behavior / Validation:
+- Optional filters are applied only when provided.
+- `startTime` and `endTime` are epoch milliseconds.
+- `limit` defaults to 10 and `offset` defaults to 0 when omitted.
+
+Allowed values:
+- status: Created, Expired, Committed.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ requestBody = io.swagger.v3.oas.annotations.parameters.RequestBody(
+ required = true,
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = UserSwapTransactionRequest::class)
+ )]
+ ),
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [
+ Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = SwapResponse::class))
+ )
+ ]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getSwapHistory(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
- @RequestBody request: UserTransactionRequest
+ @RequestBody request: UserSwapTransactionRequest
): List {
return walletProxy.getSwapTransactions(securityContext.jwtAuthentication().tokenValue(), request)
}
@PostMapping("/history/swap/count")
+ @Operation(
+ summary = "Count swap history",
+ description = """POST /opex/v1/user/history/swap/count.
+Security: Bearer user-token required. Requires authenticated user JWT.
+
+Behavior / Validation:
+- Optional filters are applied only when provided.
+- `startTime` and `endTime` are epoch milliseconds.
+
+Allowed values:
+- status: Created, Expired, Committed.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ requestBody = io.swagger.v3.oas.annotations.parameters.RequestBody(
+ required = true,
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = UserSwapTransactionRequest::class)
+ )]
+ ),
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(mediaType = "application/json", schema = Schema(implementation = Long::class))]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getSwapHistoryCount(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
- @RequestBody request: UserTransactionRequest
+ @RequestBody request: UserSwapTransactionRequest
): 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/UserLevelAdminController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserLevelAdminController.kt
new file mode 100644
index 000000000..79a8dfffe
--- /dev/null
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserLevelAdminController.kt
@@ -0,0 +1,88 @@
+package co.nilin.opex.api.ports.opex.controller;
+
+import co.nilin.opex.api.core.inout.UserLevelConfig
+import co.nilin.opex.api.core.spi.ConfigProxy
+import co.nilin.opex.api.ports.opex.util.jwtAuthentication
+import co.nilin.opex.api.ports.opex.util.tokenValue
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
+import org.springframework.security.core.annotation.CurrentSecurityContext
+import org.springframework.security.core.context.SecurityContext
+import org.springframework.web.bind.annotation.*
+@RestController
+@RequestMapping("/opex/v1")
+@Tag(
+ name = "Admin User Config",
+ description = "User level configuration operations."
+)
+public class UserLevelAdminController(private val configProxy: ConfigProxy) {
+ @PutMapping("/admin/user-level/config")
+ @Operation(
+ summary = "Update user level config",
+ description = """PUT /opex/v1/admin/user-level/config.
+Security: Bearer admin-token required. Required authority: ROLE_admin.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ requestBody = io.swagger.v3.oas.annotations.parameters.RequestBody(
+ required = true,
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = UserLevelConfig::class)
+ )]
+ ),
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = UserLevelConfig::class)
+ )]
+ ),
+ ApiResponse(responseCode = "401", description = "Unauthorized. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. No response body.", content = [Content()])
+ ]
+ )
+ suspend fun updateUserLevelConfig(
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext,
+ @RequestBody userLevelConfig: UserLevelConfig
+ ): UserLevelConfig {
+ return configProxy.updateUserLevelConfig(securityContext.jwtAuthentication().tokenValue(), userLevelConfig)
+ }
+
+ @DeleteMapping("/admin/user-level/config/{userLevel}/{language}")
+ @Operation(
+ summary = "Delete user level config",
+ description = """DELETE /opex/v1/admin/user-level/config/{userLevel}/{language}.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Allowed values:
+- language: EN, FA, AR, UZ.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(responseCode = "401", description = "Unauthorized. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. No response body.", content = [Content()])
+ ]
+ )
+ suspend fun deleteUserLevelConfig(
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext,
+ @Parameter(name = "userLevel", description = "User level key.", required = true)
+ @PathVariable userLevel: String,
+ @Parameter(name = "language", description = "Language: EN, FA, AR, UZ.", required = true)
+ @PathVariable language: String
+ ) {
+ configProxy.deleteUserLevelConfig(securityContext.jwtAuthentication().tokenValue(), userLevel, language)
+ }
+}
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserLevelController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserLevelController.kt
new file mode 100644
index 000000000..92f21c3fe
--- /dev/null
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserLevelController.kt
@@ -0,0 +1,45 @@
+package co.nilin.opex.api.ports.opex.controller;
+
+import co.nilin.opex.api.core.inout.UserLevelConfig
+import co.nilin.opex.api.core.spi.ConfigProxy
+import co.nilin.opex.api.ports.opex.util.jwtAuthentication
+import co.nilin.opex.api.ports.opex.util.tokenValue
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
+import org.springframework.security.core.annotation.CurrentSecurityContext
+import org.springframework.security.core.context.SecurityContext
+import org.springframework.web.bind.annotation.*
+@RestController
+@RequestMapping("/opex/v1")
+@Tag(
+ name = "Use Level",
+ description = "Fetch user level config, public endpoint"
+)
+public class UserLevelController(private val configProxy: ConfigProxy) {
+ @GetMapping("/user-level/config")
+ @Operation(
+ summary = "Get user level config",
+ description = """GET /opex/v1/user-level/config.
+Security: Public endpoint. No Bearer token is required.""",
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = UserLevelConfig::class))
+ )]
+ ),
+ ApiResponse(responseCode = "401", description = "Unauthorized. No response body.", content = [Content()])
+ ]
+ )
+ suspend fun getUserLevelConfig(): List {
+ return configProxy.getUserLevelConfig()
+ }
+}
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
index 50c7800ec..f0e6ec518 100644
--- 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
@@ -3,24 +3,46 @@ 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.common.security.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
-import co.nilin.opex.api.ports.opex.util.tokenValue
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
@RestController
@RequestMapping("/opex/v1/voucher")
+@Tag(name = "Voucher", description = "Authenticated voucher submission operations.")
class VoucherController(private val walletProxy: WalletProxy) {
@PutMapping("/{code}")
+ @Operation(
+ summary = "Submit voucher",
+ description = """PUT /opex/v1/voucher/{code}.
+Security: Bearer user-token required. Required authority: PERM_voucher:submit.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response.", content = [Content(mediaType = "application/json", schema = Schema(implementation = SubmitVoucherResponse::class))]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. Required authority is missing: PERM_voucher:submit. No response body.", content = [Content()])
+ ]
+ )
suspend fun submitVoucher(
+ @Parameter(name = "code", description = "Voucher code.", required = true)
@PathVariable code: String,
+ @Parameter(hidden = true)
@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/WalletAdminController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/WalletAdminController.kt
index a0ab52916..cb5412c45 100644
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/WalletAdminController.kt
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/WalletAdminController.kt
@@ -11,20 +11,49 @@ 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
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
@RestController
@RequestMapping("/opex/v1/admin/wallet")
+@Tag(name = "Wallet Admin", description = "Admin wallet overview and total balance operations.")
class WalletAdminController(
private val walletProxy: WalletProxy
) {
@GetMapping("/users")
+ @Operation(
+ summary = "Get users wallets",
+ description = """GET /opex/v1/admin/wallet/users.
+Behavior: Optional filters include uuid, currency, excludeSystem, limit, and offset. `excludeSystem` defaults to false.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+Allowed values:
+- excludeSystem: true, false.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response.", content = [Content(mediaType = "application/json", array = ArraySchema(schema = Schema(implementation = WalletDataResponse::class)))]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. Required authority is missing: ROLE_admin. No response body.", content = [Content()])
+ ]
+ )
suspend fun getUsersWallets(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
+ @Parameter(name = "uuid", description = "User/profile/terminal UUID depending on the endpoint context.", required = false)
@RequestParam(required = false) uuid: String?,
+ @Parameter(name = "currency", description = "Currency symbol, e.g. USDT.", required = false)
@RequestParam(required = false) currency: String?,
+ @Parameter(name = "excludeSystem", description = "Whether system wallets should be excluded from the result.", required = false)
@RequestParam(required = false, defaultValue = "false") excludeSystem: Boolean,
+ @Parameter(name = "limit", description = "Optional page size.", required = false)
@RequestParam limit: Int?,
+ @Parameter(name = "offset", description = "Optional page offset.", required = false)
@RequestParam offset: Int?
): List {
return walletProxy.getUsersWallets(
@@ -38,12 +67,40 @@ class WalletAdminController(
}
@GetMapping("/system/total")
- suspend fun getSystemWalletsTotal(@CurrentSecurityContext securityContext: SecurityContext): List {
+ @Operation(
+ summary = "Get system wallets total",
+ description = """GET /opex/v1/admin/wallet/system/total.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+Allowed values:
+- excludeSystem: true, false.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response.", content = [Content(mediaType = "application/json", array = ArraySchema(schema = Schema(implementation = WalletTotal::class)))]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. Required authority is missing: ROLE_admin. No response body.", content = [Content()])
+ ]
+ )
+ suspend fun getSystemWalletsTotal(@Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext): List {
return walletProxy.getSystemWalletsTotal(securityContext.jwtAuthentication().tokenValue())
}
@GetMapping("/users/total")
- suspend fun getUsersWalletsTotal(@CurrentSecurityContext securityContext: SecurityContext): List? {
+ @Operation(
+ summary = "Get users wallets total",
+ description = """GET /opex/v1/admin/wallet/users/total.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+Allowed values:
+- excludeSystem: true, false.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response.", content = [Content(mediaType = "application/json", array = ArraySchema(schema = Schema(implementation = WalletTotal::class)))]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. Required authority is missing: ROLE_admin. No response body.", content = [Content()])
+ ]
+ )
+ suspend fun getUsersWalletsTotal(@Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext): List? {
return walletProxy.getUsersWalletsTotal(securityContext.jwtAuthentication().tokenValue())
}
}
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
index effbdc39d..f5ccedebd 100644
--- 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
@@ -15,18 +15,42 @@ 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
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
@RestController("walletOpexController")
@RequestMapping("/opex/v1/wallet")
+@Tag(name = "Wallet", description = "Authenticated user wallet asset, limits, and deposit address operations.")
class WalletController(
private val walletProxy: WalletProxy,
- private val bcGatewayProxy: BlockchainGatewayProxy,
+ private val bcGatewayProxy: BlockchainGatewayProxy
) {
@GetMapping("/asset")
+ @Operation(
+ summary = "Get user assets",
+ description = """GET /opex/v1/wallet/asset.
+Behavior: If `symbol` is omitted, all user wallet assets are returned. If provided, only that currency asset is returned.
+Security: Bearer user-token required. Requires authenticated user JWT.
+Allowed values:
+- walletType values in related wallet responses/requests: MAIN, EXCHANGE, CASHOUT.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response.", content = [Content(mediaType = "application/json", array = ArraySchema(schema = Schema(implementation = AssetResponse::class)))]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()])
+ ]
+ )
suspend fun getUserAssets(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
- @RequestParam(required = false) symbol: String?,
+ @Parameter(name = "symbol", description = "Trading pair or currency symbol, depending on endpoint.", required = false)
+ @RequestParam(required = false) symbol: String?
): List {
val auth = securityContext.jwtAuthentication()
val result = arrayListOf()
@@ -44,17 +68,46 @@ class WalletController(
}
@GetMapping("/limits")
- suspend fun getWalletOwnerLimits(@CurrentSecurityContext securityContext: SecurityContext): OwnerLimitsResponse {
+ @Operation(
+ summary = "Get wallet owner limits",
+ description = """GET /opex/v1/wallet/limits.
+Security: Bearer user-token required. Requires authenticated user JWT.
+Allowed values:
+- walletType values in related wallet responses/requests: MAIN, EXCHANGE, CASHOUT.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response.", content = [Content(mediaType = "application/json", schema = Schema(implementation = OwnerLimitsResponse::class))]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()])
+ ]
+ )
+ suspend fun getWalletOwnerLimits(@Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext): OwnerLimitsResponse {
return walletProxy.getOwnerLimits(
securityContext.jwtAuthentication().name,
- securityContext.jwtAuthentication().tokenValue(),
+ securityContext.jwtAuthentication().tokenValue()
)
}
@GetMapping("/deposit/address")
+ @Operation(
+ summary = "Assign address",
+ description = """GET /opex/v1/wallet/deposit/address.
+Source of values: `gatewayUuid` should come from server-provided gateway data for the selected currency/network.
+Security: Bearer user-token required. Requires authenticated user JWT.
+Allowed values:
+- walletType values in related wallet responses/requests: MAIN, EXCHANGE, CASHOUT.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response.", content = [Content(mediaType = "application/json", schema = Schema(implementation = AssignAddressResponse::class))]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()])
+ ]
+ )
suspend fun assignAddress(
+ @Parameter(name = "currency", description = "Currency symbol, e.g. USDT.", required = true)
@RequestParam currency: String,
+ @Parameter(name = "gatewayUuid", description = "Gateway UUID.", required = true)
@RequestParam gatewayUuid: String,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext
): AssignAddressResponse {
diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/WithdrawAdminController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/WithdrawAdminController.kt
index 8fb440bee..8a902b4e8 100644
--- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/WithdrawAdminController.kt
+++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/WithdrawAdminController.kt
@@ -8,18 +8,47 @@ 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 io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
@RestController
@RequestMapping("/opex/v1/admin/withdraw")
+@Tag(name = "Withdraw Admin", description = "Admin manual withdraw and withdraw workflow operations.")
class WithdrawAdminController(
- private val walletProxy: WalletProxy,
+ private val walletProxy: WalletProxy
) {
@PostMapping("/manually/{amount}_{symbol}/{sourceUuid}")
+ @Operation(
+ summary = "Withdraw manually",
+ description = """POST /opex/v1/admin/withdraw/manually/{amount}_{symbol}/{sourceUuid}.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+Allowed values:
+- withdraw status: REQUESTED, CREATED, ACCEPTED, CANCELED, REJECTED, DONE.
+- withdrawType: ON_CHAIN, OFF_CHAIN.
+- transferMethod: CARD, SHEBA, IPG, EXCHANGE, MANUALLY, VOUCHER, MPG, REWARD.
+- sourceWalletType where used: MAIN, EXCHANGE, CASHOUT.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response.", content = [Content(mediaType = "application/json", schema = Schema(implementation = TransferResult::class))]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. Required authority is missing: ROLE_admin. No response body.", content = [Content()])
+ ]
+ )
suspend fun withdrawManually(
+ @Parameter(name = "symbol", description = "Trading pair or currency symbol, depending on endpoint.", required = true)
@PathVariable("symbol") symbol: String,
+ @Parameter(name = "sourceUuid", description = "Source wallet owner UUID.", required = true)
@PathVariable("sourceUuid") sourceUuid: String,
+ @Parameter(name = "amount", description = "Withdraw amount.", required = true)
@PathVariable("amount") amount: BigDecimal,
@RequestBody request: ManualTransferRequest,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext
): TransferResult {
return walletProxy.withdrawManually(
@@ -32,28 +61,83 @@ class WithdrawAdminController(
}
@PostMapping("/{withdrawUuid}/accept")
+ @Operation(
+ summary = "Start the process of the withdraw",
+ description = """POST /opex/v1/admin/withdraw/{withdrawUuid}/accept.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+Allowed values:
+- withdraw status: REQUESTED, CREATED, ACCEPTED, CANCELED, REJECTED, DONE.
+- withdrawType: ON_CHAIN, OFF_CHAIN.
+- transferMethod: CARD, SHEBA, IPG, EXCHANGE, MANUALLY, VOUCHER, MPG, REWARD.
+- sourceWalletType where used: MAIN, EXCHANGE, CASHOUT.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response.", content = [Content(mediaType = "application/json", schema = Schema(implementation = WithdrawActionResult::class))]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. Required authority is missing: ROLE_admin. No response body.", content = [Content()])
+ ]
+ )
suspend fun acceptWithdraw(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
- @PathVariable withdrawUuid: String,
+ @Parameter(name = "withdrawUuid", description = "Withdraw UUID.", required = true)
+ @PathVariable withdrawUuid: String
): WithdrawActionResult {
return walletProxy.acceptWithdraw(securityContext.jwtAuthentication().tokenValue(), withdrawUuid)
}
@PostMapping("/{withdrawUuid}/done")
+ @Operation(
+ summary = "Done withdraw",
+ description = """POST /opex/v1/admin/withdraw/{withdrawUuid}/done.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+Allowed values:
+- withdraw status: REQUESTED, CREATED, ACCEPTED, CANCELED, REJECTED, DONE.
+- withdrawType: ON_CHAIN, OFF_CHAIN.
+- transferMethod: CARD, SHEBA, IPG, EXCHANGE, MANUALLY, VOUCHER, MPG, REWARD.
+- sourceWalletType where used: MAIN, EXCHANGE, CASHOUT.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response.", content = [Content(mediaType = "application/json", schema = Schema(implementation = WithdrawActionResult::class))]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. Required authority is missing: ROLE_admin. No response body.", content = [Content()])
+ ]
+ )
suspend fun doneWithdraw(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
+ @Parameter(name = "withdrawUuid", description = "Withdraw UUID.", required = true)
@PathVariable withdrawUuid: String,
- @RequestBody request: WithdrawDoneRequest,
+ @RequestBody request: WithdrawDoneRequest
): WithdrawActionResult {
return walletProxy.doneWithdraw(securityContext.jwtAuthentication().tokenValue(), withdrawUuid, request)
}
@PostMapping("/{withdrawUuid}/reject")
+ @Operation(
+ summary = "Reject withdraw",
+ description = """POST /opex/v1/admin/withdraw/{withdrawUuid}/reject.
+Validation: Request body contains the rejection reason/details required by schema.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+Allowed values:
+- withdraw status: REQUESTED, CREATED, ACCEPTED, CANCELED, REJECTED, DONE.
+- withdrawType: CARD_TO_CARD, SHEBA, ON_CHAIN, OFF_CHAIN.
+- transferMethod: CARD, SHEBA, IPG, EXCHANGE, MANUALLY, VOUCHER, MPG, REWARD.
+- sourceWalletType where used: MAIN, EXCHANGE, CASHOUT.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response.", content = [Content(mediaType = "application/json", schema = Schema(implementation = WithdrawActionResult::class))]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. Required authority is missing: ROLE_admin. No response body.", content = [Content()])
+ ]
+ )
suspend fun rejectWithdraw(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
+ @Parameter(name = "withdrawUuid", description = "Withdraw UUID.", required = true)
@PathVariable withdrawUuid: String,
- @RequestBody request: WithdrawRejectRequest,
+ @RequestBody request: WithdrawRejectRequest
): WithdrawActionResult {
return walletProxy.rejectWithdraw(securityContext.jwtAuthentication().tokenValue(), withdrawUuid, 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/WithdrawController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/WithdrawController.kt
index 5cde9598a..42a94b3c4 100644
--- 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
@@ -4,18 +4,60 @@ import co.nilin.opex.api.core.inout.*
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 io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
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")
+@Tag(name = "Withdraw", description = "Authenticated user withdraw and OTP operations.")
class WithdrawController(
- private val walletProxy: WalletProxy,
+ private val walletProxy: WalletProxy
) {
@PostMapping
+ @Operation(
+ summary = "Request withdraw",
+ description = """POST /opex/v1/withdraw.
+Security: Bearer user-token required. Required authority: PERM_withdraw:write.
+
+Behavior: The response may require an OTP next action before the withdraw can continue.
+Allowed values:
+- otpType: SMS, EMAIL.
+- withdraw status: REQUESTED, CREATED, ACCEPTED, CANCELED, REJECTED, DONE.
+- withdrawType: ON_CHAIN, OFF_CHAIN.
+- transferMethod: CARD, SHEBA, IPG, EXCHANGE, MANUALLY, VOUCHER, MPG, REWARD.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = WithdrawActionResult::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: PERM_withdraw:write. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun requestWithdraw(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@RequestBody request: RequestWithdrawBody
): WithdrawActionResult? {
@@ -26,8 +68,34 @@ class WithdrawController(
}
@PutMapping("/{withdrawUuid}/cancel")
+ @Operation(
+ summary = "Cancel withdraw",
+ description = """PUT /opex/v1/withdraw/{withdrawUuid}/cancel.
+Security: Bearer user-token required. Required authority: PERM_withdraw:write.
+Allowed values:
+- otpType: SMS, EMAIL.
+- withdraw status: REQUESTED, CREATED, ACCEPTED, CANCELED, REJECTED, DONE.
+- withdrawType: ON_CHAIN, OFF_CHAIN.
+- transferMethod: CARD, SHEBA, IPG, EXCHANGE, MANUALLY, VOUCHER, MPG, REWARD.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "No response body.", content = [Content()]),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: PERM_withdraw:write. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun cancelWithdraw(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
+ @Parameter(name = "withdrawUuid", description = "Withdraw UUID.", required = true)
@PathVariable withdrawUuid: String
) {
walletProxy.cancelWithdraw(
@@ -37,8 +105,36 @@ class WithdrawController(
}
@GetMapping("/{withdrawUuid}")
+ @Operation(
+ summary = "Find withdraw",
+ description = """GET /opex/v1/withdraw/{withdrawUuid}.
+Security: Bearer user-token required. Requires authenticated user JWT.
+Allowed values:
+- otpType: SMS, EMAIL.
+- withdraw status: REQUESTED, CREATED, ACCEPTED, CANCELED, REJECTED, DONE.
+- withdrawType: ON_CHAIN, OFF_CHAIN.
+- transferMethod: CARD, SHEBA, IPG, EXCHANGE, MANUALLY, VOUCHER, MPG, REWARD.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = WithdrawResponse::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun findWithdraw(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
+ @Parameter(name = "withdrawUuid", description = "Withdraw UUID.", required = true)
@PathVariable withdrawUuid: String
): WithdrawResponse {
return walletProxy.findWithdraw(
@@ -48,20 +144,95 @@ class WithdrawController(
}
@PostMapping("/{withdrawUuid}/otp/{otpType}/request")
+ @Operation(
+ summary = "Request otp",
+ description = """POST /opex/v1/withdraw/{withdrawUuid}/otp/{otpType}/request.
+Validation: `otpType` allowed values: SMS, EMAIL.
+Security: Bearer user-token required. Required authority: PERM_withdraw:write.
+
+Validation: `otpType` must be a supported OTP delivery type such as EMAIL or SMS.
+Allowed values:
+- otpType: SMS, EMAIL.
+- withdraw status: REQUESTED, CREATED, ACCEPTED, CANCELED, REJECTED, DONE.
+- withdrawType: ON_CHAIN, OFF_CHAIN.
+- transferMethod: CARD, SHEBA, IPG, EXCHANGE, MANUALLY, VOUCHER, MPG, REWARD.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = TempOtpResponse::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: PERM_withdraw:write. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun requestOTP(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
+ @Parameter(name = "withdrawUuid", description = "Withdraw UUID.", required = true)
@PathVariable withdrawUuid: String,
+ @Parameter(name = "otpType", description = "OTP delivery type. Allowed values: SMS, EMAIL.", required = true)
@PathVariable otpType: OTPType
): TempOtpResponse {
return walletProxy.requestWithdrawOTP(securityContext.jwtAuthentication().tokenValue(), withdrawUuid, otpType)
}
@PostMapping("/{withdrawUuid}/otp/{otpType}/verify")
+ @Operation(
+ summary = "Verify otp",
+ description = """POST /opex/v1/withdraw/{withdrawUuid}/otp/{otpType}/verify.
+Validation: `otpType` allowed values: SMS, EMAIL. `otpCode` is required.
+Security: Bearer user-token required. Required authority: PERM_withdraw:write.
+
+Validation: `otpType` must be a supported OTP delivery type such as EMAIL or SMS. `otpCode` is required for verification.
+Allowed values:
+- otpType: SMS, EMAIL.
+- withdraw status: REQUESTED, CREATED, ACCEPTED, CANCELED, REJECTED, DONE.
+- withdrawType: ON_CHAIN, OFF_CHAIN.
+- transferMethod: CARD, SHEBA, IPG, EXCHANGE, MANUALLY, VOUCHER, MPG, REWARD.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = WithdrawActionResult::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: PERM_withdraw:write. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun verifyOTP(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
+ @Parameter(name = "withdrawUuid", description = "Withdraw UUID.", required = true)
@PathVariable withdrawUuid: String,
+ @Parameter(name = "otpType", description = "OTP delivery type. Allowed values: SMS, EMAIL.", required = true)
@PathVariable otpType: OTPType,
- @RequestParam otpCode: String,
+ @Parameter(name = "otpCode", description = "OTP code received by user.", required = true)
+ @RequestParam otpCode: String
): WithdrawActionResult {
return walletProxy.verifyWithdrawOTP(
securityContext.jwtAuthentication().tokenValue(),
@@ -70,4 +241,4 @@ class WithdrawController(
otpCode
)
}
-}
\ 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/ProfileProxyImpl.kt b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/ProfileProxyImpl.kt
index 68dd031c8..08c07f63e 100644
--- a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/ProfileProxyImpl.kt
+++ b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/ProfileProxyImpl.kt
@@ -131,7 +131,7 @@ class ProfileProxyImpl(@Qualifier("generalWebClient") private val webClient: Web
override suspend fun getAllAddressBooks(token: String): List {
return webClient.get()
- .uri("$baseUrl/address-book`")
+ .uri("$baseUrl/address-book")
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer $token")
.retrieve()
@@ -188,7 +188,7 @@ class ProfileProxyImpl(@Qualifier("generalWebClient") private val webClient: Web
override suspend fun getBankAccounts(token: String): List {
return webClient.get()
- .uri("$baseUrl/bank-account`")
+ .uri("$baseUrl/bank-account")
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer $token")
.retrieve()
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 25d7b969f..a16a07d5e 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
@@ -207,7 +207,6 @@ class WalletProxyImpl(@Qualifier("generalWebClient") private val webClient: WebC
limit,
offset,
ascendingByTime == true,
- null
)
)
)
@@ -356,26 +355,6 @@ class WalletProxyImpl(@Qualifier("generalWebClient") private val webClient: WebC
}
}
- 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
@@ -442,7 +421,7 @@ class WalletProxyImpl(@Qualifier("generalWebClient") private val webClient: WebC
}
}
- override suspend fun getSwapTransactions(token: String, request: UserTransactionRequest): List {
+ override suspend fun getSwapTransactions(token: String, request: UserSwapTransactionRequest): List {
return webClient.post()
.uri("$baseUrl/v1/swap/history")
.accept(MediaType.APPLICATION_JSON)
@@ -457,7 +436,7 @@ class WalletProxyImpl(@Qualifier("generalWebClient") private val webClient: WebC
override suspend fun getSwapTransactionsCount(
token: String,
- request: UserTransactionRequest
+ request: UserSwapTransactionRequest
): Long {
return webClient.post()
.uri("$baseUrl/v1/swap/history/count")
@@ -535,7 +514,7 @@ class WalletProxyImpl(@Qualifier("generalWebClient") private val webClient: WebC
override suspend fun getSwapTransactionsForAdmin(
token: String,
- request: UserTransactionRequest
+ request: UserSwapTransactionRequest
): List {
return webClient.post()
.uri("$baseUrl/admin/v1/swap/history")
@@ -850,7 +829,7 @@ class WalletProxyImpl(@Qualifier("generalWebClient") private val webClient: WebC
terminalUuid: String
): TerminalLocalizationResponse {
return webClient.get()
- .uri("$baseUrl/admin/deposit/terminal/${terminalUuid}/localization")
+ .uri("$baseUrl/admin/terminal/${terminalUuid}/localization")
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer $token")
.retrieve()
@@ -865,7 +844,7 @@ class WalletProxyImpl(@Qualifier("generalWebClient") private val webClient: WebC
terminalLocalizations: List
): TerminalLocalizationResponse {
return webClient.post()
- .uri("$baseUrl/admin/deposit/terminal/${terminalUuid}/localization")
+ .uri("$baseUrl/admin/terminal/${terminalUuid}/localization")
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer $token")
.body(Mono.just(terminalLocalizations))
@@ -877,7 +856,7 @@ class WalletProxyImpl(@Qualifier("generalWebClient") private val webClient: WebC
override suspend fun deleteTerminalLocalization(token: String, id: Long) {
webClient.delete()
- .uri("$baseUrl/admin/deposit/terminal/localization/${id}")
+ .uri("$baseUrl/admin/terminal/localization/${id}")
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer $token")
.retrieve()
@@ -934,7 +913,7 @@ class WalletProxyImpl(@Qualifier("generalWebClient") private val webClient: WebC
terminal: TerminalCommand
): TerminalCommand? {
return webClient.post()
- .uri("$baseUrl/admin/deposit/terminal")
+ .uri("$baseUrl/admin/terminal")
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer $token")
.body(Mono.just(terminal))
@@ -947,10 +926,10 @@ class WalletProxyImpl(@Qualifier("generalWebClient") private val webClient: WebC
override suspend fun updateTerminal(
token: String,
terminalUuid: String,
- terminal: TerminalCommand
+ terminal: TerminalUpdateCommand
): TerminalCommand? {
return webClient.put()
- .uri("$baseUrl/admin/deposit/terminal/${terminalUuid}")
+ .uri("$baseUrl/admin/terminal/${terminalUuid}")
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer $token")
.body(Mono.just(terminal))
@@ -962,7 +941,7 @@ class WalletProxyImpl(@Qualifier("generalWebClient") private val webClient: WebC
override suspend fun deleteTerminal(token: String, terminalUuid: String) {
webClient.delete()
- .uri("$baseUrl/admin/deposit/terminal/${terminalUuid}")
+ .uri("$baseUrl/admin/terminal/${terminalUuid}")
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer $token")
.retrieve()
@@ -989,13 +968,13 @@ class WalletProxyImpl(@Qualifier("generalWebClient") private val webClient: WebC
terminalUuid: String
): TerminalCommand? {
return webClient.get()
- .uri("$baseUrl/admin/deposit/terminal/${terminalUuid}")
+ .uri("$baseUrl/admin/terminal/${terminalUuid}")
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer $token")
.retrieve()
.onStatus({ t -> t.isError }, { it.createException() })
.bodyToMono()
- .awaitFirstOrElse { throw OpexError.BadRequest.exception() }
+ .awaitFirstOrElse { throw OpexError.NotFound.exception() }
}
override suspend fun getAssignedGatewayToTerminal(
@@ -1003,7 +982,7 @@ class WalletProxyImpl(@Qualifier("generalWebClient") private val webClient: WebC
terminalUuid: String
): List? {
return webClient.get()
- .uri("$baseUrl/admin/deposit/terminal/${terminalUuid}/gateway")
+ .uri("$baseUrl/admin/terminal/${terminalUuid}/gateway")
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer $token")
.retrieve()
@@ -1047,7 +1026,7 @@ class WalletProxyImpl(@Qualifier("generalWebClient") private val webClient: WebC
.awaitBodilessEntity()
}
- override suspend fun addCurrencyToGateway(
+ override suspend fun addGatewayToCurrency(
token: String,
currencySymbol: String,
gatewayCommand: CurrencyGatewayCommand
@@ -1067,7 +1046,7 @@ class WalletProxyImpl(@Qualifier("generalWebClient") private val webClient: WebC
token: String,
gatewayUuid: String,
currencySymbol: String,
- gatewayCommand: CurrencyGatewayCommand
+ gatewayCommand: CurrencyGatewayUpdateCommand
): CurrencyGatewayCommand? {
return webClient.put()
.uri("$baseUrl/currency/${currencySymbol}/gateway/${gatewayUuid}")
diff --git a/auth-gateway/auth-gateway-app/pom.xml b/auth-gateway/auth-gateway-app/pom.xml
index 0ff3a5ade..1be88098a 100644
--- a/auth-gateway/auth-gateway-app/pom.xml
+++ b/auth-gateway/auth-gateway-app/pom.xml
@@ -129,6 +129,11 @@
micrometer-registry-prometheus
runtime
+
+ org.springdoc
+ springdoc-openapi-starter-webflux-api
+ 2.6.0
+
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/OpenApiConfig.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/OpenApiConfig.kt
new file mode 100644
index 000000000..f736645e7
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/OpenApiConfig.kt
@@ -0,0 +1,36 @@
+package co.nilin.opex.auth.config
+
+import io.swagger.v3.oas.models.Components
+import io.swagger.v3.oas.models.OpenAPI
+import io.swagger.v3.oas.models.info.Info
+import io.swagger.v3.oas.models.security.SecurityScheme
+import io.swagger.v3.oas.models.servers.Server
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+
+@Configuration(proxyBeanMethods = false)
+class AuthGatewayOpenApiConfig() {
+
+ @Bean
+ fun authGatewayOpenApi(): OpenAPI {
+ return OpenAPI()
+ .info(
+ Info()
+ .title("Opex Auth Gateway API")
+ .description("OpenAPI documentation for Opex Auth Gateway APIs.")
+ .version("1.0.1-beta.7")
+ .description("Backend for opex exchange.")
+ )
+ .components(
+ Components().addSecuritySchemes(
+ "bearerAuth",
+ SecurityScheme()
+ .type(SecurityScheme.Type.HTTP)
+ .scheme("bearer")
+ .bearerFormat("JWT")
+ .description("JWT Bearer token")
+ )
+ )
+ }
+}
diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/OpenApiCorsConfig.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/OpenApiCorsConfig.kt
new file mode 100644
index 000000000..5c31a0c2c
--- /dev/null
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/OpenApiCorsConfig.kt
@@ -0,0 +1,46 @@
+package co.nilin.opex.auth.config
+
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.Ordered
+import org.springframework.core.annotation.Order
+import org.springframework.web.cors.CorsConfiguration
+import org.springframework.web.cors.reactive.CorsWebFilter
+import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource
+
+@Configuration(proxyBeanMethods = false)
+class OpenApiCorsConfig(
+ @Value("\${app.swagger.cors.enabled:false}")
+ private val enabled: Boolean,
+
+ @Value("\${app.swagger.cors.allowed-origins:http://localhost:8110}")
+ private val allowedOrigins: String
+) {
+
+ @Bean
+ @Order(Ordered.HIGHEST_PRECEDENCE)
+ fun swaggerCorsWebFilter(): CorsWebFilter {
+ val config = CorsConfiguration().apply {
+ allowedOrigins = if (enabled) {
+ this@OpenApiCorsConfig.allowedOrigins
+ .split(",")
+ .map { it.trim() }
+ .filter { it.isNotBlank() }
+ } else {
+ emptyList()
+ }
+
+ allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
+ allowedHeaders = listOf("*")
+ exposedHeaders = listOf("Location", "Content-Disposition")
+ allowCredentials = false
+ maxAge = 3600
+ }
+
+ val source = UrlBasedCorsConfigurationSource()
+ source.registerCorsConfiguration("/**", config)
+
+ return CorsWebFilter(source)
+ }
+}
\ 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
index 5b2a71194..3911ec4ff 100644
--- 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
@@ -1,13 +1,13 @@
package co.nilin.opex.auth.config
import co.nilin.opex.auth.utils.AudienceValidator
-import jakarta.enterprise.inject.Default
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.annotation.Primary
import org.springframework.core.annotation.Order
-import org.springframework.security.authorization.AuthorizationDecision
+import org.springframework.http.HttpMethod
import org.springframework.security.config.Customizer
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity
@@ -15,7 +15,6 @@ import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator
import org.springframework.security.oauth2.jwt.JwtValidators
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder
-import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers
import org.springframework.web.reactive.function.client.WebClient
@@ -23,21 +22,36 @@ import org.springframework.web.reactive.function.client.WebClient
@EnableWebFluxSecurity
@Configuration
class SecurityConfig(
- @Qualifier("keycloakWebClient")
- private val webClient: WebClient,
- private val keycloakConfig: KeycloakConfig
+ @Qualifier("keycloakWebClient") private val webClient: WebClient,
+ private val keycloakConfig: KeycloakConfig,
) {
+ @Value("\${swagger.auth.enabled:false}")
+ private var swaggerAuthEnabled: Boolean = false
+
+ @Value("\${swagger.auth.authority:ROLE_admin}")
+ private lateinit var swaggerAuthority: String
+
@Bean
- @Order(2)
- 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()
- .pathMatchers("/v1/user/update/**").permitAll()
- .anyExchange().authenticated()
+ @Order(0)
+ fun swaggerSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+ val swaggerPaths = arrayOf(
+ "/swagger-ui.html",
+ "/swagger-ui/**",
+ "/v3/api-docs",
+ "/v3/api-docs/**",
+ "/webjars/**"
+ )
+
+ return http
+ .securityMatcher(ServerWebExchangeMatchers.pathMatchers(*swaggerPaths))
+ .csrf { it.disable() }
+ .authorizeExchange {
+ if (swaggerAuthEnabled) {
+ it.anyExchange().hasAuthority(swaggerAuthority)
+ } else {
+ it.anyExchange().permitAll()
+ }
}
.oauth2ResourceServer { it.jwt(Customizer.withDefaults()) }
.build()
@@ -52,19 +66,30 @@ class SecurityConfig(
"/v1/oauth/protocol/openid-connect/token/resend-otp"
)
)
+ .csrf { it.disable() }
+ .authorizeExchange { it.anyExchange().authenticated() }
+ .oauth2ResourceServer { it.jwt { jwt -> jwt.jwtDecoder(preAuthJwtDecoder()) } }
+ .build()
+ }
+
+ @Bean
+ @Order(2)
+ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+ return http
.csrf { it.disable() }
.authorizeExchange {
- it.anyExchange().authenticated()
- }
- .oauth2ResourceServer { it ->
- it.jwt {
- it.jwtDecoder(preAuthJwtDecoder())
- }
+ it.pathMatchers("/actuator/**").permitAll()
+ .pathMatchers("/v1/oauth/protocol/openid-connect/**").permitAll()
+ .pathMatchers("/v1/oauth.***").permitAll()
+ .pathMatchers("/v1/user/public/**").permitAll()
+ .pathMatchers("/v1/user/update/**").permitAll()
+ .pathMatchers(HttpMethod.OPTIONS, "/**").permitAll()
+ .anyExchange().authenticated()
}
+ .oauth2ResourceServer { it.jwt(Customizer.withDefaults()) }
.build()
}
-
@Bean
@Throws(Exception::class)
@Primary
@@ -90,7 +115,6 @@ class SecurityConfig(
return decoder
}
-
@Bean("preAuthJwtDecoder")
@Throws(Exception::class)
fun preAuthJwtDecoder(): ReactiveJwtDecoder? {
@@ -112,5 +136,3 @@ class SecurityConfig(
return decoder
}
}
-
-
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
index 04d5e12c3..5270d40d4 100644
--- 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
@@ -1,7 +1,20 @@
-package co.nilin.opex.auth.controller;
+package co.nilin.opex.auth.controller
-import co.nilin.opex.auth.model.*
+import co.nilin.opex.auth.model.ConfirmPasswordFlowTokenRequest
+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.ResendOtpRequest
+import co.nilin.opex.auth.model.ResendOtpResponse
+import co.nilin.opex.auth.model.TokenResponse
import co.nilin.opex.auth.service.LoginService
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
@@ -12,23 +25,76 @@ import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/v1/oauth/protocol/openid-connect/")
+@Tag(
+ name = "Auth Gateway - Token",
+ description = "Token, OTP confirmation, external IdP token, and refresh-token operations."
+)
class AuthController(private val loginService: LoginService) {
@PostMapping("/token")
+ @Operation(
+ summary = "Request token",
+ description = """POST /v1/oauth/protocol/openid-connect/token.
+Security: Public endpoint. No Bearer token is required.
+
+Behavior: Starts password-flow login. If OTP is required, the response contains OTP metadata instead of a final access token.
+Allowed values:
+- captchaType: INTERNAL, ARCAPTCHA, HCAPTCHA.""",
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(mediaType = "application/json", schema = Schema(implementation = TokenResponse::class))]
+ )
+ ]
+ )
suspend fun requestGetToken(@RequestBody tokenRequest: PasswordFlowTokenRequest): ResponseEntity {
val tokenResponse = loginService.requestGetToken(tokenRequest)
return ResponseEntity.ok().body(tokenResponse)
}
@PostMapping("/token/confirm")
+ @Operation(
+ summary = "Confirm token request",
+ description = """POST /v1/oauth/protocol/openid-connect/token/confirm.
+Security: Public endpoint. No Bearer token is required.
+
+Validation: `otp` and the pre-auth `token` returned by the token request flow are required.
+Behavior: Completes password-flow login after OTP verification.""",
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(mediaType = "application/json", schema = Schema(implementation = TokenResponse::class))]
+ )
+ ]
+ )
suspend fun confirmGetToken(@RequestBody tokenRequest: ConfirmPasswordFlowTokenRequest): ResponseEntity {
val tokenResponse = loginService.confirmGetToken(tokenRequest)
return ResponseEntity.ok().body(tokenResponse)
}
@PostMapping("/token/resend-otp")
+ @Operation(
+ summary = "Resend login OTP",
+ description = """POST /v1/oauth/protocol/openid-connect/token/resend-otp.
+Security: Bearer pre-auth token required.
+
+Behavior: Resends the OTP for an in-progress login flow.
+Source of values: Use the pre-auth token returned by the token request flow.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(mediaType = "application/json", schema = Schema(implementation = ResendOtpResponse::class))]
+ ),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()])
+ ]
+ )
suspend fun resendOtp(
@RequestBody resendOtpRequest: ResendOtpRequest,
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
): ResponseEntity {
val response = loginService.resendLoginOtp(resendOtpRequest, securityContext.authentication.name)
@@ -36,12 +102,41 @@ class AuthController(private val loginService: LoginService) {
}
@PostMapping("/token-external")
+ @Operation(
+ summary = "Request token by external IdP",
+ description = """POST /v1/oauth/protocol/openid-connect/token-external.
+Security: Public endpoint. No Bearer token is required.
+
+Behavior: Exchanges an external identity-provider token for an Opex token. OTP verification data may be required depending on the account state.""",
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(mediaType = "application/json", schema = Schema(implementation = TokenResponse::class))]
+ )
+ ]
+ )
suspend fun getToken(@RequestBody tokenRequest: ExternalIdpTokenRequest): ResponseEntity {
val tokenResponse = loginService.getToken(tokenRequest)
return ResponseEntity.ok().body(tokenResponse)
}
@PostMapping("/refresh")
+ @Operation(
+ summary = "Refresh token",
+ description = """POST /v1/oauth/protocol/openid-connect/refresh.
+Security: Public endpoint. No Bearer token is required.
+
+Validation: `refreshToken` and `clientId` are required.
+Behavior: Issues a new access token from a valid refresh token.""",
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(mediaType = "application/json", schema = Schema(implementation = TokenResponse::class))]
+ )
+ ]
+ )
suspend fun refreshToken(@RequestBody tokenRequest: RefreshTokenRequest): ResponseEntity {
val tokenResponse = loginService.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
index c63d8dfde..7b48faeaa 100644
--- 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
@@ -3,6 +3,11 @@ package co.nilin.opex.auth.controller
import co.nilin.opex.auth.model.*
import co.nilin.opex.auth.service.ForgetPasswordService
import co.nilin.opex.auth.service.RegisterService
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping
@@ -12,53 +17,171 @@ import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/v1/user/public")
+@Tag(
+ name = "Auth Gateway - Public User",
+ description = "Public registration and password-recovery operations."
+)
class PublicUserController(
private val forgetPasswordService: ForgetPasswordService,
private val registerService: RegisterService
) {
- //TODO IMPORTANT: remove in production
@PostMapping("/register")
+ @Operation(
+ summary = "Register user",
+ description = """POST /v1/user/public/register.
+Security: Public endpoint. No Bearer token is required.
+
+Behavior: Starts the registration flow and sends OTP if required.
+Allowed values:
+- captchaType: INTERNAL, ARCAPTCHA, HCAPTCHA.""",
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(mediaType = "application/json", schema = Schema(type = "object"))]
+ )
+ ]
+ )
suspend fun registerUser(@Valid @RequestBody request: RegisterUserRequest): ResponseEntity {
val otpResponse = registerService.registerUser(request)
return ResponseEntity.ok().body(otpResponse)
}
@PostMapping("/register/verify")
+ @Operation(
+ summary = "Verify registration OTP",
+ description = """POST /v1/user/public/register/verify.
+Security: Public endpoint. No Bearer token is required.
+
+Validation: `username` and `otp` are required.
+Behavior: Returns an action token used to confirm registration.""",
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = OTPActionTokenResponse::class)
+ )]
+ )
+ ]
+ )
suspend fun verifyRegister(@RequestBody request: VerifyOTPRequest): ResponseEntity {
val token = registerService.verifyRegister(request)
return ResponseEntity.ok(OTPActionTokenResponse(token))
}
@PostMapping("/register/confirm")
+ @Operation(
+ summary = "Confirm registration",
+ description = """POST /v1/user/public/register/confirm.
+Security: Public endpoint. No Bearer token is required.
+
+Validation: `password` and registration action `token` are required.
+Behavior: Completes registration and returns login token data.""",
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = TokenResponse::class)
+ )]
+ )
+ ]
+ )
suspend fun confirmRegister(@RequestBody request: ConfirmRegisterRequest): ResponseEntity {
val loginToken = registerService.confirmRegister(request)
return ResponseEntity.ok(loginToken)
}
@PostMapping("/register-external")
+ @Operation(
+ summary = "Register external IdP user",
+ description = """POST /v1/user/public/register-external.
+Security: Public endpoint. No Bearer token is required.
+
+Behavior: Registers a user from an external identity provider token.
+Response body: No response body.""",
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun registerExternal(@RequestBody request: ExternalIdpUserRegisterRequest): ResponseEntity {
registerService.registerExternalIdpUser(request)
return ResponseEntity.ok().build()
}
- //TODO IMPORTANT: remove in production
@PostMapping("/forget")
+ @Operation(
+ summary = "Forgot password",
+ description = """POST /v1/user/public/forget.
+Security: Public endpoint. No Bearer token is required.
+
+Behavior: Starts password-recovery flow and sends OTP if required.
+Allowed values:
+- captchaType: INTERNAL, ARCAPTCHA, HCAPTCHA.""",
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(mediaType = "application/json", schema = Schema(type = "object"))]
+ )
+ ]
+ )
suspend fun forgetPassword(@RequestBody request: ForgotPasswordRequest): ResponseEntity {
val otpResponse = forgetPasswordService.forgetPassword(request)
return ResponseEntity.ok().body(otpResponse)
}
@PostMapping("/forget/verify")
+ @Operation(
+ summary = "Verify forgot-password OTP",
+ description = """POST /v1/user/public/forget/verify.
+Security: Public endpoint. No Bearer token is required.
+
+Validation: `username` and `otp` are required.
+Behavior: Returns an action token used to confirm password reset.""",
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = OTPActionTokenResponse::class)
+ )]
+ )
+ ]
+ )
suspend fun verifyForget(@RequestBody request: VerifyOTPRequest): ResponseEntity {
val token = forgetPasswordService.verifyForget(request)
return ResponseEntity.ok(OTPActionTokenResponse(token))
}
@PostMapping("/forget/confirm")
+ @Operation(
+ summary = "Confirm forgot-password flow",
+ description = """POST /v1/user/public/forget/confirm.
+Security: Public endpoint. No Bearer token is required.
+
+Validation: `newPassword`, `newPasswordConfirmation`, and password-reset action `token` are required.
+Response body: No response body.""",
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun forgetPassword(@RequestBody request: ConfirmForgetRequest): ResponseEntity {
forgetPasswordService.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/SessionController.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/controller/SessionController.kt
index 5fe06d1f3..b2235ace6 100644
--- a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/controller/SessionController.kt
+++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/controller/SessionController.kt
@@ -7,12 +7,25 @@ import co.nilin.opex.auth.service.LogoutService
import co.nilin.opex.auth.service.SessionService
import co.nilin.opex.common.OpexError
import co.nilin.opex.common.security.jwtAuthentication
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.enums.ParameterIn
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/v1/user")
+@Tag(
+ name = "Auth Gateway - Sessions",
+ description = "Authenticated user session and logout operations."
+)
class SessionController(
private val forgetPasswordService: ForgetPasswordService,
private val logoutService: LogoutService,
@@ -20,7 +33,31 @@ class SessionController(
) {
@PostMapping("/logout")
- suspend fun logout(@CurrentSecurityContext securityContext: SecurityContext) {
+ @Operation(
+ summary = "Logout current session",
+ description = """POST /v1/user/logout.
+Security: Bearer user-token required.
+
+Behavior: Terminates the current session using the `sid` claim from the JWT.
+Response body: No response body.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
+ suspend fun logout(
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext
+ ) {
val userId = securityContext.jwtAuthentication().name
val sid = securityContext.jwtAuthentication().tokenAttributes["sid"] as String?
?: throw OpexError.InvalidToken.exception()
@@ -28,7 +65,34 @@ class SessionController(
}
@PostMapping("/session")
+ @Operation(
+ summary = "List user sessions",
+ description = """POST /v1/user/session.
+Security: Bearer user-token required.
+
+Behavior: Returns user sessions for the authenticated user. `uuid` in the request body is overwritten by the authenticated user id.
+Allowed values:
+- os: ANDROID, IOS, MOBILE_WEB, DESKTOP_WEB
+- status: ACTIVE, EXPIRED, TERMINATED.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = Sessions::class))
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun getSessions(
+ @Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext,
@RequestBody sessionRequest: SessionRequest
): List {
@@ -40,13 +104,70 @@ class SessionController(
}
@DeleteMapping("/session/{sessionId}")
- suspend fun logout(@CurrentSecurityContext securityContext: SecurityContext, @PathVariable sessionId: String) {
+ @Operation(
+ summary = "Logout one session",
+ description = """DELETE /v1/user/session/{sessionId}.
+Security: Bearer user-token required.
+
+Behavior: Terminates one user session by id.
+Response body: No response body.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ parameters = [
+ Parameter(
+ name = "sessionId",
+ `in` = ParameterIn.PATH,
+ required = true,
+ description = "Session id to terminate."
+ )
+ ],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
+ suspend fun logout(
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext,
+ @PathVariable sessionId: String
+ ) {
val uuid = securityContext.authentication.name
logoutService.logoutSession(uuid, sessionId)
}
@PostMapping("/session/delete-others")
- suspend fun logoutOthers(@CurrentSecurityContext securityContext: SecurityContext) {
+ @Operation(
+ summary = "Logout other sessions",
+ description = """POST /v1/user/session/delete-others.
+Security: Bearer user-token required.
+
+Behavior: Terminates all sessions except the current JWT session.
+Response body: No response body.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
+ suspend fun logoutOthers(
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext
+ ) {
val uuid = securityContext.authentication.name
val sid = securityContext.jwtAuthentication().tokenAttributes["sid"] as String?
?: throw OpexError.InvalidToken.exception()
@@ -54,10 +175,32 @@ class SessionController(
}
@PostMapping("/session/delete-all")
- suspend fun logoutAll(@CurrentSecurityContext securityContext: SecurityContext) {
+ @Operation(
+ summary = "Logout all sessions",
+ description = """POST /v1/user/session/delete-all.
+Security: Bearer user-token required.
+
+Behavior: Terminates all sessions for the authenticated user.
+Response body: No response body.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
+ suspend fun logoutAll(
+ @Parameter(hidden = true)
+ @CurrentSecurityContext securityContext: SecurityContext
+ ) {
val uuid = securityContext.authentication.name
logoutService.logoutAll(uuid)
}
-
}
-
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
index 148d26993..3cf756c22 100644
--- 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
@@ -25,7 +25,6 @@ data class OTPVerifyResponse(
val type: OTPResultType
)
-//TODO IMPORTANT: remove in production
data class TempOtpResponse(val otp: String?, val otpReceiver: OTPReceiver?)
enum class OTPAction {
diff --git a/auth-gateway/auth-gateway-app/src/main/resources/application.yml b/auth-gateway/auth-gateway-app/src/main/resources/application.yml
index 31b1ae63b..68dd6b18e 100644
--- a/auth-gateway/auth-gateway-app/src/main/resources/application.yml
+++ b/auth-gateway/auth-gateway-app/src/main/resources/application.yml
@@ -80,3 +80,22 @@ app:
custom-user-language:
enabled: ${CUSTOM_USER_LANGUAGE_ENABLED:false}
pre-auth-client-secret: ${PRE_AUTH_CLIENT_SECRET}
+ swagger:
+ cors:
+ enabled: true
+ allowed-origins: "http://localhost:8110"
+
+# --- Swagger / SpringDoc (env-driven) ---
+springdoc:
+ api-docs:
+ enabled: ${SWAGGER_API_DOCS_ENABLED:false}
+ path: ${SWAGGER_API_DOCS_PATH:/v3/api-docs}
+
+ swagger-ui:
+ enabled: ${SWAGGER_UI_ENABLED:false}
+ path: ${SWAGGER_UI_PATH:/swagger-ui.html}
+ try-it-out-enabled: ${SWAGGER_TRY_IT_OUT_ENABLED:true}
+ persist-authorization: ${SWAGGER_PERSIST_AUTHORIZATION:true}
+ display-request-duration: true
+ operations-sorter: method
+ tags-sorter: alpha
diff --git a/common/src/main/kotlin/co/nilin/opex/common/utils/LanguageUtils.kt b/common/src/main/kotlin/co/nilin/opex/common/utils/LanguageUtils.kt
index 9c04ab8db..fc2a0a0c5 100644
--- a/common/src/main/kotlin/co/nilin/opex/common/utils/LanguageUtils.kt
+++ b/common/src/main/kotlin/co/nilin/opex/common/utils/LanguageUtils.kt
@@ -10,7 +10,7 @@ object LanguageUtils {
Mono.deferContextual { ctx -> Mono.just(ctx.getOrDefault("lang", getDefaultUserLanguage())!!) }
fun getDefaultUserLanguage(): String {
- return try {
+ return try {
GlobalWebConfigCache.webConfig?.defaultLanguage?.toString() ?: UserLanguage.EN.toString()
} catch (e: Exception) {
UserLanguage.EN.toString()
diff --git a/common/src/main/kotlin/co/nilin/opex/common/utils/LimitedInterval.kt b/common/src/main/kotlin/co/nilin/opex/common/utils/LimitedInterval.kt
new file mode 100644
index 000000000..87a7bb7d6
--- /dev/null
+++ b/common/src/main/kotlin/co/nilin/opex/common/utils/LimitedInterval.kt
@@ -0,0 +1,32 @@
+package co.nilin.opex.common.utils
+
+import java.time.Instant
+import java.time.LocalDateTime
+import java.time.ZoneId
+import java.util.*
+import java.util.concurrent.TimeUnit
+
+enum class LimitedInterval(val label: String, val unit: TimeUnit, val duration: Long) {
+
+ Day("1d", TimeUnit.DAYS, 1),
+ Week("1w", TimeUnit.DAYS, 7),
+ Month("1M", TimeUnit.DAYS, 31),
+ Year("1Y", TimeUnit.DAYS, 365);
+
+ private fun getOffsetTime() = unit.toMillis(duration)
+
+ fun getDate() = Date(Date().time - getOffsetTime())
+
+ fun getTime() = Date().time - getOffsetTime()
+
+ fun getLocalDateTime(): LocalDateTime = with(Instant.ofEpochMilli(getDate().time)) {
+ LocalDateTime.ofInstant(this, ZoneId.systemDefault())
+ }
+
+ companion object {
+ fun findByLabel(label: String): LimitedInterval? {
+ return values().find { it.label == label }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/docker-compose.local.yml b/docker-compose.local.yml
index 556a9e475..3ab397629 100644
--- a/docker-compose.local.yml
+++ b/docker-compose.local.yml
@@ -86,4 +86,7 @@ services:
ports:
- "0.0.0.0:8099:8080"
- "127.0.0.1:1057:5005"
+ swagger-docs:
+ ports:
+ - "0.0.0.0:8110:8080"
diff --git a/docker-compose.yml b/docker-compose.yml
index b5b7c0f25..bb427887e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -422,6 +422,10 @@ services:
- CUSTOM_MESSAGE_URL=${CUSTOM_MESSAGE_URL}
- CUSTOM_MESSAGE_ENABLED=${CUSTOM_MESSAGE_ENABLED}
- CUSTOM_USER_LANGUAGE_ENABLED=${CUSTOM_USER_LANGUAGE_ENABLED}
+ - SWAGGER_API_DOCS_ENABLED=${SWAGGER_API_DOCS_ENABLED}
+ - SWAGGER_UI_ENABLED=${SWAGGER_UI_ENABLED}
+ - SWAGGER_AUTH_ENABLED=${SWAGGER_AUTH_ENABLED}
+ - SWAGGER_AUTH_AUTHORITY=${SWAGGER_AUTH_AUTHORITY}
volumes:
- auth-gateway-keys:/app/keys
depends_on:
@@ -517,6 +521,10 @@ services:
- CUSTOM_MESSAGE_URL=${CUSTOM_MESSAGE_URL}
- CUSTOM_MESSAGE_ENABLED=${CUSTOM_MESSAGE_ENABLED}
- CUSTOM_USER_LANGUAGE_ENABLED=${CUSTOM_USER_LANGUAGE_ENABLED}
+ - SWAGGER_API_DOCS_ENABLED=${SWAGGER_API_DOCS_ENABLED}
+ - SWAGGER_UI_ENABLED=${SWAGGER_UI_ENABLED}
+ - SWAGGER_AUTH_ENABLED=${SWAGGER_AUTH_ENABLED}
+ - SWAGGER_AUTH_AUTHORITY=${SWAGGER_AUTH_AUTHORITY}
depends_on:
- consul
- vault
@@ -653,6 +661,21 @@ services:
deploy:
restart_policy:
condition: on-failure
+ swagger-docs:
+ image: swaggerapi/swagger-ui:latest
+ profiles:
+ - docs
+ environment:
+ URLS: ${SWAGGER_DOCS_URLS}
+ URLS_PRIMARY_NAME: ${SWAGGER_DOCS_PRIMARY_NAME:-Opex API}
+ DOC_EXPANSION: "none"
+ DEFAULT_MODELS_EXPAND_DEPTH: "-1"
+ DISPLAY_REQUEST_DURATION: "true"
+ DEEP_LINKING: "true"
+ PERSIST_AUTHORIZATION: "true"
+ TRY_IT_OUT_ENABLED: "true"
+ FILTER: "true"
+ VALIDATOR_URL: "none"
volumes:
zookeeper-data:
zookeeper-log:
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
index 9b8486e8c..03478a113 100644
--- 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
@@ -77,7 +77,7 @@ interface ProfileApprovalRequestRepository : ReactiveCrudRepository
+ ): Flow
@Query("select * from profile_approval_request p where p.user_id = :userId order by create_date desc limit 1")
fun findByUserId(userId: String): Mono
diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/AdvancedTransferAdminController.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/AdvancedTransferAdminController.kt
index 97dfba756..703419083 100644
--- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/AdvancedTransferAdminController.kt
+++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/AdvancedTransferAdminController.kt
@@ -1,5 +1,6 @@
package co.nilin.opex.wallet.app.controller
+import co.nilin.opex.wallet.app.dto.UserSwapTransactionRequest
import co.nilin.opex.wallet.app.dto.UserTransactionRequest
import co.nilin.opex.wallet.core.inout.AdminSwapResponse
import co.nilin.opex.wallet.core.spi.ReservedTransferManager
@@ -35,8 +36,7 @@ class AdvancedTransferAdminController {
)
)
suspend fun getSwapHistory(
- @CurrentSecurityContext securityContext: SecurityContext,
- @RequestBody request: UserTransactionRequest
+ @RequestBody request: UserSwapTransactionRequest
): List? {
return with(request) {
diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/AdvancedTransferController.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/AdvancedTransferController.kt
index e3ef85e7b..0d9016edd 100644
--- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/AdvancedTransferController.kt
+++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/AdvancedTransferController.kt
@@ -4,6 +4,7 @@ import co.nilin.opex.common.OpexError
import co.nilin.opex.wallet.app.dto.ReservedTransferResponse
import co.nilin.opex.wallet.app.dto.TransferPreEvaluateResponse
import co.nilin.opex.wallet.app.dto.TransferReserveRequest
+import co.nilin.opex.wallet.app.dto.UserSwapTransactionRequest
import co.nilin.opex.wallet.app.dto.UserTransactionRequest
import co.nilin.opex.wallet.app.service.TransferService
import co.nilin.opex.wallet.core.inout.SwapResponse
@@ -140,7 +141,7 @@ class AdvancedTransferController {
)
suspend fun getSwapHistory(
@CurrentSecurityContext securityContext: SecurityContext,
- @RequestBody request: UserTransactionRequest
+ @RequestBody request: UserSwapTransactionRequest
): List? {
return with(request) {
@@ -171,7 +172,7 @@ class AdvancedTransferController {
@PostMapping("/v1/swap/history/count")
suspend fun getSwapHistoryCount(
@CurrentSecurityContext securityContext: SecurityContext,
- @RequestBody request: UserTransactionRequest
+ @RequestBody request: UserSwapTransactionRequest
): Long {
return with(request) {
diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/TerminalAdminController.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/TerminalAdminController.kt
new file mode 100644
index 000000000..9d8e9c06f
--- /dev/null
+++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/TerminalAdminController.kt
@@ -0,0 +1,98 @@
+package co.nilin.opex.wallet.app.controller
+
+import co.nilin.opex.wallet.app.dto.AdminSearchDepositRequest
+import co.nilin.opex.wallet.app.dto.ManualTransferRequest
+import co.nilin.opex.wallet.app.dto.TerminalLocalizationResponse
+import co.nilin.opex.wallet.app.service.DepositService
+import co.nilin.opex.wallet.core.inout.*
+import co.nilin.opex.wallet.core.spi.GatewayTerminalManager
+import co.nilin.opex.wallet.core.spi.TerminalManager
+import io.swagger.annotations.ApiResponse
+import io.swagger.annotations.Example
+import io.swagger.annotations.ExampleProperty
+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.time.Instant
+import java.time.LocalDateTime
+import java.time.ZoneId
+import java.util.*
+
+@RestController
+@RequestMapping("/admin/terminal")
+
+class TerminalAdminController(
+ private val depositService: DepositService,
+ private val terminalManager: TerminalManager,
+ private val gatewayTerminalManager: GatewayTerminalManager
+) {
+
+ @PostMapping("")
+ suspend fun registerTerminal(
+ @RequestBody body: TerminalCommand
+ ): TerminalCommand? {
+ return terminalManager.save(body.apply { uuid = UUID.randomUUID().toString() })
+ }
+
+
+ @PutMapping("/{uuid}")
+ suspend fun updateTerminal(
+ @PathVariable("uuid") terminalUuid: String,
+ @RequestBody body: TerminalCommand
+ ): TerminalCommand? {
+ return terminalManager.update(body.apply { uuid = terminalUuid })
+ }
+
+
+ @DeleteMapping("/{uuid}")
+ suspend fun deleteTerminal(
+ @PathVariable("uuid") terminalUuid: String,
+ ) {
+ terminalManager.delete(terminalUuid)
+ }
+
+ @GetMapping("")
+ suspend fun getTerminal(
+ ): List? {
+ return terminalManager.fetchTerminal()
+ }
+
+ @GetMapping("/{uuid}")
+ suspend fun getTerminal(
+ @PathVariable("uuid") terminalUuid: String,
+ ): TerminalCommand? {
+ return terminalManager.fetchTerminal(terminalUuid)
+ }
+
+ @GetMapping("/{uuid}/gateway")
+ suspend fun getGatewayTerminal(
+ @PathVariable("uuid") terminalUuid: String,
+ ): List? {
+ return gatewayTerminalManager.getAssignedGatewayToTerminal(terminalUuid)
+ }
+
+ @PostMapping("/{uuid}/localization")
+ suspend fun saveTerminalLocalization(
+ @PathVariable("uuid") terminalUuid: String,
+ @RequestBody terminalLocalizations: List
+ ): TerminalLocalizationResponse {
+ val terminalLocalizations = terminalManager.saveTerminalLocalizations(terminalUuid, terminalLocalizations)
+ return TerminalLocalizationResponse(terminalUuid, terminalLocalizations)
+ }
+
+ @GetMapping("/{uuid}/localization")
+ suspend fun getTerminalLocalization(
+ @PathVariable("uuid") terminalUuid: String
+ ): TerminalLocalizationResponse {
+ val terminalLocalizations = terminalManager.fetchTerminalLocalizations(terminalUuid)
+ return TerminalLocalizationResponse(terminalUuid, terminalLocalizations)
+ }
+
+ @DeleteMapping("/terminal/localization/{id}")
+ suspend fun deleteTerminalLocalization(
+ @PathVariable("id") id: Long,
+ ) {
+ terminalManager.deleteTerminalLocalizations(id)
+ }
+}
\ No newline at end of file
diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/UserSwapTransactionRequest.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/UserSwapTransactionRequest.kt
new file mode 100644
index 000000000..20a0a2335
--- /dev/null
+++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/UserSwapTransactionRequest.kt
@@ -0,0 +1,18 @@
+package co.nilin.opex.wallet.app.dto
+
+import co.nilin.opex.wallet.core.model.UserTransactionCategory
+import co.nilin.opex.wallet.core.model.otc.ReservedStatus
+
+data class UserSwapTransactionRequest(
+ val userId: String? = null,
+ val currency: String?,
+ val sourceSymbol: String?,
+ val destSymbol: String?,
+ 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
+)
+
diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/UserTransactionRequest.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/UserTransactionRequest.kt
index df18304bf..d491efc0c 100644
--- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/UserTransactionRequest.kt
+++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/UserTransactionRequest.kt
@@ -14,5 +14,5 @@ data class UserTransactionRequest(
val limit: Int? = 10,
val offset: Int? = 0,
val ascendingByTime: Boolean = false,
- val status: ReservedStatus? = ReservedStatus.Committed
-)
\ No newline at end of file
+)
+
diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/DepositService.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/DepositService.kt
index 7d758e1f5..86c96528d 100644
--- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/DepositService.kt
+++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/DepositService.kt
@@ -208,15 +208,15 @@ class DepositService(
// -------------------------------------------------------------------------
fun isValidDeposit(deposit: Deposit, gatewayData: GatewayData): Boolean {
- return gatewayData.isEnabled &&
+ return deposit.transferMethod == TransferMethod.MANUALLY || (gatewayData.isEnabled &&
deposit.amount >= gatewayData.minimum &&
- deposit.amount <= gatewayData.maximum
+ deposit.amount <= gatewayData.maximum)
}
suspend fun fetchDepositData(
gatewayUuid: String?,
symbol: String,
- depositType: co.nilin.opex.wallet.core.model.DepositType,
+ depositType: DepositType,
depositCommand: Deposit,
): GatewayData {
diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/WithdrawType.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/WithdrawType.kt
index 98ae7851c..2f93455f3 100644
--- a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/WithdrawType.kt
+++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/WithdrawType.kt
@@ -1,5 +1,5 @@
package co.nilin.opex.wallet.core.inout
enum class WithdrawType {
- CARD_TO_CARD, SHEBA, ON_CHAIN
+ OFF_CHAIN, ON_CHAIN
}
\ No newline at end of file
diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/TerminalManagerImpl.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/TerminalManagerImpl.kt
index bc41de604..6da9be671 100644
--- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/TerminalManagerImpl.kt
+++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/TerminalManagerImpl.kt
@@ -68,9 +68,11 @@ class TerminalManagerImpl(
}
override suspend fun delete(uuid: String) {
- loadTerminal(uuid)?.let {
- terminalRepository.deleteById(it.id!!).awaitSingleOrNull()
- } ?: throw OpexError.TerminalNotFound.exception()
+ val terminal = loadTerminal(uuid)
+ if (terminal != null)
+ terminalRepository.deleteById(terminal.id!!).awaitSingleOrNull()
+ else
+ throw OpexError.TerminalNotFound.exception()
}
override suspend fun fetchTerminal(): List? {