From 56289ed029e0d63c3166d8899a87ba52e0e4275d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 4 Aug 2025 11:58:25 +0200 Subject: [PATCH 01/30] feature/Add props use_tpp_signature_revocation_list --- obp-api/src/main/resources/props/sample.props.template | 4 ++++ .../src/main/scala/code/api/util/CertificateVerifier.scala | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 64c7733d3..6560b0a12 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -165,6 +165,10 @@ jwt.use.ssl=false # Bypass TPP signature validation # bypass_tpp_signature_validation = false +## Use TPP signature revocation list +## - CRLs (Certificate Revocation Lists), or +## - OCSP (Online Certificate Status Protocol). +# use_tpp_signature_revocation_list = true ## Reject Berlin Group TRANSACTIONS with status "received" after a defined time (in seconds) # berlin_group_outdated_transactions_time_in_seconds = 300 diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala index 019aabd6e..4cc0a408f 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -80,7 +80,11 @@ object CertificateVerifier extends MdcLoggable { // Set up PKIX parameters for validation val pkixParams = new PKIXParameters(trustAnchors) - pkixParams.setRevocationEnabled(false) // Disable CRL checks + if(APIUtil.getPropsAsBoolValue("use_tpp_signature_revocation_list", defaultValue = true)) { + pkixParams.setRevocationEnabled(true) // Enable CRL checks + } else { + pkixParams.setRevocationEnabled(false) // Disable CRL checks + } // Validate certificate chain val certPath = CertificateFactory.getInstance("X.509").generateCertPath(Collections.singletonList(certificate)) From 6341584e228e7b124b6d4539155dcd7207fb5101 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Mon, 4 Aug 2025 16:19:50 +0200 Subject: [PATCH 02/30] refactor/Change status field to Option type for better null handling --- .../api/ResourceDocs1_4_0/MessageDocsSwaggerDefinitions.scala | 2 +- .../bankconnectors/rabbitmq/RabbitMQConnector_vOct2024.scala | 4 ++-- .../code/bankconnectors/rest/RestConnector_vMar2019.scala | 4 ++-- .../storedprocedure/StoredProcedureConnector_vDec2019.scala | 4 ++-- obp-api/src/main/scala/code/model/ModeratedBankingData.scala | 2 +- obp-api/src/main/scala/code/model/View.scala | 2 +- .../src/main/scala/code/transaction/MappedTransaction.scala | 2 +- .../berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3Test.scala | 2 +- .../scala/com/openbankproject/commons/model/CommonModel.scala | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/MessageDocsSwaggerDefinitions.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/MessageDocsSwaggerDefinitions.scala index e7dc68c5c..2decaab96 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/MessageDocsSwaggerDefinitions.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/MessageDocsSwaggerDefinitions.scala @@ -232,7 +232,7 @@ object MessageDocsSwaggerDefinitions startDate = DateWithDayExampleObject, finishDate = Some(DateWithDayExampleObject), balance = BigDecimal(balanceAmountExample.value), - status = transactionStatusExample.value, + status = Some(transactionStatusExample.value), ) val accountRouting = AccountRouting("","") diff --git a/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnector_vOct2024.scala b/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnector_vOct2024.scala index d03236dd0..c644945b7 100644 --- a/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnector_vOct2024.scala +++ b/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnector_vOct2024.scala @@ -1552,7 +1552,7 @@ trait RabbitMQConnector_vOct2024 extends Connector with MdcLoggable { startDate=toDate(transactionStartDateExample), finishDate=Some(toDate(transactionFinishDateExample)), balance=BigDecimal(balanceExample.value), - status=transactionStatusExample.value + status=Some(transactionStatusExample.value) ))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) @@ -1687,7 +1687,7 @@ trait RabbitMQConnector_vOct2024 extends Connector with MdcLoggable { startDate=toDate(transactionStartDateExample), finishDate=Some(toDate(transactionFinishDateExample)), balance=BigDecimal(balanceExample.value), - status=transactionStatusExample.value)) + status=Some(transactionStatusExample.value))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) diff --git a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala index 6bfee72bd..53a3b7200 100644 --- a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala +++ b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala @@ -1500,7 +1500,7 @@ trait RestConnector_vMar2019 extends Connector with MdcLoggable { startDate=toDate(transactionStartDateExample), finishDate=Some(toDate(transactionFinishDateExample)), balance=BigDecimal(balanceExample.value), - status=transactionStatusExample.value))) + status=Some(transactionStatusExample.value)))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -1634,7 +1634,7 @@ trait RestConnector_vMar2019 extends Connector with MdcLoggable { startDate=toDate(transactionStartDateExample), finishDate=Some(toDate(transactionFinishDateExample)), balance=BigDecimal(balanceExample.value), - status=transactionStatusExample.value)) + status=Some(transactionStatusExample.value))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) diff --git a/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureConnector_vDec2019.scala b/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureConnector_vDec2019.scala index 0ab7b583b..d3a89839a 100644 --- a/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureConnector_vDec2019.scala +++ b/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureConnector_vDec2019.scala @@ -1481,7 +1481,7 @@ trait StoredProcedureConnector_vDec2019 extends Connector with MdcLoggable { startDate=toDate(transactionStartDateExample), finishDate=Some(toDate(transactionFinishDateExample)), balance=BigDecimal(balanceExample.value), - status=transactionStatusExample.value))) + status=Some(transactionStatusExample.value)))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -1615,7 +1615,7 @@ trait StoredProcedureConnector_vDec2019 extends Connector with MdcLoggable { startDate=toDate(transactionStartDateExample), finishDate=Some(toDate(transactionFinishDateExample)), balance=BigDecimal(balanceExample.value), - status=transactionStatusExample.value)) + status=Some(transactionStatusExample.value))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) diff --git a/obp-api/src/main/scala/code/model/ModeratedBankingData.scala b/obp-api/src/main/scala/code/model/ModeratedBankingData.scala index 7314db295..569b48f99 100644 --- a/obp-api/src/main/scala/code/model/ModeratedBankingData.scala +++ b/obp-api/src/main/scala/code/model/ModeratedBankingData.scala @@ -57,7 +57,7 @@ class ModeratedTransaction( //the filteredBlance type in this class is a string rather than Big decimal like in Transaction trait for snippet (display) reasons. //the view should be able to return a sign (- or +) or the real value. casting signs into big decimal is not possible val balance : String, - val status : String + val status : Moderated[String] ) { def dateOption2JString(date: Option[Date]) : JString = { diff --git a/obp-api/src/main/scala/code/model/View.scala b/obp-api/src/main/scala/code/model/View.scala index 8f653f891..1ced4ecf8 100644 --- a/obp-api/src/main/scala/code/model/View.scala +++ b/obp-api/src/main/scala/code/model/View.scala @@ -178,7 +178,7 @@ case class ViewExtended(val view: View) { val transactionStatus = if (viewPermissions.exists(_ == CAN_SEE_TRANSACTION_STATUS)) transaction.status - else "" + else None new ModeratedTransaction( UUID = transactionUUID, diff --git a/obp-api/src/main/scala/code/transaction/MappedTransaction.scala b/obp-api/src/main/scala/code/transaction/MappedTransaction.scala index d17879a24..378f74dd7 100644 --- a/obp-api/src/main/scala/code/transaction/MappedTransaction.scala +++ b/obp-api/src/main/scala/code/transaction/MappedTransaction.scala @@ -156,7 +156,7 @@ class MappedTransaction extends LongKeyedMapper[MappedTransaction] with IdPK wit tStartDate.get, Some(tFinishDate.get), newBalance, - status.get)) + Some(status.get))) } } diff --git a/obp-api/src/test/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3Test.scala b/obp-api/src/test/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3Test.scala index 8e0cda195..7b77900ba 100644 --- a/obp-api/src/test/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3Test.scala +++ b/obp-api/src/test/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3Test.scala @@ -91,7 +91,7 @@ class JSONFactory_BERLIN_GROUP_1_3Test extends FeatureSpec with Matchers with Gi startDate = Some(new java.util.Date()), finishDate = Some(new java.util.Date()), balance = "900.00", - status = "booked" + status = Some("booked") ) } diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala b/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala index 5a300bf6a..c81aac363 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala @@ -1141,7 +1141,7 @@ case class Transaction( finishDate : Option[Date], //the new balance for the bank account balance : BigDecimal, - status: String + status : Option[String] ) { val bankId = thisAccount.bankId From 578bd99fe6fb3659203fb991f5655bea63198c6d Mon Sep 17 00:00:00 2001 From: Hongwei Date: Mon, 4 Aug 2025 16:21:11 +0200 Subject: [PATCH 03/30] refactor/Change status field to Option type --- .../RestConnector_vMar2019_frozen_meta_data | Bin 123942 -> 123950 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/obp-api/src/test/scala/code/connector/RestConnector_vMar2019_frozen_meta_data b/obp-api/src/test/scala/code/connector/RestConnector_vMar2019_frozen_meta_data index a802ceacdccc8c15da1964eb466c7446957eae1b..ffdf640c9c70fc3370b5af47b5710df002a7869b 100644 GIT binary patch delta 26 icmZ2>f_>cy_J%Et+fPq#5n>day!o2Q_A93ugCqc`FbmoM delta 31 ncmZ2?f_>Qu_J%Et+fPsCS;aZsMwXF#x_<#9=k`0N8G|GM)r|}x From ba36503a1efbc47c293a733e5b70baf1703d43c8 Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 5 Aug 2025 10:40:02 +0200 Subject: [PATCH 04/30] feature/used Apache Commons Email instead of LiftMail- step1 --- obp-api/pom.xml | 6 + .../code/api/util/CommonsEmailWrapper.scala | 201 ++++++++++++++++++ .../scala/code/api/v4_0_0/APIMethods400.scala | 54 ++++- 3 files changed, 257 insertions(+), 4 deletions(-) create mode 100644 obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala diff --git a/obp-api/pom.xml b/obp-api/pom.xml index 1227da21a..92bf9c7df 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -101,6 +101,12 @@ commons-text 1.10.0 + + + org.apache.commons + commons-email + 1.5 + org.postgresql postgresql diff --git a/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala b/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala new file mode 100644 index 000000000..bd695b819 --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala @@ -0,0 +1,201 @@ +package code.api.util + +import org.apache.commons.mail.{Email, SimpleEmail, HtmlEmail, MultiPartEmail, EmailAttachment, DefaultAuthenticator} +import java.io.File +import java.net.URL +import code.util.Helper.MdcLoggable +import net.liftweb.common.{Box, Empty, Full} +import net.liftweb.util.Helpers.now + +/** + * Apache Commons Email Wrapper for OBP-API + * This wrapper provides a simple interface to send emails using Apache Commons Email + * instead of Lift Web's Mailer + */ +object CommonsEmailWrapper extends MdcLoggable { + + /** + * Email configuration case class + */ + case class EmailConfig( + smtpHost: String, + smtpPort: Int, + username: String, + password: String, + useTLS: Boolean = true, + useSSL: Boolean = false, + debug: Boolean = false + ) + + /** + * Email content case class + */ + case class EmailContent( + from: String, + to: List[String], + cc: List[String] = List.empty, + bcc: List[String] = List.empty, + subject: String, + textContent: Option[String] = None, + htmlContent: Option[String] = None, + attachments: List[EmailAttachment] = List.empty + ) + + /** + * Send simple text email + */ + def sendTextEmail(config: EmailConfig, content: EmailContent): Box[String] = { + try { + logger.info(s"Sending text email from ${content.from} to ${content.to.mkString(", ")}") + + val email = new SimpleEmail() + configureEmail(email, config, content) + + // Set text content + content.textContent match { + case Some(text) => email.setMsg(text) + case None => email.setMsg("") + } + + val messageId = email.send() + logger.info(s"Email sent successfully with Message-ID: $messageId") + Full(messageId) + } catch { + case e: Exception => + logger.error(s"Failed to send text email: ${e.getMessage}", e) + Empty + } + } + + /** + * Send HTML email + */ + def sendHtmlEmail(config: EmailConfig, content: EmailContent): Box[String] = { + try { + logger.info(s"Sending HTML email from ${content.from} to ${content.to.mkString(", ")}") + + val email = new HtmlEmail() + configureEmail(email, config, content) + + // Set HTML content + content.htmlContent match { + case Some(html) => email.setHtmlMsg(html) + case None => email.setHtmlMsg("No content") + } + + // Set text content as fallback + content.textContent.foreach(email.setTextMsg) + + val messageId = email.send() + logger.info(s"HTML email sent successfully with Message-ID: $messageId") + Full(messageId) + } catch { + case e: Exception => + logger.error(s"Failed to send HTML email: ${e.getMessage}", e) + Empty + } + } + + /** + * Send email with attachments + */ + def sendEmailWithAttachments(config: EmailConfig, content: EmailContent): Box[String] = { + try { + logger.info(s"Sending email with attachments from ${content.from} to ${content.to.mkString(", ")}") + + val email = new MultiPartEmail() + configureEmail(email, config, content) + + // Set text content + content.textContent.foreach(email.setMsg) + + // Add attachments + content.attachments.foreach(email.attach) + + val messageId = email.send() + logger.info(s"Email with attachments sent successfully with Message-ID: $messageId") + Full(messageId) + } catch { + case e: Exception => + logger.error(s"Failed to send email with attachments: ${e.getMessage}", e) + Empty + } + } + + /** + * Configure email with common settings + */ + private def configureEmail(email: Email, config: EmailConfig, content: EmailContent): Unit = { + // SMTP Configuration + email.setHostName(config.smtpHost) + email.setSmtpPort(config.smtpPort) + email.setAuthenticator(new DefaultAuthenticator(config.username, config.password)) + email.setSSLOnConnect(config.useSSL) + email.setStartTLSEnabled(config.useTLS) + email.setDebug(config.debug) + + // Set charset + email.setCharset("UTF-8") + + // Set sender + email.setFrom(content.from) + + // Set recipients + content.to.foreach(email.addTo) + content.cc.foreach(email.addCc) + content.bcc.foreach(email.addBcc) + + // Set subject + email.setSubject(content.subject) + } + + /** + * Create email attachment from file + */ + def createFileAttachment(filePath: String, name: Option[String] = None): EmailAttachment = { + val attachment = new EmailAttachment() + attachment.setPath(filePath) + attachment.setDisposition(EmailAttachment.ATTACHMENT) + name.foreach(attachment.setName) + attachment + } + + /** + * Create email attachment from URL + */ + def createUrlAttachment(url: String, name: String): EmailAttachment = { + val attachment = new EmailAttachment() + attachment.setURL(new URL(url)) + attachment.setDisposition(EmailAttachment.ATTACHMENT) + attachment.setName(name) + attachment + } + + /** + * Test MailHog configuration, this for testing + */ + def testMailHogConfig(): Unit = { + val config = EmailConfig( + smtpHost = "localhost", + smtpPort = 1025, + username = "", + password = "", + useTLS = false, + debug = true + ) + + val content = EmailContent( + from = "test@localhost", + to = List("receive@mailhog.local"), + subject = "Test MailHog with Apache Commons Email", + textContent = Some("This is a test email sent to MailHog using Apache Commons Email wrapper.") + ) + + logger.info("Testing MailHog configuration with Apache Commons Email...") + + sendTextEmail(config, content) match { + case Full(messageId) => logger.info(s"MailHog email sent successfully: $messageId") + case Empty => logger.error("Failed to send MailHog email") + } + } +} \ No newline at end of file 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 c2e98117b..70ae47c69 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 @@ -83,8 +83,8 @@ import net.liftweb.json.JsonDSL._ import net.liftweb.json.Serialization.write import net.liftweb.json._ import net.liftweb.util.Helpers.{now, tryo} -import net.liftweb.util.Mailer.{From, PlainMailBodyType, Subject, To, XHTMLMailBodyType} -import net.liftweb.util.{Helpers, Mailer, StringHelpers} +import net.liftweb.util.{Helpers, StringHelpers} +import code.api.util.CommonsEmailWrapper._ import org.apache.commons.lang3.StringUtils import java.net.URLEncoder @@ -3368,7 +3368,30 @@ trait APIMethods400 extends MdcLoggable { .replace(WebUIPlaceholder.activateYourAccount, link) logger.debug(s"customHtmlText: ${customHtmlText}") logger.debug(s"Before send user invitation by email. Purpose: ${UserInvitationPurpose.DEVELOPER}") - Mailer.sendMail(From(from), Subject(subject), To(invitation.email), PlainMailBodyType(customText), XHTMLMailBodyType(XML.loadString(customHtmlText))) + + // Use Apache Commons Email wrapper instead of Lift Mailer + val emailConfig = EmailConfig( + smtpHost = APIUtil.getPropsValue("mail.smtp.host", "localhost"), + smtpPort = APIUtil.getPropsValue("mail.smtp.port", "1025").toInt, + username = APIUtil.getPropsValue("mail.smtp.user", ""), + password = APIUtil.getPropsValue("mail.smtp.password", ""), + useTLS = APIUtil.getPropsValue("mail.smtp.starttls.enable", "false").toBoolean, + debug = APIUtil.getPropsValue("mail.debug", "false").toBoolean + ) + + val emailContent = EmailContent( + from = from, + to = List(invitation.email), + subject = subject, + textContent = Some(customText), + htmlContent = Some(customHtmlText) + ) + + sendHtmlEmail(emailConfig, emailContent) match { + case Full(messageId) => logger.debug(s"Email sent successfully with Message-ID: $messageId") + case Empty => logger.error("Failed to send user invitation email") + } + logger.debug(s"After send user invitation by email. Purpose: ${UserInvitationPurpose.DEVELOPER}") } else { val subject = getWebUiPropsValue("webui_customer_user_invitation_email_subject", "Welcome to the API Playground") @@ -3380,7 +3403,30 @@ trait APIMethods400 extends MdcLoggable { .replace(WebUIPlaceholder.activateYourAccount, link) logger.debug(s"customHtmlText: ${customHtmlText}") logger.debug(s"Before send user invitation by email.") - Mailer.sendMail(From(from), Subject(subject), To(invitation.email), PlainMailBodyType(customText), XHTMLMailBodyType(XML.loadString(customHtmlText))) + + // Use Apache Commons Email wrapper instead of Lift Mailer + val emailConfig = EmailConfig( + smtpHost = APIUtil.getPropsValue("mail.smtp.host", "localhost"), + smtpPort = APIUtil.getPropsValue("mail.smtp.port", "1025").toInt, + username = APIUtil.getPropsValue("mail.smtp.user", ""), + password = APIUtil.getPropsValue("mail.smtp.password", ""), + useTLS = APIUtil.getPropsValue("mail.smtp.starttls.enable", "false").toBoolean, + debug = APIUtil.getPropsValue("mail.debug", "false").toBoolean + ) + + val emailContent = EmailContent( + from = from, + to = List(invitation.email), + subject = subject, + textContent = Some(customText), + htmlContent = Some(customHtmlText) + ) + + sendHtmlEmail(emailConfig, emailContent) match { + case Full(messageId) => logger.debug(s"Email sent successfully with Message-ID: $messageId") + case Empty => logger.error("Failed to send user invitation email") + } + logger.debug(s"After send user invitation by email.") } (JSONFactory400.createUserInvitationJson(invitation), HttpCode.`201`(callContext)) From da1bf7e615bba737329677d259a6ddeaf5d764ee Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 5 Aug 2025 10:42:02 +0200 Subject: [PATCH 05/30] refactor/ comment out unused scalaxb-maven-plugin in pom.xml --- obp-api/pom.xml | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/obp-api/pom.xml b/obp-api/pom.xml index 92bf9c7df..553a843c5 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -667,24 +667,25 @@ ${java.version} - - org.scalaxb - scalaxb-maven-plugin - 1.7.5 - - code.adapter.soap - src/main/resources/custom_webapp/wsdl - src/main/resources/custom_webapp/xsd - - - - scalaxb - - generate - - - - + + + + + + + + + + + + + + + + + + + org.apache.commons commons-email @@ -408,6 +407,12 @@ org.asynchttpclient async-http-client 2.10.4 + + + javax.activation + com.sun.activation + + @@ -416,6 +421,16 @@ org.scalikejdbc scalikejdbc_${scala.version} 3.4.0 + + + com.sun.activation + javax.activation + + + javax.activation + activation + + com.microsoft.sqlserver diff --git a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala index daed89179..fa21830e2 100644 --- a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala +++ b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala @@ -597,7 +597,6 @@ import net.liftweb.util.Helpers._ val resetPasswordLinkProps = Constant.HostName val resetPasswordLink = APIUtil.getPropsValue("portal_hostname", resetPasswordLinkProps)+ passwordResetPath.mkString("/", "/", "/")+urlEncode(u.getUniqueId()) - logger.error("222222222222222222222222222222222222222444:"+classOf[javax.activation.DataSource].getProtectionDomain.getCodeSource) // Use Apache Commons Email wrapper instead of Lift Mailer val emailBodies = generateResetEmailBodies(u, resetPasswordLink) diff --git a/obp-commons/pom.xml b/obp-commons/pom.xml index c7f68bad4..b41909faf 100644 --- a/obp-commons/pom.xml +++ b/obp-commons/pom.xml @@ -29,6 +29,16 @@ net.liftweb lift-util_${scala.version} + + + javax.activation + activation + + + javax.mail + mail + + net.liftweb From 328975f436884a2e8289c9c94ea99183686434a6 Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 7 Aug 2025 23:20:48 +0200 Subject: [PATCH 25/30] refactor/update logging level in CommonsEmailWrapper to debug for email sending operations --- .../scala/code/api/util/CommonsEmailWrapper.scala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 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 3b57e9747..d83657110 100644 --- a/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala +++ b/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala @@ -79,11 +79,11 @@ object CommonsEmailWrapper extends MdcLoggable { } /** - * Send simple text email + * Send a simple text email */ def sendTextEmail(config: EmailConfig, content: EmailContent): Box[String] = { try { - logger.info(s"Sending text email from ${content.from} to ${content.to.mkString(", ")}") + logger.debug(s"Sending text email from ${content.from} to ${content.to.mkString(", ")}") val email = new SimpleEmail() configureEmail(email, config, content) @@ -95,7 +95,7 @@ object CommonsEmailWrapper extends MdcLoggable { } val messageId = email.send() - logger.info(s"Email sent successfully with Message-ID: $messageId") + logger.debug(s"Email sent successfully with Message-ID: $messageId") Full(messageId) } catch { case e: Exception => @@ -109,7 +109,7 @@ object CommonsEmailWrapper extends MdcLoggable { */ def sendHtmlEmail(config: EmailConfig, content: EmailContent): Box[String] = { try { - logger.info(s"Sending HTML email from ${content.from} to ${content.to.mkString(", ")}") + logger.debug(s"Sending HTML email from ${content.from} to ${content.to.mkString(", ")}") val email = new HtmlEmail() configureEmail(email, config, content) @@ -124,7 +124,7 @@ object CommonsEmailWrapper extends MdcLoggable { content.textContent.foreach(email.setTextMsg) val messageId = email.send() - logger.info(s"HTML email sent successfully with Message-ID: $messageId") + logger.debug(s"HTML email sent successfully with Message-ID: $messageId") Full(messageId) } catch { case e: Exception => @@ -138,7 +138,7 @@ object CommonsEmailWrapper extends MdcLoggable { */ def sendEmailWithAttachments(config: EmailConfig, content: EmailContent): Box[String] = { try { - logger.info(s"Sending email with attachments from ${content.from} to ${content.to.mkString(", ")}") + logger.debug(s"Sending email with attachments from ${content.from} to ${content.to.mkString(", ")}") val email = new MultiPartEmail() configureEmail(email, config, content) @@ -150,7 +150,7 @@ object CommonsEmailWrapper extends MdcLoggable { content.attachments.foreach(email.attach) val messageId = email.send() - logger.info(s"Email with attachments sent successfully with Message-ID: $messageId") + logger.debug(s"Email with attachments sent successfully with Message-ID: $messageId") Full(messageId) } catch { case e: Exception => From a141dca5a7990171f48779cf2eaeec1499e011b8 Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 8 Aug 2025 00:36:07 +0200 Subject: [PATCH 26/30] refactor/used jakarta Email instead of Apache Commons Email - step9 --- obp-api/pom.xml | 15 +- .../code/api/util/CommonsEmailWrapper.scala | 215 ++++++++---------- 2 files changed, 108 insertions(+), 122 deletions(-) diff --git a/obp-api/pom.xml b/obp-api/pom.xml index 867840a7c..9955f4ad7 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -101,11 +101,6 @@ commons-text 1.10.0 - - org.apache.commons - commons-email - 1.5 - org.postgresql postgresql @@ -524,6 +519,16 @@ test + + com.sun.mail + jakarta.mail + 2.0.1 + + + jakarta.activation + jakarta.activation-api + 2.0.1 + com.sun.activation jakarta.activation 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 d83657110..f4cf89b56 100644 --- a/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala +++ b/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala @@ -1,21 +1,21 @@ package code.api.util import code.util.Helper.MdcLoggable +import jakarta.activation.{DataHandler, FileDataSource, URLDataSource} +import jakarta.mail._ +import jakarta.mail.internet._ import net.liftweb.common.{Box, Empty, Full} -import org.apache.commons.mail._ +import java.io.File import java.net.URL +import java.util.Properties /** - * Apache Commons Email Wrapper for OBP-API - * This wrapper provides a simple interface to send emails using Apache Commons Email - * instead of Lift Web's Mailer + * Jakarta Mail Wrapper for OBP-API + * This wrapper provides a simple interface to send emails using Jakarta Mail */ object CommonsEmailWrapper extends MdcLoggable { - /** - * Email configuration case class - */ case class EmailConfig( smtpHost: String, smtpPort: Int, @@ -24,12 +24,9 @@ object CommonsEmailWrapper extends MdcLoggable { useTLS: Boolean = true, useSSL: Boolean = false, debug: Boolean = false, - tlsProtocols: String = "TLSv1.2" // TLS protocols to use + tlsProtocols: String = "TLSv1.2" ) - /** - * Email content case class - */ case class EmailContent( from: String, to: List[String], @@ -41,9 +38,12 @@ object CommonsEmailWrapper extends MdcLoggable { attachments: List[EmailAttachment] = List.empty ) - /** - * Get default email configuration from OBP-API properties - */ + case class EmailAttachment( + filePath: Option[String] = None, + url: Option[String] = None, + name: Option[String] = None + ) + def getDefaultEmailConfig(): EmailConfig = { EmailConfig( smtpHost = APIUtil.getPropsValue("mail.smtp.host", "localhost"), @@ -57,46 +57,27 @@ object CommonsEmailWrapper extends MdcLoggable { ) } - /** - * Send simple text email with default configuration - */ def sendTextEmail(content: EmailContent): Box[String] = { sendTextEmail(getDefaultEmailConfig(), content) } - /** - * Send HTML email with default configuration - */ def sendHtmlEmail(content: EmailContent): Box[String] = { sendHtmlEmail(getDefaultEmailConfig(), content) } - /** - * Send email with attachments using default configuration - */ def sendEmailWithAttachments(content: EmailContent): Box[String] = { sendEmailWithAttachments(getDefaultEmailConfig(), content) } - /** - * Send a simple text email - */ def sendTextEmail(config: EmailConfig, content: EmailContent): Box[String] = { try { logger.debug(s"Sending text email from ${content.from} to ${content.to.mkString(", ")}") - - val email = new SimpleEmail() - configureEmail(email, config, content) - - // Set text content - content.textContent match { - case Some(text) => email.setMsg(text) - case None => email.setMsg("") - } - - val messageId = email.send() - logger.debug(s"Email sent successfully with Message-ID: $messageId") - Full(messageId) + val session = createSession(config) + val message = new MimeMessage(session) + setCommonHeaders(message, content) + message.setText(content.textContent.getOrElse(""), "UTF-8") + Transport.send(message) + Full(message.getMessageID) } catch { case e: Exception => logger.error(s"Failed to send text email: ${e.getMessage}", e) @@ -104,28 +85,28 @@ object CommonsEmailWrapper extends MdcLoggable { } } - /** - * Send HTML email - */ def sendHtmlEmail(config: EmailConfig, content: EmailContent): Box[String] = { try { logger.debug(s"Sending HTML email from ${content.from} to ${content.to.mkString(", ")}") - - val email = new HtmlEmail() - configureEmail(email, config, content) - - // Set HTML content - content.htmlContent match { - case Some(html) => email.setHtmlMsg(html) - case None => email.setHtmlMsg("No content") + val session = createSession(config) + val message = new MimeMessage(session) + setCommonHeaders(message, content) + val multipart = { + new MimeMultipart("alternative") } - - // Set text content as fallback - content.textContent.foreach(email.setTextMsg) - - val messageId = email.send() - logger.debug(s"HTML email sent successfully with Message-ID: $messageId") - Full(messageId) + content.textContent.foreach { text => + val textPart = new MimeBodyPart() + textPart.setText(text, "UTF-8") + multipart.addBodyPart(textPart) + } + content.htmlContent.foreach { html => + val htmlPart = new MimeBodyPart() + htmlPart.setContent(html, "text/html; charset=UTF-8") + multipart.addBodyPart(htmlPart) + } + message.setContent(multipart) + Transport.send(message) + Full(message.getMessageID) } catch { case e: Exception => logger.error(s"Failed to send HTML email: ${e.getMessage}", e) @@ -133,25 +114,45 @@ object CommonsEmailWrapper extends MdcLoggable { } } - /** - * Send email with attachments - */ def sendEmailWithAttachments(config: EmailConfig, content: EmailContent): Box[String] = { try { logger.debug(s"Sending email with attachments from ${content.from} to ${content.to.mkString(", ")}") - - val email = new MultiPartEmail() - configureEmail(email, config, content) - - // Set text content - content.textContent.foreach(email.setMsg) - + val session = createSession(config) + val message = new MimeMessage(session) + setCommonHeaders(message, content) + val multipart = new MimeMultipart() + // Add text or HTML part + (content.htmlContent, content.textContent) match { + case (Some(html), _) => + val htmlPart = new MimeBodyPart() + htmlPart.setContent(html, "text/html; charset=UTF-8") + multipart.addBodyPart(htmlPart) + case (None, Some(text)) => + val textPart = new MimeBodyPart() + textPart.setText(text, "UTF-8") + multipart.addBodyPart(textPart) + case _ => + val textPart = new MimeBodyPart() + textPart.setText("", "UTF-8") + multipart.addBodyPart(textPart) + } // Add attachments - content.attachments.foreach(email.attach) - - val messageId = email.send() - logger.debug(s"Email with attachments sent successfully with Message-ID: $messageId") - Full(messageId) + content.attachments.foreach { att => + val attachPart = new MimeBodyPart() + if (att.filePath.isDefined) { + val fds = new FileDataSource(new File(att.filePath.get)) + attachPart.setDataHandler(new DataHandler(fds)) + attachPart.setFileName(att.name.getOrElse(new File(att.filePath.get).getName)) + } else if (att.url.isDefined) { + val uds = new URLDataSource(new URL(att.url.get)) + attachPart.setDataHandler(new DataHandler(uds)) + attachPart.setFileName(att.name.getOrElse(att.url.get.split('/').last)) + } + multipart.addBodyPart(attachPart) + } + message.setContent(multipart) + Transport.send(message) + Full(message.getMessageID) } catch { case e: Exception => logger.error(s"Failed to send email with attachments: ${e.getMessage}", e) @@ -159,54 +160,34 @@ object CommonsEmailWrapper extends MdcLoggable { } } - /** - * Configure email with common settings - */ - private def configureEmail(email: Email, config: EmailConfig, content: EmailContent): Unit = { - // SMTP Configuration - email.setHostName(config.smtpHost) - email.setSmtpPort(config.smtpPort) - email.setAuthenticator(new DefaultAuthenticator(config.username, config.password)) - email.setSSLOnConnect(config.useSSL) - email.setStartTLSEnabled(config.useTLS) - email.setDebug(config.debug) - email.getMailSession.getProperties.setProperty("mail.smtp.ssl.protocols", config.tlsProtocols) - - // Set charset - email.setCharset("UTF-8") - - // Set sender - email.setFrom(content.from) - - // Set recipients - content.to.foreach(email.addTo) - content.cc.foreach(email.addCc) - content.bcc.foreach(email.addBcc) - - // Set subject - email.setSubject(content.subject) + private def createSession(config: EmailConfig): Session = { + val props = new Properties() + props.put("mail.smtp.host", config.smtpHost) + props.put("mail.smtp.port", config.smtpPort.toString) + props.put("mail.smtp.auth", "true") + props.put("mail.smtp.starttls.enable", config.useTLS.toString) + props.put("mail.smtp.ssl.enable", config.useSSL.toString) + props.put("mail.debug", config.debug.toString) + props.put("mail.smtp.ssl.protocols", config.tlsProtocols) + val authenticator = new Authenticator() { + override def getPasswordAuthentication: PasswordAuthentication = + new PasswordAuthentication(config.username, config.password) + } + Session.getInstance(props, authenticator) } - /** - * Create email attachment from file - */ - def createFileAttachment(filePath: String, name: Option[String] = None): EmailAttachment = { - val attachment = new EmailAttachment() - attachment.setPath(filePath) - attachment.setDisposition(EmailAttachment.ATTACHMENT) - name.foreach(attachment.setName) - attachment + private def setCommonHeaders(message: MimeMessage, content: EmailContent): Unit = { + message.setFrom(new InternetAddress(content.from)) + content.to.foreach(addr => message.addRecipient(Message.RecipientType.TO, new InternetAddress(addr))) + content.cc.foreach(addr => message.addRecipient(Message.RecipientType.CC, new InternetAddress(addr))) + content.bcc.foreach(addr => message.addRecipient(Message.RecipientType.BCC, new InternetAddress(addr))) + message.setSubject(content.subject, "UTF-8") } - /** - * Create email attachment from URL - */ - def createUrlAttachment(url: String, name: String): EmailAttachment = { - val attachment = new EmailAttachment() - attachment.setURL(new URL(url)) - attachment.setDisposition(EmailAttachment.ATTACHMENT) - attachment.setName(name) - attachment - } + def createFileAttachment(filePath: String, name: Option[String] = None): EmailAttachment = + EmailAttachment(filePath = Some(filePath), url = None, name = name) + + def createUrlAttachment(url: String, name: String): EmailAttachment = + EmailAttachment(filePath = None, url = Some(url), name = Some(name)) } \ No newline at end of file From 9fdfa7e34cd24a9c0ce14f2b6f6443ee3682394d Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 8 Aug 2025 00:44:08 +0200 Subject: [PATCH 27/30] refactor/used jakarta Email instead of Apache Commons Email - step10 replaced Lift Mailer with JakartaMail for password reset and validation emails, simplifying email content generation --- .../code/model/dataAccess/AuthUser.scala | 36 ++++--------------- 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala index fa21830e2..0d9334ff5 100644 --- a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala +++ b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala @@ -589,23 +589,14 @@ import net.liftweb.util.Helpers._ */ override def sendPasswordReset(name: String) { findAuthUserByUsernameLocallyLegacy(name).toList ::: findUsersByEmailLocally(name) map { - // reason of case parameter name is "u" instead of "user": trait AuthUser have constant mumber name is "user" - // So if the follow case paramter name is "user" will cause compile warnings case u if u.validated_? => u.resetUniqueId().save - //NOTE: here, if server_mode = portal, so we need modify the resetLink to portal_hostname, then developer can get proper response.. val resetPasswordLinkProps = Constant.HostName val resetPasswordLink = APIUtil.getPropsValue("portal_hostname", resetPasswordLinkProps)+ passwordResetPath.mkString("/", "/", "/")+urlEncode(u.getUniqueId()) - // Use Apache Commons Email wrapper instead of Lift Mailer - val emailBodies = generateResetEmailBodies(u, resetPasswordLink) - - // Extract text and HTML content from email bodies - val textContent = emailBodies.find(_.isInstanceOf[net.liftweb.util.Mailer.PlainMailBodyType]) - .map(_.asInstanceOf[net.liftweb.util.Mailer.PlainMailBodyType].toString.replace("PlainMailBodyType(", "").replace(")", "")) - val htmlContent = emailBodies.find(_.isInstanceOf[net.liftweb.util.Mailer.XHTMLMailBodyType]) - .map(_.asInstanceOf[net.liftweb.util.Mailer.XHTMLMailBodyType].toString.replace("XHTMLMailBodyType(", "").replace(")", "")) - + // Directly generate content using JakartaMail/CommonsEmailWrapper + val textContent = Some(s"Please use the following link to reset your password: $resetPasswordLink") + val htmlContent = Some(s"

Please use the following link to reset your password:

$resetPasswordLink

") val emailContent = EmailContent( from = emailFrom, to = List(u.getEmail), @@ -627,8 +618,6 @@ import net.liftweb.util.Helpers._ case u => sendValidationEmail(u) } - // In order to prevent any leakage of information we use the same message for all cases - // Note: Individual success/error messages are now handled in the email sending logic above } override def lostPasswordXhtml = { @@ -660,22 +649,10 @@ import net.liftweb.util.Helpers._ * Overridden to use the hostname set in the props file */ override def sendValidationEmail(user: TheUserType) { - val resetLink = Constant.HostName+"/"+validateUserPath.mkString("/")+ - "/"+urlEncode(user.getUniqueId()) - + val resetLink = Constant.HostName+"/"+validateUserPath.mkString("/")+"/"+urlEncode(user.getUniqueId()) val email: String = user.getEmail - - val msgXml = signupMailBody(user, resetLink) - - // Use Apache Commons Email wrapper instead of Lift Mailer - val emailBodies: List[Mailer.MailBodyType] = generateValidationEmailBodies(user, resetLink) - - // Extract text and HTML content from email bodies - val textContent = emailBodies.find(_.isInstanceOf[net.liftweb.util.Mailer.PlainMailBodyType]) - .map(_.asInstanceOf[net.liftweb.util.Mailer.PlainMailBodyType].toString.replace("PlainMailBodyType(", "").replace(")", "")) - val htmlContent = emailBodies.find(_.isInstanceOf[net.liftweb.util.Mailer.XHTMLMailBodyType]) - .map(_.asInstanceOf[net.liftweb.util.Mailer.XHTMLMailBodyType].toString.replace("XHTMLMailBodyType(", "").replace(")", "")) - + val textContent = Some(s"Welcome! Please validate your account by clicking the following link: $resetLink") + val htmlContent = Some(s"

Welcome! Please validate your account by clicking the following link:

$resetLink

") val emailContent = EmailContent( from = emailFrom, to = List(user.getEmail), @@ -684,7 +661,6 @@ import net.liftweb.util.Helpers._ textContent = textContent, htmlContent = htmlContent ) - sendHtmlEmail(emailContent) match { case Full(messageId) => logger.debug(s"Validation email sent successfully with Message-ID: $messageId") From a55824fbc33fc2e7d7f73ac24ec2bacb54dddbb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 13 Aug 2025 09:54:19 +0200 Subject: [PATCH 28/30] feature/Add query param withBalance at BG endpoint Read Account Details --- .../AccountInformationServiceAISApi.scala | 27 ++++++++++++++++++- .../v1_3/JSONFactory_BERLIN_GROUP_1_3.scala | 18 +++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala index bba14b21c..d3684b268 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala @@ -1036,17 +1036,27 @@ Give detailed information about the addressed account together with balance info cc => for { (Full(u), callContext) <- authenticatedAccess(cc) + withBalanceParam <- NewStyle.function.tryons(s"$InvalidUrlParameters withBalance parameter can only take two values: TRUE or FALSE!", 400, callContext) { + val withBalance = APIUtil.getHttpRequestUrlParam(cc.url, "withBalance") + if (withBalance.isEmpty) Some(false) else Some(withBalance.toBoolean) + } _ <- passesPsd2Aisp(callContext) (account: BankAccount, callContext) <- NewStyle.function.getBankAccountByAccountId(AccountId(accountId), callContext) (canReadBalancesAccounts, callContext) <- NewStyle.function.getAccountCanReadBalancesOfBerlinGroup(u, callContext) (canReadTransactionsAccounts, callContext) <- NewStyle.function.getAccountCanReadTransactionsOfBerlinGroup(u, callContext) _ <- checkAccountAccess(ViewId(SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID), u, account, callContext) + (accountBalances, callContext) <- code.api.util.newstyle.BankAccountBalanceNewStyle.getBankAccountBalances( + AccountId(accountId), + callContext + ) } yield { ( JSONFactory_BERLIN_GROUP_1_3.createAccountDetailsJson( account, canReadBalancesAccounts, canReadTransactionsAccounts, + withBalanceParam, + accountBalances, u ), callContext @@ -1105,8 +1115,23 @@ respectively the OAuth2 access token. (canReadBalancesAccounts, callContext) <- NewStyle.function.getAccountCanReadBalancesOfBerlinGroup(u, callContext) (canReadTransactionsAccounts, callContext) <- NewStyle.function.getAccountCanReadTransactionsOfBerlinGroup(u, callContext) _ <- checkAccountAccess(ViewId(SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID), u, account, callContext) + withBalanceParam <- NewStyle.function.tryons(s"$InvalidUrlParameters withBalance parameter can only take two values: TRUE or FALSE!", 400, callContext) { + val withBalance = APIUtil.getHttpRequestUrlParam(cc.url, "withBalance") + if (withBalance.isEmpty) Some(false) else Some(withBalance.toBoolean) + } + (accountBalances, callContext) <- code.api.util.newstyle.BankAccountBalanceNewStyle.getBankAccountBalances( + AccountId(accountId), + callContext + ) } yield { - (JSONFactory_BERLIN_GROUP_1_3.createCardAccountDetailsJson(account, canReadBalancesAccounts, canReadTransactionsAccounts, u), callContext) + (JSONFactory_BERLIN_GROUP_1_3.createCardAccountDetailsJson( + account, + canReadBalancesAccounts, + canReadTransactionsAccounts, + withBalanceParam, + accountBalances, + u + ), callContext) } } } diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala index f7857a00d..52d78bfb3 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala @@ -93,6 +93,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ product: String, cashAccountType: String, name: Option[String], + balances: Option[List[CoreAccountBalanceJson]] = None, _links: AccountDetailsLinksJsonV13, ) @@ -407,14 +408,18 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ def createCardAccountDetailsJson(bankAccount: BankAccount, canReadBalancesAccounts: List[BankIdAccountId], canReadTransactionsAccounts: List[BankIdAccountId], + withBalanceParam: Option[Boolean], + balances: List[BankAccountBalanceTrait], user: User): CardAccountDetailsJsonV13 = { - val accountDetailsJsonV13 = createAccountDetailsJson(bankAccount, canReadBalancesAccounts, canReadTransactionsAccounts, user) + val accountDetailsJsonV13 = createAccountDetailsJson(bankAccount, canReadBalancesAccounts, canReadTransactionsAccounts, withBalanceParam, balances, user) CardAccountDetailsJsonV13(accountDetailsJsonV13.account) } def createAccountDetailsJson(bankAccount: BankAccount, canReadBalancesAccounts: List[BankIdAccountId], canReadTransactionsAccounts: List[BankIdAccountId], + withBalanceParam: Option[Boolean], + balances: List[BankAccountBalanceTrait], user: User): AccountDetailsJsonV13 = { val (iBan: String, bBan: String) = getIbanAndBban(bankAccount) val commonPath = s"${OBP_BERLIN_GROUP_1_3.apiVersion.urlPrefix}/${OBP_BERLIN_GROUP_1_3.version}/accounts/${bankAccount.accountId.value}" @@ -423,7 +428,15 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ val transactionRef = LinkHrefJson(s"/$commonPath/transactions") val canReadTransactions = canReadTransactionsAccounts.map(_.accountId.value).contains(bankAccount.accountId.value) val cashAccountType = bankAccount.attributes.getOrElse(Nil).filter(_.name== "cashAccountType").map(_.value).headOption.getOrElse("") - + val accountBalances = if (withBalanceParam.contains(true)) { + Some(balances.filter(_.accountId.equals(bankAccount.accountId)).flatMap(balance => (List(CoreAccountBalanceJson( + balanceAmount = AmountOfMoneyV13(bankAccount.currency, balance.balanceAmount.toString()), + balanceType = balance.balanceType, + lastChangeDateTime = balance.lastChangeDateTime.map(APIUtil.DateWithMsAndTimeZoneOffset.format(_)) + ))))) + } else { + None + } val account = AccountJsonV13( resourceId = bankAccount.accountId.value, @@ -432,6 +445,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ name = if(APIUtil.getPropsAsBoolValue("BG_v1312_show_account_name", defaultValue = true)) Some(bankAccount.name) else None, cashAccountType = cashAccountType, product = bankAccount.accountType, + balances = if(canReadBalances) accountBalances else None, _links = AccountDetailsLinksJsonV13( balances = if (canReadBalances) Some(balanceRef) else None, transactions = if (canReadTransactions) Some(transactionRef) else None, From 01393cec64ee5e231954fc9a1100444d5d310c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 14 Aug 2025 14:32:34 +0200 Subject: [PATCH 29/30] feature/Add query param cbs_user_id at Get Consents endpoint --- .../src/main/scala/code/api/util/APIUtil.scala | 7 +++++-- .../src/main/scala/code/api/util/OBPParam.scala | 1 + .../code/api/v5_1_0/JSONFactory5.1.0.scala | 16 +++++++++++++--- .../main/scala/code/consent/MappedConsent.scala | 17 +++++++++++++++-- 4 files changed, 34 insertions(+), 7 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 7e6e9561e..2fade7e73 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -1147,6 +1147,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ case "iss" => Full(OBPIss(values.head)) case "consent_id" => Full(OBPConsentId(values.head)) case "user_id" => Full(OBPUserId(values.head)) + case "cbs_user_id" => Full(CBSUserId(values.head)) case "bank_id" => Full(OBPBankId(values.head)) case "account_id" => Full(OBPAccountId(values.head)) case "url" => Full(OBPUrl(values.head)) @@ -1198,6 +1199,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ iss <- getHttpParamValuesByName(httpParams,"iss") consentId <- getHttpParamValuesByName(httpParams,"consent_id") userId <- getHttpParamValuesByName(httpParams, "user_id") + cbsUserId <- getHttpParamValuesByName(httpParams, "cbs_user_id") bankId <- getHttpParamValuesByName(httpParams, "bank_id") accountId <- getHttpParamValuesByName(httpParams, "account_id") url <- getHttpParamValuesByName(httpParams, "url") @@ -1233,7 +1235,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ val ordering = OBPOrdering(None, sortDirection) //This guarantee the order List(limit, offset, ordering, sortBy, fromDate, toDate, - anon, status, consumerId, azp, iss, consentId, userId, url, appName, implementedByPartialFunction, implementedInVersion, + anon, status, consumerId, azp, iss, consentId, userId, cbsUserId, url, appName, implementedByPartialFunction, implementedInVersion, verb, correlationId, duration, excludeAppNames, excludeUrlPattern, excludeImplementedByPartialfunctions, includeAppNames, includeUrlPattern, includeImplementedByPartialfunctions, connectorName,functionName, bankId, accountId, customerId, lockedStatus, deletedStatus @@ -1276,6 +1278,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ val azp = getHttpRequestUrlParam(httpRequestUrl,"azp") val consentId = getHttpRequestUrlParam(httpRequestUrl,"consent_id") val userId = getHttpRequestUrlParam(httpRequestUrl, "user_id") + val cbsUserId = getHttpRequestUrlParam(httpRequestUrl, "cbs_user_id") val bankId = getHttpRequestUrlParam(httpRequestUrl, "bank_id") val accountId = getHttpRequestUrlParam(httpRequestUrl, "account_id") val url = getHttpRequestUrlParam(httpRequestUrl, "url") @@ -1305,7 +1308,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ Full(List( HTTPParam("sort_by",sortBy), HTTPParam("sort_direction",sortDirection), HTTPParam("from_date",fromDate), HTTPParam("to_date", toDate), HTTPParam("limit",limit), HTTPParam("offset",offset), - HTTPParam("anon", anon), HTTPParam("status", status), HTTPParam("consumer_id", consumerId), HTTPParam("azp", azp), HTTPParam("iss", iss), HTTPParam("consent_id", consentId), HTTPParam("user_id", userId), HTTPParam("url", url), HTTPParam("app_name", appName), + HTTPParam("anon", anon), HTTPParam("status", status), HTTPParam("consumer_id", consumerId), HTTPParam("azp", azp), HTTPParam("iss", iss), HTTPParam("consent_id", consentId), HTTPParam("user_id", userId), HTTPParam("cbs_user_id", cbsUserId), HTTPParam("url", url), HTTPParam("app_name", appName), HTTPParam("implemented_by_partial_function",implementedByPartialFunction), HTTPParam("implemented_in_version",implementedInVersion), HTTPParam("verb", verb), HTTPParam("correlation_id", correlationId), HTTPParam("duration", duration), HTTPParam("exclude_app_names", excludeAppNames), HTTPParam("exclude_url_patterns", excludeUrlPattern),HTTPParam("exclude_implemented_by_partial_functions", excludeImplementedByPartialfunctions), diff --git a/obp-api/src/main/scala/code/api/util/OBPParam.scala b/obp-api/src/main/scala/code/api/util/OBPParam.scala index 49bd62193..a4587e88e 100644 --- a/obp-api/src/main/scala/code/api/util/OBPParam.scala +++ b/obp-api/src/main/scala/code/api/util/OBPParam.scala @@ -31,6 +31,7 @@ case class OBPAzp(value: String) extends OBPQueryParam case class OBPIss(value: String) extends OBPQueryParam case class OBPConsentId(value: String) extends OBPQueryParam case class OBPUserId(value: String) extends OBPQueryParam +case class CBSUserId(value: String) extends OBPQueryParam case class OBPStatus(value: String) extends OBPQueryParam case class OBPBankId(value: String) extends OBPQueryParam case class OBPAccountId(value: String) extends OBPQueryParam 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 db1354069..b05898569 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 @@ -46,12 +46,13 @@ import code.consent.MappedConsent import code.metrics.APIMetric import code.model.Consumer import code.users.{UserAttribute, Users} +import code.util.Helper.MdcLoggable import code.views.system.{AccountAccess, ViewDefinition, ViewPermission} import com.openbankproject.commons.model._ import com.openbankproject.commons.util.ApiVersion import net.liftweb.common.{Box, Full} import net.liftweb.json -import net.liftweb.json.{JString, JValue, parse, parseOpt} +import net.liftweb.json.{JString, JValue, MappingException, parse, parseOpt} import java.text.SimpleDateFormat import java.util.Date @@ -676,7 +677,7 @@ case class ViewPermissionJson( extra_data: Option[List[String]] ) -object JSONFactory510 extends CustomJsonFormats { +object JSONFactory510 extends CustomJsonFormats with MdcLoggable { def createTransactionRequestJson(tr : TransactionRequest, transactionRequestAttributes: List[TransactionRequestAttributeTrait] ) : TransactionRequestJsonV510 = { TransactionRequestJsonV510( @@ -980,7 +981,16 @@ object JSONFactory510 extends CustomJsonFormats { def createConsentsJsonV510(consents: List[MappedConsent]): ConsentsJsonV510 = { ConsentsJsonV510( consents.map { c => - val jwtPayload = JwtUtil.getSignedPayloadAsJson(c.jsonWebToken).map(parse(_).extract[ConsentJWT]).toOption + val jwtPayload = JwtUtil + .getSignedPayloadAsJson(c.jsonWebToken) + .flatMap { payload => + Try(parse(payload).extract[ConsentJWT]).recover { + case e: MappingException => + logger.warn(s"Invalid JWT payload: ${e.getMessage}") + null + }.toOption + }.toOption + AllConsentJsonV510( consent_reference_id = c.consentReferenceId, consumer_id = c.consumerId, diff --git a/obp-api/src/main/scala/code/consent/MappedConsent.scala b/obp-api/src/main/scala/code/consent/MappedConsent.scala index 21f5bca8a..e1e4c00c9 100644 --- a/obp-api/src/main/scala/code/consent/MappedConsent.scala +++ b/obp-api/src/main/scala/code/consent/MappedConsent.scala @@ -1,9 +1,10 @@ package code.consent import java.util.Date -import code.api.util.{APIUtil, Consent, ErrorMessages, OBPBankId, OBPConsentId, OBPConsumerId, OBPLimit, OBPOffset, OBPQueryParam, OBPSortBy, OBPStatus, OBPUserId, SecureRandomUtil} +import code.api.util.{APIUtil, CBSUserId, Consent, ErrorMessages, OBPBankId, OBPConsentId, OBPConsumerId, OBPLimit, OBPOffset, OBPQueryParam, OBPSortBy, OBPStatus, OBPUserId, SecureRandomUtil} import code.consent.ConsentStatus.ConsentStatus import code.model.Consumer +import code.model.dataAccess.ResourceUser import code.util.MappedUUID import com.openbankproject.commons.model.User import com.openbankproject.commons.util.ApiStandards @@ -71,6 +72,18 @@ object MappedConsentProvider extends ConsentProvider { // The optional variables: val consumerId = queryParams.collectFirst { case OBPConsumerId(value) => By(MappedConsent.mConsumerId, value) } val consentId = queryParams.collectFirst { case OBPConsentId(value) => By(MappedConsent.mConsentId, value) } + val cbsUserId: Option[Cmp[MappedConsent, String]] = queryParams.collectFirst { + case CBSUserId(value) => + ResourceUser.findAll(By(ResourceUser.providerId, value)) match { + case Nil => + Some(By(MappedConsent.mUserId, "-1")) // no result + case x :: Nil => // exactly one + Some(By(MappedConsent.mUserId, x.userId)) + case moreThanOneRow => // more than one + None + } + }.flatten + val userId = queryParams.collectFirst { case OBPUserId(value) => By(MappedConsent.mUserId, value) } val status = queryParams.collectFirst { case OBPStatus(value) => @@ -96,7 +109,7 @@ object MappedConsentProvider extends ConsentProvider { offset.toSeq, limit.toSeq, status.toSeq, - userId.toSeq, + userId.orElse(cbsUserId).toSeq, consentId.toSeq, consumerId.toSeq ).flatten From 253327c937f36a32ab39c5572154d55348d5b17e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 15 Aug 2025 12:17:46 +0200 Subject: [PATCH 30/30] feature/Add query param provider_provider_id at Get Consents endpoint --- .../main/scala/code/api/util/APIUtil.scala | 12 +++++----- .../main/scala/code/api/util/OBPParam.scala | 2 +- .../scala/code/api/v5_1_0/APIMethods510.scala | 4 ++++ .../scala/code/consent/MappedConsent.scala | 22 +++++++++++-------- 4 files changed, 24 insertions(+), 16 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 2fade7e73..176726d85 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -1147,7 +1147,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ case "iss" => Full(OBPIss(values.head)) case "consent_id" => Full(OBPConsentId(values.head)) case "user_id" => Full(OBPUserId(values.head)) - case "cbs_user_id" => Full(CBSUserId(values.head)) + case "provider_provider_id" => Full(ProviderProviderId(values.head)) case "bank_id" => Full(OBPBankId(values.head)) case "account_id" => Full(OBPAccountId(values.head)) case "url" => Full(OBPUrl(values.head)) @@ -1199,7 +1199,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ iss <- getHttpParamValuesByName(httpParams,"iss") consentId <- getHttpParamValuesByName(httpParams,"consent_id") userId <- getHttpParamValuesByName(httpParams, "user_id") - cbsUserId <- getHttpParamValuesByName(httpParams, "cbs_user_id") + providerProviderId <- getHttpParamValuesByName(httpParams, "provider_provider_id") bankId <- getHttpParamValuesByName(httpParams, "bank_id") accountId <- getHttpParamValuesByName(httpParams, "account_id") url <- getHttpParamValuesByName(httpParams, "url") @@ -1233,9 +1233,9 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ */ //val sortBy = json.header("obp_sort_by") val ordering = OBPOrdering(None, sortDirection) - //This guarantee the order + //This guarantee the order List(limit, offset, ordering, sortBy, fromDate, toDate, - anon, status, consumerId, azp, iss, consentId, userId, cbsUserId, url, appName, implementedByPartialFunction, implementedInVersion, + anon, status, consumerId, azp, iss, consentId, userId, providerProviderId, url, appName, implementedByPartialFunction, implementedInVersion, verb, correlationId, duration, excludeAppNames, excludeUrlPattern, excludeImplementedByPartialfunctions, includeAppNames, includeUrlPattern, includeImplementedByPartialfunctions, connectorName,functionName, bankId, accountId, customerId, lockedStatus, deletedStatus @@ -1278,7 +1278,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ val azp = getHttpRequestUrlParam(httpRequestUrl,"azp") val consentId = getHttpRequestUrlParam(httpRequestUrl,"consent_id") val userId = getHttpRequestUrlParam(httpRequestUrl, "user_id") - val cbsUserId = getHttpRequestUrlParam(httpRequestUrl, "cbs_user_id") + val providerProviderId = getHttpRequestUrlParam(httpRequestUrl, "provider_provider_id") val bankId = getHttpRequestUrlParam(httpRequestUrl, "bank_id") val accountId = getHttpRequestUrlParam(httpRequestUrl, "account_id") val url = getHttpRequestUrlParam(httpRequestUrl, "url") @@ -1308,7 +1308,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ Full(List( HTTPParam("sort_by",sortBy), HTTPParam("sort_direction",sortDirection), HTTPParam("from_date",fromDate), HTTPParam("to_date", toDate), HTTPParam("limit",limit), HTTPParam("offset",offset), - HTTPParam("anon", anon), HTTPParam("status", status), HTTPParam("consumer_id", consumerId), HTTPParam("azp", azp), HTTPParam("iss", iss), HTTPParam("consent_id", consentId), HTTPParam("user_id", userId), HTTPParam("cbs_user_id", cbsUserId), HTTPParam("url", url), HTTPParam("app_name", appName), + HTTPParam("anon", anon), HTTPParam("status", status), HTTPParam("consumer_id", consumerId), HTTPParam("azp", azp), HTTPParam("iss", iss), HTTPParam("consent_id", consentId), HTTPParam("user_id", userId), HTTPParam("provider_provider_id", providerProviderId), HTTPParam("url", url), HTTPParam("app_name", appName), HTTPParam("implemented_by_partial_function",implementedByPartialFunction), HTTPParam("implemented_in_version",implementedInVersion), HTTPParam("verb", verb), HTTPParam("correlation_id", correlationId), HTTPParam("duration", duration), HTTPParam("exclude_app_names", excludeAppNames), HTTPParam("exclude_url_patterns", excludeUrlPattern),HTTPParam("exclude_implemented_by_partial_functions", excludeImplementedByPartialfunctions), diff --git a/obp-api/src/main/scala/code/api/util/OBPParam.scala b/obp-api/src/main/scala/code/api/util/OBPParam.scala index a4587e88e..bc42c0465 100644 --- a/obp-api/src/main/scala/code/api/util/OBPParam.scala +++ b/obp-api/src/main/scala/code/api/util/OBPParam.scala @@ -31,7 +31,7 @@ case class OBPAzp(value: String) extends OBPQueryParam case class OBPIss(value: String) extends OBPQueryParam case class OBPConsentId(value: String) extends OBPQueryParam case class OBPUserId(value: String) extends OBPQueryParam -case class CBSUserId(value: String) extends OBPQueryParam +case class ProviderProviderId(value: String) extends OBPQueryParam case class OBPStatus(value: String) extends OBPQueryParam case class OBPBankId(value: String) extends OBPQueryParam case class OBPAccountId(value: String) extends OBPQueryParam 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 32f8ce6c2..01851b749 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 @@ -1707,6 +1707,10 @@ trait APIMethods510 { | |7 bank_id (ignore if omitted) | + |8 provider_provider_id (ignore if omitted) + |provider and provider_id values are separated by pipe char + |eg: provider_provider_id=http%3A%2F%2Flocalhost%3A7070%2Frealms%2Fmaster|7837ee9c-3446-4d8c-9b90-301a52b4851d + | |eg:/management/consents?consumer_id=78&limit=10&offset=10 | """.stripMargin, diff --git a/obp-api/src/main/scala/code/consent/MappedConsent.scala b/obp-api/src/main/scala/code/consent/MappedConsent.scala index e1e4c00c9..6fa453465 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, CBSUserId, Consent, ErrorMessages, OBPBankId, OBPConsentId, OBPConsumerId, OBPLimit, OBPOffset, OBPQueryParam, OBPSortBy, OBPStatus, OBPUserId, SecureRandomUtil} +import code.api.util.{APIUtil, Consent, ErrorMessages, 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 @@ -9,10 +9,12 @@ import code.util.MappedUUID import com.openbankproject.commons.model.User import com.openbankproject.commons.util.ApiStandards import net.liftweb.common.{Box, Empty, Failure, Full} -import net.liftweb.mapper.{MappedString, _} +import net.liftweb.mapper._ import net.liftweb.util.Helpers.{now, tryo} import org.mindrot.jbcrypt.BCrypt +import java.net.URLDecoder +import java.nio.charset.StandardCharsets import scala.collection.immutable.List object MappedConsentProvider extends ConsentProvider { @@ -72,14 +74,16 @@ object MappedConsentProvider extends ConsentProvider { // The optional variables: val consumerId = queryParams.collectFirst { case OBPConsumerId(value) => By(MappedConsent.mConsumerId, value) } val consentId = queryParams.collectFirst { case OBPConsentId(value) => By(MappedConsent.mConsentId, value) } - val cbsUserId: Option[Cmp[MappedConsent, String]] = queryParams.collectFirst { - case CBSUserId(value) => - ResourceUser.findAll(By(ResourceUser.providerId, value)) match { - case Nil => - Some(By(MappedConsent.mUserId, "-1")) // no result + val providerProviderId: Option[Cmp[MappedConsent, String]] = queryParams.collectFirst { + case ProviderProviderId(value) => + val (provider, providerId) = value.split("\\|") match { // split by literal '|' + case Array(a, b) => (a, b) + case _ => ("", "") // fallback if format is unexpected + } + ResourceUser.findAll(By(ResourceUser.provider_, provider), By(ResourceUser.providerId, providerId)) match { case x :: Nil => // exactly one Some(By(MappedConsent.mUserId, x.userId)) - case moreThanOneRow => // more than one + case _ => None } }.flatten @@ -109,7 +113,7 @@ object MappedConsentProvider extends ConsentProvider { offset.toSeq, limit.toSeq, status.toSeq, - userId.orElse(cbsUserId).toSeq, + userId.orElse(providerProviderId).toSeq, consentId.toSeq, consumerId.toSeq ).flatten