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 @@ -193,7 +193,9 @@ object MessageDocsSwaggerDefinitions
lastOkDate = DateWithDayExampleObject,
title =titleExample.value,
branchId = branchIdExample.value,
nameSuffix = nameSuffixExample.value
nameSuffix = nameSuffixExample.value,
customerType = "",
parentCustomerId = ""
)

val customerAttribute = CustomerAttributeCommons(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2561,9 +2561,48 @@ object SwaggerDefinitionsJSON {
last_ok_date = Some(oneYearAgoDate),
title = Some(ExampleValue.titleExample.value),
branch_id = Some(ExampleValue.branchIdExample.value),
name_suffix = Some(ExampleValue.nameSuffixExample.value),
customer_type = Some(ExampleValue.customerTypeExample.value),
parent_customer_id = None
)

lazy val postRetailCustomerJsonV600 =
PostRetailCustomerJsonV600(
legal_name = ExampleValue.legalNameExample.value,
customer_number = Some(ExampleValue.customerNumberExample.value),
mobile_phone_number = ExampleValue.mobilePhoneNumberExample.value,
email = Some(ExampleValue.emailExample.value),
face_image = Some(customerFaceImageJson),
date_of_birth = Some("1990-05-15"),
relationship_status = Some(ExampleValue.relationshipStatusExample.value),
dependants = Some(ExampleValue.dependantsExample.value.toInt),
dob_of_dependants = Some(List("2015-03-20", "2018-07-10")),
credit_rating = Some(customerCreditRatingJSON),
credit_limit = Some(amountOfMoneyJsonV121),
highest_education_attained = Some(ExampleValue.highestEducationAttainedExample.value),
employment_status = Some(ExampleValue.employmentStatusExample.value),
kyc_status = Some(ExampleValue.kycStatusExample.value.toBoolean),
last_ok_date = Some(oneYearAgoDate),
title = Some(ExampleValue.titleExample.value),
branch_id = Some(ExampleValue.branchIdExample.value),
name_suffix = Some(ExampleValue.nameSuffixExample.value)
)


lazy val postCorporateCustomerJsonV600 =
PostCorporateCustomerJsonV600(
legal_name = ExampleValue.legalNameExample.value,
customer_number = Some(ExampleValue.customerNumberExample.value),
mobile_phone_number = ExampleValue.mobilePhoneNumberExample.value,
email = Some(ExampleValue.emailExample.value),
credit_rating = Some(customerCreditRatingJSON),
credit_limit = Some(amountOfMoneyJsonV121),
kyc_status = Some(ExampleValue.kycStatusExample.value.toBoolean),
last_ok_date = Some(oneYearAgoDate),
branch_id = Some(ExampleValue.branchIdExample.value),
customer_type = Some("CORPORATE"),
parent_customer_id = None
)

lazy val customerJsonV600 = CustomerJsonV600(
bank_id = bankIdExample.value,
customer_id = ExampleValue.customerIdExample.value,
Expand All @@ -2584,7 +2623,9 @@ object SwaggerDefinitionsJSON {
last_ok_date = oneYearAgoDate,
title = ExampleValue.titleExample.value,
branch_id = ExampleValue.branchIdExample.value,
name_suffix = ExampleValue.nameSuffixExample.value
name_suffix = ExampleValue.nameSuffixExample.value,
customer_type = ExampleValue.customerTypeExample.value,
parent_customer_id = ExampleValue.parentCustomerIdExample.value
)

lazy val customerJSONsV600 = CustomerJSONsV600(List(customerJsonV600))
Expand Down Expand Up @@ -2626,6 +2667,8 @@ object SwaggerDefinitionsJSON {
migration_script_logs = List(migrationScriptLogJsonV600)
)

lazy val customerChildrenJsonV600 = CustomerJSONsV600(List(customerJsonV600))

lazy val customerWithAttributesJsonV600 = CustomerWithAttributesJsonV600(
bank_id = bankIdExample.value,
customer_id = ExampleValue.customerIdExample.value,
Expand All @@ -2647,6 +2690,8 @@ object SwaggerDefinitionsJSON {
title = ExampleValue.titleExample.value,
branch_id = ExampleValue.branchIdExample.value,
name_suffix = ExampleValue.nameSuffixExample.value,
customer_type = ExampleValue.customerTypeExample.value,
parent_customer_id = ExampleValue.parentCustomerIdExample.value,
customer_attributes = List(customerAttributeResponseJson)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,11 @@ package code.api.dynamic.entity

import code.DynamicData.{DynamicData, DynamicDataProvider}
import code.api.Constant.PARAM_LOCALE
import code.api.dynamic.endpoint.helper.{DynamicEndpointHelper, MockResponseHolder}
import code.api.dynamic.endpoint.helper.DynamicEndpointHelper.DynamicReq
import code.api.dynamic.endpoint.helper.MockResponseHolder
import code.api.dynamic.entity.helper.{CommunityEntityName, DynamicEntityHelper, DynamicEntityInfo, EntityName, PublicEntityName}
import code.api.dynamic.entity.helper._
import code.api.util.APIUtil._
import code.api.util.ErrorMessages._
import code.api.util.NewStyle.HttpCode
import code.api.util._
import code.endpointMapping.EndpointMappingCommons
import com.openbankproject.commons.model.enums.TransactionRequestTypes._
import com.openbankproject.commons.model.enums.PaymentServiceTypes._
import code.util.Helper
import com.openbankproject.commons.ExecutionContext.Implicits.global
import com.openbankproject.commons.model._
Expand All @@ -28,7 +22,6 @@ import net.liftweb.json._
import net.liftweb.util.StringHelpers
import org.apache.commons.lang3.StringUtils

import scala.collection.immutable.List
import scala.collection.mutable.ArrayBuffer
import scala.concurrent.Future

Expand Down Expand Up @@ -212,7 +205,8 @@ trait APIMethodsDynamicEntity {
jsonResponse.isEmpty
}

(box, _) <- NewStyle.function.invokeDynamicConnector(operation, entityName, Some(json.asInstanceOf[JObject]), None, bankId, None, Some(u.userId), isPersonalEntity, Some(cc))
// Pass userId for all authenticated requests - personal records are filtered by userId
(box, _) <- NewStyle.function.invokeDynamicConnector(operation, entityName, Some(json.asInstanceOf[JObject]), None, bankId, None, Some(u.userId), isPersonalEntity, Some(cc))
singleObject: JValue = unboxResult(box.asInstanceOf[Box[JValue]], entityName)
} yield {
val result: JObject = (singleName -> singleObject)
Expand Down
2 changes: 2 additions & 0 deletions obp-api/src/main/scala/code/api/util/ApiTag.scala
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ object ApiTag {
val apiTagCounterparty = ResourceDocTag("Counterparty")
val apiTagKyc = ResourceDocTag("KYC")
val apiTagCustomer = ResourceDocTag("Customer")
val apiTagRetailCustomer = ResourceDocTag("Retail-Customer")
val apiTagCorporateCustomer = ResourceDocTag("Corporate-Customer")
val apiTagCustomerAttribute = ResourceDocTag("Customer-Attribute")
val apiTagOnboarding = ResourceDocTag("Onboarding")
val apiTagUser = ResourceDocTag("User") // Use for User Management / Info APIs
Expand Down
6 changes: 5 additions & 1 deletion obp-api/src/main/scala/code/api/util/ErrorMessages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,10 @@ object ErrorMessages {
val InvalidAccountNumber = "OBP-30270: Account not found. Please specify a valid value for ACCOUNT_NUMBER."
val BankAccountNotFoundByRoutings = "OBP-30271: Bank Account not found. Please specify valid values for routing schemes and addresses."

val InvalidCustomerType = "OBP-30272: Invalid customer_type. Must be one of: INDIVIDUAL, CORPORATE, SUBSIDIARY."
val ParentCustomerNotFound = "OBP-30273: Parent customer not found. The parent_customer_id must reference an existing customer at the same bank."
val CustomerTypeMismatch = "OBP-30274: Customer type does not match the endpoint. Use the generic /customers endpoint or the correct type-specific endpoint."

val TaxResidenceNotFound = "OBP-30300: Tax Residence not found by TAX_RESIDENCE_ID. "
val CustomerAddressNotFound = "OBP-30310: Customer's Address not found by CUSTOMER_ADDRESS_ID. "
val AccountApplicationNotFound = "OBP-30311: AccountApplication not found by ACCOUNT_APPLICATION_ID. "
Expand Down Expand Up @@ -753,7 +757,7 @@ object ErrorMessages {
val UnspecifiedCbsError = "OBP-50013: The Core Banking System returned an unspecified error or response."
val RefreshUserError = "OBP-50014: Can not refresh User."
val InternalServerError = "OBP-50015: The server encountered an unexpected condition which prevented it from fulfilling the request."
val NotAllowedEndpoint = "OBP-50017: The endpoint is forbidden at this API instance."
val NotAllowedEndpoint = "OBP-50017: The endpoint is not enabled at this OBP API instance."
val UnderConstructionError = "OBP-50018: Under Construction Error."
val DatabaseConnectionClosedError = "OBP-50019: Cannot connect to the OBP database."

Expand Down
6 changes: 6 additions & 0 deletions obp-api/src/main/scala/code/api/util/ExampleValue.scala
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ object ExampleValue {
lazy val nameSuffixExample = ConnectorField("Sr", s"suffix of the name")
glossaryItems += makeGlossaryItem("Customer.nameSuffix", nameSuffixExample)

lazy val customerTypeExample = ConnectorField("INDIVIDUAL", "Type of customer: INDIVIDUAL, CORPORATE, or SUBSIDIARY")
glossaryItems += makeGlossaryItem("Customer.customerType", customerTypeExample)

lazy val parentCustomerIdExample = ConnectorField("", "The customer_id of the parent customer in a corporate hierarchy, empty if none")
glossaryItems += makeGlossaryItem("Customer.parentCustomerId", parentCustomerIdExample)

lazy val titleExample = ConnectorField("Dr.", s"title of the name")
glossaryItems += makeGlossaryItem("Customer.title", titleExample)

Expand Down
110 changes: 110 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 @@ -5037,6 +5037,116 @@ object Glossary extends MdcLoggable {
)
)

glossaryItems += GlossaryItem(
title = "Password Reset for OBP Local Users",
description =
s"""
|### Overview
|
|The password reset flow allows a user who has forgotten their password to request a reset email and then set a new password. There are two steps:
|
|1. **Request a password reset email** (anonymous — no login required)
|2. **Set the new password** using the token from the email (anonymous — no login required)
|
|There is also an admin endpoint for requesting a reset on behalf of a user (requires authentication and the `CanCreateResetPasswordUrl` role).
|
|### Step 1: Request Password Reset Email
|
|**POST /obp/v6.0.0/users/password-reset-url**
|
|No authentication required.
|
|Request body:
|
| {
| "username": "user@example.com",
| "email": "user@example.com"
| }
|
|Response (201):
|
| {
| "message": "If the account exists, a password reset email has been sent."
| }
|
|Notes:
|
|- The response is always the same whether or not the user exists. This prevents user enumeration.
|- If the user exists, is validated, and the email matches, a reset email is sent containing a link with a reset token.
|- The App should present a form asking for username and email, call this endpoint, and then show a message saying "Check your email for a reset link."
|
|### Step 2: Complete Password Reset
|
|**POST /obp/v6.0.0/users/password**
|
|No authentication required.
|
|Request body:
|
| {
| "token": "a1b2c3d4e5f67890abcdef1234567890",
| "new_password": "NewStr0ng!Password"
| }
|
|Response (201):
|
| {
| "message": "Password has been reset successfully."
| }
|
|Error responses:
|
|- **400** — Invalid or expired token
|- **400** — Weak password
|
|Notes:
|
|- The token is a signed JWT with a configurable expiry (default: 120 minutes). The server-side expiry can be configured with the `password_reset_token_expiry_minutes` property.
|- The token comes from the reset email URL. The App should extract the token from the URL path (everything after `/user_mgt/reset_password/`) and URL-decode it before sending it to this endpoint.
|- The token is single-use. Once the password is reset, the token is invalidated. An expired token will also be rejected.
|
|### Admin Endpoint (Optional)
|
|**POST /obp/v6.0.0/management/user/reset-password-url**
|
|Authentication required. Requires the `CanCreateResetPasswordUrl` role.
|
|Request body:
|
| {
| "username": "user@example.com",
| "email": "user@example.com",
| "user_id": "9ca9a7e4-6d02-40e3-a129-0b2bf89de9b1"
| }
|
|Response (201):
|
| {
| "reset_password_url": "https://your-obp-instance.com/user_mgt/reset_password/TOKEN"
| }
|
|This endpoint returns the reset URL directly (for logging/admin purposes) and also sends the email. It requires all three fields: `username`, `email`, and `user_id`.
|
|### Typical App Flow
|
|1. User clicks "Forgot Password"
|2. App shows form with username and email fields
|3. App calls POST /obp/v6.0.0/users/password-reset-url
|4. App shows "Check your email for a reset link"
|5. User clicks link in email, App opens reset page and extracts token from URL
|6. App shows form with new password field
|7. App calls POST /obp/v6.0.0/users/password with token and new_password
|8. App shows "Password has been reset successfully. Please log in."
|
|### Password Requirements
|
|The new password must meet one of these criteria:
|
|- **10-16 characters:** Must contain at least one uppercase letter, one lowercase letter, one digit, and one special character
|- **17-512 characters:** No additional complexity requirements (length alone is sufficient)
|
""")

///////////////////////////////////////////////////////////////////
// NOTE! Some glossary items are generated in ExampleValue.scala
//////////////////////////////////////////////////////////////////
Expand Down
20 changes: 19 additions & 1 deletion obp-api/src/main/scala/code/api/util/NewStyle.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2691,7 +2691,9 @@ object NewStyle extends MdcLoggable{
title: String,
branchId: String,
nameSuffix: String,
callContext: Option[CallContext]): OBPReturnType[Customer] =
customerType: String = "",
parentCustomerId: String = "",
callContext: Option[CallContext]): OBPReturnType[Customer] =
Connector.connector.vend.createCustomerC2(
bankId: BankId,
legalName: String,
Expand All @@ -2713,6 +2715,8 @@ object NewStyle extends MdcLoggable{
title: String,
branchId: String,
nameSuffix: String,
customerType: String,
parentCustomerId: String,
callContext: Option[CallContext]
) map {
i => (unboxFullOrFail(i._1, callContext, CreateCustomerError), i._2)
Expand Down Expand Up @@ -2806,6 +2810,8 @@ object NewStyle extends MdcLoggable{
title: Option[String] = None,
branchId: Option[String] = None,
nameSuffix: Option[String] = None,
customerType: Option[String] = None,
parentCustomerId: Option[String] = None,
callContext: Option[CallContext]): OBPReturnType[Customer] =
Connector.connector.vend.updateCustomerGeneralData(
customerId,
Expand All @@ -2819,10 +2825,22 @@ object NewStyle extends MdcLoggable{
title,
branchId,
nameSuffix,
customerType,
parentCustomerId,
callContext) map {
i => (unboxFullOrFail(i._1, callContext, UpdateCustomerError), i._2)
}

def getCustomersByParentCustomerId(bankId: BankId, parentCustomerId: String, callContext: Option[CallContext]): OBPReturnType[List[Customer]] =
Connector.connector.vend.getCustomersByParentCustomerId(bankId, parentCustomerId, callContext) map {
i => (unboxFullOrFail(i._1, callContext, s"$CustomerNotFound Current BANK_ID(${bankId.value}) and PARENT_CUSTOMER_ID($parentCustomerId)"), i._2)
}

def getCustomersByCustomerTypes(bankId: BankId, customerTypes: List[String], callContext: Option[CallContext], queryParams: List[OBPQueryParam]): OBPReturnType[List[Customer]] =
Connector.connector.vend.getCustomersByCustomerTypes(bankId, customerTypes, callContext, queryParams) map {
i => (connectorEmptyResponse(i._1, callContext), i._2)
}

def createPhysicalCard(
bankCardNumber: String,
nameOnCard: String,
Expand Down
6 changes: 6 additions & 0 deletions obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4657,6 +4657,8 @@ trait APIMethods310 {
Some(putData.title),
None,
Some(putData.name_suffix),
None,
None,
callContext
)
} yield {
Expand Down Expand Up @@ -5305,6 +5307,8 @@ trait APIMethods310 {
None,
Some(putData.branch_id),
None,
None,
None,
callContext
)
} yield {
Expand Down Expand Up @@ -5361,6 +5365,8 @@ trait APIMethods310 {
None,
None,
None,
None,
None,
callContext
)
} yield {
Expand Down
2 changes: 2 additions & 0 deletions obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1405,6 +1405,8 @@ trait APIMethods500 {
postedData.title.getOrElse(""),
postedData.branch_id.getOrElse(""),
postedData.name_suffix.getOrElse(""),
"",
"",
callContext,
)
} yield {
Expand Down
Loading