From dbd046bf7c5bf6ffa791b95a54e5650ff6d4051d Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 22 Jan 2026 14:36:58 +0100 Subject: [PATCH] refactor(http4s): enhance CallContext extraction and validation chain - Add withCallContext helper method to Http4sSupport for simplified endpoint code - Document use of http4s Vault for type-safe request attributes storage - Clarify that validated entities (bank, bankAccount, view, counterparty) are stored within CallContext - Reorder validation chain in ResourceDocMiddleware to check roles before entity validation - Add special handling for resource-docs endpoint with configurable role requirement - Extract runValidationChain method to support both middleware and endpoint wrapping patterns - Improve authentication error handling with better Box pattern matching - Add comprehensive documentation and usage examples for CallContext extraction - Enhance logging for validation chain execution and debugging --- .../code/api/util/http4s/Http4sSupport.scala | 31 +++ .../util/http4s/ResourceDocMiddleware.scala | 197 +++++++++++++++++- .../scala/code/api/v7_0_0/Http4s700.scala | 185 +++++++--------- 3 files changed, 291 insertions(+), 122 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala b/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala index d17126bc6..39d5ad0ae 100644 --- a/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala +++ b/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala @@ -34,8 +34,12 @@ import scala.language.higherKinds /** * Request attribute keys for storing CallContext in http4s requests. * + * Note: Uses http4s Vault (org.typelevel.vault.Key) for type-safe request attributes. + * Validated entities (bank, bankAccount, view, counterparty) are stored within CallContext itself. */ object Http4sRequestAttributes { + import org.typelevel.vault.Key + // CallContext contains all request data and validated entities val callContextKey: Key[CallContext] = Key.newKey[IO, CallContext].unsafeRunSync()(cats.effect.unsafe.IORuntime.global) @@ -46,6 +50,33 @@ object Http4sRequestAttributes { */ def getCallContext(req: Request[IO]): Option[CallContext] = req.attributes.lookup(callContextKey) + + /** + * Helper method to extract CallContext from http4s Request and execute business logic. + * Simplifies endpoint code by handling the common pattern of extracting CallContext. + * + * Usage example: + * {{{ + * val myEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] { + * case req @ GET -> Root / "banks" => + * withCallContext(req) { cc => + * for { + * result <- yourBusinessLogic(cc) + * response <- Ok(result) + * } yield response + * } + * } + * }}} + * + * @param req The http4s request + * @param f Function that takes CallContext and returns IO[Response] + * @return IO[Response[IO]] + */ + def withCallContext(req: Request[IO])(f: CallContext => IO[Response[IO]]): IO[Response[IO]] = { + IO.fromOption(req.attributes.lookup(callContextKey))( + new RuntimeException("CallContext not found in request attributes") + ).flatMap(f) + } } /** diff --git a/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala b/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala index 74d11eef5..42d9b466d 100644 --- a/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala +++ b/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala @@ -23,10 +23,10 @@ import scala.language.higherKinds * * VALIDATION ORDER: * 1. Authentication first - * 2. BANK_ID validation (if present in path) - * 3. ACCOUNT_ID validation (if present in path) - * 4. VIEW_ID validation (if present in path) - * 5. Role authorization (if roles specified in ResourceDoc) + * 2. Roles authorization (if roles specified in ResourceDoc) + * 3. BANK_ID validation (if present in path) + * 4. ACCOUNT_ID validation (if present in path) + * 5. VIEW_ID validation (if present in path) * 6. COUNTERPARTY_ID validation (if present in path) */ object ResourceDocMiddleware extends MdcLoggable{ @@ -36,15 +36,20 @@ object ResourceDocMiddleware extends MdcLoggable{ private val jsonContentType: `Content-Type` = `Content-Type`(MediaType.application.json) /** - * Check if ResourceDoc requires authentication based on errorResponseBodies + * Check if ResourceDoc requires authentication based on errorResponseBodies or property */ private def needsAuthentication(resourceDoc: ResourceDoc): Boolean = { - // Roles always require an authenticated user to validate entitlements - resourceDoc.errorResponseBodies.contains($AuthenticatedUserIsRequired) || resourceDoc.roles.exists(_.nonEmpty) + // Special handling for resource-docs endpoint + if (resourceDoc.partialFunctionName == "getResourceDocsObpV700") { + APIUtil.getPropsAsBoolValue("resource_docs_requires_role", false) + } else { + // Standard check: roles always require an authenticated user to validate entitlements + resourceDoc.errorResponseBodies.contains($AuthenticatedUserIsRequired) || resourceDoc.roles.exists(_.nonEmpty) + } } /** - * Create middleware that applies ResourceDoc-driven validation + * Create middleware that applies ResourceDoc-driven validation to standard HttpRoutes */ def apply(resourceDocs: ArrayBuffer[ResourceDoc]): Middleware[IO] = { routes => Kleisli[HttpF, Request[IO], Response[IO]] { req => @@ -67,7 +72,7 @@ object ResourceDocMiddleware extends MdcLoggable{ case Some(resourceDoc) => val ccWithDoc = ResourceDocMatcher.attachToCallContext(cc, resourceDoc) val pathParams = ResourceDocMatcher.extractPathParams(req.uri.path, resourceDoc) - runValidationChain(req, resourceDoc, ccWithDoc, pathParams, routes) + runValidationChainForRoutes(req, resourceDoc, ccWithDoc, pathParams, routes) .map(ensureJsonContentType) case None => routes.run(req).getOrElseF(IO.pure(Response[IO](org.http4s.Status.NotFound))) @@ -83,9 +88,181 @@ object ResourceDocMiddleware extends MdcLoggable{ } /** - * Run the validation chain in order: auth → bank → account → view → roles → counterparty + * Run validation chain and return enriched CallContext. + * Used by wrapEndpoint to validate and enrich CallContext before passing to endpoint. */ private def runValidationChain( + resourceDoc: ResourceDoc, + cc: CallContext, + pathParams: Map[String, String] + ): IO[CallContext] = { + 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[Throwable, (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 => + IO.pure(Left(new RuntimeException($AuthenticatedUserIsRequired))) + case LiftFailure(msg, _, _) => + IO.pure(Left(new RuntimeException(msg))) + } + case Left(e: APIFailureNewStyle) => + IO.pure(Left(e)) + case Left(e) => + IO.pure(Left(new RuntimeException($AuthenticatedUserIsRequired))) + } + } 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 access, continue with Empty user + IO.pure(Right((Empty, cc))) + } + } + + authResult.flatMap { + case Left(error) => IO.raiseError(error) + case Right((boxUser, cc1)) => + // Step 2: Role authorization + val rolesResult: IO[Either[Throwable, CallContext]] = + resourceDoc.roles match { + case Some(roles) if roles.nonEmpty => + val shouldCheckRoles = if (resourceDoc.partialFunctionName == "getResourceDocsObpV700") { + APIUtil.getPropsAsBoolValue("resource_docs_requires_role", false) + } else { + true + } + + if (shouldCheckRoles) { + 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 IO.pure(Left(new RuntimeException(UserHasMissingRoles + roles.mkString(", ")))) + case _ => + IO.pure(Left(new RuntimeException($AuthenticatedUserIsRequired))) + } + } else { + IO.pure(Right(cc1)) + } + case _ => IO.pure(Right(cc1)) + } + + rolesResult.flatMap { + case Left(error) => IO.raiseError(error) + case Right(cc2) => + // Step 3: Bank validation + val bankResult: IO[Either[Throwable, (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) => + IO.pure(Left(e)) + case Left(e) => + IO.pure(Left(new RuntimeException(BankNotFound + ": " + bankIdStr))) + } + case None => IO.pure(Right((None, cc2))) + } + + bankResult.flatMap { + case Left(error) => IO.raiseError(error) + case Right((bankOpt, cc3)) => + // Step 4: Account validation + val accountResult: IO[Either[Throwable, (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) => + IO.pure(Left(e)) + case Left(e) => + IO.pure(Left(new RuntimeException(BankAccountNotFound + s": bankId=$bankIdStr, accountId=$accountIdStr"))) + } + case _ => IO.pure(Right((None, cc3))) + } + + accountResult.flatMap { + case Left(error) => IO.raiseError(error) + case Right((accountOpt, cc4)) => + // Step 5: View validation + val viewResult: IO[Either[Throwable, (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) => + IO.pure(Left(e)) + case Left(e) => + IO.pure(Left(new RuntimeException(UserNoPermissionAccessView + s": viewId=$viewIdStr"))) + } + case _ => IO.pure(Right((None, cc4))) + } + + viewResult.flatMap { + case Left(error) => IO.raiseError(error) + case Right((viewOpt, cc5)) => + // Step 6: Counterparty validation + val counterpartyResult: IO[Either[Throwable, (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) => + IO.pure(Left(e)) + case Left(e) => + IO.pure(Left(new RuntimeException(CounterpartyNotFound + s": counterpartyId=$counterpartyIdStr"))) + } + case _ => IO.pure(Right((None, cc5))) + } + + counterpartyResult.flatMap { + case Left(error) => IO.raiseError(error) + case Right((counterpartyOpt, finalCC)) => + // All validations passed - return enriched CallContext + val enrichedCC = finalCC.copy( + bank = bankOpt, + bankAccount = accountOpt, + view = viewOpt, + counterparty = counterpartyOpt + ) + IO.pure(enrichedCC) + } + } + } + } + } + } + } + + /** + * Run validation chain for standard HttpRoutes (returns Response). + * Used by apply() middleware for backward compatibility. + */ + private def runValidationChainForRoutes( req: Request[IO], resourceDoc: ResourceDoc, cc: CallContext, 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 09fc31bad..9811b61c7 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 @@ -105,15 +105,16 @@ object Http4s700 { // Route: GET /obp/v7.0.0/banks val getBanks: HttpRoutes[IO] = HttpRoutes.of[IO] { case req @ GET -> `prefixPath` / "banks" => - val response = for { - 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(Some(cc)) - } yield convertAnyToJsonString(JSONFactory400.createBanksJson(banks)) - }) - } yield result - Ok(response) + Http4sRequestAttributes.withCallContext(req) { cc => + for { + result <- IO.fromFuture(IO { + for { + (banks, callContext) <- NewStyle.function.getBanks(Some(cc)) + } yield convertAnyToJsonString(JSONFactory400.createBanksJson(banks)) + }) + response <- Ok(result) + } yield response + } } resourceDocs += ResourceDoc( @@ -135,16 +136,17 @@ object Http4s700 { // 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(cc.user.toOption)(new RuntimeException("User not found in CallContext")) - 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) + Http4sRequestAttributes.withCallContext(req) { cc => + for { + user <- IO.fromOption(cc.user.toOption)(new RuntimeException("User not found in CallContext")) + result <- IO.fromFuture(IO { + for { + (cards, callContext) <- NewStyle.function.getPhysicalCardsForUser(user, Some(cc)) + } yield convertAnyToJsonString(JSONFactory1_3_0.createPhysicalCardsJSON(cards, user)) + }) + response <- Ok(result) + } yield response + } } resourceDocs += ResourceDoc( @@ -167,19 +169,20 @@ object Http4s700 { // 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(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 { - 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) + Http4sRequestAttributes.withCallContext(req) { cc => + 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 { + 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)) + }) + response <- Ok(result) + } yield response + } } resourceDocs += ResourceDoc( @@ -217,88 +220,47 @@ object Http4s700 { val getResourceDocsObpV700: HttpRoutes[IO] = HttpRoutes.of[IO] { case req @ GET -> `prefixPath` / "resource-docs" / requestedApiVersionString / "obp" => import com.openbankproject.commons.ExecutionContext.Implicits.global - val response = for { - 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) - - for { - // Authentication based on property - (boxUser, cc1) <- if (resourceDocsRequireRole) - authenticatedAccess(cc) - else - anonymousAccess(cc) + Http4sRequestAttributes.withCallContext(req) { cc => + for { + result <- IO.fromFuture(IO { + // Check resource_docs_requires_role property + val resourceDocsRequireRole = getPropsAsBoolValue("resource_docs_requires_role", false) - // 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) - }) - } yield result - Ok(response) + 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 + } } - - // 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(getCounterpartyByIdWithMiddleware), -// "GET", -// "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/counterparties/COUNTERPARTY_ID", -// "Get Counterparty by Id (http4s with middleware)", -// s"""Get counterparty by id with automatic validation via ResourceDocMiddleware. -// | -// |This endpoint demonstrates the COMPLETE 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) -// |* Counterparty existence validation (COUNTERPARTY_ID in path) -// | -// |${userAuthenticationMessage(true)}""", -// EmptyBody, -// moderatedAccountJSON, -// List(AuthenticatedUserIsRequired, BankNotFound, BankAccountNotFound, ViewNotFound, UserNoPermissionAccessView, CounterpartyNotFound, UnknownError), -// apiTagCounterparty :: Nil, -// http4sPartialFunction = Some(getCounterpartyByIdWithMiddleware) -// ) - -// // Route: GET /obp/v7.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/counterparties/COUNTERPARTY_ID -// // When used with ResourceDocMiddleware, validation is automatic -// val getCounterpartyByIdWithMiddleware: HttpRoutes[IO] = HttpRoutes.of[IO] { -// case req @ GET -> `prefixPath` / "banks" / bankId / "accounts" / accountId / viewId / "counterparties" / counterpartyId => -// val responseJson = convertAnyToJsonString( -// Map( -// "bank_id" -> bankId, -// "account_id" -> accountId, -// "view_id" -> viewId, -// "counterparty_id" -> counterpartyId -// ) -// ) -// Ok(responseJson) -// } // All routes combined (without middleware - for direct use) val allRoutes: HttpRoutes[IO] = @@ -308,7 +270,6 @@ object Http4s700 { .orElse(getCards(req)) .orElse(getCardsForBank(req)) .orElse(getResourceDocsObpV700(req)) -// .orElse(getAccountByIdWithMiddleware(req)) } // Routes wrapped with ResourceDocMiddleware for automatic validation