From 57c71d548a18b7d1fece52def2669dfc90747ec8 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Sun, 22 Mar 2026 16:55:12 +0100 Subject: [PATCH 1/7] Enable additional Sort By columns --- .../main/scala/code/api/util/APIUtil.scala | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) 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 f93dc5e345..9c17c4f2df 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -1271,19 +1271,13 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ roleName <- getHttpParamValuesByName(httpParams, "role_name") httpStatusCode <- getHttpParamValuesByName(httpParams, "http_status_code") }yield{ - /** - * sortBy is currently disabled as it would open up a security hole: - * - * sortBy as currently implemented will take in a parameter that searches on the mongo field names. The issue here - * is that it will sort on the true value, and not the moderated output. So if a view is supposed to return an alias name - * rather than the true value, but someone uses sortBy on the other bank account name/holder, not only will the returned data - * have the wrong order, but information about the true account holder name will be exposed due to its position in the sorted order - * - * This applies to all fields that can have their data concealed... which in theory will eventually be most/all - * - */ - //val sortBy = json.header("obp_sort_by") - val ordering = OBPOrdering(None, sortDirection) + // Extract the sort field name from the sort_by query param (e.g. "url", "date"). + // OBPOrdering expects Option[String], but sortBy is an OBPQueryParam. + val sortField = sortBy match { + case OBPSortBy(v) => Some(v) + case _ => None + } + val ordering = OBPOrdering(sortField, sortDirection) //This guarantee the order List(limit, offset, ordering, sortBy, fromDate, toDate, anon, status, consumerId, azp, iss, consentId, userId, providerProviderId, url, appName, implementedByPartialFunction, implementedInVersion, From 6653063de0bc7d445283d26f396b82a461bc3b14 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Sun, 22 Mar 2026 16:55:21 +0100 Subject: [PATCH 2/7] Update flushall_build_and_run.sh --- flushall_build_and_run.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flushall_build_and_run.sh b/flushall_build_and_run.sh index de6a1abdd7..11356f45a1 100755 --- a/flushall_build_and_run.sh +++ b/flushall_build_and_run.sh @@ -111,8 +111,8 @@ tail -n 3 -f build.log & TAIL_PID=$! mvn -pl obp-api -am clean package -DskipTests=true -Dmaven.test.skip=true -T 4 > build.log 2>&1 BUILD_EXIT=$? -kill $TAIL_PID 2>/dev/null -wait $TAIL_PID 2>/dev/null +kill $TAIL_PID 2>/dev/null || true +wait $TAIL_PID 2>/dev/null || true if [ $BUILD_EXIT -ne 0 ]; then echo "" From b5dbcfde7ea949f214153b2c77d53645fb9d40cd Mon Sep 17 00:00:00 2001 From: simonredfern Date: Sun, 22 Mar 2026 19:22:12 +0100 Subject: [PATCH 3/7] SDKs item in Glossary.scala --- .../src/main/scala/code/api/util/Glossary.scala | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/obp-api/src/main/scala/code/api/util/Glossary.scala b/obp-api/src/main/scala/code/api/util/Glossary.scala index 0d7ae4b390..2da89d8df3 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -5111,6 +5111,20 @@ object Glossary extends MdcLoggable { | """) + glossaryItems += GlossaryItem( + title = "SDKs", + description = + s""" + |# SDKs + | + |OBP SDKs (Software Development Kits) are client libraries that make it easier to interact with the OBP API from various programming languages. + | + |SDKs are available for multiple languages including Python, Java, Scala, PHP, C#, Javascript and more. + | + |For more information see [OBP SDKs on GitHub](https://github.com/OpenBankProject/OBP-SDKs). + | +""") + /////////////////////////////////////////////////////////////////// // NOTE! Some glossary items are generated in ExampleValue.scala ////////////////////////////////////////////////////////////////// From b6e4e3833f1d9f7f8d7474e89255894c1b55acbf Mon Sep 17 00:00:00 2001 From: simonredfern Date: Sun, 22 Mar 2026 22:29:19 +0100 Subject: [PATCH 4/7] Adding consent_items which is a denomralisation of the JWT so we don't have to extract the JWT in order to query the bank_id (for get my consents at bank). Also pagination defaults to 50 rows. --- .../main/scala/bootstrap/liftweb/Boot.scala | 3 +- .../SwaggerDefinitionsJSON.scala | 2 +- .../scala/code/api/v3_1_0/APIMethods310.scala | 30 +++- .../scala/code/api/v4_0_0/APIMethods400.scala | 40 +++-- .../scala/code/api/v5_1_0/APIMethods510.scala | 75 +++++---- .../code/api/v5_1_0/JSONFactory5.1.0.scala | 7 +- .../code/consent/DoobieConsentQueries.scala | 152 +++++++++--------- .../scala/code/consent/MappedConsent.scala | 22 ++- .../code/consent/MappedConsentItems.scala | 41 +++++ 9 files changed, 249 insertions(+), 123 deletions(-) create mode 100644 obp-api/src/main/scala/code/consent/MappedConsentItems.scala diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 37c4e6667f..42843a46f9 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -63,7 +63,7 @@ import code.branches.MappedBranch import code.cardattribute.MappedCardAttribute import code.cards.{MappedPhysicalCard, PinReset} import code.connectormethod.ConnectorMethod -import code.consent.{ConsentRequest, MappedConsent} +import code.consent.{ConsentRequest, MappedConsent, MappedConsentItems} import code.consumer.Consumers import code.model.Consumer import code.context.{MappedConsentAuthContext, MappedUserAuthContext, MappedUserAuthContextUpdate} @@ -1120,6 +1120,7 @@ object ToSchemify { MappedCustomerIdMapping, MappedProductAttribute, MappedConsent, + MappedConsentItems, ConsentRequest, MigrationScriptLog, MethodRouting, diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 260dc43d0f..4f2f681613 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -4601,7 +4601,7 @@ object SwaggerDefinitionsJSON { last_action_date = dateExample.value, last_usage_date = dateTimeExample.value, jwt = jwtExample.value, - jwt_payload = Some(consentJWT), + jwt_payload = """{"createdByUserId":"user-id","sub":"subject","iss":"issuer","aud":"audience","jti":"jwt-id","iat":1611749820,"nbf":1611749820,"exp":1611753420,"request_headers":[],"name":null,"email":null,"entitlements":[],"views":[],"access":null}""", api_standard = "Berlin Group", api_version = "v1.3", ) diff --git a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala index ec49c0c5ed..64ee5f1e84 100644 --- a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala +++ b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala @@ -25,7 +25,7 @@ import code.api.v3_0_0.{CreateViewJsonV300, JSONFactory300} import code.api.v3_1_0.JSONFactory310._ import code.bankconnectors.rest.RestConnector_vMar2019 import code.bankconnectors.{Connector, LocalMappedConnector} -import code.consent.{ConsentStatus, Consents, MappedConsent} +import code.consent.{ConsentStatus, Consents, DoobieConsentQueries, MappedConsent} import code.consumer.Consumers import code.entitlement.Entitlement import code.loginattempts.LoginAttempt @@ -3726,6 +3726,10 @@ trait APIMethods310 { | |${userAuthenticationMessage(true)} | + |1 limit (for pagination: defaults to 50) eg:limit=200 + | + |2 offset (for pagination: zero index, defaults to 0) eg: offset=10 + | """.stripMargin, EmptyBody, consentsJsonV310, @@ -3739,12 +3743,32 @@ trait APIMethods310 { lazy val getConsents: OBPEndpoint = { case "banks" :: BankId(bankId) :: "my" :: "consents" :: Nil JsonGet _ => { cc => implicit val ec = EndpointContext(Some(cc)) + val url = cc.url + val limitParam = APIUtil.getHttpRequestUrlParam(url, "limit") match { + case s if s.nonEmpty => scala.util.Try(s.toInt).getOrElse(50) + case _ => 50 + } + val offsetParam = APIUtil.getHttpRequestUrlParam(url, "offset") match { + case s if s.nonEmpty => scala.util.Try(s.toInt).getOrElse(0) + case _ => 0 + } for { (Full(user), callContext) <- authenticatedAccess(cc) (_, callContext) <- NewStyle.function.getBank(bankId, callContext) - consents <- Future(Consents.consentProvider.vend.getConsentsByUser(user.userId)) + rows <- Future { + DoobieConsentQueries.getConsentsByUserAndBank( + userId = user.userId, + bankId = bankId.value, + status = None, + limit = limitParam, + offset = offsetParam, + sortField = "created_date", + sortDirection = "desc" + ) + } } yield { - (JSONFactory310.createConsentsJsonV310(consents), HttpCode.`200`(callContext)) + val consents = rows.map(r => ConsentJsonV310(r.consentId, r.jwt.getOrElse(""), r.status)) + (ConsentsJsonV310(consents), HttpCode.`200`(callContext)) } } } diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index fff39560bf..97c7ea7581 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -42,7 +42,7 @@ import code.authtypevalidation.JsonAuthTypeValidation import code.bankconnectors.LocalMappedConnectorInternal._ import code.bankconnectors.{Connector, DynamicConnector, InternalConnector, LocalMappedConnectorInternal} import code.connectormethod.{JsonConnectorMethod, JsonConnectorMethodMethodBody} -import code.consent.{ConsentStatus, Consents} +import code.consent.{ConsentStatus, Consents, DoobieConsentQueries} import code.dynamicEntity.DynamicEntityCommons import code.dynamicMessageDoc.JsonDynamicMessageDoc import code.dynamicResourceDoc.JsonDynamicResourceDoc @@ -11479,6 +11479,10 @@ trait APIMethods400 extends MdcLoggable { | |${userAuthenticationMessage(true)} | + |1 limit (for pagination: defaults to 50) eg:limit=200 + | + |2 offset (for pagination: zero index, defaults to 0) eg: offset=10 + | """.stripMargin, EmptyBody, consentsJsonV400, @@ -11494,19 +11498,33 @@ trait APIMethods400 extends MdcLoggable { case "banks" :: BankId(bankId) :: "my" :: "consents" :: Nil JsonGet _ => { cc => implicit val ec = EndpointContext(Some(cc)) + val url = cc.url + val limitParam = getHttpRequestUrlParam(url, "limit") match { + case s if s.nonEmpty => scala.util.Try(s.toInt).getOrElse(50) + case _ => 50 + } + val offsetParam = getHttpRequestUrlParam(url, "offset") match { + case s if s.nonEmpty => scala.util.Try(s.toInt).getOrElse(0) + case _ => 0 + } for { - consents <- Future { - Consents.consentProvider.vend - .getConsentsByUser(cc.userId) - .sortBy(i => (i.creationDateTime, i.apiStandard)) - .reverse + rows <- Future { + DoobieConsentQueries.getConsentsByUserAndBank( + userId = cc.userId, + bankId = bankId.value, + status = None, + limit = limitParam, + offset = offsetParam, + sortField = "created_date", + sortDirection = "desc" + ) } } yield { - val consentsOfBank = Consent.filterByBankId(consents, bankId) - ( - JSONFactory400.createConsentsJsonV400(consentsOfBank), - HttpCode.`200`(cc) - ) + val consents = rows.map(r => ConsentJsonV400( + r.consentId, r.jwt.getOrElse(""), r.status, + r.apiStandard.getOrElse(""), r.apiVersion.getOrElse("") + )) + (ConsentsJsonV400(consents), HttpCode.`200`(cc)) } } } diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index f39a1b597d..22b66c254b 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -1803,10 +1803,23 @@ trait APIMethods510 { "Get My Consents at Bank", s""" | - |This endpoint gets the Consents created by a current User. + |This endpoint gets the Consents created by a current User at the specified Bank. | |${userAuthenticationMessage(true)} | + |1 limit (for pagination: defaults to 50) eg:limit=200 + | + |2 offset (for pagination: zero index, defaults to 0) eg: offset=10 + | + |3 status (ignore if omitted) + | + |4 sort_by (defaults to created_date:desc) eg: sort_by=created_date:desc + | + |Note: This endpoint only returns consents that explicitly reference the specified BANK_ID. + |Consents created before the consent_items join table was introduced will not appear in results. + | + |eg: /banks/BANK_ID/my/consents?limit=10&offset=0&sort_by=created_date:desc + | """.stripMargin, EmptyBody, consentsInfoJsonV510, @@ -1821,26 +1834,42 @@ trait APIMethods510 { case "banks" :: BankId(bankId) :: "my" :: "consents" :: Nil JsonGet _ => { cc => implicit val ec = EndpointContext(Some(cc)) + val url = cc.url + val limitParam = getHttpRequestUrlParam(url, "limit") match { + case s if s.nonEmpty => scala.util.Try(s.toInt).getOrElse(50) + case _ => 50 + } + val offsetParam = getHttpRequestUrlParam(url, "offset") match { + case s if s.nonEmpty => scala.util.Try(s.toInt).getOrElse(0) + case _ => 0 + } + val statusParam = getHttpRequestUrlParam(url, "status") match { + case s if s.nonEmpty => Some(s) + case _ => None + } + val sortByParam = getHttpRequestUrlParam(url, "sort_by") match { + case s if s.nonEmpty => s + case _ => "created_date:desc" + } + val sortParts = sortByParam.split(":").map(_.trim.toLowerCase) + val sortField = sortParts(0) + val sortDirection = sortParts.lift(1).getOrElse("desc") for { + (Full(u), callContext) <- authenticatedAccess(cc) rows <- Future { - DoobieConsentQueries.getAllConsentsByUser(cc.userId) + DoobieConsentQueries.getConsentsByUserAndBank( + userId = u.userId, + bankId = bankId.value, + status = statusParam, + limit = limitParam, + offset = offsetParam, + sortField = sortField, + sortDirection = sortDirection + ) } } yield { - // Bank filtering requires JWT decoding — done in Scala - val filteredRows = rows.filter { row => - row.jwt.exists { jwt => - val jwtPayload: Box[ConsentJWT] = JwtUtil.getSignedPayloadAsJson(jwt).map(parse(_).extract[ConsentJWT]) - jwtPayload match { - case Full(c) if c.views.isEmpty => true - case Full(c) if c.views.map(_.bank_id).contains(bankId.value) => true - case Full(c) if c.entitlements.exists(_.bank_id.isEmpty()) => true - case Full(c) if c.entitlements.map(_.bank_id).contains(bankId.value) => true - case _ => false - } - } - } - val consents = filteredRows.map(rowToConsentInfoJsonV510) - (ConsentsInfoJsonV510(consents), HttpCode.`200`(cc)) + val consents = rows.map(rowToConsentInfoJsonV510) + (ConsentsInfoJsonV510(consents), HttpCode.`200`(callContext)) } } } @@ -1903,7 +1932,7 @@ trait APIMethods510 { val sortDirection = sortParts.lift(1).getOrElse("desc") for { (Full(u), callContext) <- authenticatedAccess(cc) - (rows, _) <- Future { + rows <- Future { DoobieConsentQueries.getConsentsByUser( userId = u.userId, status = statusParam, @@ -1920,14 +1949,6 @@ trait APIMethods510 { } private def rowToConsentInfoJsonV510(row: DoobieConsentQueries.ConsentRow): ConsentInfoJsonV510 = { - // Use pre-computed jwt_payload from DB; fall back to parsing jwt if not yet populated - val jwtPayload: Box[ConsentJWT] = row.jwtPayload match { - case Some(payload) => tryo(parse(payload).extract[ConsentJWT]) - case None => row.jwt match { - case Some(jwt) => JwtUtil.getSignedPayloadAsJson(jwt).map(parse(_).extract[ConsentJWT]) - case None => Empty - } - } ConsentInfoJsonV510( consent_reference_id = row.consentReferenceId.toString, consent_id = row.consentId, @@ -1939,7 +1960,7 @@ trait APIMethods510 { last_usage_date = row.lastUsageDate.map(d => new java.text.SimpleDateFormat(DateWithSeconds).format(d)).orNull, jwt = row.jwt.orNull, - jwt_payload = jwtPayload, + jwt_payload = row.jwtPayload.orNull, api_standard = row.apiStandard.orNull, api_version = row.apiVersion.orNull ) diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala index 54543a4c45..404bdf2504 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala @@ -172,7 +172,7 @@ case class ConsentInfoJsonV510(consent_reference_id: String, last_action_date: String, last_usage_date: String, jwt: String, - jwt_payload: Box[ConsentJWT], + jwt_payload: String, api_standard: String, api_version: String, ) @@ -1007,9 +1007,6 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable { ConsentsInfoJsonV510( consents.map { c => - val jwtPayload: Box[ConsentJWT] = - JwtUtil.getSignedPayloadAsJson(c.jsonWebToken).map(parse(_).extract[ConsentJWT]) - ConsentInfoJsonV510( consent_reference_id = c.consentReferenceId, consent_id = c.consentId, @@ -1021,7 +1018,7 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable { last_usage_date = if (c.usesSoFarTodayCounterUpdatedAt != null) new SimpleDateFormat(DateWithSeconds).format(c.usesSoFarTodayCounterUpdatedAt) else null, jwt = c.jsonWebToken, - jwt_payload = jwtPayload, + jwt_payload = null, api_standard = c.apiStandard, api_version = c.apiVersion ) diff --git a/obp-api/src/main/scala/code/consent/DoobieConsentQueries.scala b/obp-api/src/main/scala/code/consent/DoobieConsentQueries.scala index e371f33eb7..88618c629c 100644 --- a/obp-api/src/main/scala/code/consent/DoobieConsentQueries.scala +++ b/obp-api/src/main/scala/code/consent/DoobieConsentQueries.scala @@ -42,30 +42,6 @@ object DoobieConsentQueries { jwtPayload: Option[String] ) - /** - * Row type for paginated queries that include total_count via window function. - * Eliminates the need for a separate COUNT(*) query. - */ - case class ConsentRowWithCount( - consentReferenceId: Long, - consentId: String, - createdByUserId: String, - consumerId: Option[String], - status: String, - jwt: Option[String], - consentRequestId: Option[String], - apiStandard: Option[String], - apiVersion: Option[String], - lastActionDate: Option[Timestamp], - lastUsageDate: Option[Timestamp], - createdDate: Option[Timestamp], - note: Option[String], - frequencyPerDay: Option[Int], - usesSoFarTodayCounter: Option[Int], - jwtPayload: Option[String], - totalCount: Long - ) - /** * Get consents for a user with DB-level pagination, filtering, and sorting. * @@ -84,24 +60,11 @@ object DoobieConsentQueries { offset: Int, sortField: String, sortDirection: String - ): (List[ConsentRow], Long) = { + ): List[ConsentRow] = { val query = buildGetConsentsQuery(userId, status, limit, offset, sortField, sortDirection) DoobieUtil.runQuery(query) } - def getConsentsByUserFuture( - userId: String, - status: Option[String], - limit: Int, - offset: Int, - sortField: String, - sortDirection: String - )(implicit ec: ExecutionContext): Future[(List[ConsentRow], Long)] = { - Future { - getConsentsByUser(userId, status, limit, offset, sortField, sortDirection) - } - } - /** * Get all consents for a user, ordered by created_date desc. * Used by simpler endpoints that don't need pagination params. @@ -141,7 +104,7 @@ object DoobieConsentQueries { offset: Int = 0, sortField: String = "created_date", sortDirection: String = "desc" - ): (List[ConsentRow], Long) = { + ): List[ConsentRow] = { val query = buildFilteredQuery(userId, consumerId, consentId, status, limit, offset, sortField, sortDirection) DoobieUtil.runQuery(query) } @@ -153,14 +116,6 @@ object DoobieConsentQueries { note, frequency_per_day, uses_so_far_today_counter, jwt_payload FROM v_consent""" - private val selectColumnsWithCount = - fr"""SELECT consent_reference_id, consent_id, created_by_user_id, consumer_id, - status, jwt, consent_request_id, api_standard, api_version, - last_action_date, last_usage_date, created_date, - note, frequency_per_day, uses_so_far_today_counter, jwt_payload, - count(*) OVER() AS total_count - FROM v_consent""" - private def buildStatusCondition(status: Option[String]): Fragment = status match { case Some(s) => val statuses = s.split(",").toList.map(_.trim) @@ -187,23 +142,14 @@ object DoobieConsentQueries { offset: Int, sortField: String, sortDirection: String - ): ConnectionIO[(List[ConsentRow], Long)] = { + ): ConnectionIO[List[ConsentRow]] = { val statusCond = buildStatusCondition(status) val orderBy = buildOrderBy(sortField, sortDirection) val whereClause = fr"WHERE created_by_user_id = $userId " ++ statusCond - val dataQuery = selectColumnsWithCount ++ fr" " ++ whereClause ++ fr" " ++ orderBy ++ fr" LIMIT $limit OFFSET $offset" - - dataQuery.query[ConsentRowWithCount].to[List].map { rowsWithCount => - val total = rowsWithCount.headOption.map(_.totalCount).getOrElse(0L) - val rows = rowsWithCount.map(r => ConsentRow( - r.consentReferenceId, r.consentId, r.createdByUserId, r.consumerId, - r.status, r.jwt, r.consentRequestId, r.apiStandard, r.apiVersion, - r.lastActionDate, r.lastUsageDate, r.createdDate, - r.note, r.frequencyPerDay, r.usesSoFarTodayCounter, r.jwtPayload - )) - (rows, total) - } + val dataQuery = selectColumns ++ fr" " ++ whereClause ++ fr" " ++ orderBy ++ fr" LIMIT $limit OFFSET $offset" + + dataQuery.query[ConsentRow].to[List] } private def buildFilteredQuery( @@ -215,7 +161,7 @@ object DoobieConsentQueries { offset: Int, sortField: String, sortDirection: String - ): ConnectionIO[(List[ConsentRow], Long)] = { + ): ConnectionIO[List[ConsentRow]] = { val userCond = userId.map(v => fr"AND created_by_user_id = $v").getOrElse(fr"") val consumerCond = consumerId.map(v => fr"AND consumer_id = $v").getOrElse(fr"") val consentCond = consentId.map(v => fr"AND consent_id = $v").getOrElse(fr"") @@ -223,17 +169,79 @@ object DoobieConsentQueries { val orderBy = buildOrderBy(sortField, sortDirection) val whereClause = fr"WHERE 1=1 " ++ userCond ++ consumerCond ++ consentCond ++ statusCond - val dataQuery = selectColumnsWithCount ++ fr" " ++ whereClause ++ fr" " ++ orderBy ++ fr" LIMIT $limit OFFSET $offset" - - dataQuery.query[ConsentRowWithCount].to[List].map { rowsWithCount => - val total = rowsWithCount.headOption.map(_.totalCount).getOrElse(0L) - val rows = rowsWithCount.map(r => ConsentRow( - r.consentReferenceId, r.consentId, r.createdByUserId, r.consumerId, - r.status, r.jwt, r.consentRequestId, r.apiStandard, r.apiVersion, - r.lastActionDate, r.lastUsageDate, r.createdDate, - r.note, r.frequencyPerDay, r.usesSoFarTodayCounter, r.jwtPayload - )) - (rows, total) + val dataQuery = selectColumns ++ fr" " ++ whereClause ++ fr" " ++ orderBy ++ fr" LIMIT $limit OFFSET $offset" + + dataQuery.query[ConsentRow].to[List] + } + + /** + * Insert consent items from the consent's views and entitlements. + * Called at consent creation time after the JWT is set. + */ + def insertConsentItems(consentReferenceId: Long, consentJWT: code.api.util.ConsentJWT): Unit = { + val viewInserts = consentJWT.views.filter(_.bank_id.nonEmpty).map { view => + val consentItemId = java.util.UUID.randomUUID().toString + val itemType = "VIEW" + val bankId = view.bank_id + val accountId: Option[String] = Option(view.account_id).filter(_.nonEmpty) + val viewId: Option[String] = Option(view.view_id).filter(_.nonEmpty) + fr"""INSERT INTO consent_items (consent_item_id, consent_reference_id, item_type, bank_id, account_id, view_id) + VALUES ($consentItemId, $consentReferenceId, $itemType, $bankId, $accountId, $viewId)""".update.run } + val entitlementInserts = consentJWT.entitlements.filter(_.bank_id.nonEmpty).map { role => + val consentItemId = java.util.UUID.randomUUID().toString + val itemType = "ENTITLEMENT" + val bankId = role.bank_id + val roleName: Option[String] = Option(role.role_name).filter(_.nonEmpty) + fr"""INSERT INTO consent_items (consent_item_id, consent_reference_id, item_type, bank_id, role_name) + VALUES ($consentItemId, $consentReferenceId, $itemType, $bankId, $roleName)""".update.run + } + val allInserts = viewInserts ++ entitlementInserts + if (allInserts.nonEmpty) { + val combined = allInserts.reduce((a, b) => a.flatMap(_ => b)) + DoobieUtil.runQuery(combined) + } + } + + /** + * Get consents for a user filtered by bank_id, with pagination. + * Uses the consent_items join table for efficient bank-scoped queries. + */ + def getConsentsByUserAndBank( + userId: String, + bankId: String, + status: Option[String], + limit: Int, + offset: Int, + sortField: String, + sortDirection: String + ): List[ConsentRow] = { + val query = buildGetConsentsByBankQuery(userId, bankId, status, limit, offset, sortField, sortDirection) + DoobieUtil.runQuery(query) + } + + private def buildGetConsentsByBankQuery( + userId: String, + bankId: String, + status: Option[String], + limit: Int, + offset: Int, + sortField: String, + sortDirection: String + ): ConnectionIO[List[ConsentRow]] = { + val statusCond = buildStatusCondition(status) + val orderBy = buildOrderBy(sortField, sortDirection) + + val dataQuery = + fr"""SELECT DISTINCT v.consent_reference_id, v.consent_id, v.created_by_user_id, v.consumer_id, + v.status, v.jwt, v.consent_request_id, v.api_standard, v.api_version, + v.last_action_date, v.last_usage_date, v.created_date, + v.note, v.frequency_per_day, v.uses_so_far_today_counter, v.jwt_payload + FROM v_consent v + JOIN consent_items cb ON cb.consent_reference_id = v.consent_reference_id + WHERE v.created_by_user_id = $userId + AND cb.bank_id = $bankId""" ++ statusCond ++ fr" " ++ orderBy ++ fr" LIMIT $limit OFFSET $offset" + + dataQuery.query[ConsentRow].to[List] } } diff --git a/obp-api/src/main/scala/code/consent/MappedConsent.scala b/obp-api/src/main/scala/code/consent/MappedConsent.scala index 7f9687802d..8c8d611969 100644 --- a/obp-api/src/main/scala/code/consent/MappedConsent.scala +++ b/obp-api/src/main/scala/code/consent/MappedConsent.scala @@ -1,7 +1,7 @@ package code.consent import java.util.Date -import code.api.util.{APIUtil, Consent, ErrorMessages, JwtUtil, OBPBankId, OBPConsentId, OBPConsumerId, OBPLimit, OBPOffset, OBPQueryParam, OBPSortBy, OBPStatus, OBPUserId, ProviderProviderId, SecureRandomUtil} +import code.api.util.{APIUtil, Consent, ConsentJWT, ErrorMessages, JwtUtil, OBPBankId, OBPConsentId, OBPConsumerId, OBPLimit, OBPOffset, OBPQueryParam, OBPSortBy, OBPStatus, OBPUserId, ProviderProviderId, SecureRandomUtil} import code.consent.ConsentStatus.ConsentStatus import code.model.Consumer import code.model.dataAccess.ResourceUser @@ -17,7 +17,7 @@ import java.net.URLDecoder import java.nio.charset.StandardCharsets import scala.collection.immutable.List -object MappedConsentProvider extends ConsentProvider { +object MappedConsentProvider extends ConsentProvider with code.util.Helper.MdcLoggable { override def getConsentByConsentId(consentId: String): Box[MappedConsent] = { MappedConsent.find( By(MappedConsent.mConsentId, consentId) @@ -305,10 +305,26 @@ object MappedConsentProvider extends ConsentProvider { MappedConsent.find(By(MappedConsent.mConsentId, consentId)) match { case Full(consent) => val payload = JwtUtil.getSignedPayloadAsJson(jwt).openOr(null) - tryo(consent + val result = tryo(consent .mJsonWebToken(jwt) .mJsonWebTokenPayload(payload) .saveMe()) + // Denormalise bank_id, account_id, view_id and role_name from the JWT into consent_items + // so that bank-scoped queries can use an indexed SQL join instead of extracting every JWT. + result.foreach { savedConsent => + try { + if (payload != null) { + import net.liftweb.json._ + implicit val formats: DefaultFormats.type = DefaultFormats + val consentJWT = parse(payload).extract[ConsentJWT] + DoobieConsentQueries.insertConsentItems(savedConsent.id.get, consentJWT) + } + } catch { + case e: Exception => + logger.error(s"setJsonWebToken says: Failed to populate consent_items for consent $consentId: ${e.getMessage}") + } + } + result case Empty => Empty ?~! ErrorMessages.ConsentNotFound case Failure(msg, _, _) => diff --git a/obp-api/src/main/scala/code/consent/MappedConsentItems.scala b/obp-api/src/main/scala/code/consent/MappedConsentItems.scala new file mode 100644 index 0000000000..3ce0707ae6 --- /dev/null +++ b/obp-api/src/main/scala/code/consent/MappedConsentItems.scala @@ -0,0 +1,41 @@ +package code.consent + +import code.util.MappedUUID +import net.liftweb.mapper._ + +// consent_items denormalises key fields (bank_id, account_id, view_id, role_name) from the consent JWT +// so that bank-scoped queries can be done via a simple indexed SQL join instead of extracting and +// parsing every JWT. Rows are written at consent creation time alongside JWT generation. +class MappedConsentItems extends LongKeyedMapper[MappedConsentItems] with IdPK { + def getSingleton = MappedConsentItems + + object consentItemId extends MappedUUID(this) { + override def dbColumnName = "consent_item_id" + } + object consentReferenceId extends MappedLong(this) { + override def dbColumnName = "consent_reference_id" + } + object itemType extends MappedString(this, 64) { + override def dbColumnName = "item_type" + } + object bankId extends MappedString(this, 255) { + override def dbColumnName = "bank_id" + } + object accountId extends MappedString(this, 255) { + override def dbColumnName = "account_id" + override def defaultValue = null + } + object viewId extends MappedString(this, 255) { + override def dbColumnName = "view_id" + override def defaultValue = null + } + object roleName extends MappedString(this, 255) { + override def dbColumnName = "role_name" + override def defaultValue = null + } +} + +object MappedConsentItems extends MappedConsentItems with LongKeyedMetaMapper[MappedConsentItems] { + override def dbTableName = "consent_items" + override def dbIndexes = UniqueIndex(consentItemId) :: Index(consentReferenceId) :: Index(bankId) :: Index(consentReferenceId, bankId) :: super.dbIndexes +} From 27498d0d7af34019256571571c05ffc84f10113c Mon Sep 17 00:00:00 2001 From: simonredfern Date: Sun, 22 Mar 2026 23:17:22 +0100 Subject: [PATCH 5/7] Consent jwt_expires_at --- .../ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala | 1 + .../scala/code/api/util/migration/Migration.scala | 13 +++++++++++++ .../api/util/migration/MigrationOfConsentView.scala | 6 ++++-- .../main/scala/code/api/v5_1_0/APIMethods510.scala | 3 ++- .../scala/code/api/v5_1_0/JSONFactory5.1.0.scala | 4 +++- .../scala/code/consent/DoobieConsentQueries.scala | 9 +++++---- 6 files changed, 28 insertions(+), 8 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 4f2f681613..86848b97ae 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -4604,6 +4604,7 @@ object SwaggerDefinitionsJSON { jwt_payload = """{"createdByUserId":"user-id","sub":"subject","iss":"issuer","aud":"audience","jti":"jwt-id","iat":1611749820,"nbf":1611749820,"exp":1611753420,"request_headers":[],"name":null,"email":null,"entitlements":[],"views":[],"access":null}""", api_standard = "Berlin Group", api_version = "v1.3", + jwt_expires_at = dateTimeExample.value, ) lazy val consentsInfoJsonV510 = ConsentsInfoJsonV510( diff --git a/obp-api/src/main/scala/code/api/util/migration/Migration.scala b/obp-api/src/main/scala/code/api/util/migration/Migration.scala index cf792397c0..544b3ee80a 100644 --- a/obp-api/src/main/scala/code/api/util/migration/Migration.scala +++ b/obp-api/src/main/scala/code/api/util/migration/Migration.scala @@ -113,6 +113,7 @@ object Migration extends MdcLoggable { addMetricView(startedBeforeSchemifier) addConsentView(startedBeforeSchemifier) updateConsentViewAddJwtPayload(startedBeforeSchemifier) + updateConsentViewAddJwtExpiresAt(startedBeforeSchemifier) updateAccountAccessWithViewsViewUnionAll(startedBeforeSchemifier) } @@ -613,6 +614,18 @@ object Migration extends MdcLoggable { } } + private def updateConsentViewAddJwtExpiresAt(startedBeforeSchemifier: Boolean): Boolean = { + if(startedBeforeSchemifier == true) { + logger.warn(s"Migration.database.updateConsentViewAddJwtExpiresAt(true) cannot be run before Schemifier.") + true + } else { + val name = nameOf(updateConsentViewAddJwtExpiresAt(startedBeforeSchemifier)) + runOnce(name) { + MigrationOfConsentView.addConsentView(name) + } + } + } + private def addAccountAccessWithViewsView(startedBeforeSchemifier: Boolean): Boolean = { if(startedBeforeSchemifier == true) { logger.warn(s"Migration.database.addAccountAccessWithViewsView(true) cannot be run before Schemifier.") diff --git a/obp-api/src/main/scala/code/api/util/migration/MigrationOfConsentView.scala b/obp-api/src/main/scala/code/api/util/migration/MigrationOfConsentView.scala index 71c1e47a5c..2bf404de61 100644 --- a/obp-api/src/main/scala/code/api/util/migration/MigrationOfConsentView.scala +++ b/obp-api/src/main/scala/code/api/util/migration/MigrationOfConsentView.scala @@ -37,7 +37,8 @@ object MigrationOfConsentView { | mnote AS note, | mfrequencyperday AS frequency_per_day, | musessofartodaycounter AS uses_so_far_today_counter, - | mjsonwebtokenpayload AS jwt_payload + | mjsonwebtokenpayload AS jwt_payload, + | jwt_expires_at AS jwt_expires_at |FROM mappedconsent; |""".stripMargin case _ => @@ -60,7 +61,8 @@ object MigrationOfConsentView { | mnote AS note, | mfrequencyperday AS frequency_per_day, | musessofartodaycounter AS uses_so_far_today_counter, - | mjsonwebtokenpayload AS jwt_payload + | mjsonwebtokenpayload AS jwt_payload, + | jwt_expires_at AS jwt_expires_at |FROM mappedconsent; |""".stripMargin } diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index 22b66c254b..15e8233a2a 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -1962,7 +1962,8 @@ trait APIMethods510 { jwt = row.jwt.orNull, jwt_payload = row.jwtPayload.orNull, api_standard = row.apiStandard.orNull, - api_version = row.apiVersion.orNull + api_version = row.apiVersion.orNull, + jwt_expires_at = row.jwtExpiresAt.map(d => new java.text.SimpleDateFormat(DateWithSeconds).format(d)).orNull ) } diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala index 404bdf2504..09674bb233 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala @@ -175,6 +175,7 @@ case class ConsentInfoJsonV510(consent_reference_id: String, jwt_payload: String, api_standard: String, api_version: String, + jwt_expires_at: String, ) case class ConsentsInfoJsonV510(consents: List[ConsentInfoJsonV510]) @@ -1020,7 +1021,8 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable { jwt = c.jsonWebToken, jwt_payload = null, api_standard = c.apiStandard, - api_version = c.apiVersion + api_version = c.apiVersion, + jwt_expires_at = null ) } ) diff --git a/obp-api/src/main/scala/code/consent/DoobieConsentQueries.scala b/obp-api/src/main/scala/code/consent/DoobieConsentQueries.scala index 88618c629c..c4a748b4c5 100644 --- a/obp-api/src/main/scala/code/consent/DoobieConsentQueries.scala +++ b/obp-api/src/main/scala/code/consent/DoobieConsentQueries.scala @@ -39,7 +39,8 @@ object DoobieConsentQueries { note: Option[String], frequencyPerDay: Option[Int], usesSoFarTodayCounter: Option[Int], - jwtPayload: Option[String] + jwtPayload: Option[String], + jwtExpiresAt: Option[Timestamp] ) /** @@ -74,7 +75,7 @@ object DoobieConsentQueries { fr"""SELECT consent_reference_id, consent_id, created_by_user_id, consumer_id, status, jwt, consent_request_id, api_standard, api_version, last_action_date, last_usage_date, created_date, - note, frequency_per_day, uses_so_far_today_counter, jwt_payload + note, frequency_per_day, uses_so_far_today_counter, jwt_payload, jwt_expires_at FROM v_consent WHERE created_by_user_id = $userId ORDER BY created_date DESC, api_standard DESC""" @@ -113,7 +114,7 @@ object DoobieConsentQueries { fr"""SELECT consent_reference_id, consent_id, created_by_user_id, consumer_id, status, jwt, consent_request_id, api_standard, api_version, last_action_date, last_usage_date, created_date, - note, frequency_per_day, uses_so_far_today_counter, jwt_payload + note, frequency_per_day, uses_so_far_today_counter, jwt_payload, jwt_expires_at FROM v_consent""" private def buildStatusCondition(status: Option[String]): Fragment = status match { @@ -236,7 +237,7 @@ object DoobieConsentQueries { fr"""SELECT DISTINCT v.consent_reference_id, v.consent_id, v.created_by_user_id, v.consumer_id, v.status, v.jwt, v.consent_request_id, v.api_standard, v.api_version, v.last_action_date, v.last_usage_date, v.created_date, - v.note, v.frequency_per_day, v.uses_so_far_today_counter, v.jwt_payload + v.note, v.frequency_per_day, v.uses_so_far_today_counter, v.jwt_payload, v.jwt_expires_at FROM v_consent v JOIN consent_items cb ON cb.consent_reference_id = v.consent_reference_id WHERE v.created_by_user_id = $userId From 943bd62b94c0bb1be3efa3f0d405aa5a69002228 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Sun, 22 Mar 2026 23:17:43 +0100 Subject: [PATCH 6/7] Update MappedConsent.scala --- .../scala/code/consent/MappedConsent.scala | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/obp-api/src/main/scala/code/consent/MappedConsent.scala b/obp-api/src/main/scala/code/consent/MappedConsent.scala index 8c8d611969..30b92102b6 100644 --- a/obp-api/src/main/scala/code/consent/MappedConsent.scala +++ b/obp-api/src/main/scala/code/consent/MappedConsent.scala @@ -305,18 +305,34 @@ object MappedConsentProvider extends ConsentProvider with code.util.Helper.MdcLo MappedConsent.find(By(MappedConsent.mConsentId, consentId)) match { case Full(consent) => val payload = JwtUtil.getSignedPayloadAsJson(jwt).openOr(null) + // Parse JWT payload to denormalise exp and consent items + val consentJWTParsed: Option[ConsentJWT] = if (payload != null) { + try { + import net.liftweb.json._ + implicit val formats: DefaultFormats.type = DefaultFormats + Some(parse(payload).extract[ConsentJWT]) + } catch { + case e: Exception => + logger.error(s"setJsonWebToken says: Failed to parse JWT payload for consent $consentId: ${e.getMessage}") + None + } + } else None + + // Set jwt_expires_at from the JWT exp claim + consentJWTParsed.foreach { jwt => + consent.mJwtExpiresAt(new Date(jwt.exp * 1000L)) + } + val result = tryo(consent .mJsonWebToken(jwt) .mJsonWebTokenPayload(payload) .saveMe()) + // Denormalise bank_id, account_id, view_id and role_name from the JWT into consent_items // so that bank-scoped queries can use an indexed SQL join instead of extracting every JWT. result.foreach { savedConsent => try { - if (payload != null) { - import net.liftweb.json._ - implicit val formats: DefaultFormats.type = DefaultFormats - val consentJWT = parse(payload).extract[ConsentJWT] + consentJWTParsed.foreach { consentJWT => DoobieConsentQueries.insertConsentItems(savedConsent.id.get, consentJWT) } } catch { @@ -455,6 +471,10 @@ class MappedConsent extends ConsentTrait with LongKeyedMapper[MappedConsent] wit object mStatusUpdateDateTime extends MappedDateTime(this) object mNote extends MappedText(this) object mJsonWebTokenPayload extends MappedText(this) + // Denormalised from the JWT exp claim so we can query expiry without parsing the JWT. + object mJwtExpiresAt extends MappedDateTime(this) { + override def dbColumnName = "jwt_expires_at" + } override def consentId: String = mConsentId.get override def userId: String = mUserId.get From 7f1af50c15600b2bd03ea57bdd8ecb62f6360c90 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Mon, 23 Mar 2026 00:37:32 +0100 Subject: [PATCH 7/7] ConsentItem table and class name --- .../src/main/scala/bootstrap/liftweb/Boot.scala | 4 ++-- .../scala/code/api/v4_0_0/APIMethods400.scala | 16 +++++++++++++++- .../scala/code/api/v5_1_0/APIMethods510.scala | 2 +- ...appedConsentItems.scala => ConsentItem.scala} | 10 +++++----- .../code/consent/DoobieConsentQueries.scala | 8 ++++---- .../main/scala/code/consent/MappedConsent.scala | 4 ++-- 6 files changed, 29 insertions(+), 15 deletions(-) rename obp-api/src/main/scala/code/consent/{MappedConsentItems.scala => ConsentItem.scala} (77%) diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 42843a46f9..a67bb12d19 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -63,7 +63,7 @@ import code.branches.MappedBranch import code.cardattribute.MappedCardAttribute import code.cards.{MappedPhysicalCard, PinReset} import code.connectormethod.ConnectorMethod -import code.consent.{ConsentRequest, MappedConsent, MappedConsentItems} +import code.consent.{ConsentItem, ConsentRequest, MappedConsent} import code.consumer.Consumers import code.model.Consumer import code.context.{MappedConsentAuthContext, MappedUserAuthContext, MappedUserAuthContextUpdate} @@ -1120,7 +1120,7 @@ object ToSchemify { MappedCustomerIdMapping, MappedProductAttribute, MappedConsent, - MappedConsentItems, + ConsentItem, ConsentRequest, MigrationScriptLog, MethodRouting, diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 97c7ea7581..50f8bc11c1 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -12127,6 +12127,10 @@ trait APIMethods400 extends MdcLoggable { | |${userAuthenticationMessage(true)} | + |1 limit (for pagination: defaults to 50) eg:limit=200 + | + |2 offset (for pagination: zero index, defaults to 0) eg: offset=10 + | |""".stripMargin, EmptyBody, apiCollectionsJson400, @@ -12140,12 +12144,22 @@ trait APIMethods400 extends MdcLoggable { lazy val getMyApiCollections: OBPEndpoint = { case "my" :: "api-collections" :: Nil JsonGet _ => { cc => implicit val ec = EndpointContext(Some(cc)) + val url = cc.url + val limitParam = getHttpRequestUrlParam(url, "limit") match { + case s if s.nonEmpty => scala.util.Try(s.toInt).getOrElse(50) + case _ => 50 + } + val offsetParam = getHttpRequestUrlParam(url, "offset") match { + case s if s.nonEmpty => scala.util.Try(s.toInt).getOrElse(0) + case _ => 0 + } for { (apiCollections, callContext) <- NewStyle.function .getApiCollectionsByUserId(cc.userId, Some(cc)) } yield { + val paginated = apiCollections.drop(offsetParam).take(limitParam) ( - JSONFactory400.createApiCollectionsJsonV400(apiCollections), + JSONFactory400.createApiCollectionsJsonV400(paginated), HttpCode.`200`(callContext) ) } diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index 15e8233a2a..40db84cf3e 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -1816,7 +1816,7 @@ trait APIMethods510 { |4 sort_by (defaults to created_date:desc) eg: sort_by=created_date:desc | |Note: This endpoint only returns consents that explicitly reference the specified BANK_ID. - |Consents created before the consent_items join table was introduced will not appear in results. + |Consents created before the consent_item join table was introduced will not appear in results. | |eg: /banks/BANK_ID/my/consents?limit=10&offset=0&sort_by=created_date:desc | diff --git a/obp-api/src/main/scala/code/consent/MappedConsentItems.scala b/obp-api/src/main/scala/code/consent/ConsentItem.scala similarity index 77% rename from obp-api/src/main/scala/code/consent/MappedConsentItems.scala rename to obp-api/src/main/scala/code/consent/ConsentItem.scala index 3ce0707ae6..66403d81df 100644 --- a/obp-api/src/main/scala/code/consent/MappedConsentItems.scala +++ b/obp-api/src/main/scala/code/consent/ConsentItem.scala @@ -3,11 +3,11 @@ package code.consent import code.util.MappedUUID import net.liftweb.mapper._ -// consent_items denormalises key fields (bank_id, account_id, view_id, role_name) from the consent JWT +// consent_item denormalises key fields (bank_id, account_id, view_id, role_name) from the consent JWT // so that bank-scoped queries can be done via a simple indexed SQL join instead of extracting and // parsing every JWT. Rows are written at consent creation time alongside JWT generation. -class MappedConsentItems extends LongKeyedMapper[MappedConsentItems] with IdPK { - def getSingleton = MappedConsentItems +class ConsentItem extends LongKeyedMapper[ConsentItem] with IdPK { + def getSingleton = ConsentItem object consentItemId extends MappedUUID(this) { override def dbColumnName = "consent_item_id" @@ -35,7 +35,7 @@ class MappedConsentItems extends LongKeyedMapper[MappedConsentItems] with IdPK { } } -object MappedConsentItems extends MappedConsentItems with LongKeyedMetaMapper[MappedConsentItems] { - override def dbTableName = "consent_items" +object ConsentItem extends ConsentItem with LongKeyedMetaMapper[ConsentItem] { + override def dbTableName = "consent_item" override def dbIndexes = UniqueIndex(consentItemId) :: Index(consentReferenceId) :: Index(bankId) :: Index(consentReferenceId, bankId) :: super.dbIndexes } diff --git a/obp-api/src/main/scala/code/consent/DoobieConsentQueries.scala b/obp-api/src/main/scala/code/consent/DoobieConsentQueries.scala index c4a748b4c5..0be3652481 100644 --- a/obp-api/src/main/scala/code/consent/DoobieConsentQueries.scala +++ b/obp-api/src/main/scala/code/consent/DoobieConsentQueries.scala @@ -186,7 +186,7 @@ object DoobieConsentQueries { val bankId = view.bank_id val accountId: Option[String] = Option(view.account_id).filter(_.nonEmpty) val viewId: Option[String] = Option(view.view_id).filter(_.nonEmpty) - fr"""INSERT INTO consent_items (consent_item_id, consent_reference_id, item_type, bank_id, account_id, view_id) + fr"""INSERT INTO consent_item (consent_item_id, consent_reference_id, item_type, bank_id, account_id, view_id) VALUES ($consentItemId, $consentReferenceId, $itemType, $bankId, $accountId, $viewId)""".update.run } val entitlementInserts = consentJWT.entitlements.filter(_.bank_id.nonEmpty).map { role => @@ -194,7 +194,7 @@ object DoobieConsentQueries { val itemType = "ENTITLEMENT" val bankId = role.bank_id val roleName: Option[String] = Option(role.role_name).filter(_.nonEmpty) - fr"""INSERT INTO consent_items (consent_item_id, consent_reference_id, item_type, bank_id, role_name) + fr"""INSERT INTO consent_item (consent_item_id, consent_reference_id, item_type, bank_id, role_name) VALUES ($consentItemId, $consentReferenceId, $itemType, $bankId, $roleName)""".update.run } val allInserts = viewInserts ++ entitlementInserts @@ -206,7 +206,7 @@ object DoobieConsentQueries { /** * Get consents for a user filtered by bank_id, with pagination. - * Uses the consent_items join table for efficient bank-scoped queries. + * Uses the consent_item join table for efficient bank-scoped queries. */ def getConsentsByUserAndBank( userId: String, @@ -239,7 +239,7 @@ object DoobieConsentQueries { v.last_action_date, v.last_usage_date, v.created_date, v.note, v.frequency_per_day, v.uses_so_far_today_counter, v.jwt_payload, v.jwt_expires_at FROM v_consent v - JOIN consent_items cb ON cb.consent_reference_id = v.consent_reference_id + JOIN consent_item cb ON cb.consent_reference_id = v.consent_reference_id WHERE v.created_by_user_id = $userId AND cb.bank_id = $bankId""" ++ statusCond ++ fr" " ++ orderBy ++ fr" LIMIT $limit OFFSET $offset" diff --git a/obp-api/src/main/scala/code/consent/MappedConsent.scala b/obp-api/src/main/scala/code/consent/MappedConsent.scala index 30b92102b6..34eae3c43f 100644 --- a/obp-api/src/main/scala/code/consent/MappedConsent.scala +++ b/obp-api/src/main/scala/code/consent/MappedConsent.scala @@ -328,7 +328,7 @@ object MappedConsentProvider extends ConsentProvider with code.util.Helper.MdcLo .mJsonWebTokenPayload(payload) .saveMe()) - // Denormalise bank_id, account_id, view_id and role_name from the JWT into consent_items + // Denormalise bank_id, account_id, view_id and role_name from the JWT into consent_item // so that bank-scoped queries can use an indexed SQL join instead of extracting every JWT. result.foreach { savedConsent => try { @@ -337,7 +337,7 @@ object MappedConsentProvider extends ConsentProvider with code.util.Helper.MdcLo } } catch { case e: Exception => - logger.error(s"setJsonWebToken says: Failed to populate consent_items for consent $consentId: ${e.getMessage}") + logger.error(s"setJsonWebToken says: Failed to populate consent_item for consent $consentId: ${e.getMessage}") } } result