diff --git a/.gitignore b/.gitignore index d990d9c465..8b845ec5be 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ *.code-workspace .zed .cursor +.trae .classpath .project .cache diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index f9416680e8..b097bc8f99 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -491,6 +491,9 @@ webui_top_text= #webui_footer2_middle_text= +# External Portal URL, change to your instance +webui_portal_external_url = http://localhost:5174 + # API Explorer URL, change to your instance webui_api_explorer_url = https://apiexplorer.openbankproject.com diff --git a/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala b/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala index 8a8b3366ff..8207e72686 100644 --- a/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala +++ b/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala @@ -11,17 +11,16 @@ import org.http4s.implicits._ import scala.language.higherKinds object Http4sServer extends IOApp { - val services: Kleisli[({type λ[β$0$] = OptionT[IO, β$0$]})#λ, Request[IO], Response[IO]] = - code.api.v7_0_0.Http4s700.wrappedRoutesV700Services - - val httpApp: Kleisli[IO, Request[IO], Response[IO]] = (services).orNotFound - - //Start OBP relevant objects, and settings + //Start OBP relevant objects and settings; this step MUST be executed first new bootstrap.http4s.Http4sBoot().boot - val port = APIUtil.getPropsAsIntValue("http4s.port",8181) + val port = APIUtil.getPropsAsIntValue("http4s.port",8086) val host = APIUtil.getPropsValue("http4s.host","127.0.0.1") + val services: Kleisli[({type λ[β$0$] = OptionT[IO, β$0$]})#λ, Request[IO], Response[IO]] = + code.api.v7_0_0.Http4s700.wrappedRoutesV700Services + + val httpApp: Kleisli[IO, Request[IO], Response[IO]] = (services).orNotFound override def run(args: List[String]): IO[ExitCode] = EmberServerBuilder .default[IO] diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala index 9d5c894b31..b0b3c31b1d 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala @@ -1,8 +1,10 @@ package code.api.ResourceDocs1_4_0 -import code.api.Constant.{GET_DYNAMIC_RESOURCE_DOCS_TTL, GET_STATIC_RESOURCE_DOCS_TTL, PARAM_LOCALE, HostName} +import code.api.Constant.{GET_DYNAMIC_RESOURCE_DOCS_TTL, GET_STATIC_RESOURCE_DOCS_TTL, HostName, PARAM_LOCALE} import code.api.OBPRestHelper import code.api.cache.Caching +import code.api.dynamic.endpoint.OBPAPIDynamicEndpoint +import code.api.dynamic.entity.OBPAPIDynamicEntity import code.api.util.APIUtil._ import code.api.util.ApiRole.{canReadDynamicResourceDocsAtOneBank, canReadResourceDoc} import code.api.util.ApiTag._ @@ -20,12 +22,9 @@ import code.api.v4_0_0.{APIMethods400, OBPAPI4_0_0} import code.api.v5_0_0.OBPAPI5_0_0 import code.api.v5_1_0.OBPAPI5_1_0 import code.api.v6_0_0.OBPAPI6_0_0 -import code.api.dynamic.endpoint.OBPAPIDynamicEndpoint -import code.api.dynamic.entity.OBPAPIDynamicEntity import code.apicollectionendpoint.MappedApiCollectionEndpointsProvider import code.util.Helper import code.util.Helper.{MdcLoggable, ObpS, SILENCE_IS_GOLDEN} -import net.liftweb.http.S import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.model.enums.ContentParam import com.openbankproject.commons.model.enums.ContentParam.{ALL, DYNAMIC, STATIC} @@ -33,6 +32,7 @@ import com.openbankproject.commons.model.{BankId, ListResult, User} import com.openbankproject.commons.util.ApiStandards._ import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion} import net.liftweb.common.{Box, Empty, Full} +import net.liftweb.http.{LiftRules, S} import net.liftweb.http.{InMemoryResponse, LiftRules, PlainTextResponse} import net.liftweb.json import net.liftweb.json.JsonAST.{JField, JString, JValue} @@ -118,6 +118,7 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth logger.debug(s"getResourceDocsList says requestedApiVersion is $requestedApiVersion") val resourceDocs = requestedApiVersion match { + case ApiVersion.v7_0_0 => code.api.v7_0_0.Http4s700.resourceDocs case ApiVersion.v6_0_0 => OBPAPI6_0_0.allResourceDocs case ApiVersion.v5_1_0 => OBPAPI5_1_0.allResourceDocs case ApiVersion.v5_0_0 => OBPAPI5_0_0.allResourceDocs @@ -139,6 +140,7 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth logger.debug(s"There are ${resourceDocs.length} resource docs available to $requestedApiVersion") val versionRoutes = requestedApiVersion match { + case ApiVersion.v7_0_0 => Nil case ApiVersion.v6_0_0 => OBPAPI6_0_0.routes case ApiVersion.v5_1_0 => OBPAPI5_1_0.routes case ApiVersion.v5_0_0 => OBPAPI5_0_0.routes @@ -165,7 +167,10 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth val versionRoutesClasses = versionRoutes.map { vr => vr.getClass } // Only return the resource docs that have available routes - val activeResourceDocs = resourceDocs.filter(rd => versionRoutesClasses.contains(rd.partialFunction.getClass)) + val activeResourceDocs = requestedApiVersion match { + case ApiVersion.v7_0_0 => resourceDocs + case _ => resourceDocs.filter(rd => versionRoutesClasses.contains(rd.partialFunction.getClass)) + } logger.debug(s"There are ${activeResourceDocs.length} resource docs available to $requestedApiVersion") @@ -1250,4 +1255,3 @@ so the caller must specify any required filtering by catalog explicitly. } - diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index d6fb5dbb4f..381b0c2839 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -27,6 +27,7 @@ TESOBE (http://www.tesobe.com/) package code.api.util import bootstrap.liftweb.CustomDBVendor +import cats.effect.IO import code.accountholders.AccountHolders import code.api.Constant._ import code.api.OAuthHandshake._ @@ -96,6 +97,7 @@ import net.liftweb.util.Helpers._ import net.liftweb.util._ import org.apache.commons.io.IOUtils import org.apache.commons.lang3.StringUtils +import org.http4s.HttpRoutes import java.io.InputStream import java.net.URLDecoder @@ -1636,7 +1638,8 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ isFeatured: Boolean = false, specialInstructions: Option[String] = None, var specifiedUrl: Option[String] = None, // A derived value: Contains the called version (added at run time). See the resource doc for resource doc! - createdByBankId: Option[String] = None //we need to filter the resource Doc by BankId + createdByBankId: Option[String] = None, //we need to filter the resource Doc by BankId + http4sPartialFunction: Http4sEndpoint = None // http4s endpoint handler ) { // this code block will be merged to constructor. { @@ -2789,6 +2792,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ type OBPEndpoint = PartialFunction[Req, CallContext => Box[JsonResponse]] type OBPReturnType[T] = Future[(T, Option[CallContext])] + type Http4sEndpoint = Option[HttpRoutes[IO]] def getAllowedEndpoints (endpoints : Iterable[OBPEndpoint], resourceDocs: ArrayBuffer[ResourceDoc]) : List[OBPEndpoint] = { diff --git a/obp-api/src/main/scala/code/api/util/ApiVersionUtils.scala b/obp-api/src/main/scala/code/api/util/ApiVersionUtils.scala index 5e93b6f7bd..f7285febb7 100644 --- a/obp-api/src/main/scala/code/api/util/ApiVersionUtils.scala +++ b/obp-api/src/main/scala/code/api/util/ApiVersionUtils.scala @@ -19,6 +19,7 @@ object ApiVersionUtils { v5_0_0 :: v5_1_0 :: v6_0_0 :: + v7_0_0 :: `dynamic-endpoint` :: `dynamic-entity` :: scannedApis @@ -41,6 +42,7 @@ object ApiVersionUtils { case v5_0_0.fullyQualifiedVersion | v5_0_0.apiShortVersion => v5_0_0 case v5_1_0.fullyQualifiedVersion | v5_1_0.apiShortVersion => v5_1_0 case v6_0_0.fullyQualifiedVersion | v6_0_0.apiShortVersion => v6_0_0 + case v7_0_0.fullyQualifiedVersion | v7_0_0.apiShortVersion => v7_0_0 case `dynamic-endpoint`.fullyQualifiedVersion | `dynamic-endpoint`.apiShortVersion => `dynamic-endpoint` case `dynamic-entity`.fullyQualifiedVersion | `dynamic-entity`.apiShortVersion => `dynamic-entity` case version if(scannedApis.map(_.fullyQualifiedVersion).contains(version)) diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala index 877b91b72d..1f8388ebdf 100644 --- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala @@ -2,18 +2,25 @@ package code.api.v7_0_0 import cats.data.{Kleisli, OptionT} import cats.effect._ -import cats.implicits._ -import code.api.util.{APIUtil, CustomJsonFormats} +import code.api.Constant._ +import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ +import code.api.ResourceDocs1_4_0.{ResourceDocs140, ResourceDocsAPIMethodsUtil} +import code.api.util.APIUtil.{EmptyBody, _} +import code.api.util.ApiTag._ +import code.api.util.ErrorMessages._ +import code.api.util.{ApiVersionUtils, CustomJsonFormats, NewStyle} +import code.api.v1_4_0.JSONFactory1_4_0 import code.api.v4_0_0.JSONFactory400 -import code.bankconnectors.Connector -import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion} -import net.liftweb.json.Formats +import com.github.dwickern.macros.NameOf.nameOf +import com.openbankproject.commons.ExecutionContext.Implicits.global +import com.openbankproject.commons.util.{ApiVersion, ApiVersionStatus, ScannedApiVersion} import net.liftweb.json.JsonAST.prettyRender -import net.liftweb.json.Extraction +import net.liftweb.json.{Extraction, Formats} import org.http4s._ import org.http4s.dsl.io._ import org.typelevel.vault.Key +import scala.collection.mutable.ArrayBuffer import scala.concurrent.Future import scala.language.{higherKinds, implicitConversions} @@ -24,12 +31,13 @@ object Http4s700 { implicit val formats: Formats = CustomJsonFormats.formats implicit def convertAnyToJsonString(any: Any): String = prettyRender(Extraction.decompose(any)) - val apiVersion: ScannedApiVersion = ApiVersion.v7_0_0 - val apiVersionString: String = apiVersion.toString + val implementedInApiVersion: ScannedApiVersion = ApiVersion.v7_0_0 + val versionStatus = ApiVersionStatus.STABLE.toString + val resourceDocs = ArrayBuffer[ResourceDoc]() case class CallContext(userId: String, requestId: String) - import cats.effect.unsafe.implicits.global - val callContextKey: Key[CallContext] = Key.newKey[IO, CallContext].unsafeRunSync() + val callContextKey: Key[CallContext] = + Key.newKey[IO, CallContext].unsafeRunSync()(cats.effect.unsafe.IORuntime.global) object CallContextMiddleware { @@ -42,31 +50,108 @@ object Http4s700 { } } - val v700Services: HttpRoutes[IO] = HttpRoutes.of[IO] { - case req @ GET -> Root / "obp" / `apiVersionString` / "root" => - import com.openbankproject.commons.ExecutionContext.Implicits.global - val callContext = req.attributes.lookup(callContextKey).get.asInstanceOf[CallContext] - Ok(IO.fromFuture(IO( - for { - _ <- Future() // Just start async call - } yield { - convertAnyToJsonString( - JSONFactory700.getApiInfoJSON(apiVersion, s"Hello, ${callContext.userId}! Your request ID is ${callContext.requestId}.") - ) - } - ))) - - case req @ GET -> Root / "obp" / `apiVersionString` / "banks" => - import com.openbankproject.commons.ExecutionContext.Implicits.global - Ok(IO.fromFuture(IO( - for { - (banks, callContext) <- code.api.util.NewStyle.function.getBanks(None) - } yield { - convertAnyToJsonString(JSONFactory400.createBanksJson(banks)) - } - ))) + object Implementations7_0_0 { + + // Common prefix: /obp/v7.0.0 + val prefixPath = Root / ApiPathZero.toString / implementedInApiVersion.toString + + + resourceDocs += ResourceDoc( + null, + implementedInApiVersion, + nameOf(root), + "GET", + "/root", + "Get API Info (root)", + s"""Returns information about: + | + |* API version + |* Hosted by information + |* Git Commit + |${userAuthenticationMessage(false)}""", + EmptyBody, + apiInfoJSON, + List(UnknownError, "no connector set"), + apiTagApi :: Nil, + http4sPartialFunction = Some(root) + ) + + // Route: GET /obp/v7.0.0/root + val root: HttpRoutes[IO] = HttpRoutes.of[IO] { + case req @ GET -> `prefixPath` / "root" => + val callContext = req.attributes.lookup(callContextKey).get.asInstanceOf[CallContext] + Ok(IO.fromFuture(IO( + for { + _ <- Future() // Just start async call + } yield { + convertAnyToJsonString( + JSONFactory700.getApiInfoJSON(implementedInApiVersion, s"Hello, ${callContext.userId}! Your request ID is ${callContext.requestId}.") + ) + } + ))) + } + + resourceDocs += ResourceDoc( + null, + implementedInApiVersion, + nameOf(getBanks), + "GET", + "/banks", + "Get Banks", + s"""Get banks on this API instance + |Returns a list of banks supported on this server: + | + |* ID used as parameter in URLs + |* Short and full name of bank + |* Logo URL + |* Website + |${userAuthenticationMessage(false)}""", + EmptyBody, + banksJSON, + List(UnknownError), + apiTagBank :: Nil, + http4sPartialFunction = Some(getBanks) + ) + + // Route: GET /obp/v7.0.0/banks + val getBanks: HttpRoutes[IO] = HttpRoutes.of[IO] { + case req @ GET -> `prefixPath` / "banks" => + import com.openbankproject.commons.ExecutionContext.Implicits.global + Ok(IO.fromFuture(IO( + for { + (banks, callContext) <- NewStyle.function.getBanks(None) + } yield { + convertAnyToJsonString(JSONFactory400.createBanksJson(banks)) + } + ))) + } + + val getResourceDocsObpV700: HttpRoutes[IO] = HttpRoutes.of[IO] { + case req @ GET -> `prefixPath` / "resource-docs" / requestedApiVersionString / "obp" => + import com.openbankproject.commons.ExecutionContext.Implicits.global + val logic = for { + httpParams <- NewStyle.function.extractHttpParamsFromUrl(req.uri.renderString) + tagsParam = httpParams.filter(_.name == "tags").map(_.values).headOption + functionsParam = httpParams.filter(_.name == "functions").map(_.values).headOption + localeParam = httpParams.filter(param => param.name == "locale" || param.name == "language").map(_.values).flatten.headOption + contentParam = httpParams.filter(_.name == "content").map(_.values).flatten.flatMap(ResourceDocsAPIMethodsUtil.stringToContentParam).headOption + apiCollectionIdParam = httpParams.filter(_.name == "api-collection-id").map(_.values).flatten.headOption + tags = tagsParam.map(_.map(ResourceDocTag(_))) + functions = functionsParam.map(_.toList) + requestedApiVersion <- Future(ApiVersionUtils.valueOf(requestedApiVersionString)) + resourceDocs = ResourceDocs140.ImplementationsResourceDocs.getResourceDocsList(requestedApiVersion).getOrElse(Nil) + filteredDocs = ResourceDocsAPIMethodsUtil.filterResourceDocs(resourceDocs, tags, functions) + resourceDocsJson = JSONFactory1_4_0.createResourceDocsJson(filteredDocs, isVersion4OrHigher = true, localeParam) + } yield convertAnyToJsonString(resourceDocsJson) + Ok(IO.fromFuture(IO(logic))) + } + + // All routes combined + val allRoutes: HttpRoutes[IO] = + Kleisli[HttpF, Request[IO], Response[IO]] { req: Request[IO] => + root(req).orElse(getBanks(req)).orElse(getResourceDocsObpV700(req)) + } } - val wrappedRoutesV700Services: HttpRoutes[IO] = CallContextMiddleware.withCallContext(v700Services) + val wrappedRoutesV700Services: HttpRoutes[IO] = CallContextMiddleware.withCallContext(Implementations7_0_0.allRoutes) } - diff --git a/obp-api/src/main/scala/code/snippet/WebUI.scala b/obp-api/src/main/scala/code/snippet/WebUI.scala index 63214fa925..e2f4723180 100644 --- a/obp-api/src/main/scala/code/snippet/WebUI.scala +++ b/obp-api/src/main/scala/code/snippet/WebUI.scala @@ -315,8 +315,8 @@ class WebUI extends MdcLoggable{ // Uses webui_api_explorer_url + /consumers/register as default. // Configure webui_external_consumer_registration_url to override with a custom URL. def externalConsumerRegistrationLink: CssSel = { - val apiExplorerUrl = getWebUiPropsValue("webui_api_explorer_url", "http://localhost:5174") - val defaultConsumerRegisterUrl = s"$apiExplorerUrl/consumers/register" + val portalExternalUrl = getWebUiPropsValue("webui_portal_external_url", "http://localhost:5174") + val defaultConsumerRegisterUrl = s"$portalExternalUrl/consumers/register" val externalUrl = getWebUiPropsValue("webui_external_consumer_registration_url", defaultConsumerRegisterUrl) ".get-api-key-link a [href]" #> scala.xml.Unparsed(externalUrl) & ".get-api-key-link a [target]" #> "_blank" & diff --git a/obp-api/src/test/scala/code/util/ApiVersionUtilsTest.scala b/obp-api/src/test/scala/code/util/ApiVersionUtilsTest.scala index 1a14ed8965..05d1bd5104 100644 --- a/obp-api/src/test/scala/code/util/ApiVersionUtilsTest.scala +++ b/obp-api/src/test/scala/code/util/ApiVersionUtilsTest.scala @@ -20,6 +20,6 @@ class ApiVersionUtilsTest extends V400ServerSetup { versions.map(version => ApiVersionUtils.valueOf(version.fullyQualifiedVersion)) //NOTE, when we added the new version, better fix this number manually. and also check the versions - versions.length shouldBe(24) + versions.length shouldBe(25) }} } \ No newline at end of file