Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2643,14 +2643,38 @@ object SwaggerDefinitionsJSON {
is_deleted = false,
last_marketing_agreement_signed_date = Some(DateWithDayExampleObject),
is_locked = false,
last_activity_date = Some(DateWithDayExampleObject),
recent_operation_ids = List("obp.getBank", "obp.getAccounts", "obp.getTransactions", "obp.getUser", "obp.getCustomer")
created_date = Some(DateWithDayExampleObject),
updated_date = Some(DateWithDayExampleObject),
email_validated = Some(true),
last_used_locale = Some("en_GB")
)

lazy val usersInfoJsonV600 = UsersInfoJsonV600(
users = List(userInfoJsonV600)
)

lazy val userInfoDetailJsonV600 = UserInfoDetailJsonV600(
user_id = ExampleValue.userIdExample.value,
email = ExampleValue.emailExample.value,
provider_id = providerIdValueExample.value,
provider = providerValueExample.value,
username = usernameExample.value,
first_name = ExampleValue.firstNameExample.value,
last_name = ExampleValue.lastNameExample.value,
entitlements = entitlementJSONs,
views = Some(viewsJSON300),
agreements = Some(List(userAgreementJson)),
is_deleted = false,
last_marketing_agreement_signed_date = Some(DateWithDayExampleObject),
is_locked = false,
created_date = Some(DateWithDayExampleObject),
updated_date = Some(DateWithDayExampleObject),
email_validated = Some(true),
last_used_locale = Some("en_GB"),
last_activity_date = Some(DateWithDayExampleObject),
recent_operation_ids = List("obp.getBank", "obp.getAccounts", "obp.getTransactions", "obp.getUser", "obp.getCustomer")
)

lazy val userWithNamesJsonV510 = UserWithNamesJsonV510(
user_id = ExampleValue.userIdExample.value,
email = ExampleValue.emailExample.value,
Expand All @@ -2667,6 +2691,7 @@ object SwaggerDefinitionsJSON {
is_locked = false
)


lazy val migrationScriptLogJsonV600 = MigrationScriptLogJsonV600(
migration_script_log_id = "550e8400-e29b-41d4-a716-446655440000",
name = "addUniqueIndexOnResourceUserUserId",
Expand Down
43 changes: 37 additions & 6 deletions obp-api/src/main/scala/code/api/util/APIUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1188,7 +1188,9 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
value <- tryo(values.head.toBoolean) ?~! FilterIsDeletedFormatError
deleted = OBPIsDeleted(value)
} yield deleted
case "sort_by" => Full(OBPSortBy(values.head))
case "sort_by" =>
if (SortFields.All.contains(values.head)) Full(OBPSortBy(values.head))
else Failure(FilterSortByError)
case "status" => Full(OBPStatus(values.head))
case "consumer_id" => Full(OBPConsumerId(values.head))
case "azp" => Full(OBPAzp(values.head))
Expand Down Expand Up @@ -1220,6 +1222,9 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
case "customer_id" => Full(OBPCustomerId(values.head))
case "locked_status" => Full(OBPLockedStatus(values.head))
case "role_name" => Full(OBPRoleName(values.head))
case "provider" => Full(OBPProvider(values.head))
case "username" => Full(OBPUsername(values.head))
case "email" => Full(OBPEmail(values.head))
case _ => Full(OBPEmpty())
}
} yield
Expand Down Expand Up @@ -1269,6 +1274,9 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
customerId <- getHttpParamValuesByName(httpParams, "customer_id")
lockedStatus <- getHttpParamValuesByName(httpParams, "locked_status")
roleName <- getHttpParamValuesByName(httpParams, "role_name")
provider <- getHttpParamValuesByName(httpParams, "provider")
username <- getHttpParamValuesByName(httpParams, "username")
email <- getHttpParamValuesByName(httpParams, "email")
httpStatusCode <- getHttpParamValuesByName(httpParams, "http_status_code")
}yield{
// Extract the sort field name from the sort_by query param (e.g. "url", "date").
Expand All @@ -1282,8 +1290,8 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
List(limit, offset, ordering, sortBy, fromDate, toDate,
anon, status, consumerId, azp, iss, consentId, userId, providerProviderId, url, appName, implementedByPartialFunction, implementedInVersion,
verb, correlationId, duration, httpStatusCode, excludeAppNames, excludeUrlPattern, excludeImplementedByPartialfunctions,
includeAppNames, includeUrlPattern, includeImplementedByPartialfunctions,
connectorName,functionName, bankId, accountId, customerId, lockedStatus, roleName, deletedStatus
includeAppNames, includeUrlPattern, includeImplementedByPartialfunctions,
connectorName,functionName, bankId, accountId, customerId, lockedStatus, roleName, provider, username, email, deletedStatus
).filter(_ != OBPEmpty())
}
}
Expand Down Expand Up @@ -1338,6 +1346,9 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
val customerId = getHttpRequestUrlParam(httpRequestUrl, "customer_id")
val lockedStatus = getHttpRequestUrlParam(httpRequestUrl, "locked_status")
val roleName = getHttpRequestUrlParam(httpRequestUrl, "role_name")
val provider = getHttpRequestUrlParam(httpRequestUrl, "provider")
val username = getHttpRequestUrlParam(httpRequestUrl, "username")
val email = getHttpRequestUrlParam(httpRequestUrl, "email")

//The following three are not a string, it should be List of String
//eg: exclude_app_names=A,B,C --> List(A,B,C)
Expand Down Expand Up @@ -1371,7 +1382,10 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
HTTPParam("customer_id", customerId),
HTTPParam("is_deleted", isDeleted),
HTTPParam("locked_status", lockedStatus),
HTTPParam("role_name", roleName)
HTTPParam("role_name", roleName),
HTTPParam("provider", provider),
HTTPParam("username", username),
HTTPParam("email", email)
).filter(_.values.head != ""))//Here filter the field when value = "".
}

Expand All @@ -1388,8 +1402,23 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
*/
def getHttpRequestUrlParam(httpRequestUrl: String, name: String): String = {
val urlAndQueryString = if (httpRequestUrl.contains("?")) httpRequestUrl.split("\\?",2)(1) else "" // Full(from_date=$DateWithMsExampleString&to_date=$DateWithMsExampleString)
val queryStrings = urlAndQueryString.split("&").map(_.split("=")).flatten //Full(from_date, $DateWithMsExampleString, to_date, $DateWithMsExampleString)
if (queryStrings.contains(name)&& queryStrings.length > queryStrings.indexOf(name)+1) queryStrings(queryStrings.indexOf(name)+1) else ""//Full($DateWithMsExampleString)
// Parse each `k=v` pair individually. The previous implementation flattened everything into a single
// array and then used indexOf(name), which collided when a param VALUE happened to equal another
// param's NAME (e.g. `?sort_by=email` would cause lookups for `email` to return the next element).
val pairs: Map[String, String] = urlAndQueryString.split("&").iterator.flatMap { pair =>
pair.split("=", 2) match {
case Array(k, v) if k.nonEmpty => Some(k -> v)
case Array(k) if k.nonEmpty => Some(k -> "")
case _ => None
}
}.toMap
val raw = pairs.getOrElse(name, "")
// URL-decode percent-escaped values (e.g. %40 → @, %20 → space). Without this, ?email=simon%40tesobe.com
// reached filter code as the literal 18-char string containing `%40`, which never matched any DB row.
// Fall back to raw on malformed escapes so SQL-LIKE wildcard values (e.g. `%users%`) still work.
if (raw.isEmpty) raw
else try java.net.URLDecoder.decode(raw, "UTF-8")
catch { case _: IllegalArgumentException => raw }
}
//ended -- Filtering and Paging relevant methods ////////////////////////////

Expand Down Expand Up @@ -2168,6 +2197,8 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|* offset=NUMBER ==> default value: 0
|
|eg1:?limit=100&offset=0
|
|**URL encoding:** query parameter values containing `&`, `=`, `+`, `#`, `%`, or spaces must be URL-encoded (e.g. `+` → `%2B`, space → `%20`). Most other characters (including `@` in emails) are safe unencoded, but encoding is always permitted.
|""". stripMargin

val sortDirectionParameters = if (containsSortDirection) {
Expand Down
4 changes: 4 additions & 0 deletions obp-api/src/main/scala/code/api/util/ErrorMessages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ object ErrorMessages {

// General Sort and Paging
val FilterSortDirectionError = "OBP-10023: obp_sort_direction parameter can only take two values: DESC or ASC!" // was OBP-20023
val FilterSortByError = s"OBP-10042: sort_by parameter value is not in the global whitelist. Allowed values are: ${SortFields.All.toSeq.sorted.mkString(", ")}."
val FilterSortByNotAllowedForEndpoint = "OBP-10043: sort_by value is not allowed for this endpoint."
def filterSortByNotAllowedForEndpointDetail(endpoint: String, requested: String, allowed: Iterable[String]): String =
s"$FilterSortByNotAllowedForEndpoint Endpoint: $endpoint. Value requested: '$requested'. Allowed values: ${allowed.toSeq.sorted.mkString(", ")}."
val FilterOffersetError = "OBP-10024: wrong value for obp_offset parameter. Please send a positive integer (=>0)!" // was OBP-20024
val FilterLimitError = "OBP-10025: wrong value for obp_limit parameter. Please send a positive integer (=>1)!" // was OBP-20025
val FilterDateFormatError = s"OBP-10026: Failed to parse date string. Please use this format ${DateWithMsFormat.toPattern}!" // OBP-20026
Expand Down
43 changes: 43 additions & 0 deletions obp-api/src/main/scala/code/api/util/Glossary.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5211,6 +5211,49 @@ object Glossary extends MdcLoggable {
|
""")

glossaryItems += GlossaryItem(
title = "Chat Room",
description =
s"""
|# Chat Room
|
|A **Chat Room** is a named space where users and consumers (apps/bots) exchange messages. Each room is either **system-level** or scoped to a single **bank**.
|
|See also the broader [Chat](/glossary#Chat) entry, which covers messages, threads, reactions, mentions, typing indicators, gRPC streaming, and the full permissions model.
|
|## Identity and scope
|- **chat_room_id** — UUID identifying the room.
|- **bank_id** — non-empty for bank-scoped rooms, empty string for system-level rooms.
|- **name** — unique within scope (per bank, or globally for system-level).
|
|## Open vs Closed rooms
|The **is_open_room** flag controls how membership works:
|
|- **Closed room** (`is_open_room = false`): only users with an explicit Participant record can read or post. New members must present the room's joining_key.
|- **Open room** (`is_open_room = true`): every authenticated user is treated as an **implicit participant** (see `ChatPermissions.isParticipant`). They can read and post without a Participant record, but have no special permissions. Open rooms also appear in `GET /chat-rooms` for everyone, not just existing members.
|
|The auto-created system room **general** is open by default.
|
|## Joining keys
|Each Chat Room has a **joining_key** (UUID). To join a room explicitly, a user calls `POST /chat-room-participants` with `{ joining_key }` — the key alone identifies the room.
|
|- For closed rooms, the key is the only way in. It is exposed in `GET /chat-rooms` and `GET /chat-rooms/{id}` to existing participants only, who then share it out-of-band (chat, email, link).
|- For open rooms, the key still exists but is rarely needed, since users are already implicit participants. Joining explicitly creates a Participant record so the user can be granted permissions, mute the room, or track last_read_at.
|- The key can be rotated by a participant with the **can_refresh_joining_key** permission, via `PUT /chat-rooms/{id}/joining-key`. The old key becomes invalid.
|
|## Lifecycle flags
|- **is_archived** — archived rooms reject new messages and new participants but remain readable for audit.
|- **created_by / created_by_username / created_by_provider** — identifies the room creator. The creator is granted all participant permissions.
|
|## Endpoints
|Each Chat Room operation has both a system-level and bank-scoped variant:
|- System-level: `/obp/v6.0.0/chat-rooms/...`
|- Bank-scoped: `/obp/v6.0.0/banks/BANK_ID/chat-rooms/...`
|
|See the API Explorer with the **Chat** tag for the full list.
|
""")

///////////////////////////////////////////////////////////////////
// NOTE! Some glossary items are generated in ExampleValue.scala
//////////////////////////////////////////////////////////////////
Expand Down
3 changes: 3 additions & 0 deletions obp-api/src/main/scala/code/api/util/OBPParam.scala
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ case class OBPCustomerId(value: String) extends OBPQueryParam
case class OBPLockedStatus(value: String) extends OBPQueryParam
case class OBPIsDeleted(value: Boolean) extends OBPQueryParam
case class OBPRoleName(value: String) extends OBPQueryParam
case class OBPProvider(value: String) extends OBPQueryParam
case class OBPUsername(value: String) extends OBPQueryParam
case class OBPEmail(value: String) extends OBPQueryParam

object OBPQueryParam {
val LIMIT = "limit"
Expand Down
67 changes: 67 additions & 0 deletions obp-api/src/main/scala/code/api/util/SortFields.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package code.api.util

/**
* Global whitelist of field names accepted by the `sort_by` query parameter.
*
* Constants are spelled to match the wire value — `USER_ID` holds the string
* "user_id" — so `SortFields.USER_ID` reads naturally as "the user_id sort field".
*
* `All` is the union used by the central validator in
* `APIUtil.getHttpParamValuesByName`. Endpoints may narrow further via their
* own per-endpoint whitelists.
*/
object SortFields {

// Temporal
val CREATED_DATE = "created_date"
val UPDATED_DATE = "updated_date"
val DATE = "date"

// Identity
val USER_ID = "user_id"
val CONSUMER_ID = "consumer_id"
val BANK_ID = "bank_id"
val ACCOUNT_ID = "account_id"
val CUSTOMER_ID = "customer_id"
val CARD_ID = "card_id"
val TRANSACTION_ID = "transaction_id"
val COUNTERPARTY_ID = "counterparty_id"
val CONSENT_ID = "consent_id"
val CORRELATION_ID = "correlation_id"

// Descriptive
val USERNAME = "username"
val EMAIL = "email"
val PROVIDER = "provider"
val NAME = "name"
val SHORT_NAME = "short_name"
val FULL_NAME = "full_name"
val LABEL = "label"
val STATUS = "status"
val APP_NAME = "app_name"
val DEVELOPER_EMAIL = "developer_email"

// Financial
val AMOUNT = "amount"
val CURRENCY = "currency"
val BALANCE = "balance"

// Request / metrics
val URL = "url"
val VERB = "verb"
val HTTP_STATUS_CODE = "http_status_code"
val DURATION = "duration"
val IMPLEMENTED_BY_PARTIAL_FUNCTION = "implemented_by_partial_function"
val IMPLEMENTED_IN_VERSION = "implemented_in_version"

val All: Set[String] = Set(
CREATED_DATE, UPDATED_DATE, DATE,
USER_ID, CONSUMER_ID, BANK_ID, ACCOUNT_ID, CUSTOMER_ID, CARD_ID,
TRANSACTION_ID, COUNTERPARTY_ID, CONSENT_ID, CORRELATION_ID,
USERNAME, EMAIL, PROVIDER, NAME, SHORT_NAME, FULL_NAME, LABEL, STATUS,
APP_NAME, DEVELOPER_EMAIL,
AMOUNT, CURRENCY, BALANCE,
URL, VERB, HTTP_STATUS_CODE, DURATION,
IMPLEMENTED_BY_PARTIAL_FUNCTION, IMPLEMENTED_IN_VERSION
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ object Migration extends MdcLoggable {
updateConsentViewAddJwtExpiresAt(startedBeforeSchemifier)
updateAccountAccessWithViewsViewUnionAll(startedBeforeSchemifier)
migrateChatRoomIsOpenRoom()
migrateChatRoomCreatedByAndLastMessageSender()
}

private def dummyScript(): Boolean = {
Expand Down Expand Up @@ -657,6 +658,13 @@ object Migration extends MdcLoggable {
MigrationOfChatRoomIsOpenRoom.migrateColumn(name)
}
}

private def migrateChatRoomCreatedByAndLastMessageSender(): Boolean = {
val name = nameOf(migrateChatRoomCreatedByAndLastMessageSender)
runOnce(name) {
MigrationOfChatRoomCreatedByAndLastMessageSender.migrateColumns(name)
}
}
}

/**
Expand Down
Loading
Loading