refactor/tweaked code

This commit is contained in:
hongwei 2026-01-21 14:58:52 +01:00
parent ef6bff5698
commit f73ad667b9
2 changed files with 85 additions and 57 deletions

View File

@ -3,14 +3,12 @@ package code.api.util.http4s
import cats.data.{Kleisli, OptionT}
import cats.effect._
import code.api.APIFailureNewStyle
import code.util.Helper.MdcLoggable
import code.api.util.APIUtil
import code.api.util.APIUtil.ResourceDoc
import code.api.util.ErrorMessages._
import code.api.util.NewStyle
import code.api.util.{APIUtil, CallContext, NewStyle}
import code.api.util.newstyle.ViewNewStyle
import code.api.util.CallContext
import com.openbankproject.commons.model.{Bank, BankAccount, BankId, AccountId, ViewId, BankIdAccountId, CounterpartyTrait, User, View}
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`
@ -90,17 +88,16 @@ object ResourceDocMiddleware extends MdcLoggable{
private def runValidationChain(
req: Request[IO],
resourceDoc: ResourceDoc,
cc: SharedCallContext,
cc: CallContext,
pathParams: Map[String, String],
routes: HttpRoutes[IO]
): IO[Response[IO]] = {
import com.openbankproject.commons.ExecutionContext.Implicits.global
// Step 1: Authentication
val needsAuth = needsAuthentication(resourceDoc)
logger.debug(s"[ResourceDocMiddleware] needsAuthentication for ${resourceDoc.partialFunctionName}: $needsAuth")
val authResult: IO[Either[Response[IO], (Box[User], SharedCallContext)]] =
val authResult: IO[Either[Response[IO], (Box[User], CallContext)]] =
if (needsAuth) {
IO.fromFuture(IO(APIUtil.authenticatedAccess(cc))).attempt.flatMap {
case Right((boxUser, optCC)) =>
@ -149,7 +146,7 @@ object ResourceDocMiddleware extends MdcLoggable{
case Left(errorResponse) => IO.pure(errorResponse)
case Right((boxUser, cc1)) =>
// Step 2: Role authorization - BEFORE business logic validation
val rolesResult: IO[Either[Response[IO], SharedCallContext]] =
val rolesResult: IO[Either[Response[IO], CallContext]] =
resourceDoc.roles match {
case Some(roles) if roles.nonEmpty =>
boxUser match {
@ -172,7 +169,7 @@ object ResourceDocMiddleware extends MdcLoggable{
case Left(errorResponse) => IO.pure(errorResponse)
case Right(cc2) =>
// Step 3: Bank validation
val bankResult: IO[Either[Response[IO], (Option[Bank], SharedCallContext)]] =
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 {

View File

@ -6,11 +6,12 @@ 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.canReadResourceDoc
import code.api.util.ApiRole.{canGetCardsForBank, canReadResourceDoc}
import code.api.util.ApiTag._
import code.api.util.ErrorMessages._
import code.api.util.http4s.{Http4sCallContextBuilder, ResourceDocMiddleware}
import code.api.util.http4s.{Http4sRequestAttributes, ResourceDocMiddleware}
import code.api.util.{ApiRole, ApiVersionUtils, 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
@ -41,7 +42,7 @@ object Http4s700 {
// Common prefix: /obp/v7.0.0
val prefixPath = Root / ApiPathZero.toString / implementedInApiVersion.toString
// ResourceDoc with $AuthenticatedUserIsRequired in errorResponseBodies indicates auth is required
// 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(
@ -60,15 +61,14 @@ object Http4s700 {
EmptyBody,
apiInfoJSON,
List(
UnknownError,
"no connector set"
UnknownError
),
apiTagApi :: Nil,
http4sPartialFunction = Some(root)
)
// Route: GET /obp/v7.0.0/root
// Authentication is handled automatically by ResourceDocMiddleware based on $AuthenticatedUserIsRequired in ResourceDoc
// 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" =>
@ -106,16 +106,82 @@ object Http4s700 {
val getBanks: HttpRoutes[IO] = HttpRoutes.of[IO] {
case req @ GET -> `prefixPath` / "banks" =>
val response = for {
cc <- Http4sCallContextBuilder.fromRequest(req, implementedInApiVersion.toString)
cc <- IO.fromOption(req.attributes.lookup(Http4sRequestAttributes.callContextKey))(new RuntimeException("CallContext not found in request attributes"))
result <- IO.fromFuture(IO {
for {
(banks, callContext) <- NewStyle.function.getBanks(cc.callContext)
(banks, callContext) <- NewStyle.function.getBanks(Some(cc))
} yield convertAnyToJsonString(JSONFactory400.createBanksJson(banks))
})
} yield result
Ok(response)
}
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" =>
val response = for {
cc <- IO.fromOption(req.attributes.lookup(Http4sRequestAttributes.callContextKey))(new RuntimeException("CallContext not found in request attributes"))
user <- IO.fromOption(req.attributes.lookup(Http4sRequestAttributes.userKey))(new RuntimeException("User not found in request attributes"))
result <- IO.fromFuture(IO {
for {
(cards, callContext) <- NewStyle.function.getPhysicalCardsForUser(user, Some(cc))
} yield convertAnyToJsonString(JSONFactory1_3_0.createPhysicalCardsJSON(cards, user))
})
} yield result
Ok(response)
}
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" =>
val response = for {
cc <- IO.fromOption(req.attributes.lookup(Http4sRequestAttributes.callContextKey))(new RuntimeException("CallContext not found in request attributes"))
user <- IO.fromOption(req.attributes.lookup(Http4sRequestAttributes.userKey))(new RuntimeException("User not found in request attributes"))
bank <- IO.fromOption(req.attributes.lookup(Http4sRequestAttributes.bankKey))(new RuntimeException("Bank not found in request attributes"))
result <- IO.fromFuture(IO {
for {
httpParams <- NewStyle.function.extractHttpParamsFromUrl(req.uri.renderString)
(obpQueryParams, callContext) <- createQueriesByHttpParamsFuture(httpParams, Some(cc))
(cards, callContext) <- NewStyle.function.getPhysicalCardsForBank(bank, user, obpQueryParams, callContext)
} yield convertAnyToJsonString(JSONFactory1_3_0.createPhysicalCardsJSON(cards, user))
})
} yield result
Ok(response)
}
resourceDocs += ResourceDoc(
null,
implementedInApiVersion,
@ -152,7 +218,7 @@ object Http4s700 {
case req @ GET -> `prefixPath` / "resource-docs" / requestedApiVersionString / "obp" =>
import com.openbankproject.commons.ExecutionContext.Implicits.global
val response = for {
cc <- Http4sCallContextBuilder.fromRequest(req, implementedInApiVersion.toString)
cc <- IO.fromOption(req.attributes.lookup(Http4sRequestAttributes.callContextKey))(new RuntimeException("CallContext not found in request attributes"))
result <- IO.fromFuture(IO {
// Check resource_docs_requires_role property
val resourceDocsRequireRole = getPropsAsBoolValue("resource_docs_requires_role", false)
@ -194,42 +260,6 @@ object Http4s700 {
// Example endpoint demonstrating full validation chain with ResourceDocMiddleware
// This endpoint requires: authentication + bank validation + account validation + view validation
// When using ResourceDocMiddleware, these validations are automatic based on path parameters
resourceDocs += ResourceDoc(
null,
implementedInApiVersion,
nameOf(getAccountByIdWithMiddleware),
"GET",
"/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/account",
"Get Account by Id (http4s with middleware)",
s"""Get account by id with automatic validation via ResourceDocMiddleware.
|
|This endpoint demonstrates the full validation chain:
|* Authentication (required)
|* Bank existence validation (BANK_ID in path)
|* Account existence validation (ACCOUNT_ID in path)
|* View access validation (VIEW_ID in path)
|
|${userAuthenticationMessage(true)}""",
EmptyBody,
moderatedAccountJSON,
List(AuthenticatedUserIsRequired, BankNotFound, BankAccountNotFound, ViewNotFound, UserNoPermissionAccessView, UnknownError),
apiTagAccount :: Nil,
http4sPartialFunction = Some(getAccountByIdWithMiddleware)
)
// Route: GET /obp/v7.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/account
// When used with ResourceDocMiddleware, validation is automatic
val getAccountByIdWithMiddleware: HttpRoutes[IO] = HttpRoutes.of[IO] {
case req @ GET -> `prefixPath` / "banks" / bankId / "accounts" / accountId / viewId / "account" =>
val responseJson = convertAnyToJsonString(
Map(
"bank_id" -> bankId,
"account_id" -> accountId,
"view_id" -> viewId
)
)
Ok(responseJson)
}
// resourceDocs += ResourceDoc(
// null,
@ -275,9 +305,10 @@ object Http4s700 {
Kleisli[HttpF, Request[IO], Response[IO]] { req: Request[IO] =>
root(req)
.orElse(getBanks(req))
.orElse(getCards(req))
.orElse(getCardsForBank(req))
.orElse(getResourceDocsObpV700(req))
.orElse(getAccountByIdWithMiddleware(req))
// .orElse(getCounterpartyByIdWithMiddleware(req))
// .orElse(getAccountByIdWithMiddleware(req))
}
// Routes wrapped with ResourceDocMiddleware for automatic validation