From 1abb0d216b73c9b5bb5073e1ddeb4e0d75ffd5ff Mon Sep 17 00:00:00 2001 From: Hongwei Date: Tue, 7 Jul 2026 11:36:31 +0200 Subject: [PATCH 1/4] fix(dates): make shared SimpleDateFormat instances thread-safe SimpleDateFormat's parse and format methods both mutate internal state; shared singletons silently return wrong dates under concurrency. Replace mutable shared instances with ThreadLocal-backed accessors on the hot paths (APIUtil, Helper), and per-call defs for low-frequency sites (ConsentScheduler, OBPDataImport). Dead code removal: delete DateFormatWithCurrentTimeZone, which mutated the shared format's timezone and is not called anywhere. Add unit test hammering parseObpStandardDate and format/re-parse round-trip from 32 concurrent threads to ensure all results are identical and correct. --- .../main/scala/code/api/util/APIUtil.scala | 27 +++++--- .../scala/code/sandbox/OBPDataImport.scala | 3 +- .../code/scheduler/ConsentScheduler.scala | 4 +- obp-api/src/main/scala/code/util/Helper.scala | 46 +++---------- .../api/util/DateFormatConcurrencyTest.scala | 68 +++++++++++++++++++ 5 files changed, 99 insertions(+), 49 deletions(-) create mode 100644 obp-api/src/test/scala/code/api/util/DateFormatConcurrencyTest.scala diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index b64e43559d..29c9be3cb1 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -151,16 +151,27 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ val DateWithMs = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" val DateWithMsAndTimeZoneOffset = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" - val DateWithYearFormat = new SimpleDateFormat(DateWithYear) - val DateWithMonthFormat = new SimpleDateFormat(DateWithMonth) - val DateWithDayFormat = new SimpleDateFormat(DateWithDay) - val DateWithSecondsFormat = new SimpleDateFormat(DateWithSeconds) + // SimpleDateFormat is not thread-safe (parse and format both mutate the internal Calendar). + // Give every thread its own instance so shared use across concurrent requests is safe, + // while avoiding a fresh allocation on the hot from_date/to_date parse path. + private val DateWithYearFormatTL = ThreadLocal.withInitial(() => new SimpleDateFormat(DateWithYear)) + private val DateWithMonthFormatTL = ThreadLocal.withInitial(() => new SimpleDateFormat(DateWithMonth)) + private val DateWithDayFormatTL = ThreadLocal.withInitial(() => new SimpleDateFormat(DateWithDay)) + private val DateWithSecondsFormatTL = ThreadLocal.withInitial(() => new SimpleDateFormat(DateWithSeconds)) + private val DateWithMsFormatTL = ThreadLocal.withInitial(() => new SimpleDateFormat(DateWithMs)) + private val DateWithMsRollbackFormatTL = ThreadLocal.withInitial(() => new SimpleDateFormat(DateWithMsAndTimeZoneOffset)) + private val rfc7231DateTL = ThreadLocal.withInitial(() => new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH)) + + def DateWithYearFormat: SimpleDateFormat = DateWithYearFormatTL.get() + def DateWithMonthFormat: SimpleDateFormat = DateWithMonthFormatTL.get() + def DateWithDayFormat: SimpleDateFormat = DateWithDayFormatTL.get() + def DateWithSecondsFormat: SimpleDateFormat = DateWithSecondsFormatTL.get() // If you need UTC Z format, please continue to use DateWithMsFormat. eg: 2025-01-01T01:01:01.000Z - val DateWithMsFormat = new SimpleDateFormat(DateWithMs) + def DateWithMsFormat: SimpleDateFormat = DateWithMsFormatTL.get() // If you need a format with timezone offset (+0000), please use DateWithMsRollbackFormat, eg: 2025-01-01T01:01:01.000+0000 - val DateWithMsRollbackFormat = new SimpleDateFormat(DateWithMsAndTimeZoneOffset) - - val rfc7231Date = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH) + def DateWithMsRollbackFormat: SimpleDateFormat = DateWithMsRollbackFormatTL.get() + + def rfc7231Date: SimpleDateFormat = rfc7231DateTL.get() val DateWithYearExampleString: String = "1100" val DateWithMonthExampleString: String = "1100-01" diff --git a/obp-api/src/main/scala/code/sandbox/OBPDataImport.scala b/obp-api/src/main/scala/code/sandbox/OBPDataImport.scala index 0024598c09..08d343d5eb 100644 --- a/obp-api/src/main/scala/code/sandbox/OBPDataImport.scala +++ b/obp-api/src/main/scala/code/sandbox/OBPDataImport.scala @@ -50,7 +50,8 @@ trait Saveable[T] { */ trait OBPDataImport extends MdcLoggable { val datePattern = APIUtil.DateWithMs - val dateFormat = new SimpleDateFormat(datePattern) + // SimpleDateFormat is not thread-safe; build a fresh instance per call (low frequency). + protected def dateFormat = new SimpleDateFormat(datePattern) type BankType <: Bank type AccountType <: BankAccount diff --git a/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala b/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala index c26ab4abc0..d44d222899 100644 --- a/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala +++ b/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala @@ -15,8 +15,8 @@ import scala.util.{Failure, Success, Try} object ConsentScheduler extends MdcLoggable { - val dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.ENGLISH) - def currentDate = dateFormat.format(new Date()) + // SimpleDateFormat is not thread-safe; build a fresh instance per call (low frequency). + def currentDate: String = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.ENGLISH).format(new Date()) // Starts multiple scheduled tasks with different intervals. // All tasks are enabled by default. Set the interval prop to 0 to disable a task. diff --git a/obp-api/src/main/scala/code/util/Helper.scala b/obp-api/src/main/scala/code/util/Helper.scala index 1d366bff60..1e7124778f 100644 --- a/obp-api/src/main/scala/code/util/Helper.scala +++ b/obp-api/src/main/scala/code/util/Helper.scala @@ -5,7 +5,7 @@ import code.api.cache.{Redis, RedisLogger} import java.net.{Socket, SocketException, URL} import java.util.UUID.randomUUID -import java.util.{Date, GregorianCalendar} +import java.util.Date import code.api.util.{APIUtil, CallContext, CallContextLight, CustomJsonFormats} import code.api.{APIFailureNewStyle, Constant} import code.api.util.APIUtil.fullBoxOrException @@ -14,7 +14,6 @@ import code.model.dataAccess.internalMapping.MappedAccountIdMappingProvider import code.transaction.internalMapping.MappedTransactionIdMappingProvider import net.liftweb.common._ import org.json4s.Extraction._ -import org.json4s.{DateFormat, Formats} import org.apache.commons.lang3.StringUtils import com.openbankproject.commons.ExecutionContext.Implicits.global import com.openbankproject.commons.model.{AccountBalance, AccountBalances, AccountHeld, AccountId, CoreAccount, Customer, CustomerId, Transaction, TransactionCore, TransactionId} @@ -209,39 +208,6 @@ object Helper extends Loggable { */ val matchAnyStoredProcedure = "stored_procedure.*|star".r - /** - * change the TimeZone to the current TimeZOne - * reference the following trait - * org.json4s - * trait DefaultFormats - * extends Formats - */ - //TODO need clean this format, we have set the TimeZone in boot.scala - val DateFormatWithCurrentTimeZone = new Formats { - - import java.text.{ParseException, SimpleDateFormat} - - val dateFormat = new DateFormat { - def parse(s: String) = try { - Some(formatter.parse(s)) - } catch { - case e: ParseException => None - } - - def format(d: Date) = formatter.format(d) - - def timezone: java.util.TimeZone = new GregorianCalendar().getTimeZone - - private def formatter = { - val f = dateFormatter - f.setTimeZone(new GregorianCalendar().getTimeZone) - f - } - } - - protected def dateFormatter = APIUtil.DateWithMsFormat - } - def getHostname(): String = { Constant.HostName match { case s: String if s.nonEmpty => s.split(":").lift(1) match { @@ -303,11 +269,15 @@ object Helper extends Loggable { private val underlyingLogger = net.liftweb.common.Logger(loggerName) - private val dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ssX") - dateFormat.setTimeZone(java.util.TimeZone.getDefault) // force local TZ + // SimpleDateFormat is not thread-safe; one instance per thread. + private val dateFormatTL = ThreadLocal.withInitial(() => { + val f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ssX") + f.setTimeZone(java.util.TimeZone.getDefault) // force local TZ + f + }) private def toRedisFormat(msg: AnyRef): String = { - val ts = dateFormat.format(new Date()) + val ts = dateFormatTL.get().format(new Date()) val thread = Thread.currentThread().getName s"[$ts] [$thread] [$clazzName] ${msg.toString}" } diff --git a/obp-api/src/test/scala/code/api/util/DateFormatConcurrencyTest.scala b/obp-api/src/test/scala/code/api/util/DateFormatConcurrencyTest.scala new file mode 100644 index 0000000000..6d95889810 --- /dev/null +++ b/obp-api/src/test/scala/code/api/util/DateFormatConcurrencyTest.scala @@ -0,0 +1,68 @@ +package code.api.util + +import java.util.Date +import java.util.concurrent.{CountDownLatch, Executors, TimeUnit} + +import net.liftweb.common.Full +import org.scalatest.{FlatSpec, Matchers} + +import scala.collection.JavaConverters._ + +/** + * SimpleDateFormat is not thread-safe: parse and format both mutate the internal Calendar, + * so a shared instance silently produces wrong dates (or throws) under concurrency. + * APIUtil keeps its shared formats in ThreadLocals; these tests hammer the two hot paths + * (from_date/to_date parsing and response formatting) from many threads and assert every + * result is present and identical. + */ +class DateFormatConcurrencyTest extends FlatSpec with Matchers { + + private val threads = 32 + private val iterationsPerThread = 200 + + private def hammer[T](task: () => T): List[T] = { + val pool = Executors.newFixedThreadPool(threads) + val startGate = new CountDownLatch(1) + val results = new java.util.concurrent.ConcurrentLinkedQueue[T]() + try { + (1 to threads).foreach { _ => + pool.submit(new Runnable { + override def run(): Unit = { + startGate.await() + (1 to iterationsPerThread).foreach(_ => results.add(task())) + } + }) + } + startGate.countDown() + pool.shutdown() + pool.awaitTermination(60, TimeUnit.SECONDS) shouldBe true + results.asScala.toList + } finally { + pool.shutdownNow() + } + } + + "APIUtil.parseObpStandardDate" should "return the same Full(date) from every concurrent call" in { + val input = "2100-01-01T01:01:01.000Z" + val expected = APIUtil.parseObpStandardDate(input) + expected shouldBe a[Full[_]] + + val results = hammer(() => APIUtil.parseObpStandardDate(input)) + results should have size (threads * iterationsPerThread) + all(results) shouldBe expected + } + + "APIUtil.DateWithMsFormat" should "format and re-parse to the same instant from every concurrent call" in { + val instant = new Date(4102448461000L) // fixed instant, avoids per-run drift + val expected = APIUtil.DateWithMsFormat.format(instant) + + val results = hammer { () => + val formatted = APIUtil.DateWithMsFormat.format(instant) + val roundTripped = APIUtil.DateWithMsFormat.parse(formatted) + (formatted, roundTripped) + } + results should have size (threads * iterationsPerThread) + all(results.map(_._1)) shouldBe expected + all(results.map(_._2.getTime)) shouldBe instant.getTime + } +} From 54c271c70cc0524613199431aaf320944aba4ebb Mon Sep 17 00:00:00 2001 From: Hongwei Date: Tue, 7 Jul 2026 11:36:54 +0200 Subject: [PATCH 2/4] fix(db): run blocking JDBC/gRPC on a dedicated bounded pool ExecutionContext.global is CPU-sized, and blocking I/O (DataSource connection acquisition via Doobie's connectEC, gRPC dispatch) on that pool starves all request Futures waiting on the same small thread count. Introduce BlockingIoExecutionContext, a fixed-size thread pool (default 50, prop-overridable) dedicated to blocking JDBC and gRPC work. Replace global at 6 sites: - DoobieTransactor.scala (2 sites: fallback + update transactors) - StoredProcedureUtils.scala (SP connector transactor) - ProjectionDb.scala (projection transactor) - ObpGrpcServer.scala (gRPC server dispatch) - Boot.scala:1074 (production gRPC server creation) Default pool size covers Hikari maximumPoolSize sum (main 20 + SP 20) with headroom; override via blocking_io_pool.size prop if Hikari pools resize. --- .../main/scala/bootstrap/liftweb/Boot.scala | 3 +-- .../entity/projection/ProjectionDb.scala | 6 ++--- .../api/util/BlockingIoExecutionContext.scala | 24 +++++++++++++++++++ .../code/api/util/DoobieTransactor.scala | 4 ++-- .../StoredProcedureUtils.scala | 2 +- .../scala/code/obp/grpc/ObpGrpcServer.scala | 2 +- 6 files changed, 31 insertions(+), 10 deletions(-) create mode 100644 obp-api/src/main/scala/code/api/util/BlockingIoExecutionContext.scala diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index a51be6b96b..52f1c14c78 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -162,7 +162,6 @@ import org.apache.commons.io.FileUtils import java.io.{File, FileInputStream} import java.util.stream.Collectors import java.util.{Locale, TimeZone} -import scala.concurrent.ExecutionContext import scala.util.control.NonFatal @@ -1071,7 +1070,7 @@ object ToSchemify extends MdcLoggable { // start grpc server (optional) val grpcServerOpt: Option[ObpGrpcServer] = if (APIUtil.getPropsAsBoolValue("grpc.server.enabled", false)) { - val server = new ObpGrpcServer(ExecutionContext.global) + val server = new ObpGrpcServer(code.api.util.BlockingIoExecutionContext.ec) server.start() Some(server) } else None diff --git a/obp-api/src/main/scala/code/api/dynamic/entity/projection/ProjectionDb.scala b/obp-api/src/main/scala/code/api/dynamic/entity/projection/ProjectionDb.scala index 3d02ddd14a..0efde0f872 100644 --- a/obp-api/src/main/scala/code/api/dynamic/entity/projection/ProjectionDb.scala +++ b/obp-api/src/main/scala/code/api/dynamic/entity/projection/ProjectionDb.scala @@ -1,12 +1,10 @@ package code.api.dynamic.entity.projection import cats.effect.IO -import code.api.util.APIUtil +import code.api.util.{APIUtil, BlockingIoExecutionContext} import doobie._ import doobie.implicits._ -import scala.concurrent.ExecutionContext - /** * A **committing** Doobie transactor over the shared HikariCP pool, for projection operations that * run independently of any Lift request transaction — provisioner DDL/backfill and the read-path @@ -19,7 +17,7 @@ import scala.concurrent.ExecutionContext */ object ProjectionDb { private lazy val xa: Transactor[IO] = - Transactor.fromDataSource[IO].apply(APIUtil.vendor.HikariDatasource.ds, ExecutionContext.global) + Transactor.fromDataSource[IO].apply(APIUtil.vendor.HikariDatasource.ds, BlockingIoExecutionContext.ec) def run[A](program: ConnectionIO[A]): IO[A] = program.transact(xa) } diff --git a/obp-api/src/main/scala/code/api/util/BlockingIoExecutionContext.scala b/obp-api/src/main/scala/code/api/util/BlockingIoExecutionContext.scala new file mode 100644 index 0000000000..29932e1587 --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/BlockingIoExecutionContext.scala @@ -0,0 +1,24 @@ +package code.api.util + +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.{Executors, ThreadFactory} + +import scala.concurrent.ExecutionContext + +/** Dedicated bounded pool for blocking JDBC connection-acquisition (and gRPC dispatch), + * kept off the CPU-sized global pool so blocked I/O cannot starve request Futures. + * + * The default of 50 covers the sum of the Hikari maximumPoolSize values it serves + * (main pool 20 + stored-procedure pool 20) with headroom; override via the + * `blocking_io_pool.size` prop if the Hikari pools are resized. + */ +object BlockingIoExecutionContext { + private val size = APIUtil.getPropsAsIntValue("blocking_io_pool.size", 50) + private val counter = new AtomicInteger(0) + private val threadFactory: ThreadFactory = (r: Runnable) => { + val t = new Thread(r, s"obp-blocking-io-${counter.incrementAndGet()}") + t.setDaemon(true) + t + } + val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(size, threadFactory)) +} diff --git a/obp-api/src/main/scala/code/api/util/DoobieTransactor.scala b/obp-api/src/main/scala/code/api/util/DoobieTransactor.scala index e91c033102..c879028eb4 100644 --- a/obp-api/src/main/scala/code/api/util/DoobieTransactor.scala +++ b/obp-api/src/main/scala/code/api/util/DoobieTransactor.scala @@ -66,7 +66,7 @@ object DoobieUtil extends MdcLoggable { logger.info("DoobieUtil: Initialized fallback transactor sharing the application HikariCP pool") val xa = Transactor.fromDataSource[IO].apply( sharedDataSource, - ExecutionContext.global + BlockingIoExecutionContext.ec ) xa.copy(strategy0 = Strategy.void) } @@ -172,7 +172,7 @@ object DoobieUtil extends MdcLoggable { val sharedDataSource = APIUtil.vendor.HikariDatasource.ds Transactor.fromDataSource[IO].apply( sharedDataSource, - ExecutionContext.global + BlockingIoExecutionContext.ec ) // Strategy.default includes commit/rollback } diff --git a/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureUtils.scala b/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureUtils.scala index 7b2ffcc677..b9df61ec25 100644 --- a/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureUtils.scala +++ b/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureUtils.scala @@ -50,7 +50,7 @@ object StoredProcedureUtils extends MdcLoggable{ ds.setConnectionTimeout(timeoutMillis) ds.setConnectionTestQuery(validationQuery) - Transactor.fromDataSource[IO].apply(ds, scala.concurrent.ExecutionContext.global) + Transactor.fromDataSource[IO].apply(ds, code.api.util.BlockingIoExecutionContext.ec) .copy(strategy0 = Strategy.void) } diff --git a/obp-api/src/main/scala/code/obp/grpc/ObpGrpcServer.scala b/obp-api/src/main/scala/code/obp/grpc/ObpGrpcServer.scala index 6184b81bf5..8ccb72c9e4 100644 --- a/obp-api/src/main/scala/code/obp/grpc/ObpGrpcServer.scala +++ b/obp-api/src/main/scala/code/obp/grpc/ObpGrpcServer.scala @@ -30,7 +30,7 @@ import scala.concurrent.{ExecutionContext, Future} object ObpGrpcServer { def main(args: Array[String] = Array.empty): Unit = { - val server = new ObpGrpcServer(ExecutionContext.global) + val server = new ObpGrpcServer(code.api.util.BlockingIoExecutionContext.ec) server.start() server.blockUntilShutdown() } From 0c89ddab4c514dfdc7cc19ca8ed041149fa189af Mon Sep 17 00:00:00 2001 From: Hongwei Date: Tue, 7 Jul 2026 11:37:07 +0200 Subject: [PATCH 3/4] fix(email): surface password-reset SMTP failures instead of reporting sent The password-reset endpoint (POST /management/user/reset-password-url) called sendHtmlEmail and discarded the result, returning 'sent' unconditionally even when the SMTP send failed. The caller (admin with canCreateResetPasswordUrl) already knows the target user's email, so anti-enumeration does not apply here. Switch to sendHtmlEmailEither and surface SMTP send failures as 500 responses instead of lying that the email was sent. Restructure the for-comprehension to hoist token generation before the send, and fail immediately on send failure. Add test scenario: verify that when SMTP is misconfigured (closed port) and test mode is off, the endpoint returns 500 with a message indicating the send failed, not the successful 'sent' response. --- .../code/api/util/CommonsEmailWrapper.scala | 4 +- .../scala/code/api/v6_0_0/Http4s600.scala | 44 ++++++++++++------- .../code/api/v6_0_0/PasswordResetTest.scala | 21 +++++++++ 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala b/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala index 6948f9178d..2290b74811 100644 --- a/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala +++ b/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala @@ -132,8 +132,8 @@ object CommonsEmailWrapper extends MdcLoggable { // Variant that preserves the exception so callers can classify SMTP failures // (auth / connect / TLS / recipient rejection) instead of returning a generic - // EmailSendingFailed. Used by the self-test endpoint; existing fire-and-forget - // callers (signup, password reset) keep using sendTextEmail above. + // EmailSendingFailed. Used by the self-test endpoint and the password-reset + // endpoint; existing fire-and-forget callers (signup) keep using sendTextEmail above. def sendTextEmailEither(content: EmailContent): Either[Throwable, String] = { if (isTestMode) Right("test-mode-message-id-" + System.currentTimeMillis()) else sendTextEmailEither(getDefaultEmailConfig(), content) diff --git a/obp-api/src/main/scala/code/api/v6_0_0/Http4s600.scala b/obp-api/src/main/scala/code/api/v6_0_0/Http4s600.scala index fbe05e4793..068d9225a3 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/Http4s600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/Http4s600.scala @@ -1048,30 +1048,42 @@ object Http4s600 { case Full(url) => Future.successful(url) case _ => Future.failed(new Exception(s"$IncompleteServerConfiguration public_obp_portal_url (or legacy portal_external_url) is not set")) } - } yield { - val user: AuthUser = authUser - user.uniqueId.set(java.util.UUID.randomUUID().toString.replace("-", "")) - user.save - val expiryMinutes = APIUtil.getPropsAsIntValue("password_reset_token_expiry_minutes", 120) - val claimsSet = new com.nimbusds.jwt.JWTClaimsSet.Builder() - .subject(user.uniqueId.get) - .expirationTime(new java.util.Date(System.currentTimeMillis() + expiryMinutes * 60L * 1000L)) - .issueTime(new java.util.Date()).build() - val jwtToken = CertificateUtil.jwtWithHmacProtection(claimsSet) - val resetLink = portalUrl + "/reset-password/" + java.net.URLEncoder.encode(jwtToken, "UTF-8") - CommonsEmailWrapper.sendHtmlEmail(CommonsEmailWrapper.EmailContent( + resetLink <- Future { + val user: AuthUser = authUser + user.uniqueId.set(java.util.UUID.randomUUID().toString.replace("-", "")) + user.save + val expiryMinutes = APIUtil.getPropsAsIntValue("password_reset_token_expiry_minutes", 120) + val claimsSet = new com.nimbusds.jwt.JWTClaimsSet.Builder() + .subject(user.uniqueId.get) + .expirationTime(new java.util.Date(System.currentTimeMillis() + expiryMinutes * 60L * 1000L)) + .issueTime(new java.util.Date()).build() + val jwtToken = CertificateUtil.jwtWithHmacProtection(claimsSet) + portalUrl + "/reset-password/" + java.net.URLEncoder.encode(jwtToken, "UTF-8") + } + // The caller is an admin with canCreateResetPasswordUrl who already knows the + // target user's email, so anti-enumeration does not apply here: if the email + // cannot be sent, say so instead of reporting "sent". + _ <- CommonsEmailWrapper.sendHtmlEmailEither(CommonsEmailWrapper.EmailContent( from = AuthUser.emailFrom, - to = List(user.email.get), + to = List(authUser.email.get), bcc = AuthUser.bccEmail.toList, - subject = "Reset your password - " + user.username.get, + subject = "Reset your password - " + authUser.username.get, textContent = Some(s"Please reset your password: $resetLink"), htmlContent = Some(s"

Please reset your password: $resetLink

") - )) + )) match { + case Right(_) => Future.successful(()) + case Left(e) => + val json = org.json4s.native.Serialization.write( + code.api.APIFailureNewStyle(s"$UnknownError Failed to send password reset email: ${e.getMessage}", 500, Some(cc).map(_.toLight)) + )(org.json4s.DefaultFormats) + Future.failed(new Exception(json)) + } + } yield { // The reset URL is intentionally NOT returned in the response. Returning // it would let any caller with canCreateResetPasswordUrl complete a reset // without controlling the target mailbox, defeating the email-proves- // mailbox-ownership property of the flow. The link goes via email only. - JSONFactory600.ResetPasswordEmailSentJsonV600(status = "sent", to = user.email.get) + JSONFactory600.ResetPasswordEmailSentJsonV600(status = "sent", to = authUser.email.get) } } } diff --git a/obp-api/src/test/scala/code/api/v6_0_0/PasswordResetTest.scala b/obp-api/src/test/scala/code/api/v6_0_0/PasswordResetTest.scala index f08fd0a81a..2205bf2748 100644 --- a/obp-api/src/test/scala/code/api/v6_0_0/PasswordResetTest.scala +++ b/obp-api/src/test/scala/code/api/v6_0_0/PasswordResetTest.scala @@ -141,6 +141,27 @@ class PasswordResetTest extends V600ServerSetup { (response600.body \ "reset_password_url") should equal(org.json4s.JNothing) } + scenario("SMTP failure must surface as a 500, not a fake 'sent'", ApiEndpoint1, VersionOfApi) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateResetPasswordUrl.toString) + val authUser: AuthUser = AuthUser.create.email(postJson.email).username(postJson.username).validated(true).saveMe() + val resourceUser: Box[User] = Users.users.vend.getUserByResourceUserId(authUser.user.get) + And("SMTP is misconfigured (closed port) with test mode off, so the send must fail") + setPropsValues( + "mail.test.mode" -> "false", + "mail.smtp.host" -> "localhost", + "mail.smtp.port" -> "1" // reserved port, connection refused immediately + ) + When("We make a request v6.0.0") + val request600 = (v6_0_0_Request / "management" / "user" / "reset-password-url").POST <@(user1) + val response600 = makePostRequest(request600, write(postJson.copy(user_id = resourceUser.map(_.userId).getOrElse("")))) + Then("We should get a 500 that says the email could not be sent") + withClue(s"Response body: ${response600.body} ") { + response600.code should equal(500) + } + response600.body.extract[ErrorMessage].message should include("Failed to send password reset email") + // beforeEach restores mail.test.mode=true for the remaining scenarios + } + scenario("We will call the endpoint with unvalidated user", ApiEndpoint1, VersionOfApi) { Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateResetPasswordUrl.toString) val testUsername = "unvalidated@tesobe.com" From aad23d05f92e635af80192fc7684603c225e50b9 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Tue, 7 Jul 2026 11:37:19 +0200 Subject: [PATCH 4/4] fix(ukob): wrap v3.1 ResourceDoc example bodies in JvalueCaseClass Since the json4s migration, JValue itself extends scala.Product, so a raw json.parse(...) result satisfies ResourceDoc example-body parameter types and compiles. However, resource-docs serialization then reflects on it as a generic case class instead of taking the JvalueCaseClass special-case unwrap path, causing schema mismatches and potential asInstanceOf[JvalueCaseClass] failures. All 20 affected UK OB v3.1 files share an identical private parseBody helper used only for ResourceDoc example bodies (79 call sites total, never in handler logic). Wrap the helper return in JvalueCaseClass(...) so serialization takes the correct path. This matches the earlier Berlin Group v1.3 fix (30afe2a46) that resolved the same root cause. --- .../api/UKOpenBanking/v3_1_0/Http4sUKOBv310AccountAccess.scala | 2 +- .../code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Accounts.scala | 2 +- .../code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Balances.scala | 2 +- .../api/UKOpenBanking/v3_1_0/Http4sUKOBv310Beneficiaries.scala | 2 +- .../api/UKOpenBanking/v3_1_0/Http4sUKOBv310DirectDebits.scala | 2 +- .../UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticPayments.scala | 2 +- .../v3_1_0/Http4sUKOBv310DomesticScheduledPayments.scala | 2 +- .../v3_1_0/Http4sUKOBv310DomesticStandingOrders.scala | 2 +- .../api/UKOpenBanking/v3_1_0/Http4sUKOBv310FilePayments.scala | 2 +- .../UKOpenBanking/v3_1_0/Http4sUKOBv310FundsConfirmations.scala | 2 +- .../v3_1_0/Http4sUKOBv310InternationalPayments.scala | 2 +- .../v3_1_0/Http4sUKOBv310InternationalScheduledPayments.scala | 2 +- .../v3_1_0/Http4sUKOBv310InternationalStandingOrders.scala | 2 +- .../code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Offers.scala | 2 +- .../code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Partys.scala | 2 +- .../code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Products.scala | 2 +- .../UKOpenBanking/v3_1_0/Http4sUKOBv310ScheduledPayments.scala | 2 +- .../api/UKOpenBanking/v3_1_0/Http4sUKOBv310StandingOrders.scala | 2 +- .../api/UKOpenBanking/v3_1_0/Http4sUKOBv310Statements.scala | 2 +- .../api/UKOpenBanking/v3_1_0/Http4sUKOBv310Transactions.scala | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310AccountAccess.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310AccountAccess.scala index 705245375d..03032bc919 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310AccountAccess.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310AccountAccess.scala @@ -36,7 +36,7 @@ object Http4sUKOBv310AccountAccess extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion lazy val createAccountAccessConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Accounts.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Accounts.scala index 2eb6f349a1..5065276be6 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Accounts.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Accounts.scala @@ -35,7 +35,7 @@ object Http4sUKOBv310Accounts extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion lazy val getAccounts: HttpRoutes[IO] = HttpRoutes.of[IO] { diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Balances.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Balances.scala index 947f9148f7..5d9b95801a 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Balances.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Balances.scala @@ -35,7 +35,7 @@ object Http4sUKOBv310Balances extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Balances") :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Beneficiaries.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Beneficiaries.scala index 381c3162e0..734b36960f 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Beneficiaries.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Beneficiaries.scala @@ -33,7 +33,7 @@ object Http4sUKOBv310Beneficiaries extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Beneficiaries") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DirectDebits.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DirectDebits.scala index c411eebb87..5ffe13f610 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DirectDebits.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DirectDebits.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310DirectDebits extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Direct Debits") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticPayments.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticPayments.scala index 9a2fc49028..040535ab6b 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticPayments.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticPayments.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310DomesticPayments extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Domestic Payments") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticScheduledPayments.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticScheduledPayments.scala index c75204660f..a4c6d0c851 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticScheduledPayments.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticScheduledPayments.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310DomesticScheduledPayments extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Domestic Scheduled Payments") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticStandingOrders.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticStandingOrders.scala index 027bd62860..abf9d5cfbd 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticStandingOrders.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticStandingOrders.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310DomesticStandingOrders extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Domestic Standing Orders") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FilePayments.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FilePayments.scala index c3f7ba5852..806477db07 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FilePayments.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FilePayments.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310FilePayments extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("File Payments") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FundsConfirmations.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FundsConfirmations.scala index 4907147e05..485ed5fd14 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FundsConfirmations.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FundsConfirmations.scala @@ -32,7 +32,7 @@ object Http4sUKOBv310FundsConfirmations extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Funds Confirmations") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalPayments.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalPayments.scala index 7adabdfc9a..640d04f64f 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalPayments.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalPayments.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310InternationalPayments extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("International Payments") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalScheduledPayments.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalScheduledPayments.scala index 987e4e4f08..110f8fbffd 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalScheduledPayments.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalScheduledPayments.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310InternationalScheduledPayments extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("International Scheduled Payments") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalStandingOrders.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalStandingOrders.scala index c1b6fe38ce..e881929c0d 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalStandingOrders.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalStandingOrders.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310InternationalStandingOrders extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("International Standing Orders") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Offers.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Offers.scala index 7e81ef94d4..73d71abed4 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Offers.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Offers.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310Offers extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Offers") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Partys.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Partys.scala index 4b830ea02e..5f532757b3 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Partys.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Partys.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310Partys extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Partys") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Products.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Products.scala index d9ef3ddb27..0f31ad929c 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Products.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Products.scala @@ -36,7 +36,7 @@ object Http4sUKOBv310Products extends MdcLoggable { val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310ScheduledPayments.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310ScheduledPayments.scala index ac411887bb..2bb5a25db8 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310ScheduledPayments.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310ScheduledPayments.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310ScheduledPayments extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Scheduled Payments") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310StandingOrders.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310StandingOrders.scala index f7344c3373..3af41eddaf 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310StandingOrders.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310StandingOrders.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310StandingOrders extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Standing Orders") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Statements.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Statements.scala index bee31cccda..f995fa79cc 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Statements.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Statements.scala @@ -32,7 +32,7 @@ object Http4sUKOBv310Statements extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Statements") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Transactions.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Transactions.scala index 5199414fde..682cc3cce9 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Transactions.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Transactions.scala @@ -44,7 +44,7 @@ object Http4sUKOBv310Transactions extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion // -----------------------------------------------------------------------