From 7a27b188de28a652f4b9e576a7ad01d6ae9a0038 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Wed, 7 Jan 2026 18:01:41 +0330 Subject: [PATCH] add admin services --- .../src/main/resources/application.yml | 2 + .../nilin/opex/api/core/spi/StorageProxy.kt | 10 ++ .../opex/controller/StorageAdminController.kt | 45 +++++++++ .../api/ports/proxy/impl/StorageProxyImpl.kt | 97 +++++++++++++++++++ 4 files changed, 154 insertions(+) create mode 100644 api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/StorageProxy.kt create mode 100644 api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/StorageAdminController.kt create mode 100644 api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/StorageProxyImpl.kt diff --git a/api/api-app/src/main/resources/application.yml b/api/api-app/src/main/resources/application.yml index 63ec4437e..f45091932 100644 --- a/api/api-app/src/main/resources/application.yml +++ b/api/api-app/src/main/resources/application.yml @@ -125,6 +125,8 @@ app: url: http://opex-market opex-bc-gateway: url: http://opex-bc-gateway + storage: + url: http://storage auth: cert-url: http://keycloak:8080/realms/opex/protocol/openid-connect/certs iss-url: ${TOKEN_ISSUER_URL:http://keycloak:8080/realms/opex} diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/StorageProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/StorageProxy.kt new file mode 100644 index 000000000..01b52835c --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/StorageProxy.kt @@ -0,0 +1,10 @@ +package co.nilin.opex.api.core.spi + +import org.springframework.http.ResponseEntity +import org.springframework.http.codec.multipart.FilePart + +interface StorageProxy { + suspend fun adminDownload(token: String, bucket: String, key: String): ResponseEntity + suspend fun adminUpload(token: String, bucket: String, key: String, file: FilePart,isPublic : Boolean? = false) + suspend fun adminDelete(token: String, bucket: String, key: String) +} 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 new file mode 100644 index 000000000..7c8851018 --- /dev/null +++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/StorageAdminController.kt @@ -0,0 +1,45 @@ +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 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.* + +@RestController +@RequestMapping("/opex/v1/admin/storage") +class StorageAdminController( + private val storageProxy: StorageProxy, +) { + @GetMapping + suspend fun download( + @CurrentSecurityContext securityContext: SecurityContext, + @RequestParam("bucket") bucket: String, + @RequestParam("key") key: String, + ): ResponseEntity { + return storageProxy.adminDownload(securityContext.jwtAuthentication().tokenValue(), bucket, key) + } + + @PostMapping + suspend fun upload( + @CurrentSecurityContext securityContext: SecurityContext, + @RequestParam("bucket") bucket: String, + @RequestParam("key") key: String, + @RequestPart("file") file: FilePart, + @RequestParam("isPublic") isPublic: Boolean? = false, + ) { + storageProxy.adminUpload(securityContext.jwtAuthentication().tokenValue(), bucket, key, file, isPublic) + } + + @DeleteMapping + suspend fun delete( + @CurrentSecurityContext securityContext: SecurityContext, + @RequestParam("bucket") bucket: String, + @RequestParam("key") key: String, + ) { + storageProxy.adminDelete(securityContext.jwtAuthentication().tokenValue(), bucket, key) + } +} \ 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/StorageProxyImpl.kt b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/StorageProxyImpl.kt new file mode 100644 index 000000000..e6bac8b9f --- /dev/null +++ b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/StorageProxyImpl.kt @@ -0,0 +1,97 @@ +package co.nilin.opex.api.ports.proxy.impl + +import co.nilin.opex.api.core.spi.StorageProxy +import co.nilin.opex.common.utils.LoggerDelegate +import kotlinx.coroutines.reactive.awaitSingle +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.http.codec.multipart.FilePart +import org.springframework.stereotype.Component +import org.springframework.util.LinkedMultiValueMap +import org.springframework.web.reactive.function.BodyInserters +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.awaitBodilessEntity +import reactor.core.publisher.Mono + +@Component +class StorageProxyImpl(@Qualifier("generalWebClient") private val webClient: WebClient) : StorageProxy { + + private val logger by LoggerDelegate() + + @Value("\${app.storage.url}") + private lateinit var baseUrl: String + + override suspend fun adminDownload( + token: String, + bucket: String, + key: String + ): ResponseEntity { + return webClient.get() + .uri("$baseUrl/v2/admin") { + it.queryParam("bucket", bucket) + it.queryParam("key", key) + it.build() + } + .header(HttpHeaders.AUTHORIZATION, "Bearer $token") + .accept( + MediaType.APPLICATION_OCTET_STREAM, + MediaType.APPLICATION_JSON + ) + .exchangeToMono { response -> + if (response.statusCode().isError) { + response.createException().flatMap { Mono.error(it) } + } else { + response.toEntity(ByteArray::class.java) + } + } + .awaitSingle() + } + + override suspend fun adminUpload( + token: String, + bucket: String, + key: String, + file: FilePart, + isPublic : Boolean? + ) { + webClient.post() + .uri("$baseUrl/v2/admin"){ + it.queryParam("isPublic", isPublic) + it.queryParam("bucket", bucket) + it.queryParam("key", key) + it.build() + } + .header(HttpHeaders.AUTHORIZATION, "Bearer $token") + .contentType(MediaType.MULTIPART_FORM_DATA) + .body( + BodyInserters.fromMultipartData( + LinkedMultiValueMap().apply { + add("file", file) + } + )) + .retrieve() + .onStatus({ it.isError }) { it.createException() } + .awaitBodilessEntity() + } + + override suspend fun adminDelete( + token: String, + bucket: String, + key: String + ) { + webClient.delete() + .uri("$baseUrl/v2/admin") { + it.queryParam("bucket", bucket) + it.queryParam("key", key) + it.build() + } + .accept(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer $token") + .retrieve() + .onStatus({ it.isError }) { it.createException() } + .awaitBodilessEntity() + } +}