mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 11:06:49 +00:00
refactor/Refactor ResourceDocMiddleware: integrate JSON content type and improve validation DSL
- Ensure all responses (errors and successful) have JSON Content-Type - Replace repeated EitherT patterns with a clean Validation DSL (success/failure) - Add ValidationContext to accumulate user, bank, account, view, and counterparty entities - Add detailed comments for authentication, authorization, and entity validation steps - Simplify middleware logic while preserving original validation order and behavior
This commit is contained in:
parent
79ea9231a1
commit
1534831ff4
@ -1,27 +1,26 @@
|
||||
package code.api.util.http4s
|
||||
|
||||
import cats.data.{Kleisli, OptionT}
|
||||
import cats.data.{EitherT, 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.api.util.{APIUtil, CallContext, NewStyle}
|
||||
import code.util.Helper.MdcLoggable
|
||||
import com.openbankproject.commons.model._
|
||||
import net.liftweb.common.{Box, Empty, Full, Failure => LiftFailure}
|
||||
import net.liftweb.common.{Box, Empty, Full}
|
||||
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
|
||||
@ -29,18 +28,42 @@ import scala.language.higherKinds
|
||||
* 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{
|
||||
|
||||
object ResourceDocMiddleware extends MdcLoggable {
|
||||
|
||||
/** Type alias for http4s OptionT route effect */
|
||||
type HttpF[A] = OptionT[IO, A]
|
||||
type Middleware[F[_]] = HttpRoutes[F] => HttpRoutes[F]
|
||||
|
||||
/** Type alias for validation effect using EitherT */
|
||||
type Validation[A] = EitherT[IO, Response[IO], A]
|
||||
|
||||
/** JSON content type for responses */
|
||||
private val jsonContentType: `Content-Type` = `Content-Type`(MediaType.application.json)
|
||||
|
||||
|
||||
/**
|
||||
* Context that accumulates all validated entities during request processing.
|
||||
* This context is passed along the validation chain.
|
||||
*/
|
||||
final case class ValidationContext(
|
||||
user: Box[User] = Empty,
|
||||
callContext: CallContext,
|
||||
bank: Option[Bank] = None,
|
||||
account: Option[BankAccount] = None,
|
||||
view: Option[View] = None,
|
||||
counterparty: Option[CounterpartyTrait] = None
|
||||
)
|
||||
|
||||
/** Simple DSL for success/failure in the validation chain */
|
||||
object DSL {
|
||||
def success[A](a: A): Validation[A] = EitherT.rightT(a)
|
||||
def failure(resp: Response[IO]): Validation[Nothing] = EitherT.leftT(resp)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if ResourceDoc requires authentication.
|
||||
*
|
||||
*
|
||||
* Authentication is required if:
|
||||
* - ResourceDoc errorResponseBodies contains $AuthenticatedUserIsRequired
|
||||
* - ResourceDoc has roles (roles always require authenticated user)
|
||||
@ -53,241 +76,203 @@ object ResourceDocMiddleware extends MdcLoggable{
|
||||
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
|
||||
* Middleware factory: wraps HttpRoutes with ResourceDoc validation.
|
||||
* Finds the matching ResourceDoc, validates the request, and enriches CallContext.
|
||||
*/
|
||||
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)))
|
||||
def apply(resourceDocs: ArrayBuffer[ResourceDoc]): HttpRoutes[IO] => HttpRoutes[IO] = { routes =>
|
||||
Kleisli[HttpF, Request[IO], Response[IO]] { req: Request[IO] =>
|
||||
// Build initial CallContext from request
|
||||
OptionT.liftF(Http4sCallContextBuilder.fromRequest(req, "v7.0.0")).flatMap { cc =>
|
||||
ResourceDocMatcher.findResourceDoc(req.method.name, req.uri.path, resourceDocs) match {
|
||||
case Some(resourceDoc) =>
|
||||
val ccWithDoc = ResourceDocMatcher.attachToCallContext(cc, resourceDoc)
|
||||
val pathParams = ResourceDocMatcher.extractPathParams(req.uri.path, resourceDoc)
|
||||
// Run full validation chain
|
||||
OptionT(validateRequest(req, resourceDoc, pathParams, ccWithDoc, routes).map(Option(_)))
|
||||
|
||||
case None =>
|
||||
// No matching ResourceDoc: fallback to original route
|
||||
routes.run(req)
|
||||
}
|
||||
}
|
||||
} yield response
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure response has JSON content type.
|
||||
* Executes the full validation chain for the request.
|
||||
* Returns either an error Response or enriched request routed to the handler.
|
||||
*/
|
||||
private def validateRequest(
|
||||
req: Request[IO],
|
||||
resourceDoc: ResourceDoc,
|
||||
pathParams: Map[String, String],
|
||||
cc: CallContext,
|
||||
routes: HttpRoutes[IO]
|
||||
): IO[Response[IO]] = {
|
||||
|
||||
// Initial context with just CallContext
|
||||
val initialCtx = ValidationContext(callContext = cc)
|
||||
|
||||
// Compose all validation steps using EitherT
|
||||
val result: Validation[ValidationContext] = for {
|
||||
ctx1 <- authenticate(req, resourceDoc, initialCtx)
|
||||
ctx2 <- authorizeRoles(resourceDoc, pathParams, ctx1)
|
||||
ctx3 <- validateBank(pathParams, ctx2)
|
||||
ctx4 <- validateAccount(pathParams, ctx3)
|
||||
ctx5 <- validateView(pathParams, ctx4)
|
||||
ctx6 <- validateCounterparty(pathParams, ctx5)
|
||||
} yield ctx6
|
||||
|
||||
// Convert Validation result to Response
|
||||
result.value.flatMap {
|
||||
case Left(errorResponse) => IO.pure(ensureJsonContentType(errorResponse)) // Ensure all error responses are JSON
|
||||
case Right(validCtx) =>
|
||||
// Enrich request with validated CallContext
|
||||
val enrichedReq = req.withAttribute(
|
||||
Http4sRequestAttributes.callContextKey,
|
||||
validCtx.callContext.copy(
|
||||
bank = validCtx.bank,
|
||||
bankAccount = validCtx.account,
|
||||
view = validCtx.view,
|
||||
counterparty = validCtx.counterparty
|
||||
)
|
||||
)
|
||||
routes.run(enrichedReq)
|
||||
.map(ensureJsonContentType) // Ensure routed response has JSON content type
|
||||
.getOrElseF(IO.pure(ensureJsonContentType(Response[IO](org.http4s.Status.NotFound))))
|
||||
}
|
||||
}
|
||||
|
||||
/** Authentication step: verifies user and updates ValidationContext */
|
||||
private def authenticate(req: Request[IO], resourceDoc: ResourceDoc, ctx: ValidationContext): Validation[ValidationContext] = {
|
||||
val needsAuth = ResourceDocMiddleware.needsAuthentication(resourceDoc)
|
||||
logger.debug(s"[ResourceDocMiddleware] needsAuthentication for ${resourceDoc.partialFunctionName}: $needsAuth")
|
||||
|
||||
val io =
|
||||
if (needsAuth) IO.fromFuture(IO(APIUtil.authenticatedAccess(ctx.callContext)))
|
||||
else IO.fromFuture(IO(APIUtil.anonymousAccess(ctx.callContext)))
|
||||
|
||||
EitherT(
|
||||
io.attempt.flatMap {
|
||||
case Right((boxUser, Some(updatedCC))) =>
|
||||
IO.pure(Right(ctx.copy(user = boxUser, callContext = updatedCC)))
|
||||
case Right((boxUser, None)) =>
|
||||
IO.pure(Right(ctx.copy(user = boxUser)))
|
||||
case Left(e: APIFailureNewStyle) =>
|
||||
ErrorResponseConverter.createErrorResponse(e.failCode, e.failMsg, ctx.callContext).map(Left(_))
|
||||
case Left(_) =>
|
||||
ErrorResponseConverter.createErrorResponse(401, $AuthenticatedUserIsRequired, ctx.callContext).map(Left(_))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** Role authorization step: ensures user has required roles */
|
||||
private def authorizeRoles(resourceDoc: ResourceDoc, pathParams: Map[String, String], ctx: ValidationContext): Validation[ValidationContext] = {
|
||||
import DSL._
|
||||
|
||||
resourceDoc.roles match {
|
||||
case Some(roles) if roles.nonEmpty =>
|
||||
ctx.user match {
|
||||
case Full(user) =>
|
||||
val bankId = pathParams.getOrElse("BANK_ID", "")
|
||||
val ok = roles.exists { role =>
|
||||
val checkBankId = if (role.requiresBankId) bankId else ""
|
||||
APIUtil.hasEntitlement(checkBankId, user.userId, role)
|
||||
}
|
||||
if (ok) success(ctx)
|
||||
else EitherT[IO, Response[IO], ValidationContext](
|
||||
ErrorResponseConverter.createErrorResponse(403, UserHasMissingRoles + roles.mkString(", "), ctx.callContext)
|
||||
.map[Either[Response[IO], ValidationContext]](Left(_))
|
||||
)
|
||||
case _ =>
|
||||
EitherT[IO, Response[IO], ValidationContext](
|
||||
ErrorResponseConverter
|
||||
.createErrorResponse(401, $AuthenticatedUserIsRequired, ctx.callContext)
|
||||
.map[Either[Response[IO], ValidationContext]](resp => Left(resp))
|
||||
)
|
||||
}
|
||||
case _ => success(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
/** Bank validation: checks BANK_ID and fetches bank */
|
||||
private def validateBank(pathParams: Map[String, String], ctx: ValidationContext): Validation[ValidationContext] = {
|
||||
|
||||
pathParams.get("BANK_ID") match {
|
||||
case Some(bankId) =>
|
||||
EitherT(
|
||||
IO.fromFuture(IO(NewStyle.function.getBank(BankId(bankId), Some(ctx.callContext))))
|
||||
.attempt.flatMap {
|
||||
case Right((bank, Some(updatedCC))) => IO.pure(Right(ctx.copy(bank = Some(bank), callContext = updatedCC)))
|
||||
case Right((bank, None)) => IO.pure(Right(ctx.copy(bank = Some(bank))))
|
||||
case Left(e: APIFailureNewStyle) => ErrorResponseConverter.createErrorResponse(e.failCode, e.failMsg, ctx.callContext).map(Left(_))
|
||||
case Left(_) => ErrorResponseConverter.createErrorResponse(404, BankNotFound + s": $bankId", ctx.callContext).map(Left(_))
|
||||
}
|
||||
)
|
||||
case None => DSL.success(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
/** Account validation: checks ACCOUNT_ID and fetches bank account */
|
||||
private def validateAccount(pathParams: Map[String, String], ctx: ValidationContext): Validation[ValidationContext] = {
|
||||
|
||||
(pathParams.get("BANK_ID"), pathParams.get("ACCOUNT_ID")) match {
|
||||
case (Some(bankId), Some(accountId)) =>
|
||||
EitherT(
|
||||
IO.fromFuture(IO(NewStyle.function.getBankAccount(BankId(bankId), AccountId(accountId), Some(ctx.callContext))))
|
||||
.attempt.flatMap {
|
||||
case Right((acc, Some(updatedCC))) => IO.pure(Right(ctx.copy(account = Some(acc), callContext = updatedCC)))
|
||||
case Right((acc, None)) => IO.pure(Right(ctx.copy(account = Some(acc))))
|
||||
case Left(e: APIFailureNewStyle) => ErrorResponseConverter.createErrorResponse(e.failCode, e.failMsg, ctx.callContext).map(Left(_))
|
||||
case Left(_) => ErrorResponseConverter.createErrorResponse(404, BankAccountNotFound + s": bankId=$bankId, accountId=$accountId", ctx.callContext).map(Left(_))
|
||||
}
|
||||
)
|
||||
case _ => DSL.success(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
/** View validation: checks VIEW_ID and user access */
|
||||
private def validateView(pathParams: Map[String, String], ctx: ValidationContext): Validation[ValidationContext] = {
|
||||
|
||||
(pathParams.get("BANK_ID"), pathParams.get("ACCOUNT_ID"), pathParams.get("VIEW_ID")) match {
|
||||
case (Some(bankId), Some(accountId), Some(viewId)) =>
|
||||
EitherT(
|
||||
IO.fromFuture(IO(ViewNewStyle.checkViewAccessAndReturnView(ViewId(viewId), BankIdAccountId(BankId(bankId), AccountId(accountId)), ctx.user.toOption, Some(ctx.callContext))))
|
||||
.attempt.flatMap {
|
||||
case Right(view) => IO.pure(Right(ctx.copy(view = Some(view))))
|
||||
case Left(e: APIFailureNewStyle) => ErrorResponseConverter.createErrorResponse(e.failCode, e.failMsg, ctx.callContext).map(Left(_))
|
||||
case Left(_) => ErrorResponseConverter.createErrorResponse(403, UserNoPermissionAccessView + s": viewId=$viewId", ctx.callContext).map(Left(_))
|
||||
}
|
||||
)
|
||||
case _ => DSL.success(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
/** Counterparty validation: checks COUNTERPARTY_ID and fetches counterparty */
|
||||
private def validateCounterparty(pathParams: Map[String, String], ctx: ValidationContext): Validation[ValidationContext] = {
|
||||
|
||||
(pathParams.get("BANK_ID"), pathParams.get("ACCOUNT_ID"), pathParams.get("COUNTERPARTY_ID")) match {
|
||||
case (Some(bankId), Some(accountId), Some(counterpartyId)) =>
|
||||
EitherT(
|
||||
IO.fromFuture(IO(NewStyle.function.getCounterpartyTrait(BankId(bankId), AccountId(accountId), counterpartyId, Some(ctx.callContext))))
|
||||
.attempt.flatMap {
|
||||
case Right((cp, Some(updatedCC))) => IO.pure(Right(ctx.copy(counterparty = Some(cp), callContext = updatedCC)))
|
||||
case Right((cp, None)) => IO.pure(Right(ctx.copy(counterparty = Some(cp))))
|
||||
case Left(e: APIFailureNewStyle) => ErrorResponseConverter.createErrorResponse(e.failCode, e.failMsg, ctx.callContext).map(Left(_))
|
||||
case Left(_) => ErrorResponseConverter.createErrorResponse(404, CounterpartyNotFound + s": counterpartyId=$counterpartyId", ctx.callContext).map(Left(_))
|
||||
}
|
||||
)
|
||||
case _ => DSL.success(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
/** Ensure the 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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user