mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 11:06:49 +00:00
Merge remote-tracking branch 'upstream/develop' into develop
This commit is contained in:
commit
6ad3689fd3
3
.gitignore
vendored
3
.gitignore
vendored
@ -12,6 +12,7 @@
|
||||
.zed
|
||||
.cursor
|
||||
.trae
|
||||
.kiro
|
||||
.classpath
|
||||
.project
|
||||
.cache
|
||||
@ -44,4 +45,4 @@ coursier
|
||||
metals.sbt
|
||||
obp-http4s-runner/src/main/resources/git.properties
|
||||
test-results
|
||||
untracked_files/
|
||||
untracked_files/
|
||||
@ -416,6 +416,69 @@ object DirectLogin extends RestHelper with MdcLoggable {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Validator that uses pre-extracted parameters from CallContext (for http4s support)
|
||||
* This avoids dependency on S.request which is not available in http4s context
|
||||
*/
|
||||
def validatorFutureWithParams(requestType: String, httpMethod: String, parameters: Map[String, String]): Future[(Int, String, Map[String, String])] = {
|
||||
|
||||
def validAccessTokenFuture(tokenKey: String) = {
|
||||
Tokens.tokens.vend.getTokenByKeyAndTypeFuture(tokenKey, TokenType.Access) map {
|
||||
case Full(token) => token.isValid
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
var message = ""
|
||||
var httpCode: Int = 500
|
||||
|
||||
val missingParams = missingDirectLoginParameters(parameters, requestType)
|
||||
val validParams = validDirectLoginParameters(parameters)
|
||||
|
||||
val validF =
|
||||
if (requestType == "protectedResource") {
|
||||
validAccessTokenFuture(parameters.getOrElse("token", ""))
|
||||
} else if (requestType == "authorizationToken" &&
|
||||
APIUtil.getPropsAsBoolValue("direct_login_consumer_key_mandatory", true)) {
|
||||
APIUtil.registeredApplicationFuture(parameters.getOrElse("consumer_key", ""))
|
||||
} else {
|
||||
Future { true }
|
||||
}
|
||||
|
||||
for {
|
||||
valid <- validF
|
||||
} yield {
|
||||
if (parameters.get("error").isDefined) {
|
||||
message = parameters.get("error").getOrElse("")
|
||||
httpCode = 400
|
||||
}
|
||||
else if (missingParams.nonEmpty) {
|
||||
message = ErrorMessages.DirectLoginMissingParameters + missingParams.mkString(", ")
|
||||
httpCode = 400
|
||||
}
|
||||
else if (SILENCE_IS_GOLDEN != validParams.mkString("")) {
|
||||
message = validParams.mkString("")
|
||||
httpCode = 400
|
||||
}
|
||||
else if (requestType == "protectedResource" && !valid) {
|
||||
message = ErrorMessages.DirectLoginInvalidToken + parameters.getOrElse("token", "")
|
||||
httpCode = 401
|
||||
}
|
||||
else if (requestType == "authorizationToken" &&
|
||||
APIUtil.getPropsAsBoolValue("direct_login_consumer_key_mandatory", true) &&
|
||||
!valid) {
|
||||
logger.error("application: " + parameters.getOrElse("consumer_key", "") + " not found")
|
||||
message = ErrorMessages.InvalidConsumerKey
|
||||
httpCode = 401
|
||||
}
|
||||
else
|
||||
httpCode = 200
|
||||
if (message.nonEmpty)
|
||||
logger.error("error message : " + message)
|
||||
(httpCode, message, parameters)
|
||||
}
|
||||
}
|
||||
|
||||
private def generateTokenAndSecret(claims: JWTClaimsSet): (String, String) =
|
||||
{
|
||||
// generate random string
|
||||
@ -473,12 +536,20 @@ object DirectLogin extends RestHelper with MdcLoggable {
|
||||
}
|
||||
|
||||
def getUserFromDirectLoginHeaderFuture(sc: CallContext) : Future[(Box[User], Option[CallContext])] = {
|
||||
val httpMethod = S.request match {
|
||||
val httpMethod = if (sc.verb.nonEmpty) sc.verb else S.request match {
|
||||
case Full(r) => r.request.method
|
||||
case _ => "GET"
|
||||
}
|
||||
// Prefer directLoginParams from CallContext (http4s), fall back to S.request (Lift)
|
||||
val directLoginParamsFromCC = sc.directLoginParams
|
||||
for {
|
||||
(httpCode, message, directLoginParameters) <- validatorFuture("protectedResource", httpMethod)
|
||||
(httpCode, message, directLoginParameters) <- if (directLoginParamsFromCC.nonEmpty && directLoginParamsFromCC.contains("token")) {
|
||||
// Use params from CallContext (http4s path)
|
||||
validatorFutureWithParams("protectedResource", httpMethod, directLoginParamsFromCC)
|
||||
} else {
|
||||
// Fall back to S.request (Lift path), e.g. we still use Lift to generate the token and secret, so we need to maintain backward compatibility here.
|
||||
validatorFuture("protectedResource", httpMethod)
|
||||
}
|
||||
_ <- Future { if (httpCode == 400 || httpCode == 401) Empty else Full("ok") } map { x => fullBoxOrException(x ?~! message) }
|
||||
consumer <- OAuthHandshake.getConsumerFromTokenFuture(200, (if (directLoginParameters.isDefinedAt("token")) directLoginParameters.get("token") else Empty))
|
||||
user <- OAuthHandshake.getUserFromTokenFuture(200, (if (directLoginParameters.isDefinedAt("token")) directLoginParameters.get("token") else Empty))
|
||||
|
||||
@ -334,6 +334,17 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
commit
|
||||
}
|
||||
|
||||
// API info props helpers (keep values centralized)
|
||||
lazy val hostedByOrganisation: String = getPropsValue("hosted_by.organisation", "TESOBE")
|
||||
lazy val hostedByEmail: String = getPropsValue("hosted_by.email", "contact@tesobe.com")
|
||||
lazy val hostedByPhone: String = getPropsValue("hosted_by.phone", "+49 (0)30 8145 3994")
|
||||
lazy val organisationWebsite: String = getPropsValue("organisation_website", "https://www.tesobe.com")
|
||||
lazy val hostedAtOrganisation: String = getPropsValue("hosted_at.organisation", "")
|
||||
lazy val hostedAtOrganisationWebsite: String = getPropsValue("hosted_at.organisation_website", "")
|
||||
lazy val energySourceOrganisation: String = getPropsValue("energy_source.organisation", "")
|
||||
lazy val energySourceOrganisationWebsite: String = getPropsValue("energy_source.organisation_website", "")
|
||||
lazy val resourceDocsRequiresRole: Boolean = getPropsAsBoolValue("resource_docs_requires_role", false)
|
||||
|
||||
|
||||
/**
|
||||
* Caching of unchanged resources
|
||||
@ -3039,18 +3050,49 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
def getUserAndSessionContextFuture(cc: CallContext): OBPReturnType[Box[User]] = {
|
||||
val s = S
|
||||
val spelling = getSpellingParam()
|
||||
val body: Box[String] = getRequestBody(S.request)
|
||||
val implementedInVersion = S.request.openOrThrowException(attemptedToOpenAnEmptyBox).view
|
||||
val verb = S.request.openOrThrowException(attemptedToOpenAnEmptyBox).requestType.method
|
||||
val url = URLDecoder.decode(ObpS.uriAndQueryString.getOrElse(""),"UTF-8")
|
||||
val correlationId = getCorrelationId()
|
||||
val reqHeaders = S.request.openOrThrowException(attemptedToOpenAnEmptyBox).request.headers
|
||||
|
||||
// NEW: Prefer CallContext fields, fall back to S.request for Lift compatibility
|
||||
// This allows http4s to use the same auth chain by populating CallContext fields
|
||||
val body: Box[String] = cc.httpBody match {
|
||||
case Some(b) => Full(b)
|
||||
case None => getRequestBody(S.request)
|
||||
}
|
||||
|
||||
val implementedInVersion = if (cc.implementedInVersion.nonEmpty)
|
||||
cc.implementedInVersion
|
||||
else
|
||||
S.request.openOrThrowException(attemptedToOpenAnEmptyBox).view
|
||||
|
||||
val verb = if (cc.verb.nonEmpty)
|
||||
cc.verb
|
||||
else
|
||||
S.request.openOrThrowException(attemptedToOpenAnEmptyBox).requestType.method
|
||||
|
||||
val url = if (cc.url.nonEmpty)
|
||||
cc.url
|
||||
else
|
||||
URLDecoder.decode(ObpS.uriAndQueryString.getOrElse(""),"UTF-8")
|
||||
|
||||
val correlationId = if (cc.correlationId.nonEmpty)
|
||||
cc.correlationId
|
||||
else
|
||||
getCorrelationId()
|
||||
|
||||
val reqHeaders = if (cc.requestHeaders.nonEmpty)
|
||||
cc.requestHeaders
|
||||
else
|
||||
S.request.openOrThrowException(attemptedToOpenAnEmptyBox).request.headers
|
||||
|
||||
val remoteIpAddress = if (cc.ipAddress.nonEmpty)
|
||||
cc.ipAddress
|
||||
else
|
||||
getRemoteIpAddress()
|
||||
|
||||
val xRequestId: Option[String] =
|
||||
reqHeaders.find(_.name.toLowerCase() == RequestHeader.`X-Request-ID`.toLowerCase())
|
||||
.map(_.values.mkString(","))
|
||||
logger.debug(s"Request Headers for verb: $verb, URL: $url")
|
||||
logger.debug(reqHeaders.map(h => h.name + ": " + h.values.mkString(",")).mkString)
|
||||
val remoteIpAddress = getRemoteIpAddress()
|
||||
|
||||
val authHeaders = AuthorisationUtil.getAuthorisationHeaders(reqHeaders)
|
||||
val authHeadersWithEmptyValues = RequestHeadersUtil.checkEmptyRequestHeaderValues(reqHeaders)
|
||||
|
||||
@ -55,7 +55,12 @@ case class CallContext(
|
||||
xRateLimitRemaining : Long = -1,
|
||||
xRateLimitReset : Long = -1,
|
||||
paginationOffset : Option[String] = None,
|
||||
paginationLimit : Option[String] = None
|
||||
paginationLimit : Option[String] = None,
|
||||
// Validated entities from ResourceDoc middleware (http4s)
|
||||
bank: Option[Bank] = None,
|
||||
bankAccount: Option[BankAccount] = None,
|
||||
view: Option[View] = None,
|
||||
counterparty: Option[CounterpartyTrait] = None
|
||||
) extends MdcLoggable {
|
||||
override def toString: String = SecureLogging.maskSensitive(
|
||||
s"${this.getClass.getSimpleName}(${this.productIterator.mkString(", ")})"
|
||||
|
||||
@ -0,0 +1,116 @@
|
||||
package code.api.util.http4s
|
||||
|
||||
import cats.effect._
|
||||
import code.api.APIFailureNewStyle
|
||||
import code.api.util.ErrorMessages._
|
||||
import code.api.util.CallContext
|
||||
import net.liftweb.common.{Failure => LiftFailure}
|
||||
import net.liftweb.json.compactRender
|
||||
import net.liftweb.json.JsonDSL._
|
||||
import org.http4s._
|
||||
import org.http4s.headers.`Content-Type`
|
||||
import org.typelevel.ci.CIString
|
||||
|
||||
/**
|
||||
* Converts OBP errors to http4s Response[IO].
|
||||
*
|
||||
* Handles:
|
||||
* - APIFailureNewStyle (structured errors with code and message)
|
||||
* - Box Failure (Lift framework errors)
|
||||
* - Unknown exceptions
|
||||
*
|
||||
* All responses include:
|
||||
* - JSON body with code and message
|
||||
* - Correlation-Id header for request tracing
|
||||
* - Appropriate HTTP status code
|
||||
*/
|
||||
object ErrorResponseConverter {
|
||||
import net.liftweb.json.Formats
|
||||
import code.api.util.CustomJsonFormats
|
||||
|
||||
implicit val formats: Formats = CustomJsonFormats.formats
|
||||
private val jsonContentType: `Content-Type` = `Content-Type`(MediaType.application.json)
|
||||
|
||||
/**
|
||||
* OBP standard error response format.
|
||||
*/
|
||||
case class OBPErrorResponse(
|
||||
code: Int,
|
||||
message: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Convert error response to JSON string using Lift JSON.
|
||||
*/
|
||||
private def toJsonString(error: OBPErrorResponse): String = {
|
||||
val json = ("code" -> error.code) ~ ("message" -> error.message)
|
||||
compactRender(json)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert any error to http4s Response[IO].
|
||||
*/
|
||||
def toHttp4sResponse(error: Throwable, callContext: CallContext): IO[Response[IO]] = {
|
||||
error match {
|
||||
case e: APIFailureNewStyle => apiFailureToResponse(e, callContext)
|
||||
case _ => unknownErrorToResponse(error, callContext)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert APIFailureNewStyle to http4s Response.
|
||||
* Uses failCode as HTTP status and failMsg as error message.
|
||||
*/
|
||||
def apiFailureToResponse(failure: APIFailureNewStyle, callContext: CallContext): IO[Response[IO]] = {
|
||||
val errorJson = OBPErrorResponse(failure.failCode, failure.failMsg)
|
||||
val status = org.http4s.Status.fromInt(failure.failCode).getOrElse(org.http4s.Status.BadRequest)
|
||||
IO.pure(
|
||||
Response[IO](status)
|
||||
.withEntity(toJsonString(errorJson))
|
||||
.withContentType(jsonContentType)
|
||||
.putHeaders(org.http4s.Header.Raw(CIString("Correlation-Id"), callContext.correlationId))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Lift Box Failure to http4s Response.
|
||||
* Returns 400 Bad Request with failure message.
|
||||
*/
|
||||
def boxFailureToResponse(failure: LiftFailure, callContext: CallContext): IO[Response[IO]] = {
|
||||
val errorJson = OBPErrorResponse(400, failure.msg)
|
||||
IO.pure(
|
||||
Response[IO](org.http4s.Status.BadRequest)
|
||||
.withEntity(toJsonString(errorJson))
|
||||
.withContentType(jsonContentType)
|
||||
.putHeaders(org.http4s.Header.Raw(CIString("Correlation-Id"), callContext.correlationId))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert unknown error to http4s Response.
|
||||
* Returns 500 Internal Server Error.
|
||||
*/
|
||||
def unknownErrorToResponse(e: Throwable, callContext: CallContext): IO[Response[IO]] = {
|
||||
val errorJson = OBPErrorResponse(500, s"$UnknownError: ${e.getMessage}")
|
||||
IO.pure(
|
||||
Response[IO](org.http4s.Status.InternalServerError)
|
||||
.withEntity(toJsonString(errorJson))
|
||||
.withContentType(jsonContentType)
|
||||
.putHeaders(org.http4s.Header.Raw(CIString("Correlation-Id"), callContext.correlationId))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create error response with specific status code and message.
|
||||
*/
|
||||
def createErrorResponse(statusCode: Int, message: String, callContext: CallContext): IO[Response[IO]] = {
|
||||
val errorJson = OBPErrorResponse(statusCode, message)
|
||||
val status = org.http4s.Status.fromInt(statusCode).getOrElse(org.http4s.Status.BadRequest)
|
||||
IO.pure(
|
||||
Response[IO](status)
|
||||
.withEntity(toJsonString(errorJson))
|
||||
.withContentType(jsonContentType)
|
||||
.putHeaders(org.http4s.Header.Raw(CIString("Correlation-Id"), callContext.correlationId))
|
||||
)
|
||||
}
|
||||
}
|
||||
422
obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala
Normal file
422
obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala
Normal file
@ -0,0 +1,422 @@
|
||||
package code.api.util.http4s
|
||||
|
||||
import cats.effect._
|
||||
import code.api.APIFailureNewStyle
|
||||
import code.api.util.APIUtil.ResourceDoc
|
||||
import code.api.util.ErrorMessages._
|
||||
import code.api.util.CallContext
|
||||
import com.openbankproject.commons.model.{Bank, BankAccount, BankId, AccountId, ViewId, BankIdAccountId, CounterpartyTrait, User, View}
|
||||
import net.liftweb.common.{Box, Empty, Full, Failure => LiftFailure}
|
||||
import net.liftweb.http.provider.HTTPParam
|
||||
import net.liftweb.json.{Extraction, compactRender}
|
||||
import net.liftweb.json.JsonDSL._
|
||||
import org.http4s._
|
||||
import org.http4s.dsl.io._
|
||||
import org.http4s.headers.`Content-Type`
|
||||
import org.typelevel.ci.CIString
|
||||
import org.typelevel.vault.Key
|
||||
|
||||
import java.util.{Date, UUID}
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
import scala.concurrent.Future
|
||||
import scala.language.higherKinds
|
||||
|
||||
/**
|
||||
* Http4s support utilities for OBP API.
|
||||
*
|
||||
* This file contains three main components:
|
||||
*
|
||||
* 1. Http4sRequestAttributes: Request attribute management and endpoint helpers
|
||||
* - Stores CallContext in http4s request Vault
|
||||
* - Provides helper methods to simplify endpoint implementations
|
||||
* - Validated entities are stored in CallContext fields
|
||||
*
|
||||
* 2. Http4sCallContextBuilder: Builds CallContext from http4s Request[IO]
|
||||
* - Extracts headers, auth params, and request metadata
|
||||
* - Supports DirectLogin, OAuth, and Gateway authentication
|
||||
*
|
||||
* 3. ResourceDocMatcher: Matches requests to ResourceDoc entries
|
||||
* - Finds ResourceDoc by HTTP verb and URL pattern
|
||||
* - Extracts path parameters (BANK_ID, ACCOUNT_ID, etc.)
|
||||
* - Attaches ResourceDoc to CallContext for metrics/rate limiting
|
||||
*/
|
||||
|
||||
/**
|
||||
* Request attributes and helper methods for http4s endpoints.
|
||||
*
|
||||
* CallContext is stored in request attributes using http4s Vault (type-safe key-value store).
|
||||
* Validated entities (user, bank, bankAccount, view, counterparty) are stored within CallContext.
|
||||
*/
|
||||
object Http4sRequestAttributes {
|
||||
|
||||
/**
|
||||
* Vault key for storing CallContext in http4s request attributes.
|
||||
* CallContext contains request data and validated entities (user, bank, account, view, counterparty).
|
||||
*/
|
||||
val callContextKey: Key[CallContext] =
|
||||
Key.newKey[IO, CallContext].unsafeRunSync()(cats.effect.unsafe.IORuntime.global)
|
||||
|
||||
/**
|
||||
* Implicit class that adds .callContext accessor to Request[IO].
|
||||
*
|
||||
* Usage:
|
||||
* {{{
|
||||
* import Http4sRequestAttributes.RequestOps
|
||||
*
|
||||
* case req @ GET -> Root / "banks" =>
|
||||
* implicit val cc: CallContext = req.callContext
|
||||
* // Use cc for business logic
|
||||
* }}}
|
||||
*/
|
||||
implicit class RequestOps(val req: Request[IO]) extends AnyVal {
|
||||
/**
|
||||
* Extract CallContext from request attributes.
|
||||
* Throws RuntimeException if not found (should never happen with ResourceDocMiddleware).
|
||||
*/
|
||||
def callContext: CallContext = {
|
||||
req.attributes.lookup(callContextKey).getOrElse(
|
||||
throw new RuntimeException("CallContext not found in request attributes")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper methods to eliminate boilerplate in endpoint implementations.
|
||||
*
|
||||
* These methods handle:
|
||||
* - CallContext extraction from request
|
||||
* - User/Bank extraction from CallContext
|
||||
* - Future execution with IO.fromFuture
|
||||
* - JSON serialization with Lift JSON
|
||||
* - Ok response creation
|
||||
*/
|
||||
object EndpointHelpers {
|
||||
import net.liftweb.json.{Extraction, Formats}
|
||||
import net.liftweb.json.JsonAST.prettyRender
|
||||
|
||||
/**
|
||||
* Execute Future-based business logic and return JSON response.
|
||||
*
|
||||
* Handles: Future execution, JSON conversion, Ok response.
|
||||
*
|
||||
* @param req http4s request
|
||||
* @param f Business logic: CallContext => Future[A]
|
||||
* @return IO[Response[IO]] with JSON body
|
||||
*/
|
||||
def executeAndRespond[A](req: Request[IO])(f: CallContext => Future[A])(implicit formats: Formats): IO[Response[IO]] = {
|
||||
implicit val cc: CallContext = req.callContext
|
||||
for {
|
||||
result <- IO.fromFuture(IO(f(cc)))
|
||||
jsonString = prettyRender(Extraction.decompose(result))
|
||||
response <- Ok(jsonString)
|
||||
} yield response
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute business logic requiring validated User.
|
||||
*
|
||||
* Extracts User from CallContext, executes logic, returns JSON response.
|
||||
*
|
||||
* @param req http4s request
|
||||
* @param f Business logic: (User, CallContext) => Future[A]
|
||||
* @return IO[Response[IO]] with JSON body
|
||||
*/
|
||||
def withUser[A](req: Request[IO])(f: (User, CallContext) => Future[A])(implicit formats: Formats): IO[Response[IO]] = {
|
||||
implicit val cc: CallContext = req.callContext
|
||||
for {
|
||||
user <- IO.fromOption(cc.user.toOption)(new RuntimeException("User not found in CallContext"))
|
||||
result <- IO.fromFuture(IO(f(user, cc)))
|
||||
jsonString = prettyRender(Extraction.decompose(result))
|
||||
response <- Ok(jsonString)
|
||||
} yield response
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute business logic requiring validated Bank.
|
||||
*
|
||||
* Extracts Bank from CallContext, executes logic, returns JSON response.
|
||||
*
|
||||
* @param req http4s request
|
||||
* @param f Business logic: (Bank, CallContext) => Future[A]
|
||||
* @return IO[Response[IO]] with JSON body
|
||||
*/
|
||||
def withBank[A](req: Request[IO])(f: (Bank, CallContext) => Future[A])(implicit formats: Formats): IO[Response[IO]] = {
|
||||
implicit val cc: CallContext = req.callContext
|
||||
for {
|
||||
bank <- IO.fromOption(cc.bank)(new RuntimeException("Bank not found in CallContext"))
|
||||
result <- IO.fromFuture(IO(f(bank, cc)))
|
||||
jsonString = prettyRender(Extraction.decompose(result))
|
||||
response <- Ok(jsonString)
|
||||
} yield response
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute business logic requiring both User and Bank.
|
||||
*
|
||||
* Extracts both from CallContext, executes logic, returns JSON response.
|
||||
*
|
||||
* @param req http4s request
|
||||
* @param f Business logic: (User, Bank, CallContext) => Future[A]
|
||||
* @return IO[Response[IO]] with JSON body
|
||||
*/
|
||||
def withUserAndBank[A](req: Request[IO])(f: (User, Bank, CallContext) => Future[A])(implicit formats: Formats): IO[Response[IO]] = {
|
||||
implicit val cc: CallContext = req.callContext
|
||||
for {
|
||||
user <- IO.fromOption(cc.user.toOption)(new RuntimeException("User not found in CallContext"))
|
||||
bank <- IO.fromOption(cc.bank)(new RuntimeException("Bank not found in CallContext"))
|
||||
result <- IO.fromFuture(IO(f(user, bank, cc)))
|
||||
jsonString = prettyRender(Extraction.decompose(result))
|
||||
response <- Ok(jsonString)
|
||||
} yield response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds shared CallContext from http4s Request[IO].
|
||||
*
|
||||
* This builder extracts all necessary request data and populates the shared CallContext,
|
||||
* enabling the existing authentication and validation code to work with http4s requests.
|
||||
*/
|
||||
object Http4sCallContextBuilder {
|
||||
|
||||
/**
|
||||
* Build CallContext from http4s Request[IO]
|
||||
* Populates all fields needed by getUserAndSessionContextFuture
|
||||
*
|
||||
* @param request The http4s request
|
||||
* @param apiVersion The API version string (e.g., "v7.0.0")
|
||||
* @return IO[CallContext] with all request data populated
|
||||
*/
|
||||
def fromRequest(request: Request[IO], apiVersion: String): IO[CallContext] = {
|
||||
for {
|
||||
body <- request.bodyText.compile.string.map(s => if (s.isEmpty) None else Some(s))
|
||||
} yield CallContext(
|
||||
url = request.uri.renderString,
|
||||
verb = request.method.name,
|
||||
implementedInVersion = apiVersion,
|
||||
correlationId = extractCorrelationId(request),
|
||||
ipAddress = extractIpAddress(request),
|
||||
requestHeaders = extractHeaders(request),
|
||||
httpBody = body,
|
||||
authReqHeaderField = extractAuthHeader(request),
|
||||
directLoginParams = extractDirectLoginParams(request),
|
||||
oAuthParams = extractOAuthParams(request),
|
||||
startTime = Some(new Date())
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract headers from http4s request and convert to List[HTTPParam]
|
||||
*/
|
||||
private def extractHeaders(request: Request[IO]): List[HTTPParam] = {
|
||||
request.headers.headers.map { h =>
|
||||
HTTPParam(h.name.toString, List(h.value))
|
||||
}.toList
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract correlation ID from X-Request-ID header or generate a new UUID
|
||||
*/
|
||||
private def extractCorrelationId(request: Request[IO]): String = {
|
||||
request.headers.get(CIString("X-Request-ID"))
|
||||
.map(_.head.value)
|
||||
.getOrElse(UUID.randomUUID().toString)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract IP address from X-Forwarded-For header or request remote address
|
||||
*/
|
||||
private def extractIpAddress(request: Request[IO]): String = {
|
||||
request.headers.get(CIString("X-Forwarded-For"))
|
||||
.map(_.head.value.split(",").head.trim)
|
||||
.orElse(request.remoteAddr.map(_.toUriString))
|
||||
.getOrElse("")
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Authorization header value as Box[String]
|
||||
*/
|
||||
private def extractAuthHeader(request: Request[IO]): Box[String] = {
|
||||
request.headers.get(CIString("Authorization"))
|
||||
.map(h => Full(h.head.value))
|
||||
.getOrElse(Empty)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract DirectLogin header parameters if present
|
||||
* Supports two formats:
|
||||
* 1. New format (2021): DirectLogin: token=xxx
|
||||
* 2. Old format (deprecated): Authorization: DirectLogin token=xxx
|
||||
*/
|
||||
private def extractDirectLoginParams(request: Request[IO]): Map[String, String] = {
|
||||
// Try new format first: DirectLogin header
|
||||
request.headers.get(CIString("DirectLogin"))
|
||||
.map(h => parseDirectLoginHeader(h.head.value))
|
||||
.getOrElse {
|
||||
// Fall back to old format: Authorization: DirectLogin token=xxx
|
||||
request.headers.get(CIString("Authorization"))
|
||||
.filter(_.head.value.contains("DirectLogin"))
|
||||
.map(h => parseDirectLoginHeader(h.head.value))
|
||||
.getOrElse(Map.empty)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse DirectLogin header value into parameter map
|
||||
* Matches Lift's parsing logic in directlogin.scala getAllParameters
|
||||
* Supports formats:
|
||||
* - DirectLogin token="xxx"
|
||||
* - DirectLogin token=xxx
|
||||
* - token="xxx", username="yyy"
|
||||
*/
|
||||
private def parseDirectLoginHeader(headerValue: String): Map[String, String] = {
|
||||
val directLoginPossibleParameters = List("consumer_key", "token", "username", "password")
|
||||
|
||||
// Strip "DirectLogin" prefix and split by comma, then trim each part (matches Lift logic)
|
||||
val cleanedParameterList = headerValue.stripPrefix("DirectLogin").split(",").map(_.trim).toList
|
||||
|
||||
cleanedParameterList.flatMap { input =>
|
||||
if (input.contains("=")) {
|
||||
val split = input.split("=", 2)
|
||||
val paramName = split(0).trim
|
||||
// Remove surrounding quotes if present
|
||||
val paramValue = split(1).replaceAll("^\"|\"$", "").trim
|
||||
if (directLoginPossibleParameters.contains(paramName) && paramValue.nonEmpty)
|
||||
Some(paramName -> paramValue)
|
||||
else
|
||||
None
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}.toMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract OAuth parameters from Authorization header if OAuth
|
||||
*/
|
||||
private def extractOAuthParams(request: Request[IO]): Map[String, String] = {
|
||||
request.headers.get(CIString("Authorization"))
|
||||
.filter(_.head.value.startsWith("OAuth "))
|
||||
.map(h => parseOAuthHeader(h.head.value))
|
||||
.getOrElse(Map.empty)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse OAuth Authorization header value into parameter map
|
||||
* Format: OAuth oauth_consumer_key="xxx", oauth_token="yyy", ...
|
||||
*/
|
||||
private def parseOAuthHeader(headerValue: String): Map[String, String] = {
|
||||
val oauthPart = headerValue.stripPrefix("OAuth ").trim
|
||||
val pattern = """(\w+)="([^"]*)"""".r
|
||||
pattern.findAllMatchIn(oauthPart).map { m =>
|
||||
m.group(1) -> m.group(2)
|
||||
}.toMap
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches http4s requests to ResourceDoc entries.
|
||||
*
|
||||
* ResourceDoc entries use URL templates with uppercase variable names:
|
||||
* - BANK_ID, ACCOUNT_ID, VIEW_ID, COUNTERPARTY_ID
|
||||
*
|
||||
* This matcher finds the corresponding ResourceDoc for a given request
|
||||
* and extracts path parameters.
|
||||
*/
|
||||
object ResourceDocMatcher {
|
||||
|
||||
// API prefix pattern: /obp/vX.X.X
|
||||
private val apiPrefixPattern = """^/obp/v\d+\.\d+\.\d+""".r
|
||||
|
||||
/**
|
||||
* Find ResourceDoc matching the given verb and path
|
||||
*
|
||||
* @param verb HTTP verb (GET, POST, PUT, DELETE, etc.)
|
||||
* @param path Request path
|
||||
* @param resourceDocs Collection of ResourceDoc entries to search
|
||||
* @return Option[ResourceDoc] if a match is found
|
||||
*/
|
||||
def findResourceDoc(
|
||||
verb: String,
|
||||
path: Uri.Path,
|
||||
resourceDocs: ArrayBuffer[ResourceDoc]
|
||||
): Option[ResourceDoc] = {
|
||||
val pathString = path.renderString
|
||||
// Strip the API prefix (/obp/vX.X.X) from the path for matching
|
||||
val strippedPath = apiPrefixPattern.replaceFirstIn(pathString, "")
|
||||
resourceDocs.find { doc =>
|
||||
doc.requestVerb.equalsIgnoreCase(verb) && matchesUrlTemplate(strippedPath, doc.requestUrl)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path matches a URL template
|
||||
* Template segments in uppercase are treated as variables
|
||||
*/
|
||||
private def matchesUrlTemplate(path: String, template: String): Boolean = {
|
||||
val pathSegments = path.split("/").filter(_.nonEmpty)
|
||||
val templateSegments = template.split("/").filter(_.nonEmpty)
|
||||
|
||||
if (pathSegments.length != templateSegments.length) {
|
||||
false
|
||||
} else {
|
||||
pathSegments.zip(templateSegments).forall { case (pathSeg, templateSeg) =>
|
||||
// Uppercase segments are variables (BANK_ID, ACCOUNT_ID, etc.)
|
||||
isTemplateVariable(templateSeg) || pathSeg == templateSeg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a template segment is a variable (uppercase)
|
||||
*/
|
||||
private def isTemplateVariable(segment: String): Boolean = {
|
||||
segment.nonEmpty && segment.forall(c => c.isUpper || c == '_' || c.isDigit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract path parameters from matched ResourceDoc
|
||||
*
|
||||
* @param path Request path
|
||||
* @param resourceDoc Matched ResourceDoc
|
||||
* @return Map with keys: BANK_ID, ACCOUNT_ID, VIEW_ID, COUNTERPARTY_ID (if present)
|
||||
*/
|
||||
def extractPathParams(
|
||||
path: Uri.Path,
|
||||
resourceDoc: ResourceDoc
|
||||
): Map[String, String] = {
|
||||
val pathString = path.renderString
|
||||
// Strip the API prefix (/obp/vX.X.X) from the path for matching
|
||||
val strippedPath = apiPrefixPattern.replaceFirstIn(pathString, "")
|
||||
val pathSegments = strippedPath.split("/").filter(_.nonEmpty)
|
||||
val templateSegments = resourceDoc.requestUrl.split("/").filter(_.nonEmpty)
|
||||
|
||||
if (pathSegments.length != templateSegments.length) {
|
||||
Map.empty
|
||||
} else {
|
||||
pathSegments.zip(templateSegments).collect {
|
||||
case (pathSeg, templateSeg) if isTemplateVariable(templateSeg) =>
|
||||
templateSeg -> pathSeg
|
||||
}.toMap
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update CallContext with matched ResourceDoc
|
||||
* MUST be called after successful match for metrics/rate limiting consistency
|
||||
*
|
||||
* @param callContext Current CallContext
|
||||
* @param resourceDoc Matched ResourceDoc
|
||||
* @return Updated CallContext with resourceDocument and operationId set
|
||||
*/
|
||||
def attachToCallContext(
|
||||
callContext: CallContext,
|
||||
resourceDoc: ResourceDoc
|
||||
): CallContext = {
|
||||
callContext.copy(
|
||||
resourceDocument = Some(resourceDoc),
|
||||
operationId = Some(resourceDoc.operationId)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,293 @@
|
||||
package code.api.util.http4s
|
||||
|
||||
import cats.data.{Kleisli, OptionT}
|
||||
import cats.effect._
|
||||
import code.api.APIFailureNewStyle
|
||||
import code.api.util.APIUtil.ResourceDoc
|
||||
import code.api.util.ErrorMessages._
|
||||
import code.api.util.{APIUtil, CallContext, NewStyle}
|
||||
import code.api.util.newstyle.ViewNewStyle
|
||||
import code.util.Helper.MdcLoggable
|
||||
import com.openbankproject.commons.model._
|
||||
import net.liftweb.common.{Box, Empty, Full, Failure => LiftFailure}
|
||||
import org.http4s._
|
||||
import org.http4s.headers.`Content-Type`
|
||||
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
import scala.language.higherKinds
|
||||
|
||||
/**
|
||||
* ResourceDoc-driven validation middleware for http4s.
|
||||
*
|
||||
* This middleware wraps http4s routes with automatic validation based on ResourceDoc metadata.
|
||||
* Validation is performed in a specific order to ensure security and proper error responses.
|
||||
*
|
||||
* VALIDATION ORDER:
|
||||
* 1. Authentication - Check if user is authenticated (if required by ResourceDoc)
|
||||
* 2. Authorization - Verify user has required roles/entitlements
|
||||
* 3. Bank validation - Validate BANK_ID path parameter (if present)
|
||||
* 4. Account validation - Validate ACCOUNT_ID path parameter (if present)
|
||||
* 5. View validation - Validate VIEW_ID and check user access (if present)
|
||||
* 6. Counterparty validation - Validate COUNTERPARTY_ID (if present)
|
||||
*
|
||||
* Validated entities are stored in CallContext fields for use in endpoint handlers.
|
||||
*/
|
||||
object ResourceDocMiddleware extends MdcLoggable{
|
||||
|
||||
type HttpF[A] = OptionT[IO, A]
|
||||
type Middleware[F[_]] = HttpRoutes[F] => HttpRoutes[F]
|
||||
private val jsonContentType: `Content-Type` = `Content-Type`(MediaType.application.json)
|
||||
|
||||
/**
|
||||
* Check if ResourceDoc requires authentication.
|
||||
*
|
||||
* Authentication is required if:
|
||||
* - ResourceDoc errorResponseBodies contains $AuthenticatedUserIsRequired
|
||||
* - ResourceDoc has roles (roles always require authenticated user)
|
||||
* - Special case: resource-docs endpoint checks resource_docs_requires_role property
|
||||
*/
|
||||
private def needsAuthentication(resourceDoc: ResourceDoc): Boolean = {
|
||||
if (resourceDoc.partialFunctionName == "getResourceDocsObpV700") {
|
||||
APIUtil.getPropsAsBoolValue("resource_docs_requires_role", false)
|
||||
} else {
|
||||
resourceDoc.errorResponseBodies.contains($AuthenticatedUserIsRequired) || resourceDoc.roles.exists(_.nonEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create middleware that applies ResourceDoc-driven validation.
|
||||
*
|
||||
* @param resourceDocs Collection of ResourceDoc entries for matching
|
||||
* @return Middleware that wraps HttpRoutes with validation
|
||||
*/
|
||||
def apply(resourceDocs: ArrayBuffer[ResourceDoc]): Middleware[IO] = { routes =>
|
||||
Kleisli[HttpF, Request[IO], Response[IO]] { req =>
|
||||
OptionT(validateAndRoute(req, routes, resourceDocs).map(Option(_)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate request and route to handler if validation passes.
|
||||
*
|
||||
* Steps:
|
||||
* 1. Build CallContext from request
|
||||
* 2. Find matching ResourceDoc
|
||||
* 3. Run validation chain
|
||||
* 4. Route to handler with enriched CallContext
|
||||
*/
|
||||
private def validateAndRoute(
|
||||
req: Request[IO],
|
||||
routes: HttpRoutes[IO],
|
||||
resourceDocs: ArrayBuffer[ResourceDoc]
|
||||
): IO[Response[IO]] = {
|
||||
for {
|
||||
cc <- Http4sCallContextBuilder.fromRequest(req, "v7.0.0")
|
||||
resourceDocOpt = ResourceDocMatcher.findResourceDoc(req.method.name, req.uri.path, resourceDocs)
|
||||
response <- resourceDocOpt match {
|
||||
case Some(resourceDoc) =>
|
||||
val ccWithDoc = ResourceDocMatcher.attachToCallContext(cc, resourceDoc)
|
||||
val pathParams = ResourceDocMatcher.extractPathParams(req.uri.path, resourceDoc)
|
||||
runValidationChainForRoutes(req, resourceDoc, ccWithDoc, pathParams, routes)
|
||||
.map(ensureJsonContentType)
|
||||
case None =>
|
||||
routes.run(req).getOrElseF(IO.pure(Response[IO](org.http4s.Status.NotFound)))
|
||||
}
|
||||
} yield response
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure response has JSON content type.
|
||||
*/
|
||||
private def ensureJsonContentType(response: Response[IO]): Response[IO] = {
|
||||
response.contentType match {
|
||||
case Some(contentType) if contentType.mediaType == MediaType.application.json => response
|
||||
case _ => response.withContentType(jsonContentType)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run validation chain for HttpRoutes and return Response.
|
||||
*
|
||||
* This method performs all validation steps in order:
|
||||
* 1. Authentication (if required)
|
||||
* 2. Role authorization (if roles specified)
|
||||
* 3. Bank validation (if BANK_ID in path)
|
||||
* 4. Account validation (if ACCOUNT_ID in path)
|
||||
* 5. View validation (if VIEW_ID in path)
|
||||
* 6. Counterparty validation (if COUNTERPARTY_ID in path)
|
||||
*
|
||||
* On success: Enriches CallContext with validated entities and routes to handler
|
||||
* On failure: Returns error response immediately
|
||||
*/
|
||||
private def runValidationChainForRoutes(
|
||||
req: Request[IO],
|
||||
resourceDoc: ResourceDoc,
|
||||
cc: CallContext,
|
||||
pathParams: Map[String, String],
|
||||
routes: HttpRoutes[IO]
|
||||
): IO[Response[IO]] = {
|
||||
|
||||
val needsAuth = needsAuthentication(resourceDoc)
|
||||
logger.debug(s"[ResourceDocMiddleware] needsAuthentication for ${resourceDoc.partialFunctionName}: $needsAuth")
|
||||
|
||||
// Step 1: Authentication
|
||||
val authResult: IO[Either[Response[IO], (Box[User], CallContext)]] =
|
||||
if (needsAuth) {
|
||||
IO.fromFuture(IO(APIUtil.authenticatedAccess(cc))).attempt.flatMap {
|
||||
case Right((boxUser, optCC)) =>
|
||||
val updatedCC = optCC.getOrElse(cc)
|
||||
boxUser match {
|
||||
case Full(user) =>
|
||||
IO.pure(Right((boxUser, updatedCC)))
|
||||
case Empty =>
|
||||
ErrorResponseConverter.createErrorResponse(401, $AuthenticatedUserIsRequired, updatedCC).map(Left(_))
|
||||
case LiftFailure(msg, _, _) =>
|
||||
ErrorResponseConverter.createErrorResponse(401, msg, updatedCC).map(Left(_))
|
||||
}
|
||||
case Left(e: APIFailureNewStyle) =>
|
||||
ErrorResponseConverter.createErrorResponse(e.failCode, e.failMsg, cc).map(Left(_))
|
||||
case Left(e) =>
|
||||
val (code, msg) = try {
|
||||
import net.liftweb.json._
|
||||
implicit val formats = net.liftweb.json.DefaultFormats
|
||||
val json = parse(e.getMessage)
|
||||
val failCode = (json \ "failCode").extractOpt[Int].getOrElse(401)
|
||||
val failMsg = (json \ "failMsg").extractOpt[String].getOrElse($AuthenticatedUserIsRequired)
|
||||
(failCode, failMsg)
|
||||
} catch {
|
||||
case _: Exception => (401, $AuthenticatedUserIsRequired)
|
||||
}
|
||||
ErrorResponseConverter.createErrorResponse(code, msg, cc).map(Left(_))
|
||||
}
|
||||
} else {
|
||||
IO.fromFuture(IO(APIUtil.anonymousAccess(cc))).attempt.flatMap {
|
||||
case Right((boxUser, Some(updatedCC))) =>
|
||||
IO.pure(Right((boxUser, updatedCC)))
|
||||
case Right((boxUser, None)) =>
|
||||
IO.pure(Right((boxUser, cc)))
|
||||
case Left(e) =>
|
||||
// For anonymous endpoints, continue with Empty user even if auth fails
|
||||
IO.pure(Right((Empty, cc)))
|
||||
}
|
||||
}
|
||||
|
||||
authResult.flatMap {
|
||||
case Left(errorResponse) => IO.pure(errorResponse)
|
||||
case Right((boxUser, cc1)) =>
|
||||
// Step 2: Role authorization
|
||||
val rolesResult: IO[Either[Response[IO], CallContext]] =
|
||||
resourceDoc.roles match {
|
||||
case Some(roles) if roles.nonEmpty =>
|
||||
boxUser match {
|
||||
case Full(user) =>
|
||||
val userId = user.userId
|
||||
val bankId = pathParams.get("BANK_ID").getOrElse("")
|
||||
val hasRole = roles.exists { role =>
|
||||
val checkBankId = if (role.requiresBankId) bankId else ""
|
||||
APIUtil.hasEntitlement(checkBankId, userId, role)
|
||||
}
|
||||
if (hasRole) IO.pure(Right(cc1))
|
||||
else ErrorResponseConverter.createErrorResponse(403, UserHasMissingRoles + roles.mkString(", "), cc1).map(Left(_))
|
||||
case _ =>
|
||||
ErrorResponseConverter.createErrorResponse(401, $AuthenticatedUserIsRequired, cc1).map(Left(_))
|
||||
}
|
||||
case _ => IO.pure(Right(cc1))
|
||||
}
|
||||
|
||||
rolesResult.flatMap {
|
||||
case Left(errorResponse) => IO.pure(errorResponse)
|
||||
case Right(cc2) =>
|
||||
// Step 3: Bank validation
|
||||
val bankResult: IO[Either[Response[IO], (Option[Bank], CallContext)]] =
|
||||
pathParams.get("BANK_ID") match {
|
||||
case Some(bankIdStr) =>
|
||||
IO.fromFuture(IO(NewStyle.function.getBank(BankId(bankIdStr), Some(cc2)))).attempt.flatMap {
|
||||
case Right((bank, Some(updatedCC))) =>
|
||||
IO.pure(Right((Some(bank), updatedCC)))
|
||||
case Right((bank, None)) =>
|
||||
IO.pure(Right((Some(bank), cc2)))
|
||||
case Left(e: APIFailureNewStyle) =>
|
||||
ErrorResponseConverter.createErrorResponse(e.failCode, e.failMsg, cc2).map(Left(_))
|
||||
case Left(e) =>
|
||||
ErrorResponseConverter.createErrorResponse(404, BankNotFound + ": " + bankIdStr, cc2).map(Left(_))
|
||||
}
|
||||
case None => IO.pure(Right((None, cc2)))
|
||||
}
|
||||
|
||||
bankResult.flatMap {
|
||||
case Left(errorResponse) => IO.pure(errorResponse)
|
||||
case Right((bankOpt, cc3)) =>
|
||||
// Step 4: Account validation (if ACCOUNT_ID in path)
|
||||
val accountResult: IO[Either[Response[IO], (Option[BankAccount], CallContext)]] =
|
||||
(pathParams.get("BANK_ID"), pathParams.get("ACCOUNT_ID")) match {
|
||||
case (Some(bankIdStr), Some(accountIdStr)) =>
|
||||
IO.fromFuture(IO(NewStyle.function.getBankAccount(BankId(bankIdStr), AccountId(accountIdStr), Some(cc3)))).attempt.flatMap {
|
||||
case Right((account, Some(updatedCC))) => IO.pure(Right((Some(account), updatedCC)))
|
||||
case Right((account, None)) => IO.pure(Right((Some(account), cc3)))
|
||||
case Left(e: APIFailureNewStyle) =>
|
||||
ErrorResponseConverter.createErrorResponse(e.failCode, e.failMsg, cc3).map(Left(_))
|
||||
case Left(e) =>
|
||||
ErrorResponseConverter.createErrorResponse(404, BankAccountNotFound + s": bankId=$bankIdStr, accountId=$accountIdStr", cc3).map(Left(_))
|
||||
}
|
||||
case _ => IO.pure(Right((None, cc3)))
|
||||
}
|
||||
|
||||
|
||||
accountResult.flatMap {
|
||||
case Left(errorResponse) => IO.pure(errorResponse)
|
||||
case Right((accountOpt, cc4)) =>
|
||||
// Step 5: View validation (if VIEW_ID in path)
|
||||
val viewResult: IO[Either[Response[IO], (Option[View], CallContext)]] =
|
||||
(pathParams.get("BANK_ID"), pathParams.get("ACCOUNT_ID"), pathParams.get("VIEW_ID")) match {
|
||||
case (Some(bankIdStr), Some(accountIdStr), Some(viewIdStr)) =>
|
||||
val bankIdAccountId = BankIdAccountId(BankId(bankIdStr), AccountId(accountIdStr))
|
||||
IO.fromFuture(IO(ViewNewStyle.checkViewAccessAndReturnView(ViewId(viewIdStr), bankIdAccountId, boxUser.toOption, Some(cc4)))).attempt.flatMap {
|
||||
case Right(view) => IO.pure(Right((Some(view), cc4)))
|
||||
case Left(e: APIFailureNewStyle) =>
|
||||
ErrorResponseConverter.createErrorResponse(e.failCode, e.failMsg, cc4).map(Left(_))
|
||||
case Left(e) =>
|
||||
ErrorResponseConverter.createErrorResponse(403, UserNoPermissionAccessView + s": viewId=$viewIdStr", cc4).map(Left(_))
|
||||
}
|
||||
case _ => IO.pure(Right((None, cc4)))
|
||||
}
|
||||
|
||||
viewResult.flatMap {
|
||||
case Left(errorResponse) => IO.pure(errorResponse)
|
||||
case Right((viewOpt, cc5)) =>
|
||||
// Step 6: Counterparty validation (if COUNTERPARTY_ID in path)
|
||||
val counterpartyResult: IO[Either[Response[IO], (Option[CounterpartyTrait], CallContext)]] =
|
||||
(pathParams.get("BANK_ID"), pathParams.get("ACCOUNT_ID"), pathParams.get("COUNTERPARTY_ID")) match {
|
||||
case (Some(bankIdStr), Some(accountIdStr), Some(counterpartyIdStr)) =>
|
||||
IO.fromFuture(IO(NewStyle.function.getCounterpartyTrait(BankId(bankIdStr), AccountId(accountIdStr), counterpartyIdStr, Some(cc5)))).attempt.flatMap {
|
||||
case Right((counterparty, Some(updatedCC))) => IO.pure(Right((Some(counterparty), updatedCC)))
|
||||
case Right((counterparty, None)) => IO.pure(Right((Some(counterparty), cc5)))
|
||||
case Left(e: APIFailureNewStyle) =>
|
||||
ErrorResponseConverter.createErrorResponse(e.failCode, e.failMsg, cc5).map(Left(_))
|
||||
case Left(e) =>
|
||||
ErrorResponseConverter.createErrorResponse(404, CounterpartyNotFound + s": counterpartyId=$counterpartyIdStr", cc5).map(Left(_))
|
||||
}
|
||||
case _ => IO.pure(Right((None, cc5)))
|
||||
}
|
||||
|
||||
counterpartyResult.flatMap {
|
||||
case Left(errorResponse) => IO.pure(errorResponse)
|
||||
case Right((counterpartyOpt, finalCC)) =>
|
||||
// All validations passed - update CallContext with validated entities
|
||||
val enrichedCC = finalCC.copy(
|
||||
bank = bankOpt,
|
||||
bankAccount = accountOpt,
|
||||
view = viewOpt,
|
||||
counterparty = counterpartyOpt
|
||||
)
|
||||
|
||||
// Store enriched CallContext in request attributes
|
||||
val updatedReq = req.withAttribute(Http4sRequestAttributes.callContextKey, enrichedCC)
|
||||
routes.run(updatedReq).getOrElseF(IO.pure(Response[IO](org.http4s.Status.NotFound)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1095,22 +1095,22 @@ case class JsonCodeTemplateJson(
|
||||
object JSONFactory400 {
|
||||
|
||||
def getApiInfoJSON(apiVersion : ApiVersion, apiVersionStatus : String) = {
|
||||
val organisation = APIUtil.getPropsValue("hosted_by.organisation", "TESOBE")
|
||||
val email = APIUtil.getPropsValue("hosted_by.email", "contact@tesobe.com")
|
||||
val phone = APIUtil.getPropsValue("hosted_by.phone", "+49 (0)30 8145 3994")
|
||||
val organisationWebsite = APIUtil.getPropsValue("organisation_website", "https://www.tesobe.com")
|
||||
val organisation = APIUtil.hostedByOrganisation
|
||||
val email = APIUtil.hostedByEmail
|
||||
val phone = APIUtil.hostedByPhone
|
||||
val organisationWebsite = APIUtil.organisationWebsite
|
||||
val hostedBy = new HostedBy400(organisation, email, phone, organisationWebsite)
|
||||
|
||||
val organisationHostedAt = APIUtil.getPropsValue("hosted_at.organisation", "")
|
||||
val organisationWebsiteHostedAt = APIUtil.getPropsValue("hosted_at.organisation_website", "")
|
||||
val organisationHostedAt = APIUtil.hostedAtOrganisation
|
||||
val organisationWebsiteHostedAt = APIUtil.hostedAtOrganisationWebsite
|
||||
val hostedAt = new HostedAt400(organisationHostedAt, organisationWebsiteHostedAt)
|
||||
|
||||
val organisationEnergySource = APIUtil.getPropsValue("energy_source.organisation", "")
|
||||
val organisationWebsiteEnergySource = APIUtil.getPropsValue("energy_source.organisation_website", "")
|
||||
val organisationEnergySource = APIUtil.energySourceOrganisation
|
||||
val organisationWebsiteEnergySource = APIUtil.energySourceOrganisationWebsite
|
||||
val energySource = new EnergySource400(organisationEnergySource, organisationWebsiteEnergySource)
|
||||
|
||||
val connector = code.api.Constant.CONNECTOR.openOrThrowException(s"$MandatoryPropertyIsNotSet. The missing prop is `connector` ")
|
||||
val resourceDocsRequiresRole = APIUtil.getPropsAsBoolValue("resource_docs_requires_role", false)
|
||||
val resourceDocsRequiresRole = APIUtil.resourceDocsRequiresRole
|
||||
|
||||
APIInfoJson400(
|
||||
apiVersion.vDottedApiVersion,
|
||||
|
||||
@ -1049,22 +1049,22 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable {
|
||||
}
|
||||
|
||||
def getApiInfoJSON(apiVersion : ApiVersion, apiVersionStatus: String) = {
|
||||
val organisation = APIUtil.getPropsValue("hosted_by.organisation", "TESOBE")
|
||||
val email = APIUtil.getPropsValue("hosted_by.email", "contact@tesobe.com")
|
||||
val phone = APIUtil.getPropsValue("hosted_by.phone", "+49 (0)30 8145 3994")
|
||||
val organisationWebsite = APIUtil.getPropsValue("organisation_website", "https://www.tesobe.com")
|
||||
val organisation = APIUtil.hostedByOrganisation
|
||||
val email = APIUtil.hostedByEmail
|
||||
val phone = APIUtil.hostedByPhone
|
||||
val organisationWebsite = APIUtil.organisationWebsite
|
||||
val hostedBy = new HostedBy400(organisation, email, phone, organisationWebsite)
|
||||
|
||||
val organisationHostedAt = APIUtil.getPropsValue("hosted_at.organisation", "")
|
||||
val organisationWebsiteHostedAt = APIUtil.getPropsValue("hosted_at.organisation_website", "")
|
||||
val organisationHostedAt = APIUtil.hostedAtOrganisation
|
||||
val organisationWebsiteHostedAt = APIUtil.hostedAtOrganisationWebsite
|
||||
val hostedAt = HostedAt400(organisationHostedAt, organisationWebsiteHostedAt)
|
||||
|
||||
val organisationEnergySource = APIUtil.getPropsValue("energy_source.organisation", "")
|
||||
val organisationWebsiteEnergySource = APIUtil.getPropsValue("energy_source.organisation_website", "")
|
||||
val organisationEnergySource = APIUtil.energySourceOrganisation
|
||||
val organisationWebsiteEnergySource = APIUtil.energySourceOrganisationWebsite
|
||||
val energySource = EnergySource400(organisationEnergySource, organisationWebsiteEnergySource)
|
||||
|
||||
val connector = code.api.Constant.CONNECTOR.openOrThrowException(s"$MandatoryPropertyIsNotSet. The missing prop is `connector` ")
|
||||
val resourceDocsRequiresRole = APIUtil.getPropsAsBoolValue("resource_docs_requires_role", false)
|
||||
val resourceDocsRequiresRole = APIUtil.resourceDocsRequiresRole
|
||||
|
||||
APIInfoJsonV510(
|
||||
version = apiVersion.vDottedApiVersion,
|
||||
|
||||
@ -6,9 +6,13 @@ 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.ApiRole.{canGetCardsForBank, canReadResourceDoc}
|
||||
import code.api.util.ApiTag._
|
||||
import code.api.util.ErrorMessages._
|
||||
import code.api.util.{ApiVersionUtils, CustomJsonFormats, NewStyle}
|
||||
import code.api.util.http4s.{Http4sRequestAttributes, ResourceDocMiddleware}
|
||||
import code.api.util.http4s.Http4sRequestAttributes.{RequestOps, EndpointHelpers}
|
||||
import code.api.util.{ApiRole, ApiVersionUtils, CallContext, CustomJsonFormats, NewStyle}
|
||||
import code.api.v1_3_0.JSONFactory1_3_0
|
||||
import code.api.v1_4_0.JSONFactory1_4_0
|
||||
import code.api.v4_0_0.JSONFactory400
|
||||
import com.github.dwickern.macros.NameOf.nameOf
|
||||
@ -18,7 +22,6 @@ import net.liftweb.json.JsonAST.prettyRender
|
||||
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
|
||||
@ -35,27 +38,14 @@ object Http4s700 {
|
||||
val versionStatus = ApiVersionStatus.STABLE.toString
|
||||
val resourceDocs = ArrayBuffer[ResourceDoc]()
|
||||
|
||||
case class CallContext(userId: String, requestId: String)
|
||||
val callContextKey: Key[CallContext] =
|
||||
Key.newKey[IO, CallContext].unsafeRunSync()(cats.effect.unsafe.IORuntime.global)
|
||||
|
||||
object CallContextMiddleware {
|
||||
|
||||
def withCallContext(routes: HttpRoutes[IO]): HttpRoutes[IO] =
|
||||
Kleisli[HttpF, Request[IO], Response[IO]] { req: Request[IO] =>
|
||||
val callContext = CallContext(userId = "example-user", requestId = java.util.UUID.randomUUID().toString)
|
||||
val updatedAttributes = req.attributes.insert(callContextKey, callContext)
|
||||
val updatedReq = req.withAttributes(updatedAttributes)
|
||||
routes(updatedReq)
|
||||
}
|
||||
}
|
||||
|
||||
object Implementations7_0_0 {
|
||||
|
||||
// Common prefix: /obp/v7.0.0
|
||||
val prefixPath = Root / ApiPathZero.toString / implementedInApiVersion.toString
|
||||
|
||||
|
||||
// ResourceDoc with AuthenticatedUserIsRequired in errorResponseBodies indicates auth is required
|
||||
// ResourceDocMiddleware will automatically handle authentication based on this metadata
|
||||
// No explicit auth code needed in the endpoint handler - just like Lift's wrappedWithAuthCheck
|
||||
resourceDocs += ResourceDoc(
|
||||
null,
|
||||
implementedInApiVersion,
|
||||
@ -68,27 +58,25 @@ object Http4s700 {
|
||||
|* API version
|
||||
|* Hosted by information
|
||||
|* Git Commit
|
||||
|${userAuthenticationMessage(false)}""",
|
||||
""",
|
||||
EmptyBody,
|
||||
apiInfoJSON,
|
||||
List(UnknownError, "no connector set"),
|
||||
apiInfoJSON,
|
||||
List(
|
||||
UnknownError
|
||||
),
|
||||
apiTagApi :: Nil,
|
||||
http4sPartialFunction = Some(root)
|
||||
)
|
||||
|
||||
// Route: GET /obp/v7.0.0/root
|
||||
// Authentication is handled automatically by ResourceDocMiddleware based on AuthenticatedUserIsRequired in ResourceDoc
|
||||
// The endpoint code only contains business logic - validated User is available from request attributes
|
||||
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}.")
|
||||
)
|
||||
}
|
||||
)))
|
||||
val responseJson = convertAnyToJsonString(
|
||||
JSONFactory700.getApiInfoJSON(implementedInApiVersion, versionStatus)
|
||||
)
|
||||
Ok(responseJson)
|
||||
}
|
||||
|
||||
resourceDocs += ResourceDoc(
|
||||
@ -104,11 +92,12 @@ object Http4s700 {
|
||||
|* ID used as parameter in URLs
|
||||
|* Short and full name of bank
|
||||
|* Logo URL
|
||||
|* Website
|
||||
|${userAuthenticationMessage(false)}""",
|
||||
|* Website""",
|
||||
EmptyBody,
|
||||
banksJSON,
|
||||
List(UnknownError),
|
||||
List(
|
||||
UnknownError
|
||||
),
|
||||
apiTagBank :: Nil,
|
||||
http4sPartialFunction = Some(getBanks)
|
||||
)
|
||||
@ -116,42 +105,160 @@ object Http4s700 {
|
||||
// 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(
|
||||
EndpointHelpers.executeAndRespond(req) { implicit cc =>
|
||||
for {
|
||||
(banks, callContext) <- NewStyle.function.getBanks(None)
|
||||
} yield {
|
||||
convertAnyToJsonString(JSONFactory400.createBanksJson(banks))
|
||||
}
|
||||
)))
|
||||
(banks, callContext) <- NewStyle.function.getBanks(Some(cc))
|
||||
} yield JSONFactory400.createBanksJson(banks)
|
||||
}
|
||||
}
|
||||
|
||||
resourceDocs += ResourceDoc(
|
||||
null,
|
||||
implementedInApiVersion,
|
||||
nameOf(getCards),
|
||||
"GET",
|
||||
"/cards",
|
||||
"Get cards for the current user",
|
||||
"Returns data about all the physical cards a user has been issued. These could be debit cards, credit cards, etc.",
|
||||
EmptyBody,
|
||||
physicalCardsJSON,
|
||||
List(AuthenticatedUserIsRequired, UnknownError),
|
||||
apiTagCard :: Nil,
|
||||
http4sPartialFunction = Some(getCards)
|
||||
)
|
||||
|
||||
// Route: GET /obp/v7.0.0/cards
|
||||
// Authentication handled by ResourceDocMiddleware based on AuthenticatedUserIsRequired
|
||||
val getCards: HttpRoutes[IO] = HttpRoutes.of[IO] {
|
||||
case req @ GET -> `prefixPath` / "cards" =>
|
||||
EndpointHelpers.withUser(req) { (user, cc) =>
|
||||
for {
|
||||
(cards, callContext) <- NewStyle.function.getPhysicalCardsForUser(user, Some(cc))
|
||||
} yield JSONFactory1_3_0.createPhysicalCardsJSON(cards, user)
|
||||
}
|
||||
}
|
||||
|
||||
resourceDocs += ResourceDoc(
|
||||
null,
|
||||
implementedInApiVersion,
|
||||
nameOf(getCardsForBank),
|
||||
"GET",
|
||||
"/banks/BANK_ID/cards",
|
||||
"Get cards for the specified bank",
|
||||
"",
|
||||
EmptyBody,
|
||||
physicalCardsJSON,
|
||||
List(AuthenticatedUserIsRequired, BankNotFound, UnknownError),
|
||||
apiTagCard :: Nil,
|
||||
Some(List(canGetCardsForBank)),
|
||||
http4sPartialFunction = Some(getCardsForBank)
|
||||
)
|
||||
|
||||
// Route: GET /obp/v7.0.0/banks/BANK_ID/cards
|
||||
// Authentication and bank validation handled by ResourceDocMiddleware
|
||||
val getCardsForBank: HttpRoutes[IO] = HttpRoutes.of[IO] {
|
||||
case req @ GET -> `prefixPath` / "banks" / bankId / "cards" =>
|
||||
EndpointHelpers.withUserAndBank(req) { (user, bank, cc) =>
|
||||
for {
|
||||
httpParams <- NewStyle.function.extractHttpParamsFromUrl(req.uri.renderString)
|
||||
(obpQueryParams, callContext) <- createQueriesByHttpParamsFuture(httpParams, Some(cc))
|
||||
(cards, callContext) <- NewStyle.function.getPhysicalCardsForBank(bank, user, obpQueryParams, callContext)
|
||||
} yield JSONFactory1_3_0.createPhysicalCardsJSON(cards, user)
|
||||
}
|
||||
}
|
||||
|
||||
resourceDocs += ResourceDoc(
|
||||
null,
|
||||
implementedInApiVersion,
|
||||
nameOf(getResourceDocsObpV700),
|
||||
"GET",
|
||||
"/resource-docs/API_VERSION/obp",
|
||||
"Get Resource Docs",
|
||||
s"""Get documentation about the RESTful resources on this server including example body payloads.
|
||||
|
|
||||
|* API_VERSION: The version of the API for which you want documentation
|
||||
|
|
||||
|Returns JSON containing information about the endpoints including:
|
||||
|* Method (GET, POST, etc.)
|
||||
|* URL path
|
||||
|* Summary and description
|
||||
|* Example request and response bodies
|
||||
|* Required roles and permissions
|
||||
|
|
||||
|Optional query parameters:
|
||||
|* tags - filter by API tags
|
||||
|* functions - filter by function names
|
||||
|* locale - specify language for descriptions
|
||||
|* content - filter by content type""",
|
||||
EmptyBody,
|
||||
EmptyBody,
|
||||
List(
|
||||
UnknownError
|
||||
),
|
||||
List(apiTagDocumentation, apiTagApi),
|
||||
http4sPartialFunction = Some(getResourceDocsObpV700)
|
||||
)
|
||||
|
||||
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)))
|
||||
implicit val cc: CallContext = req.callContext
|
||||
for {
|
||||
result <- IO.fromFuture(IO {
|
||||
// Check resource_docs_requires_role property
|
||||
val resourceDocsRequireRole = getPropsAsBoolValue("resource_docs_requires_role", false)
|
||||
|
||||
for {
|
||||
// Authentication based on property
|
||||
(boxUser, cc1) <- if (resourceDocsRequireRole)
|
||||
authenticatedAccess(cc)
|
||||
else
|
||||
anonymousAccess(cc)
|
||||
|
||||
// Role check based on property
|
||||
_ <- if (resourceDocsRequireRole) {
|
||||
NewStyle.function.hasAtLeastOneEntitlement(
|
||||
failMsg = UserHasMissingRoles + canReadResourceDoc.toString
|
||||
)("", boxUser.map(_.userId).getOrElse(""), ApiRole.canReadResourceDoc :: Nil, cc1)
|
||||
} else {
|
||||
Future.successful(())
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
response <- Ok(result)
|
||||
} yield response
|
||||
}
|
||||
|
||||
// All routes combined
|
||||
|
||||
// All routes combined (without middleware - for direct use)
|
||||
val allRoutes: HttpRoutes[IO] =
|
||||
Kleisli[HttpF, Request[IO], Response[IO]] { req: Request[IO] =>
|
||||
root(req).orElse(getBanks(req)).orElse(getResourceDocsObpV700(req))
|
||||
root(req)
|
||||
.orElse(getBanks(req))
|
||||
.orElse(getCards(req))
|
||||
.orElse(getCardsForBank(req))
|
||||
.orElse(getResourceDocsObpV700(req))
|
||||
}
|
||||
|
||||
// Routes wrapped with ResourceDocMiddleware for automatic validation
|
||||
val allRoutesWithMiddleware: HttpRoutes[IO] =
|
||||
ResourceDocMiddleware.apply(resourceDocs)(allRoutes)
|
||||
}
|
||||
|
||||
val wrappedRoutesV700Services: HttpRoutes[IO] = CallContextMiddleware.withCallContext(Implementations7_0_0.allRoutes)
|
||||
// Routes with ResourceDocMiddleware - provides automatic validation based on ResourceDoc metadata
|
||||
// Authentication is automatic based on $AuthenticatedUserIsRequired in ResourceDoc errorResponseBodies
|
||||
// This matches Lift's wrappedWithAuthCheck behavior
|
||||
val wrappedRoutesV700Services: HttpRoutes[IO] = Implementations7_0_0.allRoutesWithMiddleware
|
||||
}
|
||||
|
||||
@ -6,20 +6,9 @@ import code.api.util.ErrorMessages.MandatoryPropertyIsNotSet
|
||||
import code.api.v4_0_0.{EnergySource400, HostedAt400, HostedBy400}
|
||||
import code.util.Helper.MdcLoggable
|
||||
import com.openbankproject.commons.util.ApiVersion
|
||||
import net.liftweb.util.Props
|
||||
|
||||
object JSONFactory700 extends MdcLoggable {
|
||||
|
||||
// Get git commit from build info
|
||||
lazy val gitCommit: String = {
|
||||
val commit = try {
|
||||
Props.get("git.commit.id", "unknown")
|
||||
} catch {
|
||||
case _: Throwable => "unknown"
|
||||
}
|
||||
commit
|
||||
}
|
||||
|
||||
case class APIInfoJsonV700(
|
||||
version: String,
|
||||
version_status: String,
|
||||
@ -31,32 +20,31 @@ object JSONFactory700 extends MdcLoggable {
|
||||
hosted_by: HostedBy400,
|
||||
hosted_at: HostedAt400,
|
||||
energy_source: EnergySource400,
|
||||
resource_docs_requires_role: Boolean,
|
||||
message: String
|
||||
resource_docs_requires_role: Boolean
|
||||
)
|
||||
|
||||
def getApiInfoJSON(apiVersion: ApiVersion, message: String): APIInfoJsonV700 = {
|
||||
val organisation = APIUtil.getPropsValue("hosted_by.organisation", "TESOBE")
|
||||
val email = APIUtil.getPropsValue("hosted_by.email", "contact@tesobe.com")
|
||||
val phone = APIUtil.getPropsValue("hosted_by.phone", "+49 (0)30 8145 3994")
|
||||
val organisationWebsite = APIUtil.getPropsValue("organisation_website", "https://www.tesobe.com")
|
||||
def getApiInfoJSON(apiVersion: ApiVersion, apiVersionStatus: String): APIInfoJsonV700 = {
|
||||
val organisation = APIUtil.hostedByOrganisation
|
||||
val email = APIUtil.hostedByEmail
|
||||
val phone = APIUtil.hostedByPhone
|
||||
val organisationWebsite = APIUtil.organisationWebsite
|
||||
val hostedBy = new HostedBy400(organisation, email, phone, organisationWebsite)
|
||||
|
||||
val organisationHostedAt = APIUtil.getPropsValue("hosted_at.organisation", "")
|
||||
val organisationWebsiteHostedAt = APIUtil.getPropsValue("hosted_at.organisation_website", "")
|
||||
val organisationHostedAt = APIUtil.hostedAtOrganisation
|
||||
val organisationWebsiteHostedAt = APIUtil.hostedAtOrganisationWebsite
|
||||
val hostedAt = HostedAt400(organisationHostedAt, organisationWebsiteHostedAt)
|
||||
|
||||
val organisationEnergySource = APIUtil.getPropsValue("energy_source.organisation", "")
|
||||
val organisationWebsiteEnergySource = APIUtil.getPropsValue("energy_source.organisation_website", "")
|
||||
val organisationEnergySource = APIUtil.energySourceOrganisation
|
||||
val organisationWebsiteEnergySource = APIUtil.energySourceOrganisationWebsite
|
||||
val energySource = EnergySource400(organisationEnergySource, organisationWebsiteEnergySource)
|
||||
|
||||
val connector = code.api.Constant.CONNECTOR.openOrThrowException(s"$MandatoryPropertyIsNotSet. The missing prop is `connector` ")
|
||||
val resourceDocsRequiresRole = APIUtil.getPropsAsBoolValue("resource_docs_requires_role", false)
|
||||
val resourceDocsRequiresRole = APIUtil.resourceDocsRequiresRole
|
||||
|
||||
APIInfoJsonV700(
|
||||
version = apiVersion.vDottedApiVersion,
|
||||
version_status = "BLEEDING_EDGE",
|
||||
git_commit = gitCommit,
|
||||
version_status = apiVersionStatus,
|
||||
git_commit = APIUtil.gitCommit,
|
||||
connector = connector,
|
||||
hostname = Constant.HostName,
|
||||
stage = System.getProperty("run.mode"),
|
||||
@ -64,8 +52,7 @@ object JSONFactory700 extends MdcLoggable {
|
||||
hosted_by = hostedBy,
|
||||
hosted_at = hostedAt,
|
||||
energy_source = energySource,
|
||||
resource_docs_requires_role = resourceDocsRequiresRole,
|
||||
message = message
|
||||
resource_docs_requires_role = resourceDocsRequiresRole
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user