Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*.code-workspace
.zed
.cursor
.trae
.classpath
.project
.cache
Expand Down
3 changes: 3 additions & 0 deletions obp-api/src/main/resources/props/sample.props.template
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 6 additions & 7 deletions obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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._
Expand All @@ -20,19 +22,17 @@ 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}
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}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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")

Expand Down Expand Up @@ -1250,4 +1255,3 @@ so the caller must specify any required filtering by catalog explicitly.


}

6 changes: 5 additions & 1 deletion obp-api/src/main/scala/code/api/util/APIUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
{
Expand Down Expand Up @@ -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] = {
Expand Down
2 changes: 2 additions & 0 deletions obp-api/src/main/scala/code/api/util/ApiVersionUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ object ApiVersionUtils {
v5_0_0 ::
v5_1_0 ::
v6_0_0 ::
v7_0_0 ::
`dynamic-endpoint` ::
`dynamic-entity` ::
scannedApis
Expand All @@ -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))
Expand Down
155 changes: 120 additions & 35 deletions obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand All @@ -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 {

Expand All @@ -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)
}

4 changes: 2 additions & 2 deletions obp-api/src/main/scala/code/snippet/WebUI.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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" &
Expand Down
2 changes: 1 addition & 1 deletion obp-api/src/test/scala/code/util/ApiVersionUtilsTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}}
}