Merge remote-tracking branch 'Simon/develop' into develop

This commit is contained in:
hongwei 2025-08-18 16:28:25 +02:00
commit 4fb6ce25a0
35 changed files with 617 additions and 131 deletions

View File

@ -178,6 +178,11 @@
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.11.1</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
@ -397,6 +402,12 @@
<groupId>org.asynchttpclient</groupId>
<artifactId>async-http-client</artifactId>
<version>2.10.4</version>
<exclusions>
<exclusion>
<artifactId>javax.activation</artifactId>
<groupId>com.sun.activation</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- grpc related end-->
@ -405,6 +416,16 @@
<groupId>org.scalikejdbc</groupId>
<artifactId>scalikejdbc_${scala.version}</artifactId>
<version>3.4.0</version>
<exclusions>
<exclusion>
<groupId>com.sun.activation</groupId>
<artifactId>javax.activation</artifactId>
</exclusion>
<exclusion>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
@ -498,6 +519,16 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>jakarta.mail</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>jakarta.activation</groupId>
<artifactId>jakarta.activation-api</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>com.sun.activation</groupId>
<artifactId>javax.activation</artifactId>

View File

@ -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
@ -266,20 +270,59 @@ dev.port=8080
#Default value is obp (very highly recomended)
apiPathZero=obp
## Sending mail out
## Not need in dev mode, but important for production
mail.api.consumer.registered.sender.address=no-reply@example.com
mail.api.consumer.registered.notification.addresses=you@example.com
## Not need in dev mode, but important for production
## We send an email after any exception
# mail.exception.sender.address=no-reply@example.com
# mail.exception.registered.notification.addresses=notify@example.com,notify2@example.com,notify3@example.com
# This property allows sending API registration data to developer's email.
#mail.api.consumer.registered.notification.send=false
We only send consumer keys and secret if this is true
#mail.api.consumer.registered.notification.send.sensistive=false
## Email Configuration (CommonsEmailWrapper)
## ===========================================
##
## This section configures email sending using CommonsEmailWrapper instead of Lift Mailer.
## All email functionality (password reset, validation, notifications) now uses these settings.
##
## SMTP Server Configuration
## -------------------------
## Basic SMTP settings
mail.smtp.host=127.0.0.1
mail.smtp.port=25
mail.smtp.auth=false
mail.smtp.user=
mail.smtp.password=
## TLS/SSL Configuration
## ---------------------
## Enable STARTTLS (recommended for most SMTP servers)
mail.smtp.starttls.enable=false
## Enable SSL (use with port 465)
mail.smtp.ssl.enable=false
## TLS protocols to use (recommended: TLSv1.2)
mail.smtp.ssl.protocols=TLSv1.2
## Trust all certificates (for development only)
#mail.smtp.ssl.trust=*
## Debug Configuration
## ------------------
## Enable email debugging (shows SMTP communication)
mail.debug=false
## Email Sender Configuration
## -------------------------
## Default sender address for all emails
mail.users.userinfo.sender.address=no-reply@example.com
## Consumer Registration Email
## --------------------------
## Enable/disable consumer registration notifications
mail.api.consumer.registered.notification.send=false
## Sender address for consumer registration emails
mail.api.consumer.registered.sender.address=no-reply@example.com
## Recipient addresses for consumer registration notifications (comma-separated)
mail.api.consumer.registered.notification.addresses=you@example.com
## Send sensitive information (consumer keys/secrets) via email
mail.api.consumer.registered.notification.send.sensistive=false
## Exception Notification Email
## ---------------------------
## Sender address for exception notifications
mail.exception.sender.address=no-reply@example.com
## Recipient addresses for exception notifications (comma-separated)
mail.exception.registered.notification.addresses=notify@example.com,notify2@example.com,notify3@example.com
## Oauth token timeout
token_expiration_weeks=4

View File

@ -46,6 +46,7 @@ import code.api.util.ApiRole.CanCreateEntitlementAtAnyBank
import code.api.util.ErrorMessages.MandatoryPropertyIsNotSet
import code.api.util._
import code.api.util.migration.Migration
import code.api.util.CommonsEmailWrapper
import code.api.util.migration.Migration.DbFunction
import code.apicollection.ApiCollection
import code.apicollectionendpoint.ApiCollectionEndpoint
@ -151,7 +152,6 @@ import org.apache.commons.io.FileUtils
import java.io.{File, FileInputStream}
import java.util.stream.Collectors
import java.util.{Locale, TimeZone}
import javax.mail.internet.MimeMessage
import scala.concurrent.ExecutionContext
/**
@ -693,10 +693,6 @@ class Boot extends MdcLoggable {
case e: ExceptionInInitializerError => logger.warn(s"BankAccountCreationListener Exception: $e")
}
Mailer.devModeSend.default.set( (m : MimeMessage) => {
logger.info("Would have sent email if not in dev mode: " + m.getContent)
})
LiftRules.exceptionHandler.prepend{
case(_, r, e) if e.isInstanceOf[NullPointerException] && e.getMessage.contains("Looking for Connection Identifier") => {
logger.error(s"Exception being returned to browser when processing url is ${r.request.uri}, method is ${r.request.method}, exception detail is $e", e)
@ -880,7 +876,7 @@ class Boot extends MdcLoggable {
}
private def sendExceptionEmail(exception: Throwable): Unit = {
import Mailer.{From, PlainMailBodyType, Subject, To}
import net.liftweb.util.Helpers.now
val outputStream = new java.io.ByteArrayOutputStream
@ -899,18 +895,18 @@ class Boot extends MdcLoggable {
//technically doesn't work for all valid email addresses so this will mess up if someone tries to send emails to "foo,bar"@example.com
val to = toAddressesString.split(",").toList
val toParams = to.map(To(_))
val params = PlainMailBodyType(error) :: toParams
//this is an async call
Mailer.sendMail(
From(from),
Subject(s"you got an exception on $host"),
params :_*
val emailContent = CommonsEmailWrapper.EmailContent(
from = from,
to = to,
subject = s"you got an exception on $host",
textContent = Some(error)
)
//this is an async call∆∆
CommonsEmailWrapper.sendTextEmail(emailContent)
}
//if Mailer.sendMail wasn't called (note: this actually isn't checking if the mail failed to send as that is being done asynchronously)
if(mailSent.isEmpty)
logger.warn(s"Exception notification failed: $mailSent")
}

View File

@ -410,10 +410,21 @@ object OAuth2Login extends RestHelper with MdcLoggable {
validateAccessToken(token) match {
case Full(_) =>
val user = getOrCreateResourceUser(token)
val consumer = getOrCreateConsumer(token, user.map(_.userId), Some("OAuth 2.0"))
LoginAttempt.userIsLocked(user.map(_.provider).getOrElse(""), user.map(_.name).getOrElse("")) match {
case true => ((Failure(UsernameHasBeenLocked), Some(cc.copy(consumer = consumer))))
case false => (user, Some(cc.copy(consumer = consumer)))
val consumer: Box[Consumer] = getOrCreateConsumer(token, user.map(_.userId), Some("OAuth 2.0"))
consumer match {
case Full(_) =>
LoginAttempt.userIsLocked(user.map(_.provider).getOrElse(""), user.map(_.name).getOrElse("")) match {
case true => ((Failure(UsernameHasBeenLocked), Some(cc.copy(consumer = consumer))))
case false => (user, Some(cc.copy(consumer = consumer)))
}
case ParamFailure(msg, exception, chain, apiFailure: APIFailure) =>
logger.debug(s"ParamFailure - message: $msg, param: $apiFailure, exception: ${exception.map(_.getMessage).openOr("none")}, chain: ${chain.map(_.msg).openOr("none")}")
(ParamFailure(msg, exception, chain, apiFailure: APIFailure), Some(cc))
case Failure(msg, exception, c) =>
logger.error(s"Failure - message: $msg, exception: ${exception.map(_.getMessage).openOr("none")}")
(Failure(msg, exception, c), Some(cc))
case _ =>
(Failure(CreateConsumerError), Some(cc))
}
case ParamFailure(a, b, c, apiFailure: APIFailure) =>
(ParamFailure(a, b, c, apiFailure: APIFailure), Some(cc))

View File

@ -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("","")

View File

@ -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)
}
}
}

View File

@ -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,

View File

@ -71,7 +71,7 @@ import code.util.{Helper, JsonSchemaUtil}
import code.views.system.AccountAccess
import code.views.{MapperViews, Views}
import code.webuiprops.MappedWebUiPropsProvider.getWebUiPropsValue
import com.alibaba.ttl.internal.javassist.CannotCompileException
import javassist.CannotCompileException
import com.github.dwickern.macros.NameOf.{nameOf, nameOfType}
import com.openbankproject.commons.ExecutionContext.Implicits.global
import com.openbankproject.commons.model._
@ -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 "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))
@ -1198,6 +1199,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
iss <- getHttpParamValuesByName(httpParams,"iss")
consentId <- getHttpParamValuesByName(httpParams,"consent_id")
userId <- getHttpParamValuesByName(httpParams, "user_id")
providerProviderId <- getHttpParamValuesByName(httpParams, "provider_provider_id")
bankId <- getHttpParamValuesByName(httpParams, "bank_id")
accountId <- getHttpParamValuesByName(httpParams, "account_id")
url <- getHttpParamValuesByName(httpParams, "url")
@ -1231,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, 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
@ -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 providerProviderId = getHttpRequestUrlParam(httpRequestUrl, "provider_provider_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("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),
@ -3020,6 +3023,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
// Identify consumer via certificate
val consumerByCertificate = Consent.getCurrentConsumerViaTppSignatureCertOrMtls(callContext = cc)
logger.debug(s"consumerByCertificate: $consumerByCertificate")
val method = APIUtil.getPropsValue(nameOfProperty = "consumer_validation_method_for_consent", defaultValue = "CONSUMER_CERTIFICATE")
val consumerByConsumerKey = getConsumerKey(reqHeaders) match {
case Some(consumerKey) if method == "CONSUMER_KEY_VALUE" =>
@ -3027,6 +3031,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
case None =>
Empty
}
logger.debug(s"consumerByConsumerKey: $consumerByConsumerKey")
val res =
if (authHeadersWithEmptyValues.nonEmpty) { // Check Authorization Headers Empty Values

View File

@ -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))

View File

@ -0,0 +1,193 @@
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 java.io.File
import java.net.URL
import java.util.Properties
/**
* Jakarta Mail Wrapper for OBP-API
* This wrapper provides a simple interface to send emails using Jakarta Mail
*/
object CommonsEmailWrapper extends MdcLoggable {
case class EmailConfig(
smtpHost: String,
smtpPort: Int,
username: String,
password: String,
useTLS: Boolean = true,
useSSL: Boolean = false,
debug: Boolean = false,
tlsProtocols: String = "TLSv1.2"
)
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
)
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"),
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,
useSSL = APIUtil.getPropsValue("mail.smtp.ssl.enable", "false").toBoolean,
debug = APIUtil.getPropsValue("mail.debug", "false").toBoolean,
tlsProtocols = APIUtil.getPropsValue("mail.smtp.ssl.protocols", "TLSv1.2")
)
}
def sendTextEmail(content: EmailContent): Box[String] = {
sendTextEmail(getDefaultEmailConfig(), content)
}
def sendHtmlEmail(content: EmailContent): Box[String] = {
sendHtmlEmail(getDefaultEmailConfig(), content)
}
def sendEmailWithAttachments(content: EmailContent): Box[String] = {
sendEmailWithAttachments(getDefaultEmailConfig(), content)
}
def sendTextEmail(config: EmailConfig, content: EmailContent): Box[String] = {
try {
logger.debug(s"Sending text email from ${content.from} to ${content.to.mkString(", ")}")
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)
Empty
}
}
def sendHtmlEmail(config: EmailConfig, content: EmailContent): Box[String] = {
try {
logger.debug(s"Sending HTML email from ${content.from} to ${content.to.mkString(", ")}")
val session = createSession(config)
val message = new MimeMessage(session)
setCommonHeaders(message, content)
val multipart = {
new MimeMultipart("alternative")
}
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)
Empty
}
}
def sendEmailWithAttachments(config: EmailConfig, content: EmailContent): Box[String] = {
try {
logger.debug(s"Sending email with attachments from ${content.from} to ${content.to.mkString(", ")}")
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 { 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)
Empty
}
}
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)
}
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")
}
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))
}

View File

@ -238,6 +238,7 @@ object Consent extends MdcLoggable {
private def tppIsConsentHolder(consumerIdFromConsent: String, callContext: CallContext): Boolean = {
val consumerIdFromCurrentCall = callContext.consumer.map(_.consumerId.get).orNull
logger.debug(s"consumerIdFromConsent == consumerIdFromCurrentCall ($consumerIdFromConsent == $consumerIdFromCurrentCall)")
consumerIdFromConsent == consumerIdFromCurrentCall
}

View File

@ -6,8 +6,7 @@ import code.users.Users
import code.util.Helper.MdcLoggable
import com.openbankproject.commons.model.User
import net.liftweb.common.Box
import net.liftweb.util.Mailer
import net.liftweb.util.Mailer._
import scala.collection.immutable.List
@ -27,14 +26,14 @@ object NotificationUtil extends MdcLoggable {
|
|Cheers
|""".stripMargin
val params = PlainMailBodyType(bodyOfMessage) :: List(To(user.emailAddress))
val subjectOfMessage = "You have been granted the role"
//this is an async call
Mailer.sendMail(
From(from),
Subject(subjectOfMessage),
params :_*
val emailContent = CommonsEmailWrapper.EmailContent(
from = from,
to = List(user.emailAddress),
subject = s"You have been granted the role: ${entitlement.roleName}",
textContent = Some(bodyOfMessage)
)
//this is an async call
CommonsEmailWrapper.sendTextEmail(emailContent)
}
if(mailSent.isEmpty) {
val info =

View File

@ -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 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

View File

@ -100,6 +100,7 @@ object Migration extends MdcLoggable {
// populateViewDefinitionCanSeeTransactionStatus()
alterCounterpartyLimitFieldType()
populateMigrationOfViewPermissions(startedBeforeSchemifier)
changeTypeOfAudFieldAtConsumerTable()
}
private def dummyScript(): Boolean = {
@ -254,6 +255,12 @@ object Migration extends MdcLoggable {
MigrationOfConsumer.populateAzpAndSub(name)
}
}
private def changeTypeOfAudFieldAtConsumerTable(): Boolean = {
val name = nameOf(changeTypeOfAudFieldAtConsumerTable)
runOnce(name) {
MigrationOfConsumer.alterTypeofAud(name)
}
}
private def alterTableMappedUserAuthContext(startedBeforeSchemifier: Boolean): Boolean = {
if(startedBeforeSchemifier == true) {
logger.warn(s"Migration.database.alterTableMappedUserAuthContext(true) cannot be run before Schemifier.")

View File

@ -2,11 +2,11 @@ package code.api.util.migration
import java.time.format.DateTimeFormatter
import java.time.{ZoneId, ZonedDateTime}
import code.api.util.APIUtil
import code.api.util.migration.Migration.{DbFunction, saveLog}
import code.model.{AppType, Consumer}
import net.liftweb.mapper.DB
import net.liftweb.common.Full
import net.liftweb.mapper.{DB, Schemifier}
import net.liftweb.util.{DefaultConnectionIdentifier, Helpers}
object MigrationOfConsumer {
@ -107,4 +107,52 @@ object MigrationOfConsumer {
isSuccessful
}
}
def alterTypeofAud(name: String): Boolean = {
DbFunction.tableExists(Consumer) match {
case true =>
val startDate = System.currentTimeMillis()
val commitId: String = APIUtil.gitCommit
var isSuccessful = false
val executedSql =
DbFunction.maybeWrite(true, Schemifier.infoF _) {
APIUtil.getPropsValue("db.driver") match {
case Full(dbDriver) if dbDriver.contains("com.microsoft.sqlserver.jdbc.SQLServerDriver") =>
() =>
"""
|ALTER TABLE consumer ALTER COLUMN aud VARCHAR(MAX) NULL;
|""".stripMargin
case _ =>
() =>
"""
|ALTER TABLE consumer ALTER COLUMN aud TYPE text;
|""".stripMargin
}
}
val endDate = System.currentTimeMillis()
val comment: String =
s"""Executed SQL:
|$executedSql
|""".stripMargin
isSuccessful = true
saveLog(name, commitId, isSuccessful, startDate, endDate, comment)
isSuccessful
case false =>
val startDate = System.currentTimeMillis()
val commitId: String = APIUtil.gitCommit
val isSuccessful = false
val endDate = System.currentTimeMillis()
val comment: String =
s"""${Consumer._dbTableNameLC} table does not exist""".stripMargin
saveLog(name, commitId, isSuccessful, startDate, endDate, comment)
isSuccessful
}
}
}

View File

@ -12,6 +12,7 @@ import code.api.dynamic.entity.helper.DynamicEntityInfo
import code.api.util.APIUtil.{fullBoxOrException, _}
import code.api.util.ApiRole._
import code.api.util.ApiTag._
import code.api.util.CommonsEmailWrapper._
import code.api.util.DynamicUtil.Validation
import code.api.util.ErrorMessages.{BankNotFound, _}
import code.api.util.ExampleValue._
@ -79,8 +80,7 @@ import net.liftweb.json.JsonAST.JValue
import net.liftweb.json.JsonDSL._
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 org.apache.commons.lang3.StringUtils
import java.net.URLEncoder
@ -91,7 +91,6 @@ import scala.collection.immutable.{List, Nil}
import scala.collection.mutable.ArrayBuffer
import scala.concurrent.Future
import scala.jdk.CollectionConverters.collectionAsScalaIterableConverter
import scala.xml.XML
trait APIMethods400 extends MdcLoggable {
self: RestHelper =>
@ -3363,7 +3362,21 @@ 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 emailContent = EmailContent(
from = from,
to = List(invitation.email),
subject = subject,
textContent = Some(customText),
htmlContent = Some(customHtmlText)
)
sendHtmlEmail(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")
@ -3375,7 +3388,21 @@ 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 emailContent = EmailContent(
from = from,
to = List(invitation.email),
subject = subject,
textContent = Some(customText),
htmlContent = Some(customHtmlText)
)
sendHtmlEmail(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))

View File

@ -1708,6 +1708,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,

View File

@ -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
@ -705,7 +706,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(
@ -1009,7 +1010,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,

View File

@ -79,8 +79,7 @@ import net.liftweb.json
import net.liftweb.json.{JArray, JBool, JObject, JValue}
import net.liftweb.mapper._
import net.liftweb.util.Helpers.{hours, now, time, tryo}
import net.liftweb.util.Mailer.{From, PlainMailBodyType, Subject, To}
import net.liftweb.util.{Helpers, Mailer}
import net.liftweb.util.Helpers
import org.mindrot.jbcrypt.BCrypt
import scalikejdbc.DB.CPContext
import scalikejdbc.{ConnectionPool, ConnectionPoolSettings, MultipleConnectionPoolContext, DB => scalikeDB, _}
@ -383,8 +382,13 @@ object LocalMappedConnector extends Connector with MdcLoggable {
val hashedPassword = createHashedPassword(challengeAnswer)
APIUtil.getEmailsByUserId(userId) map {
pair =>
val params = PlainMailBodyType(s"Your OTP challenge : ${challengeAnswer}") :: List(To(pair._2))
Mailer.sendMail(From("challenge@tesobe.com"), Subject("Challenge"), params: _*)
val emailContent = CommonsEmailWrapper.EmailContent(
from = mailUsersUserinfoSenderAddress,
to = List(pair._2),
subject = "Challenge",
textContent = Some(s"Your OTP challenge : ${challengeAnswer}")
)
CommonsEmailWrapper.sendTextEmail(emailContent)
}
hashedPassword
case Some(StrongCustomerAuthentication.SMS) | Some(StrongCustomerAuthentication.SMS_OTP) =>
@ -5185,12 +5189,13 @@ object LocalMappedConnector extends Connector with MdcLoggable {
_ <- Future{
scaMethod match {
case v if v == StrongCustomerAuthentication.EMAIL.toString => // Send the email
val params = PlainMailBodyType(userAuthContextUpdate.challenge) :: List(To(customer.email))
Mailer.sendMail(
From("challenge@tesobe.com"),
Subject("Challenge request"),
params :_*
val emailContent = CommonsEmailWrapper.EmailContent(
from = mailUsersUserinfoSenderAddress,
to = List(customer.email),
subject = "Challenge request",
textContent = Some(userAuthContextUpdate.challenge)
)
CommonsEmailWrapper.sendTextEmail(emailContent)
case v if v == StrongCustomerAuthentication.SMS.toString => // Not implemented
case _ => // Not handled
}
@ -5211,8 +5216,13 @@ object LocalMappedConnector extends Connector with MdcLoggable {
callContext: Option[CallContext]
): OBPReturnType[Box[String]] = {
if (scaMethod == StrongCustomerAuthentication.EMAIL){ // Send the email
val params = PlainMailBodyType(message) :: List(To(recipient))
Mailer.sendMail(From("challenge@tesobe.com"), Subject("OBP Consent Challenge"), params :_*)
val emailContent = CommonsEmailWrapper.EmailContent(
from = mailUsersUserinfoSenderAddress,
to = List(recipient),
subject = "OBP Consent Challenge",
textContent = Some(message)
)
CommonsEmailWrapper.sendTextEmail(emailContent)
Future{(Full("Success"), callContext)}
} else if (scaMethod == StrongCustomerAuthentication.SMS){ // Send the SMS
for {

View File

@ -2,14 +2,11 @@ package code.bankconnectors.rabbitmq
import code.api.util.APIUtil
import com.rabbitmq.client.{Connection, ConnectionFactory}
import org.apache.commons.pool2.impl.{GenericObjectPool, GenericObjectPoolConfig}
import org.apache.commons.pool2.BasePooledObjectFactory
import org.apache.commons.pool2.PooledObject
import org.apache.commons.pool2.impl.DefaultPooledObject
import code.api.util.{APIUtil, ErrorMessages}
import code.bankconnectors.rabbitmq.RabbitMQUtils._
import code.api.util.ErrorMessages
import com.rabbitmq.client.{Connection, ConnectionFactory}
import org.apache.commons.pool2.{BasePooledObjectFactory, PooledObject}
import org.apache.commons.pool2.impl.{DefaultPooledObject, GenericObjectPool, GenericObjectPoolConfig}
class RabbitMQConnectionFactory extends BasePooledObjectFactory[Connection] {
@ -59,7 +56,7 @@ class RabbitMQConnectionFactory extends BasePooledObjectFactory[Connection] {
// Pool to manage RabbitMQ connections
object RabbitMQConnectionPool {
private val poolConfig = new GenericObjectPoolConfig()
private val poolConfig = new GenericObjectPoolConfig[Connection]()
poolConfig.setMaxTotal(5) // Maximum number of connections
poolConfig.setMinIdle(2) // Minimum number of idle connections
poolConfig.setMaxIdle(5) // Maximum number of idle connections

View File

@ -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))
)

View File

@ -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))
)

View File

@ -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))
)

View File

@ -1,17 +1,20 @@
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, 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
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 {
@ -71,6 +74,20 @@ 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 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 _ =>
None
}
}.flatten
val userId = queryParams.collectFirst { case OBPUserId(value) => By(MappedConsent.mUserId, value) }
val status = queryParams.collectFirst {
case OBPStatus(value) =>
@ -96,7 +113,7 @@ object MappedConsentProvider extends ConsentProvider {
offset.toSeq,
limit.toSeq,
status.toSeq,
userId.toSeq,
userId.orElse(providerProviderId).toSeq,
consentId.toSeq,
consumerId.toSeq
).flatten

View File

@ -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 = {

View File

@ -541,7 +541,7 @@ class Consumer extends LongKeyedMapper[Consumer] with CreatedUpdated{
// because different databases treat unique indexes on NULL values differently.
override def defaultValue = APIUtil.generateUUID()
}
object aud extends MappedString(this, 250) {
object aud extends MappedText(this) {
override def defaultValue = null
}
object iss extends MappedString(this, 250) {

View File

@ -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,

View File

@ -33,6 +33,7 @@ import code.api.cache.Caching
import code.api.dynamic.endpoint.helper.DynamicEndpointHelper
import code.api.util.APIUtil._
import code.api.util.CommonFunctions.validUri
import code.api.util.CommonsEmailWrapper._
import code.api.util.ErrorMessages._
import code.api.util._
import code.bankconnectors.Connector
@ -55,7 +56,6 @@ import net.liftweb.http.S.fmapFunc
import net.liftweb.http._
import net.liftweb.mapper._
import net.liftweb.sitemap.Loc.{If, LocParam, Template}
import net.liftweb.util.Mailer.{BCC, From, Subject, To}
import net.liftweb.util._
import org.apache.commons.lang3.StringUtils
import sh.ory.hydra.api.AdminApi
@ -589,24 +589,35 @@ 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())
Mailer.sendMail(From(emailFrom),Subject(passwordResetEmailSubject + " - " + u.username),
To(u.getEmail) ::
generateResetEmailBodies(u, resetPasswordLink) :::
(bccEmail.toList.map(BCC(_))) :_*)
// Directly generate content using JakartaMail/CommonsEmailWrapper
val textContent = Some(s"Please use the following link to reset your password: $resetPasswordLink")
val htmlContent = Some(s"<p>Please use the following link to reset your password:</p><p><a href='$resetPasswordLink'>$resetPasswordLink</a></p>")
val emailContent = EmailContent(
from = emailFrom,
to = List(u.getEmail),
bcc = bccEmail.toList,
subject = passwordResetEmailSubject + " - " + u.username,
textContent = textContent,
htmlContent = htmlContent
)
sendHtmlEmail(emailContent) match {
case Full(messageId) =>
logger.debug(s"Password reset email sent successfully with Message-ID: $messageId")
S.notice("Password reset email sent successfully. Please check your email.")
S.redirectTo(homePage)
case Empty =>
logger.error("Failed to send password reset email")
S.error("Failed to send password reset email. Please try again.")
S.redirectTo(homePage)
}
case u =>
sendValidationEmail(u)
}
// In order to prevent any leakage of information we use the same message for all cases
S.notice(userNameNotFoundString)
S.redirectTo(homePage)
}
override def lostPasswordXhtml = {
@ -638,17 +649,26 @@ 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)
Mailer.sendMail(From(emailFrom),Subject(signupMailSubject),
To(user.getEmail) ::
generateValidationEmailBodies(user, resetLink) :::
(bccEmail.toList.map(BCC(_))) :_* )
val textContent = Some(s"Welcome! Please validate your account by clicking the following link: $resetLink")
val htmlContent = Some(s"<p>Welcome! Please validate your account by clicking the following link:</p><p><a href='$resetLink'>$resetLink</a></p>")
val emailContent = EmailContent(
from = emailFrom,
to = List(user.getEmail),
bcc = bccEmail.toList,
subject = signupMailSubject,
textContent = textContent,
htmlContent = htmlContent
)
sendHtmlEmail(emailContent) match {
case Full(messageId) =>
logger.debug(s"Validation email sent successfully with Message-ID: $messageId")
S.notice("Validation email sent successfully. Please check your email.")
case Empty =>
logger.error("Failed to send validation email")
S.error("Failed to send validation email. Please try again.")
}
}
def grantDefaultEntitlementsToAuthUser(user: TheUserType) = {

View File

@ -28,7 +28,7 @@ package code.snippet
import java.util
import code.api.{Constant, DirectLogin}
import code.api.util.{APIUtil, ErrorMessages, KeycloakAdmin, X509}
import code.api.util.{APIUtil, ErrorMessages, KeycloakAdmin, X509, CommonsEmailWrapper}
import code.consumer.Consumers
import code.model.dataAccess.AuthUser
import code.model.{Consumer, _}
@ -424,18 +424,19 @@ class ConsumerRegistration extends MdcLoggable {
s"Direct Login Documentation: ${oauthDocumentationUrl} \n" +
s"$registrationMoreInfoText: $registrationMoreInfoUrl"
val params = PlainMailBodyType(registrationMessage) :: List(To(registered.developerEmail.get))
val webuiRegisterConsumerSuccessMssageEmail : String = getWebUiPropsValue(
"webui_register_consumer_success_message_email",
"Thank you for registering to use the Open Bank Project API.")
//this is an async call
Mailer.sendMail(
From(from),
Subject(webuiRegisterConsumerSuccessMssageEmail),
params :_*
val emailContent = CommonsEmailWrapper.EmailContent(
from = from,
to = List(registered.developerEmail.get),
subject = webuiRegisterConsumerSuccessMssageEmail,
textContent = Some(registrationMessage)
)
//this is an async call
CommonsEmailWrapper.sendTextEmail(emailContent)
}
if(mailSent.isEmpty)
@ -445,8 +446,6 @@ class ConsumerRegistration extends MdcLoggable {
// This is to let the system administrators / API managers know that someone has registered a consumer key.
def notifyRegistrationOccurred(registered : Consumer) = {
import net.liftweb.util.Mailer
import net.liftweb.util.Mailer._
val mailSent = for {
// e.g mail.api.consumer.registered.sender.address=no-reply@example.com
@ -465,18 +464,18 @@ class ConsumerRegistration extends MdcLoggable {
//technically doesn't work for all valid email addresses so this will mess up if someone tries to send emails to "foo,bar"@example.com
val to = toAddressesString.split(",").toList
val toParams = to.map(To(_))
val params = PlainMailBodyType(registrationMessage) :: toParams
val emailContent = CommonsEmailWrapper.EmailContent(
from = from,
to = to,
subject = s"New API user registered on $thisApiInstance",
textContent = Some(registrationMessage)
)
//this is an async call
Mailer.sendMail(
From(from),
Subject(s"New API user registered on $thisApiInstance"),
params :_*
)
CommonsEmailWrapper.sendTextEmail(emailContent)
}
//if Mailer.sendMail wasn't called (note: this actually isn't checking if the mail failed to send as that is being done asynchronously)
if(mailSent.isEmpty)
this.logger.warn(s"API consumer registration failed: $mailSent")

View File

@ -156,7 +156,7 @@ class MappedTransaction extends LongKeyedMapper[MappedTransaction] with IdPK wit
tStartDate.get,
Some(tFinishDate.get),
newBalance,
status.get))
Some(status.get)))
}
}

View File

@ -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")
)
}

View File

@ -29,6 +29,16 @@
<dependency>
<groupId>net.liftweb</groupId>
<artifactId>lift-util_${scala.version}</artifactId>
<exclusions>
<exclusion>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
</exclusion>
<exclusion>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>net.liftweb</groupId>

View File

@ -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

View File

@ -3,6 +3,20 @@
### Most recent changes at top of file
```
Date Commit Action
04/08/2025 d282d266 Enhanced Email Configuration with CommonsEmailWrapper
Replaced Lift Mailer with Apache Commons Email for improved email functionality.
Added comprehensive SMTP configuration options:
- mail.smtp.auth (authentication support)
- mail.smtp.user (SMTP username)
- mail.smtp.password (SMTP password)
- mail.smtp.starttls.enable (STARTTLS support)
- mail.smtp.ssl.enable (SSL support)
- mail.smtp.ssl.protocols (TLS protocol selection)
- mail.smtp.ssl.trust (certificate trust options)
- mail.debug (SMTP debugging)
- mail.users.userinfo.sender.address (default sender)
Added configuration examples forTesobe mail servers.
All email functionality (password reset, validation, notifications) now uses CommonsEmailWrapper.
25/06/2025 e49ebb4f Added props BG_remove_sign_of_amounts, default is false.
If set to true, the sign of amounts will be removed in the BGv1.3 getTransaction endpoints.
17/03/2025 166e4f2a Removed Kafka commits: 166e4f2a,7f24802e,6f0a3b53,f22763c3,