mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 16:36:54 +00:00
Merge remote-tracking branch 'Simon/develop' into develop-Simon
# Conflicts: # .gitignore
This commit is contained in:
commit
f796169ff9
8
.gitignore
vendored
8
.gitignore
vendored
@ -8,6 +8,9 @@
|
||||
.settings
|
||||
.metals
|
||||
.vscode
|
||||
*.code-workspace
|
||||
.zed
|
||||
.cursor
|
||||
.classpath
|
||||
.project
|
||||
.cache
|
||||
@ -30,9 +33,8 @@ obp-api/src/main/scala/code/api/v3_0_0/custom/
|
||||
marketing_diagram_generation/outputs/*
|
||||
|
||||
.bloop
|
||||
!.bloop/*.json
|
||||
.bsp
|
||||
.specstory
|
||||
project/project
|
||||
coursier
|
||||
*.code-workspace
|
||||
.cursor
|
||||
coursier
|
||||
76
.metals-config.json
Normal file
76
.metals-config.json
Normal file
@ -0,0 +1,76 @@
|
||||
{
|
||||
"maven": {
|
||||
"enabled": true
|
||||
},
|
||||
"metals": {
|
||||
"serverVersion": "1.0.0",
|
||||
"javaHome": "/usr/lib/jvm/java-17-openjdk-amd64",
|
||||
"bloopVersion": "2.0.0",
|
||||
"superMethodLensesEnabled": true,
|
||||
"enableSemanticHighlighting": true,
|
||||
"compileOnSave": true,
|
||||
"testUserInterface": "Code Lenses",
|
||||
"inlayHints": {
|
||||
"enabled": true,
|
||||
"hintsInPatternMatch": {
|
||||
"enabled": true
|
||||
},
|
||||
"implicitArguments": {
|
||||
"enabled": true
|
||||
},
|
||||
"implicitConversions": {
|
||||
"enabled": true
|
||||
},
|
||||
"inferredTypes": {
|
||||
"enabled": true
|
||||
},
|
||||
"typeParameters": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"buildTargets": [
|
||||
{
|
||||
"id": "obp-commons",
|
||||
"displayName": "obp-commons",
|
||||
"baseDirectory": "file:///home/marko/Tesobe/GitHub/constantine2nd/OBP-API/obp-commons/",
|
||||
"tags": ["library"],
|
||||
"languageIds": ["scala", "java"],
|
||||
"dependencies": [],
|
||||
"capabilities": {
|
||||
"canCompile": true,
|
||||
"canTest": true,
|
||||
"canRun": false,
|
||||
"canDebug": true
|
||||
},
|
||||
"dataKind": "scala",
|
||||
"data": {
|
||||
"scalaOrganization": "org.scala-lang",
|
||||
"scalaVersion": "2.12.20",
|
||||
"scalaBinaryVersion": "2.12",
|
||||
"platform": "jvm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "obp-api",
|
||||
"displayName": "obp-api",
|
||||
"baseDirectory": "file:///home/marko/Tesobe/GitHub/constantine2nd/OBP-API/obp-api/",
|
||||
"tags": ["application"],
|
||||
"languageIds": ["scala", "java"],
|
||||
"dependencies": ["obp-commons"],
|
||||
"capabilities": {
|
||||
"canCompile": true,
|
||||
"canTest": true,
|
||||
"canRun": true,
|
||||
"canDebug": true
|
||||
},
|
||||
"dataKind": "scala",
|
||||
"data": {
|
||||
"scalaOrganization": "org.scala-lang",
|
||||
"scalaVersion": "2.12.20",
|
||||
"scalaBinaryVersion": "2.12",
|
||||
"platform": "jvm"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
81
README.md
81
README.md
@ -46,11 +46,22 @@ This project is dual licensed under the AGPL V3 (see NOTICE) and commercial lice
|
||||
The project uses Maven 3 as its build tool.
|
||||
|
||||
To compile and run Jetty, install Maven 3, create your configuration in `obp-api/src/main/resources/props/default.props` and execute:
|
||||
To compile and run Jetty, install Maven 3, create your configuration in `obp-api/src/main/resources/props/`, copy `sample.props.template` to `default.props` and edit the latter. Then:
|
||||
|
||||
```sh
|
||||
mvn install -pl .,obp-commons && mvn jetty:run -pl obp-api
|
||||
```
|
||||
|
||||
### ZED IDE Setup
|
||||
|
||||
For ZED IDE users, we provide a complete development environment with Scala language server support:
|
||||
|
||||
```bash
|
||||
./zed/setup-zed-ide.sh
|
||||
```
|
||||
|
||||
This sets up automated build tasks, code navigation, and real-time error checking. See [`zed/README.md`](zed/README.md) for complete documentation.
|
||||
|
||||
In case the above command fails try the next one:
|
||||
|
||||
```sh
|
||||
@ -206,6 +217,23 @@ Once Postgres is installed (On macOS, use `brew`):
|
||||
|
||||
1. Grant all on database `obpdb` to `obp`; (So OBP-API can create tables etc.)
|
||||
|
||||
#### For newer versions of postgres 16 and above, you need to follow the following instructions
|
||||
|
||||
-- Connect to the sandbox database
|
||||
\c sandbox;
|
||||
|
||||
-- Grant schema usage and creation privileges
|
||||
GRANT USAGE ON SCHEMA public TO obp;
|
||||
GRANT CREATE ON SCHEMA public TO obp;
|
||||
|
||||
-- Grant all privileges on existing tables (if any)
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO obp;
|
||||
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO obp;
|
||||
|
||||
-- Grant privileges on future tables and sequences
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO obp;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO obp;
|
||||
|
||||
1. Then, set the `db.url` in your Props:
|
||||
|
||||
```
|
||||
@ -638,6 +666,59 @@ allow_oauth2_login=true
|
||||
oauth2.jwk_set.url=https://www.googleapis.com/oauth2/v3/certs
|
||||
```
|
||||
|
||||
### OAuth2 JWKS URI Configuration
|
||||
|
||||
The `oauth2.jwk_set.url` property is critical for OAuth2 JWT token validation. OBP-API uses this to verify the authenticity of JWT tokens by fetching the JSON Web Key Set (JWKS) from the specified URI(s).
|
||||
|
||||
#### Configuration Methods
|
||||
|
||||
The `oauth2.jwk_set.url` property is resolved in the following order of priority:
|
||||
|
||||
1. **Environment Variable**
|
||||
|
||||
```bash
|
||||
export OBP_OAUTH2_JWK_SET_URL="https://your-oidc-server.com/jwks"
|
||||
```
|
||||
|
||||
2. **Properties Files** (located in `obp-api/src/main/resources/props/`)
|
||||
- `production.default.props` (for production deployments)
|
||||
- `default.props` (for development)
|
||||
- `test.default.props` (for testing)
|
||||
|
||||
#### Supported Formats
|
||||
|
||||
- **Single URL**: `oauth2.jwk_set.url=http://localhost:9000/obp-oidc/jwks`
|
||||
- **Multiple URLs**: `oauth2.jwk_set.url=http://localhost:8080/jwk.json,https://www.googleapis.com/oauth2/v3/certs`
|
||||
|
||||
#### Common OAuth2 Provider Examples
|
||||
|
||||
- **Google**: `https://www.googleapis.com/oauth2/v3/certs`
|
||||
- **OBP-OIDC**: `http://localhost:9000/obp-oidc/jwks`
|
||||
- **Keycloak**: `http://localhost:7070/realms/master/protocol/openid-connect/certs`
|
||||
- **Azure AD**: `https://login.microsoftonline.com/common/discovery/v2.0/keys`
|
||||
|
||||
#### Troubleshooting OBP-20208 Error
|
||||
|
||||
If you encounter the error "OBP-20208: Cannot match the issuer and JWKS URI at this server instance", check the following:
|
||||
|
||||
1. **Verify JWT Issuer Claim**: The JWT token's `iss` (issuer) claim must match one of the configured identity providers
|
||||
2. **Check JWKS URL Configuration**: Ensure `oauth2.jwk_set.url` contains URLs that correspond to your JWT issuer
|
||||
3. **Case-Insensitive Matching**: OBP-API performs case-insensitive substring matching between the issuer and JWKS URLs
|
||||
4. **URL Format Consistency**: Check for trailing slashes or URL formatting differences
|
||||
|
||||
**Debug Logging**: Enable debug logging to see detailed information about the matching process:
|
||||
|
||||
```properties
|
||||
# Add to your logging configuration
|
||||
logger.code.api.OAuth2=DEBUG
|
||||
```
|
||||
|
||||
The debug logs will show:
|
||||
|
||||
- Expected identity provider vs actual JWT issuer claim
|
||||
- Available JWKS URIs from configuration
|
||||
- Matching logic results
|
||||
|
||||
---
|
||||
|
||||
## Frozen APIs
|
||||
|
||||
@ -652,13 +652,18 @@ defaultBank.bank_id=OBP
|
||||
|
||||
|
||||
################################################################################
|
||||
## Super Admin Users are used to boot strap User Entitlements (access to Roles).
|
||||
## Super Admins are receive **ONLY TWO** implicit entitlements which are:
|
||||
## Super Admin Users are used to boot-strap User Entitlements (access to Roles).
|
||||
## Super Admins listed below can grant them selves the following entitlements:
|
||||
## CanCreateEntitlementAtAnyBank
|
||||
## and
|
||||
## CanCreateEntitlementAtOneBank
|
||||
## After they have granted these roles, they can grant further roles and remove their
|
||||
# user_id from the super_admin_user_ids list because its redundant.
|
||||
## Once you have the roles above you can grant any other system or bank related roles to yourself.
|
||||
##
|
||||
## THUS, probably the first thing a Super Admin will do is to grant themselves or other users a number of Roles
|
||||
## For instance, a Super Admin *CANNOT delete an entitlement* unless they grant themselves CanDeleteEntitlementAtAnyBank or CanDeleteEntitlementAtOneBank
|
||||
## For instance, a Super Admin defined by their user_id in super_admin_user_ids CANNOT carry out actions unless they first give themselves an actual Entitlment to a Role.
|
||||
|
||||
## List the Users here, with their user_id(s), that should be Super Admins
|
||||
super_admin_user_ids=USER_ID1,USER_ID2,
|
||||
################################################################################
|
||||
@ -1491,6 +1496,8 @@ validate_iban=false
|
||||
# sample props regulated_entities = [{"certificate_authority_ca_owner_id":"CY_CBC","entity_certificate_public_key":"-----BEGIN CERTIFICATE-----MIICsjCCAZqgAwIBAgIGAYwQ62R0MA0GCSqGSIb3DQEBCwUAMBoxGDAWBgNVBAMMD2FwcC5leGFtcGxlLmNvbTAeFw0yMzExMjcxMzE1MTFaFw0yNTExMjYxMzE1MTFaMBoxGDAWBgNVBAMMD2FwcC5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK9WIodZHWzKyCcf9YfWEhPURbfO6zKuMqzHN27GdqHsVVEGxP4F/J4mso+0ENcRr6ur4u81iREaVdCc40rHDHVJNEtniD8Icbz7tcsqAewIVhc/q6WXGqImJpCq7hA0m247dDsaZT0lb/MVBiMoJxDEmAE/GYYnWTEn84R35WhJsMvuQ7QmLvNg6RkChY6POCT/YKe9NKwa1NqI1U+oA5RFzAaFtytvZCE3jtp+aR0brL7qaGfgxm6B7dEpGyhg0NcVCV7xMQNq2JxZTVdAr6lcsRGaAFulakmW3aNnmK+L35Wu8uW+OxNxwUuC6f3b4FVBa276FMuUTRfu7gc+k6kCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAAU5CjEyAoyTn7PgFpQD48ZNPuUsEQ19gzYgJvHMzFIoZ7jKBodjO5mCzWBcR7A4mpeAsdyiNBl2sTiZscSnNqxk61jVzP5Ba1D7XtOjjr7+3iqowrThj6BY40QqhYh/6BSY9fDzVZQiHnvlo6ZUM5kUK6OavZOovKlp5DIl5sGqoP0qAJnpQ4nhB2WVVsKfPlOXc+2KSsbJ23g9l8zaTMr+X0umlvfEKqyEl1Fa2L1dO0y/KFQ+ILmxcZLpRdq1hRAjd0quq9qGC8ucXhRWDgM4hslVpau0da68g0aItWNez3mc5lB82b3dcZpFMzO41bgw7gvw10AvvTfQDqEYIuQ==-----END CERTIFICATE-----","entity_code":"PSD_PICY_CBC!12345","entity_type":"PSD_PI","entity_address":"EXAMPLE COMPANY LTD, 5 SOME STREET","entity_town_city":"SOME CITY","entity_post_code":"1060","entity_country":"CY","entity_web_site":"www.example.com","services":[{"CY":["PS_010","PS_020","PS_03C","PS_04C"]}]}]
|
||||
regulated_entities = []
|
||||
|
||||
|
||||
# Trusted Consumer pairs
|
||||
#In OBP Create Consent if the app that is creating the consent (grantor_consumer_id) wants to create a consent for the grantee_consumer_id App, then we should skip SCA.
|
||||
#The use case is API Explorer II giving a consent to Opey . In such a case API Explorer II and Opey are effectively the same App as far as the user is concerned.
|
||||
|
||||
@ -1511,3 +1518,48 @@ regulated_entities = []
|
||||
|
||||
|
||||
# Note: For secure and http only settings for cookies see resources/web.xml which is mentioned in the README.md
|
||||
|
||||
|
||||
|
||||
##########################################################
|
||||
# Redis Logging #
|
||||
##########################################################
|
||||
## Enable Redis logging (true/false)
|
||||
redis_logging_enabled = false
|
||||
|
||||
## Batch size for sending logs to Redis
|
||||
## Smaller batch size reduces latency for logging critical messages.
|
||||
redis_logging_batch_size = 50
|
||||
|
||||
## Flush interval for batch logs in milliseconds
|
||||
## Flush every 500ms to keep Redis queues up-to-date without too much delay.
|
||||
redis_logging_flush_interval_ms = 500
|
||||
|
||||
## Maximum number of retries for failed log writes
|
||||
## Ensures transient Redis errors are retried before failing.
|
||||
redis_logging_max_retries = 3
|
||||
|
||||
## Number of consecutive failures before opening circuit breaker
|
||||
## Prevents hammering Redis when it is down.
|
||||
redis_logging_circuit_breaker_threshold = 10
|
||||
|
||||
## Number of threads for asynchronous Redis operations
|
||||
## Keep small for lightweight logging; adjust if heavy logging is expected.
|
||||
redis_logging_thread_pool_size = 2
|
||||
|
||||
## SIX different FIFO Redis queues. Each queue has a maximum number of entries.
|
||||
## These control how many messages are kept per log level.
|
||||
## 1000 is a reasonable default; adjust if you expect higher traffic.
|
||||
redis_logging_trace_queue_max_entries = 1000 # Max TRACE messages
|
||||
redis_logging_debug_queue_max_entries = 1000 # Max DEBUG messages
|
||||
redis_logging_info_queue_max_entries = 1000 # Max INFO messages
|
||||
redis_logging_warning_queue_max_entries = 1000 # Max WARNING messages
|
||||
redis_logging_error_queue_max_entries = 1000 # Max ERROR messages
|
||||
redis_logging_all_queue_max_entries = 1000 # Max ALL messages
|
||||
|
||||
## Optional: Circuit breaker reset interval (ms)
|
||||
## How long before retrying after circuit breaker opens. Default 60s.
|
||||
redis_logging_circuit_breaker_reset_ms = 60000
|
||||
##########################################################
|
||||
# Redis Logging #
|
||||
##########################################################
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package bootstrap.liftweb
|
||||
|
||||
import code.api.util.APIUtil
|
||||
import code.util.Helper.MdcLoggable
|
||||
import com.zaxxer.hikari.pool.ProxyConnection
|
||||
import com.zaxxer.hikari.{HikariConfig, HikariDataSource}
|
||||
|
||||
@ -21,19 +22,17 @@ import net.liftweb.util.Helpers.tryo
|
||||
class CustomDBVendor(driverName: String,
|
||||
dbUrl: String,
|
||||
dbUser: Box[String],
|
||||
dbPassword: Box[String]) extends CustomProtoDBVendor {
|
||||
|
||||
private val logger = Logger(classOf[CustomDBVendor])
|
||||
dbPassword: Box[String]) extends CustomProtoDBVendor with MdcLoggable {
|
||||
|
||||
object HikariDatasource {
|
||||
val config = new HikariConfig()
|
||||
|
||||
|
||||
val connectionTimeout = APIUtil.getPropsAsLongValue("hikari.connectionTimeout")
|
||||
val maximumPoolSize = APIUtil.getPropsAsIntValue("hikari.maximumPoolSize")
|
||||
val idleTimeout = APIUtil.getPropsAsLongValue("hikari.idleTimeout")
|
||||
val keepaliveTime = APIUtil.getPropsAsLongValue("hikari.keepaliveTime")
|
||||
val maxLifetime = APIUtil.getPropsAsLongValue("hikari.maxLifetime")
|
||||
|
||||
|
||||
if(connectionTimeout.isDefined){
|
||||
config.setConnectionTimeout(connectionTimeout.head)
|
||||
}
|
||||
@ -63,7 +62,7 @@ class CustomDBVendor(driverName: String,
|
||||
case _ =>
|
||||
config.setJdbcUrl(dbUrl)
|
||||
}
|
||||
|
||||
|
||||
config.addDataSourceProperty("cachePrepStmts", "true")
|
||||
config.addDataSourceProperty("prepStmtCacheSize", "250")
|
||||
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048")
|
||||
@ -79,8 +78,7 @@ class CustomDBVendor(driverName: String,
|
||||
def closeAllConnections_!(): Unit = HikariDatasource.ds.close()
|
||||
}
|
||||
|
||||
trait CustomProtoDBVendor extends ConnectionManager {
|
||||
private val logger = Logger(classOf[CustomProtoDBVendor])
|
||||
trait CustomProtoDBVendor extends ConnectionManager with MdcLoggable {
|
||||
|
||||
def createOne: Box[Connection]
|
||||
|
||||
@ -90,4 +88,4 @@ trait CustomProtoDBVendor extends ConnectionManager {
|
||||
|
||||
def releaseConnection(conn: Connection): Unit = {conn.asInstanceOf[ProxyConnection].close()}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import com.openbankproject.commons.model.enums.AccountAttributeType
|
||||
import com.openbankproject.commons.model.{AccountAttribute, AccountId, BankId, BankIdAccountId, ProductAttribute, ProductCode, ViewId}
|
||||
import net.liftweb.common.{Box, Logger}
|
||||
import net.liftweb.util.SimpleInjector
|
||||
import code.util.Helper.MdcLoggable
|
||||
|
||||
import scala.collection.immutable.List
|
||||
import scala.concurrent.Future
|
||||
@ -16,7 +17,7 @@ object AccountAttributeX extends SimpleInjector {
|
||||
val accountAttributeProvider = new Inject(buildOne _) {}
|
||||
|
||||
def buildOne: AccountAttributeProvider = MappedAccountAttributeProvider
|
||||
|
||||
|
||||
// Helper to get the count out of an option
|
||||
def countOfAccountAttribute(listOpt: Option[List[AccountAttribute]]): Int = {
|
||||
val count = listOpt match {
|
||||
@ -29,17 +30,15 @@ object AccountAttributeX extends SimpleInjector {
|
||||
|
||||
}
|
||||
|
||||
trait AccountAttributeProvider {
|
||||
|
||||
private val logger = Logger(classOf[AccountAttributeProvider])
|
||||
trait AccountAttributeProvider extends MdcLoggable {
|
||||
|
||||
def getAccountAttributesFromProvider(accountId: AccountId, productCode: ProductCode): Future[Box[List[AccountAttribute]]]
|
||||
def getAccountAttributesByAccount(bankId: BankId,
|
||||
accountId: AccountId): Future[Box[List[AccountAttribute]]]
|
||||
def getAccountAttributesByAccountCanBeSeenOnView(bankId: BankId,
|
||||
accountId: AccountId,
|
||||
def getAccountAttributesByAccountCanBeSeenOnView(bankId: BankId,
|
||||
accountId: AccountId,
|
||||
viewId: ViewId): Future[Box[List[AccountAttribute]]]
|
||||
def getAccountAttributesByAccountsCanBeSeenOnView(accounts: List[BankIdAccountId],
|
||||
def getAccountAttributesByAccountsCanBeSeenOnView(accounts: List[BankIdAccountId],
|
||||
viewId: ViewId): Future[Box[List[AccountAttribute]]]
|
||||
|
||||
def getAccountAttributeById(productAttributeId: String): Future[Box[AccountAttribute]]
|
||||
@ -58,10 +57,10 @@ trait AccountAttributeProvider {
|
||||
productCode: ProductCode,
|
||||
accountAttributes: List[ProductAttribute],
|
||||
productInstanceCode: Option[String]): Future[Box[List[AccountAttribute]]]
|
||||
|
||||
|
||||
def deleteAccountAttribute(accountAttributeId: String): Future[Box[Boolean]]
|
||||
|
||||
def getAccountIdsByParams(bankId: BankId, params: Map[String, List[String]]): Future[Box[List[String]]]
|
||||
|
||||
// End of Trait
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,6 +107,7 @@ object OAuth2Login extends RestHelper with MdcLoggable {
|
||||
} else if (Azure.isIssuer(value)) {
|
||||
Azure.applyIdTokenRulesFuture(value, cc)
|
||||
} else if (OBPOIDC.isIssuer(value)) {
|
||||
logger.debug("getUserFuture says: I will call OBPOIDC.applyIdTokenRulesFuture")
|
||||
OBPOIDC.applyIdTokenRulesFuture(value, cc)
|
||||
} else if (Keycloak.isIssuer(value)) {
|
||||
Keycloak.applyRulesFuture(value, cc)
|
||||
@ -230,14 +231,65 @@ object OAuth2Login extends RestHelper with MdcLoggable {
|
||||
def checkUrlOfJwkSets(identityProvider: String) = {
|
||||
val url: List[String] = Constant.oauth2JwkSetUrl.toList
|
||||
val jwksUris: List[String] = url.map(_.toLowerCase()).map(_.split(",").toList).flatten
|
||||
|
||||
|
||||
logger.debug(s"checkUrlOfJwkSets - identityProvider: '$identityProvider'")
|
||||
logger.debug(s"checkUrlOfJwkSets - oauth2.jwk_set.url raw value: '${Constant.oauth2JwkSetUrl}'")
|
||||
logger.debug(s"checkUrlOfJwkSets - parsed jwksUris: $jwksUris")
|
||||
|
||||
// Enhanced matching for both URL-based and semantic identifiers
|
||||
val identityProviderLower = identityProvider.toLowerCase()
|
||||
val jwksUri = jwksUris.filter(_.contains(identityProviderLower))
|
||||
|
||||
|
||||
logger.debug(s"checkUrlOfJwkSets - identityProviderLower: '$identityProviderLower'")
|
||||
logger.debug(s"checkUrlOfJwkSets - filtered jwksUri: $jwksUri")
|
||||
|
||||
jwksUri match {
|
||||
case x :: _ => Full(x)
|
||||
case Nil => Failure(Oauth2CannotMatchIssuerAndJwksUriException)
|
||||
case x :: _ =>
|
||||
logger.debug(s"checkUrlOfJwkSets - SUCCESS: Found matching JWKS URI: '$x'")
|
||||
Full(x)
|
||||
case Nil =>
|
||||
logger.debug(s"checkUrlOfJwkSets - FAILURE: Cannot match issuer '$identityProvider' with any JWKS URI")
|
||||
logger.debug(s"checkUrlOfJwkSets - Expected issuer pattern: '$identityProvider' (case-insensitive contains match)")
|
||||
logger.debug(s"checkUrlOfJwkSets - Available JWKS URIs: $jwksUris")
|
||||
logger.debug(s"checkUrlOfJwkSets - Identity provider (lowercase): '$identityProviderLower'")
|
||||
logger.debug(s"checkUrlOfJwkSets - Matching logic: Looking for JWKS URIs containing '$identityProviderLower'")
|
||||
Failure(Oauth2CannotMatchIssuerAndJwksUriException)
|
||||
}
|
||||
}
|
||||
|
||||
def checkUrlOfJwkSetsWithToken(identityProvider: String, jwtToken: String) = {
|
||||
val actualIssuer = JwtUtil.getIssuer(jwtToken).getOrElse("NO_ISSUER_CLAIM")
|
||||
val url: List[String] = Constant.oauth2JwkSetUrl.toList
|
||||
val jwksUris: List[String] = url.map(_.toLowerCase()).map(_.split(",").toList).flatten
|
||||
|
||||
logger.debug(s"checkUrlOfJwkSetsWithToken - Expected identity provider: '$identityProvider'")
|
||||
logger.debug(s"checkUrlOfJwkSetsWithToken - Actual JWT issuer claim: '$actualIssuer'")
|
||||
logger.debug(s"checkUrlOfJwkSetsWithToken - oauth2.jwk_set.url raw value: '${Constant.oauth2JwkSetUrl}'")
|
||||
logger.debug(s"checkUrlOfJwkSetsWithToken - parsed jwksUris: $jwksUris")
|
||||
|
||||
// Enhanced matching for both URL-based and semantic identifiers
|
||||
val identityProviderLower = identityProvider.toLowerCase()
|
||||
val jwksUri = jwksUris.filter(_.contains(identityProviderLower))
|
||||
|
||||
logger.debug(s"checkUrlOfJwkSetsWithToken - identityProviderLower: '$identityProviderLower'")
|
||||
logger.debug(s"checkUrlOfJwkSetsWithToken - filtered jwksUri: $jwksUri")
|
||||
|
||||
jwksUri match {
|
||||
case x :: _ =>
|
||||
logger.debug(s"checkUrlOfJwkSetsWithToken - SUCCESS: Found matching JWKS URI: '$x'")
|
||||
Full(x)
|
||||
case Nil =>
|
||||
logger.debug(s"checkUrlOfJwkSetsWithToken - FAILURE: Cannot match issuer with any JWKS URI")
|
||||
logger.debug(s"checkUrlOfJwkSetsWithToken - Expected identity provider: '$identityProvider'")
|
||||
logger.debug(s"checkUrlOfJwkSetsWithToken - Actual JWT issuer claim: '$actualIssuer'")
|
||||
logger.debug(s"checkUrlOfJwkSetsWithToken - Available JWKS URIs: $jwksUris")
|
||||
logger.debug(s"checkUrlOfJwkSetsWithToken - Expected pattern (lowercase): '$identityProviderLower'")
|
||||
logger.debug(s"checkUrlOfJwkSetsWithToken - Matching logic: Looking for JWKS URIs containing '$identityProviderLower'")
|
||||
logger.debug(s"checkUrlOfJwkSetsWithToken - TROUBLESHOOTING:")
|
||||
logger.debug(s"checkUrlOfJwkSetsWithToken - 1. Verify oauth2.jwk_set.url contains URL matching '$identityProvider'")
|
||||
logger.debug(s"checkUrlOfJwkSetsWithToken - 2. Check if JWT issuer '$actualIssuer' should match identity provider '$identityProvider'")
|
||||
logger.debug(s"checkUrlOfJwkSetsWithToken - 3. Ensure case-insensitive substring matching works: does any JWKS URI contain '$identityProviderLower'?")
|
||||
Failure(Oauth2CannotMatchIssuerAndJwksUriException)
|
||||
}
|
||||
}
|
||||
|
||||
@ -258,14 +310,33 @@ object OAuth2Login extends RestHelper with MdcLoggable {
|
||||
}.getOrElse(false)
|
||||
}
|
||||
def validateIdToken(idToken: String): Box[IDTokenClaimsSet] = {
|
||||
logger.debug(s"validateIdToken - attempting to validate ID token")
|
||||
|
||||
// Extract issuer for better error reporting
|
||||
val actualIssuer = JwtUtil.getIssuer(idToken).getOrElse("NO_ISSUER_CLAIM")
|
||||
logger.debug(s"validateIdToken - JWT issuer claim: '$actualIssuer'")
|
||||
|
||||
urlOfJwkSets match {
|
||||
case Full(url) =>
|
||||
logger.debug(s"validateIdToken - using JWKS URL: '$url'")
|
||||
JwtUtil.validateIdToken(idToken, url)
|
||||
case ParamFailure(a, b, c, apiFailure : APIFailure) =>
|
||||
logger.debug(s"validateIdToken - ParamFailure: $a, $b, $c, $apiFailure")
|
||||
logger.debug(s"validateIdToken - JWT issuer was: '$actualIssuer'")
|
||||
ParamFailure(a, b, c, apiFailure : APIFailure)
|
||||
case Failure(msg, t, c) =>
|
||||
logger.debug(s"validateIdToken - Failure getting JWKS URL: $msg")
|
||||
logger.debug(s"validateIdToken - JWT issuer was: '$actualIssuer'")
|
||||
if (msg.contains("OBP-20208")) {
|
||||
logger.debug("validateIdToken - OBP-20208 Error Details:")
|
||||
logger.debug(s"validateIdToken - JWT issuer claim: '$actualIssuer'")
|
||||
logger.debug(s"validateIdToken - oauth2.jwk_set.url value: '${Constant.oauth2JwkSetUrl}'")
|
||||
logger.debug("validateIdToken - Check that the JWKS URL configuration matches the JWT issuer")
|
||||
}
|
||||
Failure(msg, t, c)
|
||||
case _ =>
|
||||
logger.debug("validateIdToken - No JWKS URL available")
|
||||
logger.debug(s"validateIdToken - JWT issuer was: '$actualIssuer'")
|
||||
Failure(Oauth2ThereIsNoUrlOfJwkSet)
|
||||
}
|
||||
}
|
||||
@ -327,28 +398,42 @@ object OAuth2Login extends RestHelper with MdcLoggable {
|
||||
def getOrCreateResourceUser(idToken: String): Box[User] = {
|
||||
val uniqueIdGivenByProvider = JwtUtil.getSubject(idToken).getOrElse("")
|
||||
val provider = resolveProvider(idToken)
|
||||
Users.users.vend.getUserByProviderId(provider = provider, idGivenByProvider = uniqueIdGivenByProvider).or { // Find a user
|
||||
Users.users.vend.createResourceUser( // Otherwise create a new one
|
||||
provider = provider,
|
||||
providerId = Some(uniqueIdGivenByProvider),
|
||||
None,
|
||||
name = getClaim(name = "given_name", idToken = idToken).orElse(Some(uniqueIdGivenByProvider)),
|
||||
email = getClaim(name = "email", idToken = idToken),
|
||||
userId = None,
|
||||
createdByUserInvitationId = None,
|
||||
company = None,
|
||||
lastMarketingAgreementSignedDate = None
|
||||
)
|
||||
KeycloakFederatedUserReference.parse(uniqueIdGivenByProvider) match {
|
||||
case Right(fedRef) => // Users log on via Keycloak, which uses User Federation to access the external OBP database.
|
||||
logger.debug(s"External ID = ${fedRef.externalId}")
|
||||
logger.debug(s"Storage Provider ID = ${fedRef.storageProviderId}")
|
||||
Users.users.vend.getUserByResourceUserId(fedRef.externalId)
|
||||
case Left(error) =>
|
||||
logger.debug(s"Parse error: $error")
|
||||
Users.users.vend.getUserByProviderId(provider = provider, idGivenByProvider = uniqueIdGivenByProvider).or { // Find a user
|
||||
Users.users.vend.createResourceUser( // Otherwise create a new one
|
||||
provider = provider,
|
||||
providerId = Some(uniqueIdGivenByProvider),
|
||||
None,
|
||||
name = getClaim(name = "given_name", idToken = idToken).orElse(Some(uniqueIdGivenByProvider)),
|
||||
email = getClaim(name = "email", idToken = idToken),
|
||||
userId = None,
|
||||
createdByUserInvitationId = None,
|
||||
company = None,
|
||||
lastMarketingAgreementSignedDate = None
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def resolveProvider(idToken: String) = {
|
||||
HydraUtil.integrateWithHydra && isIssuer(jwtToken = idToken, identityProvider = hydraPublicUrl) match {
|
||||
case true if HydraUtil.hydraUsesObpUserCredentials => // Case that source of the truth of Hydra user management is the OBP-API mapper DB
|
||||
// In case that ORY Hydra login url is "hostname/user_mgt/login" we MUST override hydraPublicUrl as provider
|
||||
logger.debug("resolveProvider says: we are in Hydra ")
|
||||
// In case that ORY Hydra login url is "hostname/user_mgt/login" we MUST override hydraPublicUrl as provider
|
||||
// in order to avoid creation of a new user
|
||||
Constant.localIdentityProvider
|
||||
// if its OBPOIDC issuer
|
||||
case false if OBPOIDC.isIssuer(idToken) =>
|
||||
logger.debug("resolveProvider says: we are in OBPOIDC ")
|
||||
Constant.localIdentityProvider
|
||||
case _ => // All other cases implies a new user creation
|
||||
logger.debug("resolveProvider says: Other cases ")
|
||||
// TODO raise exception in case of else case
|
||||
JwtUtil.getIssuer(idToken).getOrElse("")
|
||||
}
|
||||
@ -399,8 +484,15 @@ object OAuth2Login extends RestHelper with MdcLoggable {
|
||||
}
|
||||
|
||||
def applyIdTokenRules(token: String, cc: CallContext): (Box[User], Some[CallContext]) = {
|
||||
logger.debug("applyIdTokenRules - starting ID token validation")
|
||||
|
||||
// Extract issuer from token for debugging
|
||||
val actualIssuer = JwtUtil.getIssuer(token).getOrElse("NO_ISSUER_CLAIM")
|
||||
logger.debug(s"applyIdTokenRules - JWT issuer claim: '$actualIssuer'")
|
||||
|
||||
validateIdToken(token) match {
|
||||
case Full(_) =>
|
||||
logger.debug("applyIdTokenRules - ID token validation successful")
|
||||
val user = getOrCreateResourceUser(token)
|
||||
val consumer = getOrCreateConsumer(token, user.map(_.userId), Some(OpenIdConnect.openIdConnect))
|
||||
LoginAttempt.userIsLocked(user.map(_.provider).getOrElse(""), user.map(_.name).getOrElse("")) match {
|
||||
@ -408,10 +500,26 @@ object OAuth2Login extends RestHelper with MdcLoggable {
|
||||
case false => (user, Some(cc.copy(consumer = consumer)))
|
||||
}
|
||||
case ParamFailure(a, b, c, apiFailure : APIFailure) =>
|
||||
logger.debug(s"applyIdTokenRules - ParamFailure during token validation: $a")
|
||||
logger.debug(s"applyIdTokenRules - JWT issuer was: '$actualIssuer'")
|
||||
(ParamFailure(a, b, c, apiFailure : APIFailure), Some(cc))
|
||||
case Failure(msg, t, c) =>
|
||||
logger.debug(s"applyIdTokenRules - Failure during token validation: $msg")
|
||||
logger.debug(s"applyIdTokenRules - JWT issuer was: '$actualIssuer'")
|
||||
if (msg.contains("OBP-20208")) {
|
||||
logger.debug("applyIdTokenRules - OBP-20208: JWKS URI matching failed. Diagnostic info:")
|
||||
logger.debug(s"applyIdTokenRules - Actual JWT issuer: '$actualIssuer'")
|
||||
logger.debug(s"applyIdTokenRules - oauth2.jwk_set.url config: '${Constant.oauth2JwkSetUrl}'")
|
||||
logger.debug("applyIdTokenRules - Resolution steps:")
|
||||
logger.debug("1. Verify oauth2.jwk_set.url contains URLs that match the JWT issuer")
|
||||
logger.debug("2. Check if JWT issuer claim matches expected identity provider")
|
||||
logger.debug("3. Ensure case-insensitive substring matching works between issuer and JWKS URLs")
|
||||
logger.debug("4. Consider if trailing slashes or URL formatting might be causing mismatch")
|
||||
}
|
||||
(Failure(msg, t, c), Some(cc))
|
||||
case _ =>
|
||||
logger.debug("applyIdTokenRules - Unknown failure during token validation")
|
||||
logger.debug(s"applyIdTokenRules - JWT issuer was: '$actualIssuer'")
|
||||
(Failure(Oauth2IJwtCannotBeVerified), Some(cc))
|
||||
}
|
||||
}
|
||||
@ -519,7 +627,7 @@ object OAuth2Login extends RestHelper with MdcLoggable {
|
||||
*/
|
||||
override def wellKnownOpenidConfiguration: URI =
|
||||
new URI(
|
||||
APIUtil.getPropsValue(nameOfProperty = "oauth2.keycloak.well_known", "http://localhost:7070/realms/master/.well-known/openid-configuration")
|
||||
APIUtil.getPropsValue(nameOfProperty = "oauth2.keycloak.well_known", "http://localhost:8000/realms/master/.well-known/openid-configuration")
|
||||
)
|
||||
override def urlOfJwkSets: Box[String] = checkUrlOfJwkSets(identityProvider = keycloakHost)
|
||||
def isIssuer(jwt: String): Boolean = isIssuer(jwtToken=jwt, identityProvider = keycloakHost)
|
||||
|
||||
@ -29,6 +29,7 @@ import com.openbankproject.commons.model._
|
||||
import com.openbankproject.commons.model.enums.TransactionRequestTypes._
|
||||
import com.openbankproject.commons.model.enums.{AttributeCategory, CardAttributeType, ChallengeType, TransactionRequestStatus}
|
||||
import com.openbankproject.commons.util.{ApiVersion, FieldNameApiVersions, ReflectUtils}
|
||||
import net.liftweb.common.Full
|
||||
import net.liftweb.json
|
||||
|
||||
import java.net.URLEncoder
|
||||
@ -4255,6 +4256,8 @@ object SwaggerDefinitionsJSON {
|
||||
consent_reference_id = consentReferenceIdExample.value,
|
||||
consumer_id = consumerIdExample.value,
|
||||
created_by_user_id = userIdExample.value,
|
||||
provider = Some(providerValueExample.value),
|
||||
provider_id = Some(providerIdExample.value),
|
||||
last_action_date = dateExample.value,
|
||||
last_usage_date = dateTimeExample.value,
|
||||
status = ConsentStatus.INITIATED.toString,
|
||||
@ -4282,7 +4285,7 @@ object SwaggerDefinitionsJSON {
|
||||
consents = List(consentInfoJsonV510)
|
||||
)
|
||||
|
||||
lazy val consentsJsonV510 = ConsentsJsonV510(List(allConsentJsonV510))
|
||||
lazy val consentsJsonV510 = ConsentsJsonV510(number_of_rows = 1, List(allConsentJsonV510))
|
||||
|
||||
lazy val revokedConsentJsonV310 = ConsentJsonV310(
|
||||
consent_id = "9d429899-24f5-42c8-8565-943ffa6a7945",
|
||||
|
||||
@ -36,10 +36,10 @@ object BgSpecValidation {
|
||||
|
||||
if (date.isBefore(today)) {
|
||||
Left(s"$InvalidDateFormat The `validUntil` date ($dateStr) cannot be in the past!")
|
||||
} else if (date.isEqual(MaxValidDays) || date.isAfter(MaxValidDays)) {
|
||||
} else if (date.isAfter(MaxValidDays)) {
|
||||
Left(s"$InvalidDateFormat The `validUntil` date ($dateStr) exceeds the maximum allowed period of 180 days (until $MaxValidDays).")
|
||||
} else {
|
||||
Right(date) // Valid date
|
||||
Right(date) // Valid date (inclusive of 180 days)
|
||||
}
|
||||
} catch {
|
||||
case _: DateTimeParseException =>
|
||||
@ -55,23 +55,4 @@ object BgSpecValidation {
|
||||
}
|
||||
}
|
||||
|
||||
// Example usage
|
||||
def main(args: Array[String]): Unit = {
|
||||
val testDates = Seq(
|
||||
"2025-05-10", // More than 180 days ahead
|
||||
"9999-12-31", // Exceeds max allowed
|
||||
"2015-01-01", // In the past
|
||||
"invalid-date", // Invalid format
|
||||
LocalDate.now().plusDays(90).toString, // Valid (within 180 days)
|
||||
LocalDate.now().plusDays(180).toString, // Valid (exactly 180 days)
|
||||
LocalDate.now().plusDays(181).toString // More than 180 days
|
||||
)
|
||||
|
||||
testDates.foreach { date =>
|
||||
validateValidUntil(date) match {
|
||||
case Right(validDate) => println(s"Valid date: $validDate")
|
||||
case Left(error) => println(s"Error: $error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -491,16 +491,16 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{
|
||||
val in: Boolean = !out
|
||||
|
||||
val isIban = transaction.bankAccount.flatMap(_.accountRoutingScheme.map(_.toUpperCase == "IBAN")).getOrElse(false)
|
||||
// Creditor
|
||||
val creditorName = if(in) transaction.otherBankAccount.map(_.label.display) else None
|
||||
val creditorAccountIban = if(in) {
|
||||
// Creditor - when Direction is OUT
|
||||
val creditorName = if(out) transaction.otherBankAccount.map(_.label.display) else None
|
||||
val creditorAccountIban = if(out) {
|
||||
val creditorIban = if(isIban) transaction.otherBankAccount.map(_.iban.getOrElse("")) else Some("")
|
||||
Some(BgTransactionAccountJson(iban = creditorIban))
|
||||
} else None
|
||||
|
||||
// Debtor
|
||||
val debtorName = if(out) transaction.bankAccount.map(_.label.getOrElse("")) else None
|
||||
val debtorAccountIban = if(out) {
|
||||
// Debtor - when direction is IN
|
||||
val debtorName = if(in) transaction.bankAccount.map(_.label.getOrElse("")) else None
|
||||
val debtorAccountIban = if(in) {
|
||||
val debtorIban = if(isIban) transaction.bankAccount.map(_.accountRoutingAddress.getOrElse("")) else Some("")
|
||||
Some(BgTransactionAccountJson(iban = debtorIban))
|
||||
} else None
|
||||
@ -579,8 +579,8 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{
|
||||
currency = Some(bankAccount.currency)
|
||||
)
|
||||
|
||||
val bookedTransactions = transactions.filter(_.status==TransactionRequestStatus.COMPLETED.toString).map(transaction => createTransactionJSON(transaction))
|
||||
val pendingTransactions = transactions.filter(_.status!=TransactionRequestStatus.COMPLETED.toString).map(transaction => createTransactionJSON(transaction))
|
||||
val bookedTransactions = transactions.filter(_.status==Some(TransactionRequestStatus.COMPLETED.toString)).map(transaction => createTransactionJSON(transaction))
|
||||
val pendingTransactions = transactions.filter(_.status!=Some(TransactionRequestStatus.COMPLETED.toString)).map(transaction => createTransactionJSON(transaction))
|
||||
logger.debug(s"createTransactionsJson.bookedTransactions = $bookedTransactions")
|
||||
logger.debug(s"createTransactionsJson.pendingTransactions = $pendingTransactions")
|
||||
|
||||
|
||||
@ -57,6 +57,22 @@ object Redis extends MdcLoggable {
|
||||
|
||||
def jedisPoolDestroy: Unit = jedisPool.destroy()
|
||||
|
||||
def isRedisReady: Boolean = {
|
||||
var jedisConnection: Option[Jedis] = None
|
||||
try {
|
||||
jedisConnection = Some(jedisPool.getResource)
|
||||
val pong = jedisConnection.get.ping() // sends PING command
|
||||
pong == "PONG"
|
||||
} catch {
|
||||
case e: Throwable =>
|
||||
logger.error(s"Redis is not ready: ${e.getMessage}")
|
||||
false
|
||||
} finally {
|
||||
jedisConnection.foreach(_.close())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private def configureSslContext(): SSLContext = {
|
||||
|
||||
// Load the CA certificate
|
||||
|
||||
339
obp-api/src/main/scala/code/api/cache/RedisLogger.scala
vendored
Normal file
339
obp-api/src/main/scala/code/api/cache/RedisLogger.scala
vendored
Normal file
@ -0,0 +1,339 @@
|
||||
package code.api.cache
|
||||
|
||||
import code.api.util.ApiRole._
|
||||
import code.api.util.{APIUtil, ApiRole}
|
||||
|
||||
import net.liftweb.common.{Box, Empty, Failure => LiftFailure, Full, Logger}
|
||||
import redis.clients.jedis.{Jedis, Pipeline}
|
||||
|
||||
import java.util.concurrent.{Executors, ScheduledThreadPoolExecutor, TimeUnit}
|
||||
import java.util.concurrent.atomic.{AtomicBoolean, AtomicLong}
|
||||
import scala.collection.JavaConverters._
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/**
|
||||
* Redis queue configuration per log level.
|
||||
*/
|
||||
case class RedisLogConfig(
|
||||
queueName: String,
|
||||
maxEntries: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* Simple Redis FIFO log writer.
|
||||
*/
|
||||
object RedisLogger {
|
||||
|
||||
private val logger = Logger(RedisLogger.getClass)
|
||||
|
||||
// Performance and reliability improvements
|
||||
private val redisLoggingEnabled = APIUtil.getPropsAsBoolValue("redis_logging_enabled", false)
|
||||
private val batchSize = APIUtil.getPropsAsIntValue("redis_logging_batch_size", 100)
|
||||
private val flushIntervalMs = APIUtil.getPropsAsIntValue("redis_logging_flush_interval_ms", 1000)
|
||||
private val maxRetries = APIUtil.getPropsAsIntValue("redis_logging_max_retries", 3)
|
||||
private val circuitBreakerThreshold = APIUtil.getPropsAsIntValue("redis_logging_circuit_breaker_threshold", 10)
|
||||
|
||||
// Circuit breaker state
|
||||
private val consecutiveFailures = new AtomicLong(0)
|
||||
private val circuitBreakerOpen = new AtomicBoolean(false)
|
||||
private var lastFailureTime = 0L
|
||||
|
||||
// Async executor for Redis operations
|
||||
private val redisExecutor: ScheduledThreadPoolExecutor = Executors.newScheduledThreadPool(
|
||||
APIUtil.getPropsAsIntValue("redis_logging_thread_pool_size", 2)
|
||||
).asInstanceOf[ScheduledThreadPoolExecutor]
|
||||
private implicit val redisExecutionContext: ExecutionContext = ExecutionContext.fromExecutor(redisExecutor)
|
||||
|
||||
// Batch logging support
|
||||
private val logBuffer = new java.util.concurrent.ConcurrentLinkedQueue[LogEntry]()
|
||||
|
||||
case class LogEntry(level: LogLevel.LogLevel, message: String, timestamp: Long = System.currentTimeMillis())
|
||||
|
||||
// Start background flusher
|
||||
startBackgroundFlusher()
|
||||
|
||||
/**
|
||||
* Redis-backed logging utilities for OBP.
|
||||
*/
|
||||
object LogLevel extends Enumeration {
|
||||
type LogLevel = Value
|
||||
val TRACE, DEBUG, INFO, WARNING, ERROR, ALL = Value
|
||||
|
||||
/** Parse a string into LogLevel, throw if unknown */
|
||||
def valueOf(str: String): LogLevel = str.toUpperCase match {
|
||||
case "TRACE" => TRACE
|
||||
case "DEBUG" => DEBUG
|
||||
case "INFO" => INFO
|
||||
case "WARN" | "WARNING" => WARNING
|
||||
case "ERROR" => ERROR
|
||||
case "ALL" => ALL
|
||||
case other => throw new IllegalArgumentException(s"Invalid log level: $other")
|
||||
}
|
||||
|
||||
/** Map a LogLevel to its required entitlements */
|
||||
def requiredRoles(level: LogLevel): List[ApiRole] = level match {
|
||||
case TRACE => List(canGetTraceLevelLogsAtAllBanks, canGetAllLevelLogsAtAllBanks)
|
||||
case DEBUG => List(canGetDebugLevelLogsAtAllBanks, canGetAllLevelLogsAtAllBanks)
|
||||
case INFO => List(canGetInfoLevelLogsAtAllBanks, canGetAllLevelLogsAtAllBanks)
|
||||
case WARNING => List(canGetWarningLevelLogsAtAllBanks, canGetAllLevelLogsAtAllBanks)
|
||||
case ERROR => List(canGetErrorLevelLogsAtAllBanks, canGetAllLevelLogsAtAllBanks)
|
||||
case ALL => List(canGetAllLevelLogsAtAllBanks)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Define FIFO queues, sizes configurable via props
|
||||
val configs: Map[LogLevel.Value, RedisLogConfig] = Map(
|
||||
LogLevel.TRACE -> RedisLogConfig("obp_trace_logs", APIUtil.getPropsAsIntValue("redis_logging_trace_queue_max_entries", 1000)),
|
||||
LogLevel.DEBUG -> RedisLogConfig("obp_debug_logs", APIUtil.getPropsAsIntValue("redis_logging_debug_queue_max_entries", 1000)),
|
||||
LogLevel.INFO -> RedisLogConfig("obp_info_logs", APIUtil.getPropsAsIntValue("redis_logging_info_queue_max_entries", 1000)),
|
||||
LogLevel.WARNING -> RedisLogConfig("obp_warning_logs", APIUtil.getPropsAsIntValue("redis_logging_warning_queue_max_entries", 1000)),
|
||||
LogLevel.ERROR -> RedisLogConfig("obp_error_logs", APIUtil.getPropsAsIntValue("redis_logging_error_queue_max_entries", 1000)),
|
||||
LogLevel.ALL -> RedisLogConfig("obp_all_logs", APIUtil.getPropsAsIntValue("redis_logging_all_queue_max_entries", 1000))
|
||||
)
|
||||
|
||||
/**
|
||||
* Synchronous log (blocking until Redis writes are done).
|
||||
*/
|
||||
def logSync(level: LogLevel.LogLevel, message: String): Try[Unit] = {
|
||||
if (!redisLoggingEnabled || circuitBreakerOpen.get()) {
|
||||
return Success(()) // Skip if disabled or circuit breaker is open
|
||||
}
|
||||
|
||||
var attempt = 0
|
||||
var lastException: Throwable = null
|
||||
|
||||
while (attempt < maxRetries) {
|
||||
try {
|
||||
withPipeline { pipeline =>
|
||||
// log to requested level
|
||||
configs.get(level).foreach(cfg => pushLog(pipeline, cfg, message))
|
||||
// also log to ALL
|
||||
configs.get(LogLevel.ALL).foreach(cfg => pushLog(pipeline, cfg, s"[$level] $message"))
|
||||
pipeline.sync()
|
||||
}
|
||||
|
||||
// Reset circuit breaker on success
|
||||
consecutiveFailures.set(0)
|
||||
circuitBreakerOpen.set(false)
|
||||
return Success(())
|
||||
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
lastException = e
|
||||
attempt += 1
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
Thread.sleep(100 * attempt) // Exponential backoff
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle circuit breaker
|
||||
val failures = consecutiveFailures.incrementAndGet()
|
||||
if (failures >= circuitBreakerThreshold) {
|
||||
circuitBreakerOpen.set(true)
|
||||
lastFailureTime = System.currentTimeMillis()
|
||||
logger.warn(s"Redis logging circuit breaker opened after $failures consecutive failures")
|
||||
}
|
||||
|
||||
Failure(lastException)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronous log with batching support (fire-and-forget).
|
||||
* Returns a Future[Unit], failures are handled gracefully.
|
||||
*/
|
||||
def logAsync(level: LogLevel.LogLevel, message: String): Future[Unit] = {
|
||||
if (!redisLoggingEnabled) {
|
||||
return Future.successful(())
|
||||
}
|
||||
|
||||
// Add to batch buffer for better performance
|
||||
logBuffer.offer(LogEntry(level, message))
|
||||
|
||||
// If buffer is full, flush immediately
|
||||
if (logBuffer.size() >= batchSize) {
|
||||
Future {
|
||||
flushLogBuffer()
|
||||
}(redisExecutionContext).recover {
|
||||
case e => logger.debug(s"RedisLogger batch flush failed: ${e.getMessage}")
|
||||
}
|
||||
} else {
|
||||
Future.successful(())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediate async log without batching for critical messages.
|
||||
*/
|
||||
def logAsyncImmediate(level: LogLevel.LogLevel, message: String): Future[Unit] = {
|
||||
Future {
|
||||
logSync(level, message) match {
|
||||
case Success(_) => // ok
|
||||
case Failure(e) => logger.debug(s"RedisLogger immediate async failed: ${e.getMessage}")
|
||||
}
|
||||
}(redisExecutionContext)
|
||||
}
|
||||
|
||||
private def withPipeline(block: Pipeline => Unit): Unit = {
|
||||
Option(Redis.jedisPool).foreach { pool =>
|
||||
val jedis = pool.getResource()
|
||||
try {
|
||||
val pipeline: Pipeline = jedis.pipelined()
|
||||
block(pipeline)
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
logger.debug(s"Redis pipeline operation failed: ${e.getMessage}")
|
||||
throw e
|
||||
} finally {
|
||||
if (jedis != null) {
|
||||
jedis.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def flushLogBuffer(): Unit = {
|
||||
if (logBuffer.isEmpty || circuitBreakerOpen.get()) {
|
||||
return
|
||||
}
|
||||
|
||||
val entriesToFlush = new java.util.ArrayList[LogEntry]()
|
||||
var entry = logBuffer.poll()
|
||||
while (entry != null && entriesToFlush.size() < batchSize) {
|
||||
entriesToFlush.add(entry)
|
||||
entry = logBuffer.poll()
|
||||
}
|
||||
|
||||
if (!entriesToFlush.isEmpty) {
|
||||
try {
|
||||
withPipeline { pipeline =>
|
||||
entriesToFlush.asScala.foreach { logEntry =>
|
||||
configs.get(logEntry.level).foreach(cfg => pushLog(pipeline, cfg, logEntry.message))
|
||||
configs.get(LogLevel.ALL).foreach(cfg => pushLog(pipeline, cfg, s"[${logEntry.level}] ${logEntry.message}"))
|
||||
}
|
||||
pipeline.sync()
|
||||
}
|
||||
|
||||
// Reset circuit breaker on success
|
||||
consecutiveFailures.set(0)
|
||||
circuitBreakerOpen.set(false)
|
||||
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
val failures = consecutiveFailures.incrementAndGet()
|
||||
if (failures >= circuitBreakerThreshold) {
|
||||
circuitBreakerOpen.set(true)
|
||||
lastFailureTime = System.currentTimeMillis()
|
||||
logger.warn(s"Redis logging circuit breaker opened after batch flush failure")
|
||||
}
|
||||
logger.debug(s"Redis batch flush failed: ${e.getMessage}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def startBackgroundFlusher(): Unit = {
|
||||
val flusher = new Runnable {
|
||||
override def run(): Unit = {
|
||||
try {
|
||||
// Check if circuit breaker should be reset (after 60 seconds)
|
||||
if (circuitBreakerOpen.get() && System.currentTimeMillis() - lastFailureTime > 60000) {
|
||||
circuitBreakerOpen.set(false)
|
||||
consecutiveFailures.set(0)
|
||||
logger.info("Redis logging circuit breaker reset")
|
||||
}
|
||||
|
||||
flushLogBuffer()
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
logger.debug(s"Background log flusher failed: ${e.getMessage}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
redisExecutor.scheduleAtFixedRate(
|
||||
flusher,
|
||||
flushIntervalMs,
|
||||
flushIntervalMs,
|
||||
TimeUnit.MILLISECONDS
|
||||
)
|
||||
}
|
||||
|
||||
private def pushLog(pipeline: Pipeline, cfg: RedisLogConfig, msg: String): Unit = {
|
||||
if (cfg.maxEntries > 0) {
|
||||
pipeline.lpush(cfg.queueName, msg)
|
||||
pipeline.ltrim(cfg.queueName, 0, cfg.maxEntries - 1)
|
||||
}
|
||||
}
|
||||
|
||||
case class LogTailEntry(level: String, message: String)
|
||||
case class LogTail(entries: List[LogTailEntry])
|
||||
|
||||
private val LogPattern = """\[(\w+)\]\s+(.*)""".r
|
||||
|
||||
/**
|
||||
* Read latest messages from Redis FIFO queue.
|
||||
*/
|
||||
def getLogTail(level: LogLevel.LogLevel): LogTail = {
|
||||
Option(Redis.jedisPool).map { pool =>
|
||||
val jedis = pool.getResource()
|
||||
try {
|
||||
val cfg = configs(level)
|
||||
val rawLogs = jedis.lrange(cfg.queueName, 0, -1).asScala.toList
|
||||
|
||||
val entries: List[LogTailEntry] = level match {
|
||||
case LogLevel.ALL =>
|
||||
rawLogs.collect {
|
||||
case LogPattern(lvl, msg) => LogTailEntry(lvl, msg)
|
||||
}
|
||||
case other =>
|
||||
rawLogs.map(msg => LogTailEntry(other.toString, msg))
|
||||
}
|
||||
|
||||
LogTail(entries)
|
||||
} finally {
|
||||
jedis.close()
|
||||
}
|
||||
}.getOrElse(LogTail(Nil))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Redis logging statistics
|
||||
*/
|
||||
def getStats: Map[String, Any] = Map(
|
||||
"redisLoggingEnabled" -> redisLoggingEnabled,
|
||||
"circuitBreakerOpen" -> circuitBreakerOpen.get(),
|
||||
"consecutiveFailures" -> consecutiveFailures.get(),
|
||||
"bufferSize" -> logBuffer.size(),
|
||||
"batchSize" -> batchSize,
|
||||
"flushIntervalMs" -> flushIntervalMs,
|
||||
"threadPoolActiveCount" -> redisExecutor.getActiveCount,
|
||||
"threadPoolQueueSize" -> redisExecutor.getQueue.size()
|
||||
)
|
||||
|
||||
/**
|
||||
* Shutdown the Redis logger gracefully
|
||||
*/
|
||||
def shutdown(): Unit = {
|
||||
logger.info("Shutting down Redis logger...")
|
||||
|
||||
// Flush remaining logs
|
||||
flushLogBuffer()
|
||||
|
||||
// Shutdown executor
|
||||
redisExecutor.shutdown()
|
||||
try {
|
||||
if (!redisExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
redisExecutor.shutdownNow()
|
||||
}
|
||||
} catch {
|
||||
case _: InterruptedException =>
|
||||
redisExecutor.shutdownNow()
|
||||
}
|
||||
|
||||
logger.info("Redis logger shutdown complete")
|
||||
}
|
||||
}
|
||||
@ -329,7 +329,8 @@ object Constant extends MdcLoggable {
|
||||
CAN_SEE_BANK_ACCOUNT_LABEL,
|
||||
CAN_SEE_BANK_ACCOUNT_BALANCE,
|
||||
CAN_SEE_BANK_ACCOUNT_ROUTING_ADDRESS,
|
||||
CAN_SEE_BANK_ACCOUNT_CURRENCY
|
||||
CAN_SEE_BANK_ACCOUNT_CURRENCY,
|
||||
CAN_SEE_TRANSACTION_STATUS
|
||||
)
|
||||
|
||||
final val SYSTEM_VIEW_PERMISSION_COMMON = List(
|
||||
|
||||
@ -903,7 +903,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
}
|
||||
}
|
||||
|
||||
/** only A-Z, a-z, 0-9, -, _, ., and max length <= 16 */
|
||||
/** only A-Z, a-z, 0-9, -, _, ., and max length <= 16. NOTE: This function requires at least ONE character (+ in the regx). If you want to accept zero characters use checkOptionalShortString. */
|
||||
def checkShortString(value:String): String ={
|
||||
val valueLength = value.length
|
||||
val regex = """^([A-Za-z0-9\-._]+)$""".r
|
||||
@ -914,6 +914,18 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
}
|
||||
}
|
||||
|
||||
/** only A-Z, a-z, 0-9, -, _, ., and max length <= 16, allows empty string */
|
||||
def checkOptionalShortString(value:String): String ={
|
||||
val valueLength = value.length
|
||||
val regex = """^([A-Za-z0-9\-._]*)$""".r
|
||||
value match {
|
||||
case regex(e) if(valueLength <= 16) => SILENCE_IS_GOLDEN
|
||||
case regex(e) if(valueLength > 16) => ErrorMessages.InvalidValueLength
|
||||
case _ => ErrorMessages.InvalidValueCharacters
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** only A-Z, a-z, 0-9, -, _, ., and max length <= 36
|
||||
* OBP APIUtil.generateUUID() length is 36 here.*/
|
||||
@ -2940,8 +2952,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
val errorResponse = getFilteredOrFullErrorMessage(e)
|
||||
Full(reply.apply(errorResponse))
|
||||
case Failure(msg, e, _) =>
|
||||
surroundErrorMessage(msg)
|
||||
e.foreach(logger.debug("", _))
|
||||
e.foreach(logger.error(msg, _))
|
||||
extractAPIFailureNewStyle(msg) match {
|
||||
case Some(af) =>
|
||||
val callContextLight = af.ccl.map(_.copy(httpCode = Some(af.failCode)))
|
||||
@ -2999,6 +3010,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
|
||||
|
||||
/**
|
||||
* TODO: Update this Doc string:
|
||||
* This function is planed to be used at an endpoint in order to get a User based on Authorization Header data
|
||||
* It has to do the same thing as function OBPRestHelper.failIfBadAuthorizationHeader does
|
||||
* The only difference is that this function use Akka's Future in non-blocking way i.e. without using Await.result
|
||||
@ -3016,8 +3028,8 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
val xRequestId: Option[String] =
|
||||
reqHeaders.find(_.name.toLowerCase() == RequestHeader.`X-Request-ID`.toLowerCase())
|
||||
.map(_.values.mkString(","))
|
||||
val title = s"Request Headers for verb: $verb, URL: $url"
|
||||
surroundDebugMessage(reqHeaders.map(h => h.name + ": " + h.values.mkString(",")).mkString, title)
|
||||
logger.debug(s"Request Headers for verb: $verb, URL: $url")
|
||||
logger.debug(reqHeaders.map(h => h.name + ": " + h.values.mkString(",")).mkString)
|
||||
val remoteIpAddress = getRemoteIpAddress()
|
||||
|
||||
val authHeaders = AuthorisationUtil.getAuthorisationHeaders(reqHeaders)
|
||||
@ -3032,7 +3044,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
// Step 1: Always attempt to identify consumer via certificate/mTLS
|
||||
// This looks for TPP-Signature-Certificate or PSD2-CERT headers, or mTLS client certificates
|
||||
val consumerByCertificate = Consent.getCurrentConsumerViaTppSignatureCertOrMtls(callContext = cc)
|
||||
logger.debug(s"consumerByCertificate: $consumerByCertificate")
|
||||
logger.debug(s"getUserAndSessionContextFuture says consumerByCertificate is: $consumerByCertificate")
|
||||
|
||||
// Step 2: Check which validation method is configured for consent requests
|
||||
// Default is CONSUMER_CERTIFICATE (certificate-based validation)
|
||||
@ -3058,7 +3070,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
// This is normal for certificate-based validation or anonymous requests
|
||||
Empty
|
||||
}
|
||||
logger.debug(s"consumerByConsumerKey: $consumerByConsumerKey")
|
||||
logger.debug(s"getUserAndSessionContextFuture says consumerByConsumerKey is: $consumerByConsumerKey")
|
||||
|
||||
val res =
|
||||
if (authHeadersWithEmptyValues.nonEmpty) { // Check Authorization Headers Empty Values
|
||||
@ -3200,8 +3212,10 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
|
||||
// COMMON POST AUTHENTICATION CODE GOES BELOW
|
||||
|
||||
// Check is it Consumer disabled
|
||||
val consumerIsDisabled: Future[(Box[User], Option[CallContext])] = AfterApiAuth.checkConsumerIsDisabled(res)
|
||||
// Check is it a user deleted or locked
|
||||
val userIsLockedOrDeleted: Future[(Box[User], Option[CallContext])] = AfterApiAuth.checkUserIsDeletedOrLocked(res)
|
||||
val userIsLockedOrDeleted: Future[(Box[User], Option[CallContext])] = AfterApiAuth.checkUserIsDeletedOrLocked(consumerIsDisabled)
|
||||
// Check Rate Limiting
|
||||
val resultWithRateLimiting: Future[(Box[User], Option[CallContext])] = AfterApiAuth.checkRateLimiting(userIsLockedOrDeleted)
|
||||
// User init actions
|
||||
@ -4003,18 +4017,22 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
val consumerName = cc.flatMap(_.consumer.map(_.name.get)).getOrElse("")
|
||||
val certificate = getCertificateFromTppSignatureCertificate(requestHeaders)
|
||||
for {
|
||||
tpp <- BerlinGroupSigning.getTppByCertificate(certificate, cc)
|
||||
tpps <- BerlinGroupSigning.getRegulatedEntityByCertificate(certificate, cc)
|
||||
} yield {
|
||||
if (tpp.nonEmpty) {
|
||||
val berlinGroupRole = PemCertificateRole.toBerlinGroup(serviceProvider)
|
||||
val hasRole = tpp.exists(_.services.contains(berlinGroupRole))
|
||||
if (hasRole) {
|
||||
Full(true)
|
||||
} else {
|
||||
Failure(X509ActionIsNotAllowed)
|
||||
}
|
||||
} else {
|
||||
Failure("No valid Tpp")
|
||||
tpps match {
|
||||
case Nil =>
|
||||
Failure(RegulatedEntityNotFoundByCertificate)
|
||||
case single :: Nil =>
|
||||
// Only one match, proceed to role check
|
||||
if (single.services.contains(serviceProvider)) {
|
||||
Full(true)
|
||||
} else {
|
||||
Failure(X509ActionIsNotAllowed)
|
||||
}
|
||||
case multiple =>
|
||||
// Ambiguity detected: more than one TPP matches the certificate
|
||||
val names = multiple.map(e => s"'${e.entityName}' (Code: ${e.entityCode})").mkString(", ")
|
||||
Failure(s"$RegulatedEntityAmbiguityByCertificate: multiple TPPs found: $names")
|
||||
}
|
||||
}
|
||||
case value if value.toUpperCase == "CERTIFICATE" => Future {
|
||||
|
||||
@ -6,7 +6,7 @@ import code.accountholders.AccountHolders
|
||||
import code.api.Constant
|
||||
import code.api.util.APIUtil.getPropsAsBoolValue
|
||||
import code.api.util.ApiRole.{CanCreateAccount, CanCreateHistoricalTransactionAtBank}
|
||||
import code.api.util.ErrorMessages.{UserIsDeleted, UsernameHasBeenLocked}
|
||||
import code.api.util.ErrorMessages.{ConsumerIsDisabled, UserIsDeleted, UsernameHasBeenLocked}
|
||||
import code.api.util.RateLimitingJson.CallLimit
|
||||
import code.bankconnectors.{Connector, LocalMappedConnectorInternal}
|
||||
import code.entitlement.Entitlement
|
||||
@ -78,6 +78,18 @@ object AfterApiAuth extends MdcLoggable{
|
||||
}
|
||||
}
|
||||
}
|
||||
def checkConsumerIsDisabled(res: Future[(Box[User], Option[CallContext])]): Future[(Box[User], Option[CallContext])] = {
|
||||
for {
|
||||
(user: Box[User], cc) <- res
|
||||
} yield {
|
||||
cc.map(_.consumer) match {
|
||||
case Some(Full(consumer)) if !consumer.isActive.get => // There is a consumer. Check it.
|
||||
(Failure(ConsumerIsDisabled), cc) // The Consumer is DISABLED.
|
||||
case _ => // There is no Consumer. Just forward the result.
|
||||
(user, cc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This block of code needs to update Call Context with Rate Limiting
|
||||
|
||||
@ -107,6 +107,38 @@ object ApiRole extends MdcLoggable{
|
||||
|
||||
case class CanCreateCustomer(requiresBankId: Boolean = true) extends ApiRole
|
||||
lazy val canCreateCustomer = CanCreateCustomer()
|
||||
|
||||
|
||||
// TRACE
|
||||
case class CanGetTraceLevelLogsAtOneBank(requiresBankId: Boolean = true) extends ApiRole
|
||||
lazy val canGetTraceLevelLogsAtOneBank = CanGetTraceLevelLogsAtOneBank()
|
||||
case class CanGetTraceLevelLogsAtAllBanks(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canGetTraceLevelLogsAtAllBanks = CanGetTraceLevelLogsAtAllBanks()
|
||||
// DEBUG
|
||||
case class CanGetDebugLevelLogsAtOneBank(requiresBankId: Boolean = true) extends ApiRole
|
||||
lazy val canGetDebugLevelLogsAtOneBank = CanGetDebugLevelLogsAtOneBank()
|
||||
case class CanGetDebugLevelLogsAtAllBanks(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canGetDebugLevelLogsAtAllBanks = CanGetDebugLevelLogsAtAllBanks()
|
||||
// INFO
|
||||
case class CanGetInfoLevelLogsAtOneBank(requiresBankId: Boolean = true) extends ApiRole
|
||||
lazy val canGetInfoLevelLogsAtOneBank = CanGetInfoLevelLogsAtOneBank()
|
||||
case class CanGetInfoLevelLogsAtAllBanks(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canGetInfoLevelLogsAtAllBanks = CanGetInfoLevelLogsAtAllBanks()
|
||||
// WARNING
|
||||
case class CanGetWarningLevelLogsAtOneBank(requiresBankId: Boolean = true) extends ApiRole
|
||||
lazy val canGetWarningLevelLogsAtOneBank = CanGetWarningLevelLogsAtOneBank()
|
||||
case class CanGetWarningLevelLogsAtAllBanks(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canGetWarningLevelLogsAtAllBanks = CanGetWarningLevelLogsAtAllBanks()
|
||||
// ERROR
|
||||
case class CanGetErrorLevelLogsAtOneBank(requiresBankId: Boolean = true) extends ApiRole
|
||||
lazy val canGetErrorLevelLogsAtOneBank = CanGetErrorLevelLogsAtOneBank()
|
||||
case class CanGetErrorLevelLogsAtAllBanks(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canGetErrorLevelLogsAtAllBanks = CanGetErrorLevelLogsAtAllBanks()
|
||||
// ALL
|
||||
case class CanGetAllLevelLogsAtOneBank(requiresBankId: Boolean = true) extends ApiRole
|
||||
lazy val canGetAllLevelLogsAtOneBank = CanGetAllLevelLogsAtOneBank()
|
||||
case class CanGetAllLevelLogsAtAllBanks(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canGetAllLevelLogsAtAllBanks = CanGetAllLevelLogsAtAllBanks()
|
||||
|
||||
case class CanUpdateAgentStatusAtAnyBank(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canUpdateAgentStatusAtAnyBank = CanUpdateAgentStatusAtAnyBank()
|
||||
|
||||
@ -46,6 +46,9 @@ object BerlinGroupError {
|
||||
*/
|
||||
def translateToBerlinGroupError(code: String, message: String): String = {
|
||||
code match {
|
||||
// If this error occurs it implies that its error handling MUST be refined in OBP code
|
||||
case "400" if message.contains("OBP-50005") => "INTERNAL_ERROR"
|
||||
|
||||
case "401" if message.contains("OBP-20001") => "PSU_CREDENTIALS_INVALID"
|
||||
case "401" if message.contains("OBP-20201") => "PSU_CREDENTIALS_INVALID"
|
||||
case "401" if message.contains("OBP-20214") => "PSU_CREDENTIALS_INVALID"
|
||||
|
||||
@ -8,7 +8,7 @@ import code.consumer.Consumers
|
||||
import code.model.Consumer
|
||||
import code.util.Helper.MdcLoggable
|
||||
import com.openbankproject.commons.ExecutionContext.Implicits.global
|
||||
import com.openbankproject.commons.model.{RegulatedEntityTrait, User}
|
||||
import com.openbankproject.commons.model.{RegulatedEntityAttributeSimple, RegulatedEntityTrait, User}
|
||||
import net.liftweb.common.{Box, Empty, Failure, Full}
|
||||
import net.liftweb.http.provider.HTTPParam
|
||||
import net.liftweb.util.Helpers
|
||||
@ -111,29 +111,40 @@ object BerlinGroupSigning extends MdcLoggable {
|
||||
certificate
|
||||
}
|
||||
|
||||
def getTppByCertificate(certificate: X509Certificate, callContext: Option[CallContext]): Future[List[RegulatedEntityTrait]] = {
|
||||
// Use the regular expression to find the value of CN
|
||||
val extractedCN = cnPattern.findFirstMatchIn(certificate.getIssuerDN.getName) match {
|
||||
case Some(m) => m.group(1) // Extract the value of CN
|
||||
case None => "CN not found"
|
||||
}
|
||||
val issuerCommonName = extractedCN // Certificate.caCert
|
||||
def getRegulatedEntityByCertificate(certificate: X509Certificate, callContext: Option[CallContext]): Future[List[RegulatedEntityTrait]] = {
|
||||
val issuerCN = cnPattern.findFirstMatchIn(certificate.getIssuerDN.getName)
|
||||
.map(_.group(1).trim)
|
||||
.getOrElse("CN not found")
|
||||
|
||||
val serialNumber = certificate.getSerialNumber.toString
|
||||
val regulatedEntities: Future[List[RegulatedEntityTrait]] = for {
|
||||
|
||||
for {
|
||||
(entities, _) <- getRegulatedEntitiesNewStyle(callContext)
|
||||
} yield {
|
||||
logger.debug("Regulated Entities: " + entities)
|
||||
entities.filter { entity =>
|
||||
val hasSerialNumber = entity.attributes.exists(_.exists(a =>
|
||||
a.name == "CERTIFICATE_SERIAL_NUMBER" && a.value == serialNumber
|
||||
))
|
||||
val hasCaName = entity.attributes.exists(_.exists(a =>
|
||||
a.name == "CERTIFICATE_CA_NAME" && a.value == issuerCommonName
|
||||
))
|
||||
hasSerialNumber && hasCaName
|
||||
val attrs = entity.attributes.getOrElse(Nil)
|
||||
|
||||
// Extract serial number and CA name from attributes
|
||||
val serialOpt = attrs.collectFirst { case a if a.name.equalsIgnoreCase("CERTIFICATE_SERIAL_NUMBER") => a.value.trim }
|
||||
val caNameOpt = attrs.collectFirst { case a if a.name.equalsIgnoreCase("CERTIFICATE_CA_NAME") => a.value.trim }
|
||||
|
||||
val serialMatches = serialOpt.contains(serialNumber)
|
||||
val caNameMatches = caNameOpt.exists(_.equalsIgnoreCase(issuerCN))
|
||||
|
||||
val isMatch = serialMatches && caNameMatches
|
||||
|
||||
// Log everything for debugging
|
||||
val serialLog = serialOpt.getOrElse("N/A")
|
||||
val caNameLog = caNameOpt.getOrElse("N/A")
|
||||
val allAttrsLog = attrs.map(a => s"${a.name}='${a.value}'").mkString(", ")
|
||||
|
||||
if (isMatch)
|
||||
logger.debug(s"[MATCH] Entity '${entity.entityName}' (Code: ${entity.entityCode}) matches CN='$issuerCN', Serial='$serialNumber' " +
|
||||
s"(Attributes found: Serial='$serialLog', CA Name='$caNameLog', All Attributes: [$allAttrsLog])")
|
||||
|
||||
isMatch
|
||||
}
|
||||
}
|
||||
regulatedEntities
|
||||
}
|
||||
|
||||
|
||||
@ -280,7 +291,7 @@ object BerlinGroupSigning extends MdcLoggable {
|
||||
}
|
||||
|
||||
for {
|
||||
entities <- getTppByCertificate(certificate, forwardResult._2) // Find TPP via certificate
|
||||
entities <- getRegulatedEntityByCertificate(certificate, forwardResult._2) // Find Regulated Entity via certificate
|
||||
} yield {
|
||||
// Certificate can be changed but this value is permanent per Regulated entity
|
||||
val idno = entities.map(_.entityCode).headOption.getOrElse("")
|
||||
|
||||
@ -127,6 +127,7 @@ object ErrorMessages {
|
||||
val ScaMethodNotDefined = "OBP-10030: Strong customer authentication method is not defined at this instance."
|
||||
|
||||
val createFxCurrencyIssue = "OBP-10050: Cannot create FX currency. "
|
||||
val invalidLogLevel = "OBP-10051: Invalid log level. "
|
||||
|
||||
|
||||
|
||||
@ -579,6 +580,7 @@ object ErrorMessages {
|
||||
val RegulatedEntityNotFound = "OBP-34100: Regulated Entity not found. Please specify a valid value for REGULATED_ENTITY_ID."
|
||||
val RegulatedEntityNotDeleted = "OBP-34101: Regulated Entity cannot be deleted. Please specify a valid value for REGULATED_ENTITY_ID."
|
||||
val RegulatedEntityNotFoundByCertificate = "OBP-34102: Regulated Entity cannot be found by provided certificate."
|
||||
val RegulatedEntityAmbiguityByCertificate = "OBP-34103: More than 1 Regulated Entity found by provided certificate."
|
||||
val PostJsonIsNotSigned = "OBP-34110: JWT at the post json cannot be verified."
|
||||
|
||||
// Consents
|
||||
|
||||
@ -3,16 +3,13 @@ package code.api.util
|
||||
|
||||
import code.api.OAuth2Login.Keycloak
|
||||
import code.model.{AppType, Consumer}
|
||||
import code.util.Helper.MdcLoggable
|
||||
import net.liftweb.common.{Box, Failure, Full}
|
||||
import okhttp3._
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
|
||||
object KeycloakAdmin {
|
||||
|
||||
// Initialize Logback logger
|
||||
private val logger = LoggerFactory.getLogger("okhttp3")
|
||||
object KeycloakAdmin extends MdcLoggable {
|
||||
|
||||
val integrateWithKeycloak: Boolean = APIUtil.getPropsAsBoolValue("integrate_with_keycloak", defaultValue = false)
|
||||
// Define variables (replace with actual values)
|
||||
@ -104,4 +101,4 @@ object KeycloakAdmin {
|
||||
Failure(e.getMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
package code.api.util
|
||||
|
||||
import java.util.UUID
|
||||
import scala.util.Try
|
||||
|
||||
final case class KeycloakFederatedUserReference(
|
||||
prefix: Char,
|
||||
storageProviderId: UUID, // Keycloak component UUID
|
||||
externalId: Long // autoincrement PK in external DB
|
||||
)
|
||||
|
||||
object KeycloakFederatedUserReference {
|
||||
// Pattern: f:<storageProviderId>:<externalId>
|
||||
private val Pattern =
|
||||
"^([A-Za-z]):([0-9a-fA-F-]{8}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{12}):(\\d+)$".r
|
||||
|
||||
/** Safe parser */
|
||||
def parse(s: String): Either[String, KeycloakFederatedUserReference] =
|
||||
s match {
|
||||
case Pattern(p, providerIdStr, externalIdStr) if p == "f" =>
|
||||
for {
|
||||
providerId <- Try(UUID.fromString(providerIdStr))
|
||||
.toEither.left.map(_ => s"Invalid storageProviderId: $providerIdStr")
|
||||
externalId <- Try(externalIdStr.toLong)
|
||||
.toEither.left.map(_ => s"Invalid externalId: $externalIdStr")
|
||||
} yield KeycloakFederatedUserReference('f', providerId, externalId)
|
||||
|
||||
case Pattern(p, _, _) =>
|
||||
Left(s"Invalid prefix: '$p'. Expected 'f'.")
|
||||
|
||||
case _ =>
|
||||
Left("Invalid format. Expected: f:<storageProviderId>:<externalId>")
|
||||
}
|
||||
|
||||
def unsafe(s: String): KeycloakFederatedUserReference =
|
||||
parse(s).fold(err => throw new IllegalArgumentException(err), identity)
|
||||
}
|
||||
@ -153,13 +153,14 @@ object RateLimitingUtil extends MdcLoggable {
|
||||
|
||||
def getInfo(consumerKey: String, period: LimitCallPeriod): ((Option[Long], Option[Long]), LimitCallPeriod) = {
|
||||
val key = createUniqueKey(consumerKey, period)
|
||||
val ttl = Redis.use(JedisMethod.TTL, key).get.toLong
|
||||
ttl match {
|
||||
case -2 =>
|
||||
((None, None), period)
|
||||
case _ =>
|
||||
((Redis.use(JedisMethod.TTL, key).map(_.toLong), Some(ttl)), period)
|
||||
}
|
||||
|
||||
// get TTL
|
||||
val ttlOpt: Option[Long] = Redis.use(JedisMethod.TTL, key).map(_.toLong)
|
||||
|
||||
// get value (assuming string storage)
|
||||
val valueOpt: Option[Long] = Redis.use(JedisMethod.GET, key).map(_.toLong)
|
||||
|
||||
((valueOpt, ttlOpt), period)
|
||||
}
|
||||
|
||||
getInfo(consumerKey, RateLimitingPeriod.PER_SECOND) ::
|
||||
@ -167,7 +168,7 @@ object RateLimitingUtil extends MdcLoggable {
|
||||
getInfo(consumerKey, RateLimitingPeriod.PER_HOUR) ::
|
||||
getInfo(consumerKey, RateLimitingPeriod.PER_DAY) ::
|
||||
getInfo(consumerKey, RateLimitingPeriod.PER_WEEK) ::
|
||||
getInfo(consumerKey, RateLimitingPeriod.PER_MONTH) ::
|
||||
getInfo(consumerKey, RateLimitingPeriod.PER_MONTH) ::
|
||||
Nil
|
||||
}
|
||||
|
||||
|
||||
@ -182,7 +182,7 @@ trait APIMethods500 {
|
||||
}
|
||||
|
||||
//if postJson.id is empty, just return SILENCE_IS_GOLDEN, and will pass the guard.
|
||||
checkShortStringValue = APIUtil.checkShortString(postJson.id.getOrElse(SILENCE_IS_GOLDEN))
|
||||
checkShortStringValue = APIUtil.checkOptionalShortString(postJson.id.getOrElse(SILENCE_IS_GOLDEN))
|
||||
_ <- Helper.booleanToFuture(failMsg = s"$checkShortStringValue.", cc = cc.callContext) {
|
||||
checkShortStringValue == SILENCE_IS_GOLDEN
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import code.api.Constant._
|
||||
import code.api.OAuth2Login.{Keycloak, OBPOIDC}
|
||||
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._
|
||||
import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{ConsentAccessAccountsJson, ConsentAccessJson}
|
||||
import code.api.cache.RedisLogger
|
||||
import code.api.util.APIUtil._
|
||||
import code.api.util.ApiRole._
|
||||
import code.api.util.ApiTag._
|
||||
@ -28,7 +29,7 @@ import code.api.v3_1_0._
|
||||
import code.api.v4_0_0.JSONFactory400.{createAccountBalancesJson, createBalancesJson, createNewCoreBankAccountJson}
|
||||
import code.api.v4_0_0._
|
||||
import code.api.v5_0_0.JSONFactory500
|
||||
import code.api.v5_1_0.JSONFactory510.{createConsentsInfoJsonV510, createConsentsJsonV510, createRegulatedEntitiesJson, createRegulatedEntityJson}
|
||||
import code.api.v5_1_0.JSONFactory510.{createCallLimitJson, createConsentsInfoJsonV510, createConsentsJsonV510, createRegulatedEntitiesJson, createRegulatedEntityJson}
|
||||
import code.atmattribute.AtmAttribute
|
||||
import code.bankconnectors.Connector
|
||||
import code.consent.{ConsentRequests, ConsentStatus, Consents, MappedConsent}
|
||||
@ -38,6 +39,7 @@ import code.loginattempts.LoginAttempt
|
||||
import code.metrics.APIMetrics
|
||||
import code.model.dataAccess.{AuthUser, MappedBankAccount}
|
||||
import code.model.{AppType, Consumer}
|
||||
import code.ratelimiting.{RateLimiting, RateLimitingDI}
|
||||
import code.regulatedentities.MappedRegulatedEntityProvider
|
||||
import code.userlocks.UserLocksProvider
|
||||
import code.users.Users
|
||||
@ -203,6 +205,47 @@ trait APIMethods510 {
|
||||
}
|
||||
}
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
logCacheEndpoint,
|
||||
implementedInApiVersion,
|
||||
nameOf(logCacheEndpoint),
|
||||
"GET",
|
||||
"/dev-ops/log-cache/LOG_LEVEL",
|
||||
"Get Log Cache",
|
||||
"""Returns information about:
|
||||
|
|
||||
|* Log Cache
|
||||
""",
|
||||
EmptyBody,
|
||||
EmptyBody,
|
||||
List($UserNotLoggedIn, UnknownError),
|
||||
apiTagApi :: Nil,
|
||||
Some(List(canGetAllLevelLogsAtAllBanks)))
|
||||
|
||||
lazy val logCacheEndpoint: OBPEndpoint = {
|
||||
case "dev-ops" :: "log-cache" :: logLevel :: Nil JsonGet _ =>
|
||||
cc =>
|
||||
implicit val ec = EndpointContext(Some(cc))
|
||||
for {
|
||||
// Parse and validate log level
|
||||
level <- NewStyle.function.tryons(ErrorMessages.invalidLogLevel, 400, cc.callContext) {
|
||||
RedisLogger.LogLevel.valueOf(logLevel)
|
||||
}
|
||||
// Check entitlements using helper
|
||||
_ <- NewStyle.function.handleEntitlementsAndScopes(
|
||||
bankId = "",
|
||||
userId = cc.userId,
|
||||
roles = RedisLogger.LogLevel.requiredRoles(level),
|
||||
callContext = cc.callContext
|
||||
)
|
||||
// Fetch logs
|
||||
logs <- Future(RedisLogger.getLogTail(level))
|
||||
} yield {
|
||||
(logs, HttpCode.`200`(cc.callContext))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
getRegulatedEntityById,
|
||||
implementedInApiVersion,
|
||||
@ -1677,12 +1720,12 @@ trait APIMethods510 {
|
||||
for {
|
||||
httpParams <- NewStyle.function.extractHttpParamsFromUrl(cc.url)
|
||||
(obpQueryParams, callContext) <- createQueriesByHttpParamsFuture(httpParams, cc.callContext)
|
||||
consents <- Future {
|
||||
(consents, totalPages) <- Future {
|
||||
Consents.consentProvider.vend.getConsents(obpQueryParams)
|
||||
}
|
||||
} yield {
|
||||
val consentsOfBank = Consent.filterByBankId(consents, bankId)
|
||||
(createConsentsJsonV510(consentsOfBank), HttpCode.`200`(callContext))
|
||||
(createConsentsJsonV510(consentsOfBank, totalPages), HttpCode.`200`(callContext))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1739,11 +1782,11 @@ trait APIMethods510 {
|
||||
for {
|
||||
httpParams <- NewStyle.function.extractHttpParamsFromUrl(cc.url)
|
||||
(obpQueryParams, callContext) <- createQueriesByHttpParamsFuture(httpParams, cc.callContext)
|
||||
consents <- Future {
|
||||
(consents, totalPages) <- Future {
|
||||
Consents.consentProvider.vend.getConsents(obpQueryParams)
|
||||
}
|
||||
} yield {
|
||||
(createConsentsJsonV510(consents), HttpCode.`200`(callContext))
|
||||
(createConsentsJsonV510(consents, totalPages), HttpCode.`200`(callContext))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2189,12 +2232,29 @@ trait APIMethods510 {
|
||||
grantorConsumerId = callContext.map(_.consumer.toOption.map(_.consumerId.get)).flatten.getOrElse("Unknown")
|
||||
//this is from json body
|
||||
granteeConsumerId = consentJson.consumer_id.getOrElse("Unknown")
|
||||
|
||||
// Log consent SCA skip check to ai.log
|
||||
_ <- Future.successful {
|
||||
println(s"[skip_consent_sca_for_consumer_id_pairs] Checking SCA skip for consent creation")
|
||||
println(s"[skip_consent_sca_for_consumer_id_pairs] grantorConsumerId (from callContext): $grantorConsumerId")
|
||||
println(s"[skip_consent_sca_for_consumer_id_pairs] granteeConsumerId (from json body): $granteeConsumerId")
|
||||
println(s"[skip_consent_sca_for_consumer_id_pairs] skipConsentScaForConsumerIdPairs config: ${APIUtil.skipConsentScaForConsumerIdPairs}")
|
||||
}
|
||||
|
||||
shouldSkipConsentScaForConsumerIdPair = APIUtil.skipConsentScaForConsumerIdPairs.contains(
|
||||
APIUtil.ConsumerIdPair(
|
||||
grantorConsumerId,
|
||||
granteeConsumerId
|
||||
))
|
||||
_ <- Future.successful {
|
||||
println(s"[skip_consent_sca_for_consumer_id_pairs] shouldSkipConsentScaForConsumerIdPair: $shouldSkipConsentScaForConsumerIdPair")
|
||||
if (!shouldSkipConsentScaForConsumerIdPair) {
|
||||
println(s"[skip_consent_sca_for_consumer_id_pairs] Consumer pair NOT found in skip list. Looking for: ConsumerIdPair(grantor_consumer_id='$grantorConsumerId', grantee_consumer_id='$granteeConsumerId')")
|
||||
println(s"[skip_consent_sca_for_consumer_id_pairs] Available pairs in config: ${APIUtil.skipConsentScaForConsumerIdPairs.map(pair => s"ConsumerIdPair(grantor_consumer_id='${pair.grantor_consumer_id}', grantee_consumer_id='${pair.grantee_consumer_id}')").mkString(", ")}")
|
||||
} else {
|
||||
println(s"[skip_consent_sca_for_consumer_id_pairs] Consumer pair FOUND in skip list - SCA will be skipped")
|
||||
}
|
||||
}
|
||||
mappedConsent <- if (shouldSkipConsentScaForConsumerIdPair) {
|
||||
Future{
|
||||
MappedConsent.find(By(MappedConsent.mConsentId, createdConsent.consentId)).map(_.mStatus(ConsentStatus.ACCEPTED.toString).saveMe()).head
|
||||
@ -2240,30 +2300,39 @@ trait APIMethods510 {
|
||||
createdConsent
|
||||
}
|
||||
case v if v == StrongCustomerAuthentication.IMPLICIT.toString =>
|
||||
for {
|
||||
(consentImplicitSCA, callContext) <- NewStyle.function.getConsentImplicitSCA(user, callContext)
|
||||
status <- consentImplicitSCA.scaMethod match {
|
||||
case v if v == StrongCustomerAuthentication.EMAIL => // Send the email
|
||||
NewStyle.function.sendCustomerNotification(
|
||||
StrongCustomerAuthentication.EMAIL,
|
||||
consentImplicitSCA.recipient,
|
||||
Some("OBP Consent Challenge"),
|
||||
challengeText,
|
||||
callContext
|
||||
)
|
||||
case v if v == StrongCustomerAuthentication.SMS =>
|
||||
NewStyle.function.sendCustomerNotification(
|
||||
StrongCustomerAuthentication.SMS,
|
||||
consentImplicitSCA.recipient,
|
||||
None,
|
||||
challengeText,
|
||||
callContext
|
||||
)
|
||||
case _ => Future {
|
||||
"Success"
|
||||
}
|
||||
}} yield {
|
||||
createdConsent
|
||||
// For IMPLICIT consents, check if SCA should be skipped first
|
||||
if (shouldSkipConsentScaForConsumerIdPair) {
|
||||
println(s"[skip_consent_sca_for_consumer_id_pairs] IMPLICIT consent auto-accepted due to skip_consent_sca_for_consumer_id_pairs config")
|
||||
Future {
|
||||
MappedConsent.find(By(MappedConsent.mConsentId, createdConsent.consentId)).map(_.mStatus(ConsentStatus.ACCEPTED.toString).saveMe()).head
|
||||
}
|
||||
} else {
|
||||
println(s"[skip_consent_sca_for_consumer_id_pairs] IMPLICIT consent requires SCA - proceeding with implicit SCA flow")
|
||||
for {
|
||||
(consentImplicitSCA, callContext) <- NewStyle.function.getConsentImplicitSCA(user, callContext)
|
||||
status <- consentImplicitSCA.scaMethod match {
|
||||
case v if v == StrongCustomerAuthentication.EMAIL => // Send the email
|
||||
NewStyle.function.sendCustomerNotification(
|
||||
StrongCustomerAuthentication.EMAIL,
|
||||
consentImplicitSCA.recipient,
|
||||
Some("OBP Consent Challenge"),
|
||||
challengeText,
|
||||
callContext
|
||||
)
|
||||
case v if v == StrongCustomerAuthentication.SMS =>
|
||||
NewStyle.function.sendCustomerNotification(
|
||||
StrongCustomerAuthentication.SMS,
|
||||
consentImplicitSCA.recipient,
|
||||
None,
|
||||
challengeText,
|
||||
callContext
|
||||
)
|
||||
case _ => Future {
|
||||
"Success"
|
||||
}
|
||||
}} yield {
|
||||
createdConsent
|
||||
}
|
||||
}
|
||||
case _ => Future {
|
||||
createdConsent
|
||||
@ -3222,6 +3291,50 @@ trait APIMethods510 {
|
||||
}
|
||||
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
getCallsLimit,
|
||||
implementedInApiVersion,
|
||||
nameOf(getCallsLimit),
|
||||
"GET",
|
||||
"/management/consumers/CONSUMER_ID/consumer/call-limits",
|
||||
"Get Call Limits for a Consumer",
|
||||
s"""
|
||||
|Get Calls limits per Consumer.
|
||||
|${userAuthenticationMessage(true)}
|
||||
|
|
||||
|""".stripMargin,
|
||||
EmptyBody,
|
||||
callLimitJson,
|
||||
List(
|
||||
$UserNotLoggedIn,
|
||||
InvalidJsonFormat,
|
||||
InvalidConsumerId,
|
||||
ConsumerNotFoundByConsumerId,
|
||||
UserHasMissingRoles,
|
||||
UpdateConsumerError,
|
||||
UnknownError
|
||||
),
|
||||
List(apiTagConsumer),
|
||||
Some(List(canReadCallLimits)))
|
||||
|
||||
|
||||
lazy val getCallsLimit: OBPEndpoint = {
|
||||
case "management" :: "consumers" :: consumerId :: "consumer" :: "call-limits" :: Nil JsonGet _ => {
|
||||
cc =>
|
||||
implicit val ec = EndpointContext(Some(cc))
|
||||
for {
|
||||
// (Full(u), callContext) <- authenticatedAccess(cc)
|
||||
// _ <- NewStyle.function.hasEntitlement("", cc.userId, canReadCallLimits, callContext)
|
||||
consumer <- NewStyle.function.getConsumerByConsumerId(consumerId, cc.callContext)
|
||||
rateLimiting: Option[RateLimiting] <- RateLimitingDI.rateLimiting.vend.findMostRecentRateLimit(consumerId, None, None, None)
|
||||
rateLimit <- Future(RateLimitingUtil.consumerRateLimitState(consumer.consumerId.get).toList)
|
||||
} yield {
|
||||
(createCallLimitJson(consumer, rateLimiting, rateLimit), HttpCode.`200`(cc.callContext))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
updateConsumerRedirectURL,
|
||||
implementedInApiVersion,
|
||||
|
||||
@ -31,6 +31,7 @@ import code.api.berlin.group.ConstantsBG
|
||||
import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.ConsentAccessJson
|
||||
import code.api.util.APIUtil.{DateWithDay, DateWithSeconds, gitCommit, stringOrNull}
|
||||
import code.api.util.ErrorMessages.MandatoryPropertyIsNotSet
|
||||
import code.api.util.RateLimitingPeriod.LimitCallPeriod
|
||||
import code.api.util._
|
||||
import code.api.v1_2_1.BankRoutingJsonV121
|
||||
import code.api.v1_4_0.JSONFactory1_4_0.{ChallengeJsonV140, LocationJsonV140, MetaJsonV140, TransactionRequestAccountJsonV140, transformToLocationFromV140, transformToMetaFromV140}
|
||||
@ -38,6 +39,7 @@ import code.api.v2_0_0.TransactionRequestChargeJsonV200
|
||||
import code.api.v2_1_0.ResourceUserJSON
|
||||
import code.api.v3_0_0.JSONFactory300.{createLocationJson, createMetaJson, transformToAddressFromV300}
|
||||
import code.api.v3_0_0.{AddressJsonV300, OpeningTimesV300}
|
||||
import code.api.v3_1_0.{CallLimitJson, RateLimit, RedisCallLimitJson}
|
||||
import code.api.v4_0_0.{EnergySource400, HostedAt400, HostedBy400}
|
||||
import code.api.v5_0_0.PostConsentRequestJsonV500
|
||||
import code.atmattribute.AtmAttribute
|
||||
@ -45,6 +47,7 @@ import code.atms.Atms.Atm
|
||||
import code.consent.MappedConsent
|
||||
import code.metrics.APIMetric
|
||||
import code.model.Consumer
|
||||
import code.ratelimiting.RateLimiting
|
||||
import code.users.{UserAttribute, Users}
|
||||
import code.util.Helper.MdcLoggable
|
||||
import code.views.system.{AccountAccess, ViewDefinition, ViewPermission}
|
||||
@ -128,6 +131,9 @@ case class RegulatedEntityPostJsonV510(
|
||||
)
|
||||
case class RegulatedEntitiesJsonV510(entities: List[RegulatedEntityJsonV510])
|
||||
|
||||
case class LogCacheJsonV510(level: String, message: String)
|
||||
case class LogsCacheJsonV510(logs: List[String])
|
||||
|
||||
case class WaitingForGodotJsonV510(sleep_in_milliseconds: Long)
|
||||
|
||||
case class CertificateInfoJsonV510(
|
||||
@ -144,8 +150,8 @@ case class CheckSystemIntegrityJsonV510(
|
||||
debug_info: Option[String] = None
|
||||
)
|
||||
|
||||
case class ConsentJsonV510(consent_id: String,
|
||||
jwt: String,
|
||||
case class ConsentJsonV510(consent_id: String,
|
||||
jwt: String,
|
||||
status: String,
|
||||
consent_request_id: Option[String],
|
||||
scopes: Option[List[Role]],
|
||||
@ -171,6 +177,8 @@ case class PutConsentPayloadJsonV510(access: ConsentAccessJson)
|
||||
case class AllConsentJsonV510(consent_reference_id: String,
|
||||
consumer_id: String,
|
||||
created_by_user_id: String,
|
||||
provider: Option[String],
|
||||
provider_id: Option[String],
|
||||
status: String,
|
||||
last_action_date: String,
|
||||
last_usage_date: String,
|
||||
@ -181,7 +189,7 @@ case class AllConsentJsonV510(consent_reference_id: String,
|
||||
api_version: String,
|
||||
note: String,
|
||||
)
|
||||
case class ConsentsJsonV510(consents: List[AllConsentJsonV510])
|
||||
case class ConsentsJsonV510(number_of_rows: Long, consents: List[AllConsentJsonV510])
|
||||
|
||||
|
||||
case class CurrencyJsonV510(alphanumeric_code: String)
|
||||
@ -461,7 +469,7 @@ case class ConsumerJsonV510(consumer_id: String,
|
||||
certificate_info: Option[CertificateInfoJsonV510],
|
||||
created_by_user: ResourceUserJSON,
|
||||
enabled: Boolean,
|
||||
created: Date,
|
||||
created: Date,
|
||||
logo_url: Option[String]
|
||||
)
|
||||
case class MyConsumerJsonV510(consumer_id: String,
|
||||
@ -477,7 +485,7 @@ case class MyConsumerJsonV510(consumer_id: String,
|
||||
certificate_info: Option[CertificateInfoJsonV510],
|
||||
created_by_user: ResourceUserJSON,
|
||||
enabled: Boolean,
|
||||
created: Date,
|
||||
created: Date,
|
||||
logo_url: Option[String]
|
||||
)
|
||||
case class ConsumerJsonOnlyForPostResponseV510(consumer_id: String,
|
||||
@ -677,6 +685,20 @@ case class ViewPermissionJson(
|
||||
extra_data: Option[List[String]]
|
||||
)
|
||||
|
||||
case class CallLimitJson510(
|
||||
from_date: Date,
|
||||
to_date: Date,
|
||||
per_second_call_limit : String,
|
||||
per_minute_call_limit : String,
|
||||
per_hour_call_limit : String,
|
||||
per_day_call_limit : String,
|
||||
per_week_call_limit : String,
|
||||
per_month_call_limit : String,
|
||||
created_at : Date,
|
||||
updated_at : Date,
|
||||
current_state: Option[RedisCallLimitJson]
|
||||
)
|
||||
|
||||
object JSONFactory510 extends CustomJsonFormats with MdcLoggable {
|
||||
|
||||
def createTransactionRequestJson(tr : TransactionRequest, transactionRequestAttributes: List[TransactionRequestAttributeTrait] ) : TransactionRequestJsonV510 = {
|
||||
@ -713,7 +735,7 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable {
|
||||
def createTransactionRequestJSONs(transactionRequests : List[TransactionRequest], transactionRequestAttributes: List[TransactionRequestAttributeTrait]) : TransactionRequestsJsonV510 = {
|
||||
TransactionRequestsJsonV510(
|
||||
transactionRequests.map(
|
||||
transactionRequest =>
|
||||
transactionRequest =>
|
||||
createTransactionRequestJson(transactionRequest, transactionRequestAttributes)
|
||||
))
|
||||
}
|
||||
@ -959,17 +981,22 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable {
|
||||
}
|
||||
|
||||
def createConsentsInfoJsonV510(consents: List[MappedConsent]): ConsentsInfoJsonV510 = {
|
||||
|
||||
ConsentsInfoJsonV510(
|
||||
consents.map { c =>
|
||||
val jwtPayload: Box[ConsentJWT] = JwtUtil.getSignedPayloadAsJson(c.jsonWebToken).map(parse(_).extract[ConsentJWT])
|
||||
val jwtPayload: Box[ConsentJWT] =
|
||||
JwtUtil.getSignedPayloadAsJson(c.jsonWebToken).map(parse(_).extract[ConsentJWT])
|
||||
|
||||
ConsentInfoJsonV510(
|
||||
consent_reference_id = c.consentReferenceId,
|
||||
consent_id = c.consentId,
|
||||
consumer_id = c.consumerId,
|
||||
created_by_user_id = c.userId,
|
||||
status = c.status,
|
||||
last_action_date = if (c.lastActionDate != null) new SimpleDateFormat(DateWithDay).format(c.lastActionDate) else null,
|
||||
last_usage_date = if (c.usesSoFarTodayCounterUpdatedAt != null) new SimpleDateFormat(DateWithSeconds).format(c.usesSoFarTodayCounterUpdatedAt) else null,
|
||||
last_action_date =
|
||||
if (c.lastActionDate != null) new SimpleDateFormat(DateWithDay).format(c.lastActionDate) else null,
|
||||
last_usage_date =
|
||||
if (c.usesSoFarTodayCounterUpdatedAt != null) new SimpleDateFormat(DateWithSeconds).format(c.usesSoFarTodayCounterUpdatedAt) else null,
|
||||
jwt = c.jsonWebToken,
|
||||
jwt_payload = jwtPayload,
|
||||
api_standard = c.apiStandard,
|
||||
@ -978,9 +1005,18 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable {
|
||||
}
|
||||
)
|
||||
}
|
||||
def createConsentsJsonV510(consents: List[MappedConsent]): ConsentsJsonV510 = {
|
||||
|
||||
def createConsentsJsonV510(consents: List[MappedConsent], totalPages: Long): ConsentsJsonV510 = {
|
||||
// Temporary cache (cleared after function ends)
|
||||
val cache = scala.collection.mutable.HashMap.empty[String, Box[User]]
|
||||
|
||||
// Cached lookup
|
||||
def getUserCached(userId: String): Box[User] = {
|
||||
cache.getOrElseUpdate(userId, Users.users.vend.getUserByUserId(userId))
|
||||
}
|
||||
ConsentsJsonV510(
|
||||
consents.map { c =>
|
||||
number_of_rows = totalPages,
|
||||
consents = consents.map { c =>
|
||||
val jwtPayload = JwtUtil
|
||||
.getSignedPayloadAsJson(c.jsonWebToken)
|
||||
.flatMap { payload =>
|
||||
@ -995,6 +1031,8 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable {
|
||||
consent_reference_id = c.consentReferenceId,
|
||||
consumer_id = c.consumerId,
|
||||
created_by_user_id = c.userId,
|
||||
provider = getUserCached(c.userId).map(_.provider).orElse(Some(null)), // cached version
|
||||
provider_id = getUserCached(c.userId).map(_.idGivenByProvider).orElse(Some(null)), // cached version
|
||||
status = c.status,
|
||||
last_action_date = if (c.lastActionDate != null) new SimpleDateFormat(DateWithDay).format(c.lastActionDate) else null,
|
||||
last_usage_date = if (c.usesSoFarTodayCounterUpdatedAt != null) new SimpleDateFormat(DateWithSeconds).format(c.usesSoFarTodayCounterUpdatedAt) else null,
|
||||
@ -1238,13 +1276,13 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable {
|
||||
if(value == null || value.isEmpty) None else Some(value.split(",").toList)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def createMinimalAgentsJson(agents: List[Agent]): MinimalAgentsJsonV510 = {
|
||||
MinimalAgentsJsonV510(
|
||||
agents
|
||||
.filter(_.isConfirmedAgent == true)
|
||||
.map(agent => MinimalAgentJsonV510(
|
||||
agent_id = agent.agentId,
|
||||
agent_id = agent.agentId,
|
||||
legal_name = agent.legalName,
|
||||
agent_number = agent.number
|
||||
)))
|
||||
@ -1285,4 +1323,48 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable {
|
||||
)
|
||||
}
|
||||
|
||||
def createCallLimitJson(consumer: Consumer, rateLimiting: Option[RateLimiting], rateLimits: List[((Option[Long], Option[Long]), LimitCallPeriod)]): CallLimitJson510 = {
|
||||
val redisRateLimit = rateLimits match {
|
||||
case Nil => None
|
||||
case _ =>
|
||||
def getInfo(period: RateLimitingPeriod.Value): Option[RateLimit] = {
|
||||
rateLimits.filter(_._2 == period) match {
|
||||
case x :: Nil =>
|
||||
x._1 match {
|
||||
case (Some(x), Some(y)) => Some(RateLimit(Some(x), Some(y)))
|
||||
case _ => None
|
||||
|
||||
}
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
||||
Some(
|
||||
RedisCallLimitJson(
|
||||
getInfo(RateLimitingPeriod.PER_SECOND),
|
||||
getInfo(RateLimitingPeriod.PER_MINUTE),
|
||||
getInfo(RateLimitingPeriod.PER_HOUR),
|
||||
getInfo(RateLimitingPeriod.PER_DAY),
|
||||
getInfo(RateLimitingPeriod.PER_WEEK),
|
||||
getInfo(RateLimitingPeriod.PER_MONTH)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
CallLimitJson510(
|
||||
from_date = rateLimiting.map(_.fromDate).orNull,
|
||||
to_date = rateLimiting.map(_.toDate).orNull,
|
||||
per_second_call_limit = rateLimiting.map(_.perSecondCallLimit.toString).getOrElse("-1"),
|
||||
per_minute_call_limit = rateLimiting.map(_.perMinuteCallLimit.toString).getOrElse("-1"),
|
||||
per_hour_call_limit = rateLimiting.map(_.perHourCallLimit.toString).getOrElse("-1"),
|
||||
per_day_call_limit = rateLimiting.map(_.perDayCallLimit.toString).getOrElse("-1"),
|
||||
per_week_call_limit = rateLimiting.map(_.perWeekCallLimit.toString).getOrElse("-1"),
|
||||
per_month_call_limit = rateLimiting.map(_.perMonthCallLimit.toString).getOrElse("-1"),
|
||||
created_at = rateLimiting.map(_.createdAt.get).orNull,
|
||||
updated_at = rateLimiting.map(_.updatedAt.get).orNull,
|
||||
redisRateLimit
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import com.openbankproject.commons.model.{AtmId, BankId}
|
||||
import com.openbankproject.commons.model.enums.AtmAttributeType
|
||||
import net.liftweb.common.{Box, Logger}
|
||||
import net.liftweb.util.SimpleInjector
|
||||
import code.util.Helper.MdcLoggable
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
@ -27,9 +28,7 @@ object AtmAttributeX extends SimpleInjector {
|
||||
|
||||
}
|
||||
|
||||
trait AtmAttributeProviderTrait {
|
||||
|
||||
private val logger = Logger(classOf[AtmAttributeProviderTrait])
|
||||
trait AtmAttributeProviderTrait extends MdcLoggable {
|
||||
|
||||
def getAtmAttributesFromProvider(bankId: BankId, atmId: AtmId): Future[Box[List[AtmAttribute]]]
|
||||
|
||||
@ -40,10 +39,10 @@ trait AtmAttributeProviderTrait {
|
||||
AtmAttributeId: Option[String],
|
||||
name: String,
|
||||
attributeType: AtmAttributeType.Value,
|
||||
value: String,
|
||||
value: String,
|
||||
isActive: Option[Boolean]): Future[Box[AtmAttribute]]
|
||||
def deleteAtmAttribute(AtmAttributeId: String): Future[Box[Boolean]]
|
||||
|
||||
|
||||
def deleteAtmAttributesByAtmId(atmId: AtmId): Future[Box[Boolean]]
|
||||
// End of Trait
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import code.api.util.OBPQueryParam
|
||||
import com.openbankproject.commons.model._
|
||||
import net.liftweb.common.{Box, Logger}
|
||||
import net.liftweb.util.SimpleInjector
|
||||
import code.util.Helper.MdcLoggable
|
||||
|
||||
import scala.collection.immutable.List
|
||||
|
||||
@ -62,7 +63,7 @@ object Atms extends SimpleInjector {
|
||||
balanceInquiryFee: Option[String] = None,
|
||||
atmType: Option[String] = None,
|
||||
phone: Option[String] = None,
|
||||
|
||||
|
||||
) extends AtmT
|
||||
|
||||
val atmsProvider = new Inject(buildOne _) {}
|
||||
@ -81,9 +82,7 @@ object Atms extends SimpleInjector {
|
||||
|
||||
}
|
||||
|
||||
trait AtmsProvider {
|
||||
|
||||
private val logger = Logger(classOf[AtmsProvider])
|
||||
trait AtmsProvider extends MdcLoggable {
|
||||
|
||||
|
||||
/*
|
||||
@ -107,9 +106,3 @@ trait AtmsProvider {
|
||||
def deleteAtm(atm: AtmT): Box[Boolean]
|
||||
// End of Trait
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -36,7 +36,8 @@ class BankAccountBalance extends BankAccountBalanceTrait with KeyedMapper[String
|
||||
override def balanceType: String = BalanceType.get
|
||||
override def balanceAmount: BigDecimal = Helper.smallestCurrencyUnitToBigDecimal(BalanceAmount.get, foreignMappedBankAccountCurrency)
|
||||
override def lastChangeDateTime: Option[Date] = Some(this.updatedAt.get)
|
||||
override def referenceDate: Option[String] = Some(ReferenceDate.get.toString)
|
||||
override def referenceDate: Option[String] = Option(ReferenceDate.get).map(_.toString)
|
||||
|
||||
}
|
||||
|
||||
object BankAccountBalance extends BankAccountBalance with KeyedMetaMapper[String, BankAccountBalance] with CreatedUpdated {}
|
||||
|
||||
@ -7,6 +7,7 @@ import com.openbankproject.commons.model.BankId
|
||||
import com.openbankproject.commons.model.enums.BankAttributeType
|
||||
import net.liftweb.common.{Box, Logger}
|
||||
import net.liftweb.util.SimpleInjector
|
||||
import code.util.Helper.MdcLoggable
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
@ -14,7 +15,7 @@ object BankAttributeX extends SimpleInjector {
|
||||
|
||||
val bankAttributeProvider = new Inject(buildOne _) {}
|
||||
|
||||
def buildOne: BankAttributeProviderTrait = BankAttributeProvider
|
||||
def buildOne: BankAttributeProviderTrait = BankAttributeProvider
|
||||
|
||||
// Helper to get the count out of an option
|
||||
def countOfBankAttribute(listOpt: Option[List[BankAttribute]]): Int = {
|
||||
@ -28,9 +29,7 @@ object BankAttributeX extends SimpleInjector {
|
||||
|
||||
}
|
||||
|
||||
trait BankAttributeProviderTrait {
|
||||
|
||||
private val logger = Logger(classOf[BankAttributeProviderTrait])
|
||||
trait BankAttributeProviderTrait extends MdcLoggable {
|
||||
|
||||
def getBankAttributesFromProvider(bankId: BankId): Future[Box[List[BankAttribute]]]
|
||||
|
||||
@ -40,8 +39,8 @@ trait BankAttributeProviderTrait {
|
||||
bankAttributeId: Option[String],
|
||||
name: String,
|
||||
attributType: BankAttributeType.Value,
|
||||
value: String,
|
||||
value: String,
|
||||
isActive: Option[Boolean]): Future[Box[BankAttribute]]
|
||||
def deleteBankAttribute(bankAttributeId: String): Future[Box[Boolean]]
|
||||
// End of Trait
|
||||
}
|
||||
}
|
||||
|
||||
@ -650,6 +650,7 @@ object LocalMappedConnectorInternal extends MdcLoggable {
|
||||
.CPOtherBankRoutingScheme(toAccount.bankRoutingScheme)
|
||||
.CPOtherBankRoutingAddress(toAccount.bankRoutingAddress)
|
||||
.chargePolicy(chargePolicy)
|
||||
.status(com.openbankproject.commons.model.enums.TransactionRequestStatus.COMPLETED.toString)
|
||||
.saveMe) ?~! s"$CreateTransactionsException, exception happened when create new mappedTransaction"
|
||||
} yield {
|
||||
mappedTransaction.theTransactionId
|
||||
|
||||
@ -7,8 +7,9 @@ package code.branches
|
||||
import code.api.util.OBPQueryParam
|
||||
|
||||
import com.openbankproject.commons.model._
|
||||
import net.liftweb.common.Logger
|
||||
import net.liftweb.common.{Box, Logger}
|
||||
import net.liftweb.util.SimpleInjector
|
||||
import code.util.Helper.MdcLoggable
|
||||
|
||||
object Branches extends SimpleInjector {
|
||||
|
||||
@ -207,9 +208,7 @@ object Branches extends SimpleInjector {
|
||||
|
||||
}
|
||||
|
||||
trait BranchesProvider {
|
||||
|
||||
private val logger = Logger(classOf[BranchesProvider])
|
||||
trait BranchesProvider extends MdcLoggable {
|
||||
|
||||
|
||||
/*
|
||||
@ -235,4 +234,3 @@ trait BranchesProvider {
|
||||
|
||||
// End of Trait
|
||||
}
|
||||
|
||||
|
||||
@ -5,10 +5,9 @@ import code.util.{TwentyFourHourClockString, UUIDString}
|
||||
import com.openbankproject.commons.model._
|
||||
import net.liftweb.common.Logger
|
||||
import net.liftweb.mapper.{By, _}
|
||||
import code.util.Helper.MdcLoggable
|
||||
|
||||
object MappedBranchesProvider extends BranchesProvider {
|
||||
|
||||
private val logger = Logger(classOf[BranchesProvider])
|
||||
object MappedBranchesProvider extends BranchesProvider with MdcLoggable {
|
||||
|
||||
override protected def getBranchFromProvider(bankId: BankId, branchId: BranchId): Option[BranchT] =
|
||||
MappedBranch.find(
|
||||
@ -18,15 +17,15 @@ object MappedBranchesProvider extends BranchesProvider {
|
||||
|
||||
override protected def getBranchesFromProvider(bankId: BankId, queryParams: List[OBPQueryParam]): Option[List[BranchT]] = {
|
||||
logger.debug(s"getBranchesFromProvider says bankId is $bankId")
|
||||
|
||||
|
||||
val limit = queryParams.collect { case OBPLimit(value) => MaxRows[MappedBranch](value) }.headOption
|
||||
val offset = queryParams.collect { case OBPOffset(value) => StartAt[MappedBranch](value) }.headOption
|
||||
|
||||
|
||||
val optionalParams : Seq[QueryParam[MappedBranch]] = Seq(limit.toSeq, offset.toSeq).flatten
|
||||
val mapperParams = Seq(By(MappedBranch.mBankId, bankId.value), By(MappedBranch.mIsDeleted, false)) ++ optionalParams
|
||||
|
||||
|
||||
val branches: Option[List[BranchT]] = Some(MappedBranch.findAll(mapperParams:_*))
|
||||
|
||||
|
||||
branches
|
||||
}
|
||||
}
|
||||
@ -285,4 +284,4 @@ Else could store a link to this with each open data record - or via config for e
|
||||
//
|
||||
//object MappedLicense extends MappedLicense with LongKeyedMetaMapper[MappedLicense] {
|
||||
// override def dbIndexes = Index(mBankId) :: super.dbIndexes
|
||||
//}
|
||||
//}
|
||||
|
||||
@ -7,6 +7,7 @@ import com.openbankproject.commons.model.enums.CardAttributeType
|
||||
import com.openbankproject.commons.model.{AccountId, BankId, CardAttribute, ProductCode}
|
||||
import net.liftweb.common.{Box, Logger}
|
||||
import net.liftweb.util.SimpleInjector
|
||||
import code.util.Helper.MdcLoggable
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
@ -27,9 +28,7 @@ object CardAttributeX extends SimpleInjector {
|
||||
|
||||
}
|
||||
|
||||
trait CardAttributeProvider {
|
||||
|
||||
private val logger = Logger(classOf[CardAttributeProvider])
|
||||
trait CardAttributeProvider extends MdcLoggable {
|
||||
|
||||
def getCardAttributesFromProvider(cardId: String): Future[Box[List[CardAttribute]]]
|
||||
|
||||
@ -43,7 +42,7 @@ trait CardAttributeProvider {
|
||||
attributeType: CardAttributeType.Value,
|
||||
value: String
|
||||
): Future[Box[CardAttribute]]
|
||||
|
||||
|
||||
def deleteCardAttribute(cardAttributeId: String): Future[Box[Boolean]]
|
||||
// End of Trait
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ object Consents extends SimpleInjector {
|
||||
}
|
||||
|
||||
trait ConsentProvider {
|
||||
def getConsents(queryParams: List[OBPQueryParam]): List[MappedConsent]
|
||||
def getConsents(queryParams: List[OBPQueryParam]): (List[MappedConsent], Long)
|
||||
def getConsentByConsentId(consentId: String): Box[MappedConsent]
|
||||
def getConsentByConsentRequestId(consentRequestId: String): Box[MappedConsent]
|
||||
def updateConsentStatus(consentId: String, status: ConsentStatus): Box[MappedConsent]
|
||||
|
||||
@ -68,57 +68,64 @@ object MappedConsentProvider extends ConsentProvider {
|
||||
}
|
||||
|
||||
|
||||
private def getQueryParams(queryParams: List[OBPQueryParam]) = {
|
||||
val limit = queryParams.collectFirst { case OBPLimit(value) => MaxRows[MappedConsent](value) }
|
||||
val offset = queryParams.collectFirst { case OBPOffset(value) => StartAt[MappedConsent](value) }
|
||||
// The optional variables:
|
||||
|
||||
private def getPagedConsents(queryParams: List[OBPQueryParam]): (List[MappedConsent], Long) = {
|
||||
// Extract pagination params
|
||||
val limitOpt = queryParams.collectFirst { case OBPLimit(value) => value }
|
||||
val offsetOpt = queryParams.collectFirst { case OBPOffset(value) => value }
|
||||
|
||||
// Extract filters (exclude limit/offset)
|
||||
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 '|'
|
||||
val (provider, providerId) = value.split("\\|") match {
|
||||
case Array(a, b) => (a, b)
|
||||
case _ => ("", "") // fallback if format is unexpected
|
||||
case _ => ("", "")
|
||||
}
|
||||
ResourceUser.findAll(By(ResourceUser.provider_, provider), By(ResourceUser.providerId, providerId)) match {
|
||||
case x :: Nil => // exactly one
|
||||
Some(By(MappedConsent.mUserId, x.userId))
|
||||
case _ =>
|
||||
None
|
||||
case x :: Nil => 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) =>
|
||||
// Split the comma-separated string into a List, and trim whitespace from each element
|
||||
val statuses: List[String] = value.split(",").toList.map(_.trim)
|
||||
|
||||
// For each distinct status:
|
||||
// - create both lowercase ancheckIsLockedd uppercase versions
|
||||
// - flatten the resulting list of lists into a single list
|
||||
// - remove duplicates from the final list
|
||||
val distinctLowerAndUpperCaseStatuses: List[String] =
|
||||
statuses.distinct // Remove duplicates (case-sensitive)
|
||||
.flatMap(s => List( // For each element, generate:
|
||||
s.toLowerCase, // - lowercase version
|
||||
s.toUpperCase // - uppercase version
|
||||
))
|
||||
.distinct // Remove any duplicates caused by lowercase/uppercase repetition
|
||||
|
||||
val statuses = value.split(",").toList.map(_.trim)
|
||||
val distinctLowerAndUpperCaseStatuses =
|
||||
statuses.distinct.flatMap(s => List(s.toLowerCase, s.toUpperCase)).distinct
|
||||
ByList(MappedConsent.mStatus, distinctLowerAndUpperCaseStatuses)
|
||||
}
|
||||
|
||||
Seq(
|
||||
offset.toSeq,
|
||||
limit.toSeq,
|
||||
// Build filters (without limit/offset)
|
||||
val filters = Seq(
|
||||
status.toSeq,
|
||||
userId.orElse(providerProviderId).toSeq,
|
||||
consentId.toSeq,
|
||||
consumerId.toSeq
|
||||
).flatten
|
||||
|
||||
// Total count for pagination
|
||||
val totalCount = MappedConsent.count(filters: _*)
|
||||
|
||||
// Apply limit/offset if provided
|
||||
val pageData = (limitOpt, offsetOpt) match {
|
||||
case (Some(limit), Some(offset)) => MappedConsent.findAll(filters: _*).drop(offset).take(limit)
|
||||
case (Some(limit), None) => MappedConsent.findAll(filters: _*).take(limit)
|
||||
case _ => MappedConsent.findAll(filters: _*)
|
||||
}
|
||||
|
||||
// Compute number of pages
|
||||
val totalPages = limitOpt match {
|
||||
case Some(limit) if limit > 0 => Math.ceil(totalCount.toDouble / limit).toInt
|
||||
case _ => 1
|
||||
}
|
||||
|
||||
(pageData, totalCount)
|
||||
}
|
||||
|
||||
|
||||
private def sortConsents(consents: List[MappedConsent], sortByParam: String): List[MappedConsent] = {
|
||||
// Parse sort_by param like "created_date:desc,status:asc,consumer_id:asc"
|
||||
val sortFields: List[(String, String)] = sortByParam
|
||||
@ -164,17 +171,20 @@ object MappedConsentProvider extends ConsentProvider {
|
||||
}
|
||||
|
||||
|
||||
override def getConsents(queryParams: List[OBPQueryParam]): List[MappedConsent] = {
|
||||
val optionalParams = getQueryParams(queryParams)
|
||||
override def getConsents(queryParams: List[OBPQueryParam]): (List[MappedConsent], Long) = {
|
||||
val sortBy: Option[String] = queryParams.collectFirst { case OBPSortBy(value) => value }
|
||||
val consents = MappedConsent.findAll(optionalParams: _*)
|
||||
val (consents, totalCount) = getPagedConsents(queryParams)
|
||||
val bankId: Option[String] = queryParams.collectFirst { case OBPBankId(value) => value }
|
||||
if(bankId.isDefined) {
|
||||
Consent.filterStrictlyByBank(consents, bankId.get)
|
||||
(Consent.filterStrictlyByBank(consents, bankId.get), totalCount)
|
||||
} else {
|
||||
sortConsents(consents, sortBy.getOrElse(""))
|
||||
(sortConsents(consents, sortBy.getOrElse("")), totalCount)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
override def createObpConsent(user: User, challengeAnswer: String, consentRequestId:Option[String], consumer: Option[Consumer]): Box[MappedConsent] = {
|
||||
tryo {
|
||||
val salt = BCrypt.gensalt()
|
||||
|
||||
@ -10,6 +10,7 @@ import code.model.dataAccess.ResourceUser
|
||||
import net.liftweb.common.Logger
|
||||
import net.liftweb.util
|
||||
import net.liftweb.util.SimpleInjector
|
||||
import code.util.Helper.MdcLoggable
|
||||
import java.util.Date
|
||||
|
||||
import com.openbankproject.commons.model.{BankId, MetaT}
|
||||
@ -48,9 +49,7 @@ object CrmEvent extends util.SimpleInjector {
|
||||
|
||||
}
|
||||
|
||||
trait CrmEventProvider {
|
||||
|
||||
private val logger = Logger(classOf[CrmEventProvider])
|
||||
trait CrmEventProvider extends MdcLoggable {
|
||||
|
||||
|
||||
/*
|
||||
|
||||
@ -8,6 +8,7 @@ import com.openbankproject.commons.model.enums.CustomerAttributeType
|
||||
import com.openbankproject.commons.model.{BankId, Customer, CustomerAttribute, CustomerId}
|
||||
import net.liftweb.common.{Box, Logger}
|
||||
import net.liftweb.util.SimpleInjector
|
||||
import code.util.Helper.MdcLoggable
|
||||
|
||||
import scala.collection.immutable.List
|
||||
import scala.concurrent.Future
|
||||
@ -16,7 +17,7 @@ object CustomerAttributeX extends SimpleInjector {
|
||||
|
||||
val customerAttributeProvider = new Inject(buildOne _) {}
|
||||
|
||||
def buildOne: CustomerAttributeProvider = MappedCustomerAttributeProvider
|
||||
def buildOne: CustomerAttributeProvider = MappedCustomerAttributeProvider
|
||||
|
||||
// Helper to get the count out of an option
|
||||
def countOfCustomerAttribute(listOpt: Option[List[CustomerAttribute]]): Int = {
|
||||
@ -30,9 +31,7 @@ object CustomerAttributeX extends SimpleInjector {
|
||||
|
||||
}
|
||||
|
||||
trait CustomerAttributeProvider {
|
||||
|
||||
private val logger = Logger(classOf[CustomerAttributeProvider])
|
||||
trait CustomerAttributeProvider extends MdcLoggable {
|
||||
|
||||
def getCustomerAttributesFromProvider(customerId: CustomerId): Future[Box[List[CustomerAttribute]]]
|
||||
def getCustomerAttributes(bankId: BankId,
|
||||
@ -41,7 +40,7 @@ trait CustomerAttributeProvider {
|
||||
def getCustomerIdsByAttributeNameValues(bankId: BankId, params: Map[String, List[String]]): Future[Box[List[String]]]
|
||||
|
||||
def getCustomerAttributesForCustomers(customers: List[Customer]): Future[Box[List[CustomerAndAttribute]]]
|
||||
|
||||
|
||||
def getCustomerAttributeById(customerAttributeId: String): Future[Box[CustomerAttribute]]
|
||||
|
||||
def createOrUpdateCustomerAttribute(bankId: BankId,
|
||||
@ -54,7 +53,7 @@ trait CustomerAttributeProvider {
|
||||
def createCustomerAttributes(bankId: BankId,
|
||||
customerId: CustomerId,
|
||||
customerAttributes: List[CustomerAttribute]): Future[Box[List[CustomerAttribute]]]
|
||||
|
||||
|
||||
def deleteCustomerAttribute(customerAttributeId: String): Future[Box[Boolean]]
|
||||
// End of Trait
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import code.api.util.APIUtil
|
||||
import com.openbankproject.commons.model.BankId
|
||||
import net.liftweb.common.Logger
|
||||
import net.liftweb.util.SimpleInjector
|
||||
import code.util.Helper.MdcLoggable
|
||||
|
||||
object Thing extends SimpleInjector {
|
||||
|
||||
@ -45,9 +46,7 @@ A trait that defines interfaces to Thing
|
||||
i.e. a ThingProvider should provide these:
|
||||
*/
|
||||
|
||||
trait ThingProvider {
|
||||
|
||||
private val logger = Logger(classOf[ThingProvider])
|
||||
trait ThingProvider extends MdcLoggable {
|
||||
|
||||
|
||||
/*
|
||||
@ -79,4 +78,3 @@ trait ThingProvider {
|
||||
protected def getThingsFromProvider(bank : BankId) : Option[List[Thing]]
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -36,12 +36,11 @@ import com.openbankproject.commons.model._
|
||||
import com.openbankproject.commons.model.enums.AccountRoutingScheme
|
||||
import net.liftweb.common._
|
||||
import net.liftweb.util.StringHelpers
|
||||
import code.util.Helper.MdcLoggable
|
||||
|
||||
import java.util.Date
|
||||
|
||||
case class ViewExtended(val view: View) {
|
||||
|
||||
val viewLogger = Logger(classOf[ViewExtended])
|
||||
case class ViewExtended(val view: View) extends MdcLoggable {
|
||||
|
||||
def getViewPermissions: List[String] =
|
||||
if (view.isSystem) {
|
||||
@ -205,7 +204,7 @@ case class ViewExtended(val view: View) {
|
||||
|
||||
if(!belongsToModeratedAccount) {
|
||||
val failMsg = "Attempted to moderate a transaction using the incorrect moderated account"
|
||||
view.viewLogger.warn(failMsg)
|
||||
logger.warn(failMsg)
|
||||
Failure(failMsg)
|
||||
} else {
|
||||
Full(moderatedTransaction)
|
||||
@ -272,7 +271,7 @@ case class ViewExtended(val view: View) {
|
||||
|
||||
if(!belongsToModeratedAccount) {
|
||||
val failMsg = "Attempted to moderate a transaction using the incorrect moderated account"
|
||||
view.viewLogger.warn(failMsg)
|
||||
logger.warn(failMsg)
|
||||
Failure(failMsg)
|
||||
} else {
|
||||
Full(moderatedTransaction)
|
||||
@ -287,7 +286,7 @@ case class ViewExtended(val view: View) {
|
||||
|
||||
// This function will only accept transactions which have the same This Account.
|
||||
if(accountUids.toSet.size > 1) {
|
||||
view.viewLogger.warn("Attempted to moderate transactions not belonging to the same account in a call where they should")
|
||||
logger.warn("Attempted to moderate transactions not belonging to the same account in a call where they should")
|
||||
Failure("Could not moderate transactions as they do not all belong to the same account")
|
||||
} else {
|
||||
Full(transactions.flatMap(
|
||||
@ -306,7 +305,7 @@ case class ViewExtended(val view: View) {
|
||||
|
||||
// This function will only accept transactions which have the same This Account.
|
||||
if(accountUids.toSet.size > 1) {
|
||||
view.viewLogger.warn("Attempted to moderate transactions not belonging to the same account in a call where they should")
|
||||
logger.warn("Attempted to moderate transactions not belonging to the same account in a call where they should")
|
||||
Failure("Could not moderate transactions as they do not all belong to the same account")
|
||||
} else {
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import code.api.v4_0_0.{BankJson400, BanksJson400, JSONFactory400, OBPAPI4_0_0}
|
||||
import code.obp.grpc.api.BanksJson400Grpc.{BankJson400Grpc, BankRoutingJsonV121Grpc}
|
||||
import code.obp.grpc.api._
|
||||
import code.util.Helper
|
||||
import code.util.Helper.MdcLoggable
|
||||
import code.views.Views
|
||||
import com.google.protobuf.empty.Empty
|
||||
import com.openbankproject.commons.ExecutionContext.Implicits.global
|
||||
@ -17,14 +18,12 @@ import net.liftweb.json.JsonAST.{JField, JObject}
|
||||
import net.liftweb.json.JsonDSL._
|
||||
import net.liftweb.json.{Extraction, JArray}
|
||||
|
||||
import java.util.logging.Logger
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
/**
|
||||
* [[https://github.com/grpc/grpc-java/blob/v0.15.0/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java]]
|
||||
*/
|
||||
object HelloWorldServer {
|
||||
private val logger = Logger.getLogger(classOf[HelloWorldServer].getName)
|
||||
|
||||
def main(args: Array[String] = Array.empty): Unit = {
|
||||
val server = new HelloWorldServer(ExecutionContext.global)
|
||||
@ -32,10 +31,10 @@ object HelloWorldServer {
|
||||
server.blockUntilShutdown()
|
||||
}
|
||||
|
||||
val port = APIUtil.getPropsAsIntValue("grpc.server.port", Helper.findAvailablePort())
|
||||
val port = APIUtil.getPropsAsIntValue("grpc.server.port", Helper.findAvailablePort())
|
||||
}
|
||||
|
||||
class HelloWorldServer(executionContext: ExecutionContext) { self =>
|
||||
class HelloWorldServer(executionContext: ExecutionContext) extends MdcLoggable { self =>
|
||||
private[this] var server: Server = null
|
||||
def start(): Unit = {
|
||||
|
||||
@ -43,7 +42,7 @@ class HelloWorldServer(executionContext: ExecutionContext) { self =>
|
||||
.addService(ObpServiceGrpc.bindService(ObpServiceImpl, executionContext))
|
||||
.asInstanceOf[ServerBuilder[_]]
|
||||
server = serverBuilder.build.start;
|
||||
HelloWorldServer.logger.info("Server started, listening on " + HelloWorldServer.port)
|
||||
logger.info("Server started, listening on " + HelloWorldServer.port)
|
||||
sys.addShutdownHook {
|
||||
System.err.println("*** shutting down gRPC server since JVM is shutting down")
|
||||
self.stop()
|
||||
@ -139,4 +138,3 @@ class HelloWorldServer(executionContext: ExecutionContext) { self =>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import com.openbankproject.commons.model.enums.ProductAttributeType
|
||||
import com.openbankproject.commons.model.{BankId, ProductAttribute, ProductCode}
|
||||
import net.liftweb.common.{Box, Logger}
|
||||
import net.liftweb.util.SimpleInjector
|
||||
import code.util.Helper.MdcLoggable
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
@ -29,9 +30,7 @@ object ProductAttributeX extends SimpleInjector {
|
||||
|
||||
}
|
||||
|
||||
trait ProductAttributeProvider {
|
||||
|
||||
private val logger = Logger(classOf[ProductAttributeProvider])
|
||||
trait ProductAttributeProvider extends MdcLoggable {
|
||||
|
||||
def getProductAttributesFromProvider(bank: BankId, productCode: ProductCode): Future[Box[List[ProductAttribute]]]
|
||||
|
||||
@ -42,7 +41,7 @@ trait ProductAttributeProvider {
|
||||
productAttributeId: Option[String],
|
||||
name: String,
|
||||
attributeType: ProductAttributeType.Value,
|
||||
value: String,
|
||||
value: String,
|
||||
isActive: Option[Boolean]): Future[Box[ProductAttribute]]
|
||||
def deleteProductAttribute(productAttributeId: String): Future[Box[Boolean]]
|
||||
// End of Trait
|
||||
|
||||
@ -6,6 +6,7 @@ import code.api.util.APIUtil
|
||||
import com.openbankproject.commons.model.{BankId, ProductCode, ProductFeeTrait}
|
||||
import net.liftweb.common.{Box, Logger}
|
||||
import net.liftweb.util.SimpleInjector
|
||||
import code.util.Helper.MdcLoggable
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.math.BigDecimal
|
||||
@ -28,9 +29,7 @@ object ProductFeeX extends SimpleInjector {
|
||||
|
||||
}
|
||||
|
||||
trait ProductFeeProvider {
|
||||
|
||||
private val logger = Logger(classOf[ProductFeeProvider])
|
||||
trait ProductFeeProvider extends MdcLoggable {
|
||||
|
||||
def getProductFeesFromProvider(bankId: BankId, productCode: ProductCode): Future[Box[List[ProductFeeTrait]]]
|
||||
|
||||
@ -48,6 +47,6 @@ trait ProductFeeProvider {
|
||||
frequency: String,
|
||||
`type`: String
|
||||
): Future[Box[ProductFeeTrait]]
|
||||
|
||||
|
||||
def deleteProductFee(productFeeId: String): Future[Box[Boolean]]
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ package code.products
|
||||
import com.openbankproject.commons.model.{BankId, ProductCode}
|
||||
import net.liftweb.common.Logger
|
||||
import net.liftweb.util.SimpleInjector
|
||||
import code.util.Helper.MdcLoggable
|
||||
import com.openbankproject.commons.model.Product
|
||||
|
||||
object Products extends SimpleInjector {
|
||||
@ -27,9 +28,7 @@ object Products extends SimpleInjector {
|
||||
|
||||
}
|
||||
|
||||
trait ProductsProvider {
|
||||
|
||||
private val logger = Logger(classOf[ProductsProvider])
|
||||
trait ProductsProvider extends MdcLoggable {
|
||||
|
||||
|
||||
/*
|
||||
@ -38,7 +37,7 @@ trait ProductsProvider {
|
||||
*/
|
||||
final def getProducts(bankId : BankId, adminView: Boolean = false) : Option[List[Product]] = {
|
||||
logger.info(s"Hello from getProducts bankId is: $bankId")
|
||||
getProductsFromProvider(bankId)
|
||||
getProductsFromProvider(bankId)
|
||||
}
|
||||
|
||||
/*
|
||||
@ -53,9 +52,3 @@ trait ProductsProvider {
|
||||
|
||||
// End of Trait
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -101,6 +101,28 @@ object MappedRateLimitingProvider extends RateLimitingProviderTrait {
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
def findMostRecentRateLimit(consumerId: String,
|
||||
bankId: Option[String],
|
||||
apiVersion: Option[String],
|
||||
apiName: Option[String]): Future[Option[RateLimiting]] = Future {
|
||||
findMostRecentRateLimitCommon(consumerId, bankId, apiVersion, apiName)
|
||||
}
|
||||
def findMostRecentRateLimitCommon(consumerId: String,
|
||||
bankId: Option[String],
|
||||
apiVersion: Option[String],
|
||||
apiName: Option[String]): Option[RateLimiting] = {
|
||||
val byConsumerParam = By(RateLimiting.ConsumerId, consumerId)
|
||||
val byBankParam = bankId.map(v => By(RateLimiting.BankId, v)).getOrElse(NullRef(RateLimiting.BankId))
|
||||
val byApiVersionParam = apiVersion.map(v => By(RateLimiting.ApiVersion, v)).getOrElse(NullRef(RateLimiting.ApiVersion))
|
||||
val byApiNameParam = apiName.map(v => By(RateLimiting.ApiName, v)).getOrElse(NullRef(RateLimiting.ApiName))
|
||||
|
||||
RateLimiting.findAll(
|
||||
byConsumerParam, byBankParam, byApiVersionParam, byApiNameParam,
|
||||
OrderBy(RateLimiting.updatedAt, Descending)
|
||||
).headOption
|
||||
}
|
||||
|
||||
def createOrUpdateConsumerCallLimits(consumerId: String,
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
@ -113,64 +135,40 @@ object MappedRateLimitingProvider extends RateLimitingProviderTrait {
|
||||
perDay: Option[String],
|
||||
perWeek: Option[String],
|
||||
perMonth: Option[String]): Future[Box[RateLimiting]] = Future {
|
||||
|
||||
def createRateLimit(c: RateLimiting): Box[RateLimiting] = {
|
||||
|
||||
def createOrUpdateRateLimit(c: RateLimiting): Box[RateLimiting] = {
|
||||
tryo {
|
||||
c.FromDate(fromDate)
|
||||
c.ToDate(toDate)
|
||||
perSecond match {
|
||||
case Some(v) => c.PerSecondCallLimit(v.toLong)
|
||||
case None =>
|
||||
}
|
||||
perMinute match {
|
||||
case Some(v) => c.PerMinuteCallLimit(v.toLong)
|
||||
case None =>
|
||||
}
|
||||
perHour match {
|
||||
case Some(v) => c.PerHourCallLimit(v.toLong)
|
||||
case None =>
|
||||
}
|
||||
perDay match {
|
||||
case Some(v) => c.PerDayCallLimit(v.toLong)
|
||||
case None =>
|
||||
}
|
||||
perWeek match {
|
||||
case Some(v) => c.PerWeekCallLimit(v.toLong)
|
||||
case None =>
|
||||
}
|
||||
perMonth match {
|
||||
case Some(v) => c.PerMonthCallLimit(v.toLong)
|
||||
case None =>
|
||||
}
|
||||
bankId match {
|
||||
case Some(v) => c.BankId(v)
|
||||
case None => c.BankId(null)
|
||||
}
|
||||
apiName match {
|
||||
case Some(v) => c.ApiName(v)
|
||||
case None => c.ApiName(null)
|
||||
}
|
||||
apiVersion match {
|
||||
case Some(v) => c.ApiVersion(v)
|
||||
case None => c.ApiVersion(null)
|
||||
}
|
||||
|
||||
perSecond.foreach(v => c.PerSecondCallLimit(v.toLong))
|
||||
perMinute.foreach(v => c.PerMinuteCallLimit(v.toLong))
|
||||
perHour.foreach(v => c.PerHourCallLimit(v.toLong))
|
||||
perDay.foreach(v => c.PerDayCallLimit(v.toLong))
|
||||
perWeek.foreach(v => c.PerWeekCallLimit(v.toLong))
|
||||
perMonth.foreach(v => c.PerMonthCallLimit(v.toLong))
|
||||
|
||||
c.BankId(bankId.orNull)
|
||||
c.ApiName(apiName.orNull)
|
||||
c.ApiVersion(apiVersion.orNull)
|
||||
c.ConsumerId(consumerId)
|
||||
|
||||
// 👇 bump timestamp for last-write-wins
|
||||
c.updatedAt(new Date())
|
||||
|
||||
c.saveMe()
|
||||
}
|
||||
}
|
||||
|
||||
val byConsumerParam = By(RateLimiting.ConsumerId, consumerId)
|
||||
val byBankParam = if(bankId.isDefined) By(RateLimiting.BankId, bankId.get) else NullRef(RateLimiting.BankId)
|
||||
val byApiVersionParam = if(apiVersion.isDefined) By(RateLimiting.ApiVersion, apiVersion.get) else NullRef(RateLimiting.ApiVersion)
|
||||
val byApiNameParam = if(apiName.isDefined) By(RateLimiting.ApiName, apiName.get) else NullRef(RateLimiting.ApiName)
|
||||
|
||||
val rateLimit = RateLimiting.find(byConsumerParam, byBankParam, byApiVersionParam, byApiNameParam)
|
||||
val result = rateLimit match {
|
||||
case Full(limit) => createRateLimit(limit)
|
||||
case _ => createRateLimit(RateLimiting.create)
|
||||
val result = findMostRecentRateLimitCommon(consumerId, bankId, apiVersion, apiName) match {
|
||||
case Some(limit) => createOrUpdateRateLimit(limit)
|
||||
case None => createOrUpdateRateLimit(RateLimiting.create)
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
class RateLimiting extends RateLimitingTrait with LongKeyedMapper[RateLimiting] with IdPK with CreatedUpdated {
|
||||
|
||||
@ -17,6 +17,7 @@ trait RateLimitingProviderTrait {
|
||||
def getAll(): Future[List[RateLimiting]]
|
||||
def getAllByConsumerId(consumerId: String, date: Option[Date] = None): Future[List[RateLimiting]]
|
||||
def getByConsumerId(consumerId: String, apiVersion: String, apiName: String, date: Option[Date] = None): Future[Box[RateLimiting]]
|
||||
def findMostRecentRateLimit(consumerId: String, bankId: Option[String], apiVersion: Option[String], apiName: Option[String]): Future[Option[RateLimiting]]
|
||||
def createOrUpdateConsumerCallLimits(consumerId: String,
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
package code.regulatedentities
|
||||
|
||||
import code.regulatedentities.attribute.RegulatedEntityAttribute
|
||||
import code.util.MappedUUID
|
||||
import com.openbankproject.commons.model.{RegulatedEntityTrait,RegulatedEntityAttributeSimple}
|
||||
import com.openbankproject.commons.model.{RegulatedEntityAttributeSimple, RegulatedEntityTrait}
|
||||
import net.liftweb.common.Box
|
||||
import net.liftweb.common.Box.tryo
|
||||
import net.liftweb.mapper._
|
||||
@ -120,20 +121,13 @@ class MappedRegulatedEntity extends RegulatedEntityTrait with LongKeyedMapper[Ma
|
||||
override def entityCountry: String = EntityCountry.get
|
||||
override def entityWebSite: String = EntityWebSite.get
|
||||
override def services: String = Services.get
|
||||
override def attributes: Option[List[RegulatedEntityAttributeSimple]] = Some(
|
||||
List(
|
||||
RegulatedEntityAttributeSimple(
|
||||
attributeType="STRING",
|
||||
name="CERTIFICATE_SERIAL_NUMBER",
|
||||
value="1082"
|
||||
),
|
||||
RegulatedEntityAttributeSimple(
|
||||
attributeType="STRING",
|
||||
name="CERTIFICATE_CA_NAME",
|
||||
value="BNM CA (test)"
|
||||
),
|
||||
))
|
||||
// override def attributes: Option[List[RegulatedEntityAttributeSimple]] = None //not for mapped mode yet, will add it later.
|
||||
override def attributes: Option[List[RegulatedEntityAttributeSimple]] = {
|
||||
Some(
|
||||
RegulatedEntityAttribute.findAll(
|
||||
By(RegulatedEntityAttribute.RegulatedEntityId_, EntityId.get)
|
||||
).map(i => RegulatedEntityAttributeSimple(i.attributeType.toString, i.name, i.value))
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -1,18 +1,17 @@
|
||||
package code.regulatedentities
|
||||
|
||||
|
||||
import com.openbankproject.commons.model.RegulatedEntityTrait
|
||||
import com.openbankproject.commons.model.{RegulatedEntityTrait}
|
||||
import net.liftweb.common.{Box, Logger}
|
||||
import net.liftweb.util.SimpleInjector
|
||||
import code.util.Helper.MdcLoggable
|
||||
|
||||
object RegulatedEntityX extends SimpleInjector {
|
||||
val regulatedEntityProvider = new Inject(buildOne _) {}
|
||||
def buildOne: RegulatedEntityProvider = MappedRegulatedEntityProvider
|
||||
}
|
||||
/* For ProductFee */
|
||||
trait RegulatedEntityProvider {
|
||||
|
||||
private val logger = Logger(classOf[RegulatedEntityProvider])
|
||||
trait RegulatedEntityProvider extends MdcLoggable {
|
||||
|
||||
def getRegulatedEntities(): List[RegulatedEntityTrait]
|
||||
|
||||
@ -33,4 +32,4 @@ trait RegulatedEntityProvider {
|
||||
|
||||
def deleteRegulatedEntity(id: String): Box[Boolean]
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
package code.signingbaskets
|
||||
|
||||
|
||||
import com.openbankproject.commons.model.{SigningBasketContent, SigningBasketTrait}
|
||||
import net.liftweb.common.{Box, Logger}
|
||||
import net.liftweb.util.SimpleInjector
|
||||
import code.util.Helper.MdcLoggable
|
||||
|
||||
object SigningBasketX extends SimpleInjector {
|
||||
val signingBasketProvider: SigningBasketX.Inject[SigningBasketProvider] = new Inject(buildOne _) {}
|
||||
private def buildOne: SigningBasketProvider = MappedSigningBasketProvider
|
||||
}
|
||||
|
||||
trait SigningBasketProvider {
|
||||
|
||||
private val logger = Logger(classOf[SigningBasketProvider])
|
||||
trait SigningBasketProvider extends MdcLoggable {
|
||||
|
||||
def getSigningBaskets(): List[SigningBasketTrait]
|
||||
|
||||
@ -24,4 +24,4 @@ trait SigningBasketProvider {
|
||||
|
||||
def deleteSigningBasket(id: String): Box[Boolean]
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,7 +156,7 @@ class MappedTransaction extends LongKeyedMapper[MappedTransaction] with IdPK wit
|
||||
tStartDate.get,
|
||||
Some(tFinishDate.get),
|
||||
newBalance,
|
||||
Some(status.get)))
|
||||
Option(status.get).map(_.toString)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3,13 +3,12 @@ package code.transactionRequestAttribute
|
||||
import com.openbankproject.commons.model.enums.TransactionRequestAttributeType
|
||||
import com.openbankproject.commons.model.{BankId, TransactionRequestAttributeJsonV400, TransactionRequestAttributeTrait, TransactionRequestId, ViewId}
|
||||
import net.liftweb.common.{Box, Logger}
|
||||
import code.util.Helper.MdcLoggable
|
||||
|
||||
import scala.collection.immutable.List
|
||||
import scala.concurrent.Future
|
||||
|
||||
trait TransactionRequestAttributeProvider {
|
||||
|
||||
private val logger = Logger(classOf[TransactionRequestAttributeProvider])
|
||||
trait TransactionRequestAttributeProvider extends MdcLoggable {
|
||||
|
||||
def getTransactionRequestAttributesFromProvider(transactionRequestId: TransactionRequestId): Future[Box[List[TransactionRequestAttributeTrait]]]
|
||||
|
||||
@ -23,9 +22,9 @@ trait TransactionRequestAttributeProvider {
|
||||
def getTransactionRequestAttributeById(transactionRequestAttributeId: String): Future[Box[TransactionRequestAttributeTrait]]
|
||||
|
||||
def getTransactionRequestIdsByAttributeNameValues(bankId: BankId, params: Map[String, List[String]], isPersonal: Boolean): Future[Box[List[String]]]
|
||||
|
||||
|
||||
def getByAttributeNameValues(bankId: BankId, params: Map[String, List[String]], isPersonal: Boolean): Future[Box[List[TransactionRequestAttributeTrait]]]
|
||||
|
||||
|
||||
def createOrUpdateTransactionRequestAttribute(bankId: BankId,
|
||||
transactionRequestId: TransactionRequestId,
|
||||
transactionRequestAttributeId: Option[String],
|
||||
@ -40,4 +39,4 @@ trait TransactionRequestAttributeProvider {
|
||||
|
||||
def deleteTransactionRequestAttribute(transactionRequestAttributeId: String): Future[Box[Boolean]]
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import com.openbankproject.commons.model.enums.TransactionAttributeType
|
||||
import com.openbankproject.commons.model.{BankId, TransactionAttribute, TransactionId, ViewId}
|
||||
import net.liftweb.common.{Box, Logger}
|
||||
import net.liftweb.util.SimpleInjector
|
||||
import code.util.Helper.MdcLoggable
|
||||
|
||||
import scala.collection.immutable.List
|
||||
import scala.concurrent.Future
|
||||
@ -29,9 +30,7 @@ object TransactionAttributeX extends SimpleInjector {
|
||||
|
||||
}
|
||||
|
||||
trait TransactionAttributeProvider {
|
||||
|
||||
private val logger = Logger(classOf[TransactionAttributeProvider])
|
||||
trait TransactionAttributeProvider extends MdcLoggable {
|
||||
|
||||
def getTransactionAttributesFromProvider(transactionId: TransactionId): Future[Box[List[TransactionAttribute]]]
|
||||
def getTransactionAttributes(bankId: BankId,
|
||||
@ -45,7 +44,7 @@ trait TransactionAttributeProvider {
|
||||
def getTransactionAttributeById(transactionAttributeId: String): Future[Box[TransactionAttribute]]
|
||||
|
||||
def getTransactionIdsByAttributeNameValues(bankId: BankId, params: Map[String, List[String]]): Future[Box[List[String]]]
|
||||
|
||||
|
||||
def createOrUpdateTransactionAttribute(bankId: BankId,
|
||||
transactionId: TransactionId,
|
||||
transactionAttributeId: Option[String],
|
||||
@ -56,7 +55,7 @@ trait TransactionAttributeProvider {
|
||||
def createTransactionAttributes(bankId: BankId,
|
||||
transactionId: TransactionId,
|
||||
transactionAttributes: List[TransactionAttribute]): Future[Box[List[TransactionAttribute]]]
|
||||
|
||||
|
||||
def deleteTransactionAttribute(transactionAttributeId: String): Future[Box[Boolean]]
|
||||
// End of Trait
|
||||
}
|
||||
|
||||
@ -12,14 +12,13 @@ import com.openbankproject.commons.model._
|
||||
import com.openbankproject.commons.model.enums.TransactionRequestTypes.{COUNTERPARTY, SEPA}
|
||||
import com.openbankproject.commons.model.enums.{AccountRoutingScheme, TransactionRequestStatus, TransactionRequestTypes}
|
||||
import net.liftweb.common.{Box, Failure, Full, Logger}
|
||||
import code.util.Helper.MdcLoggable
|
||||
import net.liftweb.json
|
||||
import net.liftweb.json.JsonAST.{JField, JObject, JString}
|
||||
import net.liftweb.mapper._
|
||||
import net.liftweb.util.Helpers._
|
||||
|
||||
object MappedTransactionRequestProvider extends TransactionRequestProvider {
|
||||
|
||||
private val logger = Logger(classOf[TransactionRequestProvider])
|
||||
object MappedTransactionRequestProvider extends TransactionRequestProvider with MdcLoggable {
|
||||
|
||||
override def getMappedTransactionRequest(transactionRequestId: TransactionRequestId): Box[MappedTransactionRequest] =
|
||||
MappedTransactionRequest.find(By(MappedTransactionRequest.mTransactionRequestId, transactionRequestId.value))
|
||||
@ -51,7 +50,7 @@ object MappedTransactionRequestProvider extends TransactionRequestProvider {
|
||||
By(MappedTransactionRequest.mTransactionIDs, transactionId.value)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
override def bulkDeleteTransactionRequests(): Boolean = {
|
||||
MappedTransactionRequest.bulkDelete_!!()
|
||||
}
|
||||
@ -101,7 +100,7 @@ object MappedTransactionRequestProvider extends TransactionRequestProvider {
|
||||
.orElse(toAccount.accountRoutings.headOption)
|
||||
case _ => toAccount.accountRoutings.headOption
|
||||
}
|
||||
|
||||
|
||||
val counterpartyIdOption = TransactionRequestTypes.withName(transactionRequestType.value) match {
|
||||
case COUNTERPARTY => Some(transactionRequestCommonBody.asInstanceOf[TransactionRequestBodyCounterpartyJSON].to.counterparty_id)
|
||||
case _ => None
|
||||
@ -109,10 +108,10 @@ object MappedTransactionRequestProvider extends TransactionRequestProvider {
|
||||
|
||||
val (paymentStartDate, paymentEndDate, executionRule, frequency, dayOfExecution) = if(paymentService == Some("periodic-payments")){
|
||||
val paymentFields = berlinGroupPayments.asInstanceOf[Option[PeriodicSepaCreditTransfersBerlinGroupV13]]
|
||||
|
||||
|
||||
val paymentStartDate = paymentFields.map(_.startDate).map(DateWithMsFormat.parse).orNull
|
||||
val paymentEndDate = paymentFields.flatMap(_.endDate).map(DateWithMsFormat.parse).orNull
|
||||
|
||||
|
||||
val executionRule = paymentFields.flatMap(_.executionRule).orNull
|
||||
val frequency = paymentFields.map(_.frequency).orNull
|
||||
val dayOfExecution = paymentFields.flatMap(_.dayOfExecution).orNull
|
||||
@ -125,7 +124,7 @@ object MappedTransactionRequestProvider extends TransactionRequestProvider {
|
||||
val consentIdOption = callContext.map(_.requestHeaders).map(APIUtil.getConsentIdRequestHeaderValue).flatten
|
||||
val consentOption = consentIdOption.map(consentId =>Consents.consentProvider.vend.getConsentByConsentId(consentId).toOption).flatten
|
||||
val consentReferenceIdOption = consentOption.map(_.consentReferenceId)
|
||||
|
||||
|
||||
// Note: We don't save transaction_ids, status and challenge here.
|
||||
val mappedTransactionRequest = MappedTransactionRequest.create
|
||||
|
||||
@ -158,9 +157,9 @@ object MappedTransactionRequestProvider extends TransactionRequestProvider {
|
||||
.mOtherBankRoutingAddress(toAccount.attributes.flatMap(_.find(_.name == "BANK_ROUTING_ADDRESS")
|
||||
.map(_.value)).getOrElse(toAccount.bankRoutingScheme))
|
||||
// We need transfer CounterpartyTrait to BankAccount, so We lost some data. can not fill the following fields .
|
||||
//.mThisBankId(toAccount.bankId.value)
|
||||
//.mThisBankId(toAccount.bankId.value)
|
||||
//.mThisAccountId(toAccount.accountId.value)
|
||||
//.mThisViewId(toAccount.v)
|
||||
//.mThisViewId(toAccount.v)
|
||||
.mCounterpartyId(counterpartyIdOption.getOrElse(null))
|
||||
//.mIsBeneficiary(toAccount.isBeneficiary)
|
||||
|
||||
@ -169,7 +168,7 @@ object MappedTransactionRequestProvider extends TransactionRequestProvider {
|
||||
.mBody_Value_Amount(transactionRequestCommonBody.value.amount)
|
||||
.mBody_Description(transactionRequestCommonBody.description)
|
||||
.mDetails(details) // This is the details / body of the request (contains all fields in the body)
|
||||
|
||||
|
||||
.mDetails(details) // This is the details / body of the request (contains all fields in the body)
|
||||
|
||||
.mPaymentStartDate(paymentStartDate)
|
||||
@ -226,9 +225,7 @@ object MappedTransactionRequestProvider extends TransactionRequestProvider {
|
||||
|
||||
}
|
||||
|
||||
class MappedTransactionRequest extends LongKeyedMapper[MappedTransactionRequest] with IdPK with CreatedUpdated with CustomJsonFormats {
|
||||
|
||||
private val logger = Logger(classOf[MappedTransactionRequest])
|
||||
class MappedTransactionRequest extends LongKeyedMapper[MappedTransactionRequest] with IdPK with CreatedUpdated with CustomJsonFormats with MdcLoggable {
|
||||
|
||||
override def getSingleton = MappedTransactionRequest
|
||||
|
||||
@ -278,56 +275,56 @@ class MappedTransactionRequest extends LongKeyedMapper[MappedTransactionRequest]
|
||||
object mOtherBankRoutingScheme extends MappedString(this, 32)
|
||||
object mOtherBankRoutingAddress extends MappedString(this, 64)
|
||||
object mIsBeneficiary extends MappedBoolean(this)
|
||||
|
||||
//Here are for Berlin Group V1.3
|
||||
|
||||
//Here are for Berlin Group V1.3
|
||||
object mPaymentStartDate extends MappedDate(this) //BGv1.3 Open API Document example value: "startDate":"2024-08-12"
|
||||
object mPaymentEndDate extends MappedDate(this) //BGv1.3 Open API Document example value: "startDate":"2025-08-01"
|
||||
object mPaymentExecutionRule extends MappedString(this, 64) //BGv1.3 Open API Document example value: "executionRule":"preceding"
|
||||
object mPaymentFrequency extends MappedString(this, 64) //BGv1.3 Open API Document example value: "frequency":"Monthly",
|
||||
object mPaymentDayOfExecution extends MappedString(this, 64)//BGv1.3 Open API Document example value: "dayOfExecution":"01"
|
||||
|
||||
object mPaymentExecutionRule extends MappedString(this, 64) //BGv1.3 Open API Document example value: "executionRule":"preceding"
|
||||
object mPaymentFrequency extends MappedString(this, 64) //BGv1.3 Open API Document example value: "frequency":"Monthly",
|
||||
object mPaymentDayOfExecution extends MappedString(this, 64)//BGv1.3 Open API Document example value: "dayOfExecution":"01"
|
||||
|
||||
object mConsentReferenceId extends MappedString(this, 64)
|
||||
|
||||
object mApiStandard extends MappedString(this, 50)
|
||||
object mApiVersion extends MappedString(this, 50)
|
||||
|
||||
|
||||
def updateStatus(newStatus: String) = {
|
||||
mStatus.set(newStatus)
|
||||
}
|
||||
|
||||
def toTransactionRequest : Option[TransactionRequest] = {
|
||||
|
||||
|
||||
val details = mDetails.toString
|
||||
|
||||
|
||||
val parsedDetails = json.parse(details)
|
||||
|
||||
|
||||
val transactionType = mType.get
|
||||
|
||||
|
||||
val t_amount = AmountOfMoney (
|
||||
currency = mBody_Value_Currency.get,
|
||||
amount = mBody_Value_Amount.get
|
||||
)
|
||||
|
||||
|
||||
val t_to_sandbox_tan = if (
|
||||
TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.SANDBOX_TAN ||
|
||||
TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.ACCOUNT_OTP ||
|
||||
TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.SANDBOX_TAN ||
|
||||
TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.ACCOUNT_OTP ||
|
||||
TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.ACCOUNT)
|
||||
Some(TransactionRequestAccount (bank_id = mTo_BankId.get, account_id = mTo_AccountId.get))
|
||||
else
|
||||
None
|
||||
|
||||
|
||||
val t_to_sepa = if (TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.SEPA){
|
||||
val ibanList: List[String] = for {
|
||||
JObject(child) <- parsedDetails
|
||||
JField("iban", JString(iban)) <- child
|
||||
} yield
|
||||
iban
|
||||
val ibanValue = if (ibanList.isEmpty) "" else ibanList.head
|
||||
val ibanValue = if (ibanList.isEmpty) "" else ibanList.head
|
||||
Some(TransactionRequestIban(iban = ibanValue))
|
||||
}
|
||||
else
|
||||
None
|
||||
|
||||
|
||||
val t_to_counterparty = if (TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.COUNTERPARTY ||
|
||||
TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.CARD){
|
||||
val counterpartyIdList: List[String] = for {
|
||||
@ -363,29 +360,29 @@ class MappedTransactionRequest extends LongKeyedMapper[MappedTransactionRequest]
|
||||
otherAccountSecondaryRoutingScheme,
|
||||
otherAccountSecondaryRoutingAddress
|
||||
)
|
||||
if(transactionRequestSimples.isEmpty)
|
||||
Some(TransactionRequestSimple("","","","","","","",""))
|
||||
else
|
||||
if(transactionRequestSimples.isEmpty)
|
||||
Some(TransactionRequestSimple("","","","","","","",""))
|
||||
else
|
||||
Some(transactionRequestSimples.head)
|
||||
}
|
||||
else
|
||||
None
|
||||
|
||||
|
||||
val t_to_transfer_to_phone = if (TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.TRANSFER_TO_PHONE && details.nonEmpty)
|
||||
Some(parsedDetails.extract[TransactionRequestTransferToPhone])
|
||||
else
|
||||
None
|
||||
|
||||
val t_to_transfer_to_atm = if (TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.TRANSFER_TO_ATM && details.nonEmpty)
|
||||
val t_to_transfer_to_atm = if (TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.TRANSFER_TO_ATM && details.nonEmpty)
|
||||
Some(parsedDetails.extract[TransactionRequestTransferToAtm])
|
||||
else
|
||||
None
|
||||
|
||||
|
||||
val t_to_transfer_to_account = if (TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.TRANSFER_TO_ACCOUNT && details.nonEmpty)
|
||||
Some(parsedDetails.extract[TransactionRequestTransferToAccount])
|
||||
else
|
||||
None
|
||||
|
||||
|
||||
val t_to_agent = if (TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.AGENT_CASH_WITHDRAWAL && details.nonEmpty) {
|
||||
val agentNumberList: List[String] = for {
|
||||
JObject(child) <- parsedDetails
|
||||
@ -406,20 +403,20 @@ class MappedTransactionRequest extends LongKeyedMapper[MappedTransactionRequest]
|
||||
}
|
||||
else
|
||||
None
|
||||
|
||||
|
||||
|
||||
|
||||
//This is Berlin Group Types:
|
||||
val t_to_sepa_credit_transfers = if (TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.SEPA_CREDIT_TRANSFERS && details.nonEmpty)
|
||||
Some(parsedDetails.extract[SepaCreditTransfers]) //TODO, here may need a internal case class, but for now, we used it from request json body.
|
||||
else
|
||||
None
|
||||
|
||||
|
||||
val t_body = TransactionRequestBodyAllTypes(
|
||||
to_sandbox_tan = t_to_sandbox_tan,
|
||||
to_sepa = t_to_sepa,
|
||||
to_counterparty = t_to_counterparty,
|
||||
to_simple = t_to_simple,
|
||||
to_transfer_to_phone = t_to_transfer_to_phone,
|
||||
to_transfer_to_phone = t_to_transfer_to_phone,
|
||||
to_transfer_to_atm = t_to_transfer_to_atm,
|
||||
to_transfer_to_account = t_to_transfer_to_account,
|
||||
to_sepa_credit_transfers = t_to_sepa_credit_transfers,
|
||||
|
||||
@ -5,6 +5,7 @@ import code.api.util.{APIUtil, CallContext}
|
||||
import com.openbankproject.commons.model.{TransactionRequest, TransactionRequestChallenge, TransactionRequestCharge, _}
|
||||
import net.liftweb.common.{Box, Logger}
|
||||
import net.liftweb.util.SimpleInjector
|
||||
import code.util.Helper.MdcLoggable
|
||||
|
||||
object TransactionRequests extends SimpleInjector {
|
||||
|
||||
@ -29,9 +30,7 @@ object TransactionRequests extends SimpleInjector {
|
||||
|
||||
}
|
||||
|
||||
trait TransactionRequestProvider {
|
||||
|
||||
private val logger = Logger(classOf[TransactionRequestProvider])
|
||||
trait TransactionRequestProvider extends MdcLoggable {
|
||||
|
||||
final def getTransactionRequest(transactionRequestId : TransactionRequestId) : Box[TransactionRequest] = {
|
||||
getTransactionRequestFromProvider(transactionRequestId)
|
||||
@ -80,7 +79,7 @@ trait TransactionRequestProvider {
|
||||
apiStandard: Option[String],
|
||||
apiVersion: Option[String],
|
||||
callContext: Option[CallContext]): Box[TransactionRequest]
|
||||
|
||||
|
||||
def saveTransactionRequestTransactionImpl(transactionRequestId: TransactionRequestId, transactionId: TransactionId): Box[Boolean]
|
||||
def saveTransactionRequestChallengeImpl(transactionRequestId: TransactionRequestId, challenge: TransactionRequestChallenge): Box[Boolean]
|
||||
def saveTransactionRequestStatusImpl(transactionRequestId: TransactionRequestId, status: String): Box[Boolean]
|
||||
@ -88,5 +87,3 @@ trait TransactionRequestProvider {
|
||||
def bulkDeleteTransactionRequestsByTransactionId(transactionId: TransactionId): Boolean
|
||||
def bulkDeleteTransactionRequests(): Boolean
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import code.TransactionTypes.TransactionTypeProvider
|
||||
import code.model._
|
||||
import code.TransactionTypes.TransactionType._
|
||||
import code.util.{MediumString, UUIDString}
|
||||
import code.util.Helper.MdcLoggable
|
||||
import net.liftweb.common._
|
||||
import net.liftweb.mapper._
|
||||
import code.api.util.ErrorMessages
|
||||
@ -61,9 +62,7 @@ object MappedTransactionTypeProvider extends TransactionTypeProvider {
|
||||
}
|
||||
|
||||
}
|
||||
class MappedTransactionType extends LongKeyedMapper[MappedTransactionType] with IdPK with CreatedUpdated {
|
||||
|
||||
private val logger = Logger(classOf[MappedTransactionType])
|
||||
class MappedTransactionType extends LongKeyedMapper[MappedTransactionType] with IdPK with CreatedUpdated with MdcLoggable {
|
||||
|
||||
override def getSingleton = MappedTransactionType
|
||||
|
||||
@ -109,4 +108,4 @@ class MappedTransactionType extends LongKeyedMapper[MappedTransactionType] with
|
||||
|
||||
object MappedTransactionType extends MappedTransactionType with LongKeyedMetaMapper[MappedTransactionType] {
|
||||
override def dbIndexes = UniqueIndex(mTransactionTypeId) :: UniqueIndex(mBankId, mShortCode) :: super.dbIndexes
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import code.transaction_types.MappedTransactionTypeProvider
|
||||
import com.openbankproject.commons.model.{AmountOfMoney, BankId, TransactionTypeId}
|
||||
import net.liftweb.common.{Box, Logger}
|
||||
import net.liftweb.util.SimpleInjector
|
||||
import code.util.Helper.MdcLoggable
|
||||
|
||||
|
||||
// See http://simply.liftweb.net/index-8.2.html for info about "vend" and SimpleInjector
|
||||
@ -48,15 +49,13 @@ object TransactionType extends SimpleInjector {
|
||||
case "mapped" => MappedTransactionTypeProvider
|
||||
case ttc: String => throw new IllegalArgumentException("No such connector for Transaction Types: " + ttc)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
trait TransactionTypeProvider {
|
||||
trait TransactionTypeProvider extends MdcLoggable {
|
||||
|
||||
import code.TransactionTypes.TransactionType.TransactionType
|
||||
|
||||
private val logger = Logger(classOf[TransactionTypeProvider])
|
||||
|
||||
|
||||
// Transaction types for bank (we may add getTransactionTypesForBankAccount and getTransactionTypesForBankAccountView)
|
||||
final def getTransactionTypesForBank(bankId : BankId) : Option[List[TransactionType]] = {
|
||||
@ -77,4 +76,3 @@ trait TransactionTypeProvider {
|
||||
|
||||
protected def createOrUpdateTransactionTypeAtProvider(postedData: TransactionTypeJsonV200): Box[TransactionType]
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
package code.users
|
||||
|
||||
/* For UserAttribute */
|
||||
|
||||
import code.api.util.APIUtil
|
||||
import com.openbankproject.commons.model.AccountAttribute
|
||||
import com.openbankproject.commons.model.enums.{AccountAttributeType, UserAttributeType}
|
||||
import code.util.Helper.MdcLoggable
|
||||
import com.openbankproject.commons.model.enums.UserAttributeType
|
||||
import net.liftweb.common.{Box, Logger}
|
||||
import net.liftweb.util.SimpleInjector
|
||||
|
||||
@ -14,7 +15,7 @@ object UserAttributeProvider extends SimpleInjector {
|
||||
|
||||
val userAttributeProvider = new Inject(buildOne _) {}
|
||||
|
||||
def buildOne: UserAttributeProvider = MappedUserAttributeProvider
|
||||
def buildOne: UserAttributeProvider = MappedUserAttributeProvider
|
||||
|
||||
// Helper to get the count out of an option
|
||||
def countOfUserAttribute(listOpt: Option[List[UserAttribute]]): Int = {
|
||||
@ -25,12 +26,9 @@ object UserAttributeProvider extends SimpleInjector {
|
||||
count
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
trait UserAttributeProvider {
|
||||
|
||||
private val logger = Logger(classOf[UserAttributeProvider])
|
||||
trait UserAttributeProvider extends MdcLoggable {
|
||||
|
||||
def getUserAttributesByUser(userId: String): Future[Box[List[UserAttribute]]]
|
||||
def getPersonalUserAttributes(userId: String): Future[Box[List[UserAttribute]]]
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package code.util
|
||||
|
||||
import code.api.cache.{Redis, RedisLogger}
|
||||
|
||||
import java.net.{Socket, SocketException, URL}
|
||||
import java.util.UUID.randomUUID
|
||||
import java.util.{Date, GregorianCalendar}
|
||||
@ -24,6 +26,7 @@ import net.liftweb.util.Helpers.tryo
|
||||
import net.sf.cglib.proxy.{Enhancer, MethodInterceptor, MethodProxy}
|
||||
|
||||
import java.lang.reflect.Method
|
||||
import java.text.SimpleDateFormat
|
||||
import scala.concurrent.Future
|
||||
import scala.util.Random
|
||||
import scala.reflect.runtime.universe.Type
|
||||
@ -171,36 +174,36 @@ object Helper extends Loggable {
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param redirectUrl eg: http://localhost:8082/oauthcallback?oauth_token=G5AEA2U1WG404EGHTIGBHKRR4YJZAPPHWKOMNEEV&oauth_verifier=53018
|
||||
* @return http://localhost:8082/oauthcallback
|
||||
*/
|
||||
def getStaticPortionOfRedirectURL(redirectUrl: String): Box[String] = {
|
||||
tryo(redirectUrl.split("\\?")(0)) //return everything before the "?"
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* extract clean redirect url from input value, because input may have some parameters, such as the following examples <br/>
|
||||
* eg1: http://localhost:8082/oauthcallback?....--> http://localhost:8082 <br/>
|
||||
* extract clean redirect url from input value, because input may have some parameters, such as the following examples <br/>
|
||||
* eg1: http://localhost:8082/oauthcallback?....--> http://localhost:8082 <br/>
|
||||
* eg2: http://localhost:8016?oautallback?=3NLMGV ...--> http://localhost:8016
|
||||
*
|
||||
* @param redirectUrl -> http://localhost:8082/oauthcallback?oauth_token=G5AEA2U1WG404EGHTIGBHKRR4YJZAPPHWKOMNEEV&oauth_verifier=53018
|
||||
* @return hostOnlyOfRedirectURL-> http://localhost:8082
|
||||
*/
|
||||
@deprecated("We can not only use hostname as the redirectUrl, now add new method `getStaticPortionOfRedirectURL` ","05.12.2023")
|
||||
@deprecated("We can not only use hostname as the redirectUrl, now add new method `getStaticPortionOfRedirectURL` ","05.12.2023")
|
||||
def getHostOnlyOfRedirectURL(redirectUrl: String): Box[String] = {
|
||||
val url = new URL(redirectUrl) //eg: http://localhost:8082/oauthcallback?oauth_token=G5AEA2U1WG404EGHTIGBHKRR4YJZAPPHWKOMNEEV&oauth_verifier=53018
|
||||
val protocol = url.getProtocol() // http
|
||||
val authority = url.getAuthority()// localhost:8082, this will contain the port.
|
||||
tryo(s"$protocol://$authority") // http://localhost:8082
|
||||
tryo(s"$protocol://$authority") // http://localhost:8082
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* extract Oauth Token String from input value, because input may have some parameters, such as the following examples <br/>
|
||||
* http://localhost:8082/oauthcallback?oauth_token=DKR242MB3IRCUVG35UZ0QQOK3MBS1G2HL2ZIKK2O&oauth_verifier=64465
|
||||
* extract Oauth Token String from input value, because input may have some parameters, such as the following examples <br/>
|
||||
* http://localhost:8082/oauthcallback?oauth_token=DKR242MB3IRCUVG35UZ0QQOK3MBS1G2HL2ZIKK2O&oauth_verifier=64465
|
||||
* --> DKR242MB3IRCUVG35UZ0QQOK3MBS1G2HL2ZIKK2O
|
||||
*
|
||||
* @param input a long url with parameters
|
||||
*
|
||||
* @param input a long url with parameters
|
||||
* @return Oauth Token String
|
||||
*/
|
||||
def extractOauthToken(input: String): Box[String] = {
|
||||
@ -236,7 +239,7 @@ object Helper extends Loggable {
|
||||
* Used for version extraction from props string
|
||||
*/
|
||||
val matchAnyStoredProcedure = "stored_procedure.*|star".r
|
||||
|
||||
|
||||
/**
|
||||
* change the TimeZone to the current TimeZOne
|
||||
* reference the following trait
|
||||
@ -246,25 +249,25 @@ object Helper extends Loggable {
|
||||
*/
|
||||
//TODO need clean this format, we have set the TimeZone in boot.scala
|
||||
val DateFormatWithCurrentTimeZone = new Formats {
|
||||
|
||||
|
||||
import java.text.{ParseException, SimpleDateFormat}
|
||||
|
||||
|
||||
val dateFormat = new DateFormat {
|
||||
def parse(s: String) = try {
|
||||
Some(formatter.parse(s))
|
||||
} catch {
|
||||
case e: ParseException => None
|
||||
}
|
||||
|
||||
|
||||
def format(d: Date) = formatter.format(d)
|
||||
|
||||
|
||||
private def formatter = {
|
||||
val f = dateFormatter
|
||||
f.setTimeZone(new GregorianCalendar().getTimeZone)
|
||||
f
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected def dateFormatter = APIUtil.DateWithMsFormat
|
||||
}
|
||||
|
||||
@ -315,33 +318,95 @@ object Helper extends Loggable {
|
||||
candidatePort
|
||||
}
|
||||
|
||||
|
||||
|
||||
trait MdcLoggable extends Loggable {
|
||||
protected def initiate(): Unit = () // The type is Unit and the only value this type can take is the literal ()
|
||||
protected def surroundWarnMessage(msg: String, title: String = ""): Unit = {
|
||||
|
||||
logger.warn(s"+-${title}${StringUtils.repeat("-", msg.length - title.length)}-+")
|
||||
logger.warn(s"| $msg |")
|
||||
logger.warn(s"+-${StringUtils.repeat("-", msg.length)}-+")
|
||||
}
|
||||
protected def surroundInfoMessage(msg: String, title: String = ""): Unit = {
|
||||
logger.info(s"+-${title}${StringUtils.repeat("-", msg.length - title.length)}-+")
|
||||
logger.info(s"| $msg |")
|
||||
logger.info(s"+-${StringUtils.repeat("-", msg.length)}-+")
|
||||
}
|
||||
protected def surroundErrorMessage(msg: String, title: String = ""): Unit = {
|
||||
logger.error(s"+-${title}${StringUtils.repeat("-", msg.length - title.length)}-+")
|
||||
logger.error(s"| $msg |")
|
||||
logger.error(s"+-${StringUtils.repeat("-", msg.length)}-+")
|
||||
}
|
||||
protected def surroundDebugMessage(msg: String, title: String = ""): Unit = {
|
||||
logger.debug(s"+-${title}${StringUtils.repeat("-", msg.length - title.length)}-+")
|
||||
logger.debug(s"| $msg |")
|
||||
logger.debug(s"+-${StringUtils.repeat("-", msg.length)}-+")
|
||||
|
||||
// Capture the class name of the component mixing in this trait
|
||||
private val clazzName: String = this.getClass.getSimpleName.replaceAll("\\$", "")
|
||||
|
||||
override protected val logger: net.liftweb.common.Logger = {
|
||||
val loggerName = this.getClass.getName
|
||||
|
||||
new net.liftweb.common.Logger {
|
||||
|
||||
private val underlyingLogger = net.liftweb.common.Logger(loggerName)
|
||||
|
||||
private val dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ssX")
|
||||
dateFormat.setTimeZone(java.util.TimeZone.getDefault) // force local TZ
|
||||
|
||||
private def toRedisFormat(msg: AnyRef): String = {
|
||||
val ts = dateFormat.format(new Date())
|
||||
val thread = Thread.currentThread().getName
|
||||
s"[$ts] [$thread] [$clazzName] ${msg.toString}"
|
||||
}
|
||||
|
||||
// INFO
|
||||
override def info(msg: => AnyRef): Unit = {
|
||||
underlyingLogger.info(msg)
|
||||
RedisLogger.logAsync(RedisLogger.LogLevel.INFO, toRedisFormat(msg))
|
||||
}
|
||||
|
||||
override def info(msg: => AnyRef, t: => Throwable): Unit = {
|
||||
underlyingLogger.info(msg, t)
|
||||
RedisLogger.logAsync(RedisLogger.LogLevel.INFO, toRedisFormat(msg) + "\n" + t.toString)
|
||||
}
|
||||
|
||||
// WARN
|
||||
override def warn(msg: => AnyRef): Unit = {
|
||||
underlyingLogger.warn(msg)
|
||||
RedisLogger.logAsync(RedisLogger.LogLevel.WARNING, toRedisFormat(msg))
|
||||
}
|
||||
|
||||
override def warn(msg: => AnyRef, t: Throwable): Unit = {
|
||||
underlyingLogger.warn(msg, t)
|
||||
RedisLogger.logAsync(RedisLogger.LogLevel.WARNING, toRedisFormat(msg) + "\n" + t.toString)
|
||||
}
|
||||
|
||||
// ERROR
|
||||
override def error(msg: => AnyRef): Unit = {
|
||||
underlyingLogger.error(msg)
|
||||
RedisLogger.logAsync(RedisLogger.LogLevel.ERROR, toRedisFormat(msg))
|
||||
}
|
||||
|
||||
override def error(msg: => AnyRef, t: Throwable): Unit = {
|
||||
underlyingLogger.error(msg, t)
|
||||
RedisLogger.logAsync(RedisLogger.LogLevel.ERROR, toRedisFormat(msg) + "\n" + t.toString)
|
||||
}
|
||||
|
||||
// DEBUG
|
||||
override def debug(msg: => AnyRef): Unit = {
|
||||
underlyingLogger.debug(msg)
|
||||
RedisLogger.logAsync(RedisLogger.LogLevel.DEBUG, toRedisFormat(msg))
|
||||
}
|
||||
|
||||
override def debug(msg: => AnyRef, t: Throwable): Unit = {
|
||||
underlyingLogger.debug(msg, t)
|
||||
RedisLogger.logAsync(RedisLogger.LogLevel.DEBUG, toRedisFormat(msg) + "\n" + t.toString)
|
||||
}
|
||||
|
||||
// TRACE
|
||||
override def trace(msg: => AnyRef): Unit = {
|
||||
underlyingLogger.trace(msg)
|
||||
RedisLogger.logAsync(RedisLogger.LogLevel.TRACE, toRedisFormat(msg))
|
||||
}
|
||||
|
||||
// Delegate enabled checks
|
||||
override def isDebugEnabled: Boolean = underlyingLogger.isDebugEnabled
|
||||
override def isErrorEnabled: Boolean = underlyingLogger.isErrorEnabled
|
||||
override def isInfoEnabled: Boolean = underlyingLogger.isInfoEnabled
|
||||
override def isTraceEnabled: Boolean = underlyingLogger.isTraceEnabled
|
||||
override def isWarnEnabled: Boolean = underlyingLogger.isWarnEnabled
|
||||
}
|
||||
}
|
||||
|
||||
protected def initiate(): Unit = ()
|
||||
|
||||
initiate()
|
||||
MDC.put("host" -> getHostname)
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Return true for Y, YES and true etc.
|
||||
*/
|
||||
@ -393,7 +458,7 @@ object Helper extends Loggable {
|
||||
case _ => Nil
|
||||
}
|
||||
default.getOrElse(words.mkString(" ") + ".")
|
||||
} else
|
||||
} else
|
||||
S.?(message)
|
||||
} else {
|
||||
logger.error(s"i18n(message($message), default${default}: Attempted to use resource bundles outside of an initialized S scope. " +
|
||||
@ -411,8 +476,8 @@ object Helper extends Loggable {
|
||||
* @return modified instance
|
||||
*/
|
||||
private def convertId[T](
|
||||
obj: T,
|
||||
customerIdConverter: String=> String,
|
||||
obj: T,
|
||||
customerIdConverter: String=> String,
|
||||
accountIdConverter: String=> String,
|
||||
transactionIdConverter: String=> String
|
||||
): T = {
|
||||
@ -433,7 +498,7 @@ object Helper extends Loggable {
|
||||
(ownerType <:< typeOf[AccountBalances] && fieldName.equalsIgnoreCase("id") && fieldType =:= typeOf[String])||
|
||||
(ownerType <:< typeOf[AccountHeld] && fieldName.equalsIgnoreCase("id") && fieldType =:= typeOf[String])
|
||||
}
|
||||
|
||||
|
||||
def isTransactionId(fieldName: String, fieldType: Type, fieldValue: Any, ownerType: Type) = {
|
||||
ownerType <:< typeOf[TransactionId] ||
|
||||
(fieldName.equalsIgnoreCase("transactionId") && fieldType =:= typeOf[String])||
|
||||
@ -502,10 +567,10 @@ object Helper extends Loggable {
|
||||
|
||||
lazy val result = method.invoke(net.liftweb.http.S, args: _*)
|
||||
val methodName = method.getName
|
||||
|
||||
|
||||
if (methodName.equals("param")&&result.isInstanceOf[Box[String]]&&result.asInstanceOf[Box[String]].isDefined) {
|
||||
//we provide the basic check for all the parameters
|
||||
val resultAfterChecked =
|
||||
val resultAfterChecked =
|
||||
if((args.length>0) && args.apply(0).toString.equalsIgnoreCase("username")) {
|
||||
result.asInstanceOf[Box[String]].filter(APIUtil.checkUsernameString(_)==SILENCE_IS_GOLDEN)
|
||||
}else if((args.length>0) && args.apply(0).toString.equalsIgnoreCase("password")){
|
||||
@ -517,7 +582,7 @@ object Helper extends Loggable {
|
||||
} else{
|
||||
result.asInstanceOf[Box[String]].filter(APIUtil.checkMediumString(_)==SILENCE_IS_GOLDEN)
|
||||
}
|
||||
if(resultAfterChecked.isEmpty) {
|
||||
if(resultAfterChecked.isEmpty) {
|
||||
logger.debug(s"ObpS.${methodName} validation failed. (resultAfterChecked.isEmpty A) The input key is: ${if (args.length>0)args.apply(0) else ""}, value is:$result")
|
||||
}
|
||||
resultAfterChecked
|
||||
@ -532,7 +597,7 @@ object Helper extends Loggable {
|
||||
} else if (methodName.equals("uriAndQueryString") && result.isInstanceOf[Box[String]] && result.asInstanceOf[Box[String]].isDefined ||
|
||||
methodName.equals("queryString") && result.isInstanceOf[Box[String]]&&result.asInstanceOf[Box[String]].isDefined){
|
||||
val resultAfterChecked = result.asInstanceOf[Box[String]].filter(APIUtil.basicUriAndQueryStringValidation(_))
|
||||
if(resultAfterChecked.isEmpty) {
|
||||
if(resultAfterChecked.isEmpty) {
|
||||
logger.debug(s"ObpS.${methodName} validation failed. (resultAfterChecked.isEmpty B) The value is:$result")
|
||||
}
|
||||
resultAfterChecked
|
||||
@ -540,7 +605,7 @@ object Helper extends Loggable {
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val enhancer: Enhancer = new Enhancer()
|
||||
enhancer.setSuperclass(classOf[S])
|
||||
enhancer.setCallback(intercept)
|
||||
@ -602,4 +667,4 @@ object Helper extends Loggable {
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,13 +87,13 @@
|
||||
|
||||
-- OIDC user credentials
|
||||
-- ⚠️ SECURITY: Change this to a strong password (20+ chars, mixed case, numbers, symbols)
|
||||
\set OIDC_USER 'oidc_user'
|
||||
\set OIDC_PASSWORD 'lakij8777fagg'
|
||||
\set OIDC_USER "oidc_user"
|
||||
\set OIDC_PASSWORD '''lakij8777fagg'''
|
||||
|
||||
-- OIDC admin user credentials (for client administration)
|
||||
-- ⚠️ SECURITY: Change this to a strong password (20+ chars, mixed case, numbers, symbols)
|
||||
\set OIDC_ADMIN_USER 'oidc_admin'
|
||||
\set OIDC_ADMIN_PASSWORD 'fhka77uefassEE'
|
||||
\set OIDC_ADMIN_USER "oidc_admin"
|
||||
\set OIDC_ADMIN_PASSWORD '''fhka77uefassEE'''
|
||||
|
||||
-- =============================================================================
|
||||
-- 1. Connect to the OBP database
|
||||
@ -120,7 +120,7 @@ ALTER ROLE :OIDC_ADMIN_USER WITH PASSWORD :OIDC_ADMIN_PASSWORD;
|
||||
|
||||
-- Create the OIDC user with limited privileges
|
||||
CREATE USER :OIDC_USER WITH
|
||||
PASSWORD :'OIDC_PASSWORD'
|
||||
PASSWORD :OIDC_PASSWORD
|
||||
NOSUPERUSER
|
||||
NOCREATEDB
|
||||
NOCREATEROLE
|
||||
@ -134,7 +134,7 @@ ALTER USER :OIDC_USER CONNECTION LIMIT 10;
|
||||
|
||||
-- Create the OIDC admin user with limited privileges
|
||||
CREATE USER :OIDC_ADMIN_USER WITH
|
||||
PASSWORD :'OIDC_ADMIN_PASSWORD'
|
||||
PASSWORD :OIDC_ADMIN_PASSWORD
|
||||
NOSUPERUSER
|
||||
NOCREATEDB
|
||||
NOCREATEROLE
|
||||
@ -143,11 +143,12 @@ CREATE USER :OIDC_ADMIN_USER WITH
|
||||
NOREPLICATION
|
||||
NOBYPASSRLS;
|
||||
|
||||
-- need this so the admin can create rows
|
||||
GRANT USAGE, SELECT ON SEQUENCE consumer_id_seq TO :OIDC_ADMIN_USER;
|
||||
-- TODO: THIS IS NOT WORKING FOR SOME REASON, WE HAVE TO MANUALLY DO THIS LATER
|
||||
-- need this so the admin can create rows
|
||||
GRANT USAGE, SELECT ON SEQUENCE consumer_id_seq TO :OIDC_ADMIN_USER;
|
||||
|
||||
-- double check this
|
||||
GRANT USAGE, SELECT ON SEQUENCE consumer_id_seq TO oidc_admin;
|
||||
-- double check this
|
||||
GRANT USAGE, SELECT ON SEQUENCE consumer_id_seq TO oidc_admin;
|
||||
|
||||
-- Set connection limit for the OIDC admin user
|
||||
ALTER USER :OIDC_ADMIN_USER CONNECTION LIMIT 5;
|
||||
@ -201,8 +202,11 @@ DROP VIEW IF EXISTS v_oidc_clients CASCADE;
|
||||
-- TODO: Add grant_types and scopes fields to consumer table if needed for full OIDC compliance
|
||||
CREATE VIEW v_oidc_clients AS
|
||||
SELECT
|
||||
key_c as client_id,
|
||||
secret as client_secret,
|
||||
consumerid as consumer_id, -- This is really an identifier for management purposes. Its also used to link trusted consumers together.
|
||||
key_c as key, -- The key is the OAuth1 identifier for the app.
|
||||
key_c as client_id, -- The client_id is the OAuth2 identifier for the app.
|
||||
secret, -- The OAuth1 secret
|
||||
secret as client_secret, -- The OAuth2 secret
|
||||
redirecturl as redirect_uris,
|
||||
'authorization_code,refresh_token' as grant_types, -- Default OIDC grant types
|
||||
'openid,profile,email' as scopes, -- Default OIDC scopes
|
||||
@ -296,6 +300,13 @@ GRANT SELECT ON v_oidc_clients TO :OIDC_USER;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON consumer TO :OIDC_ADMIN_USER;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON v_oidc_admin_clients TO :OIDC_ADMIN_USER;
|
||||
|
||||
GRANT USAGE, SELECT ON SEQUENCE consumer_id_seq TO :OIDC_ADMIN_USER;
|
||||
|
||||
-- double check this
|
||||
--GRANT USAGE, SELECT ON SEQUENCE consumer_id_seq TO oidc_admin;
|
||||
|
||||
|
||||
|
||||
\echo 'Permissions granted successfully.'
|
||||
|
||||
-- =============================================================================
|
||||
@ -377,6 +388,18 @@ FROM information_schema.role_table_grants
|
||||
WHERE grantee = :'OIDC_ADMIN_USER'
|
||||
ORDER BY table_schema, table_name;
|
||||
|
||||
|
||||
\echo 'Here are the views:'
|
||||
|
||||
|
||||
\d v_oidc_users;
|
||||
|
||||
\d v_oidc_clients;
|
||||
|
||||
\d v_oidc_admin_clients;
|
||||
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- 7. Display connection information
|
||||
-- =============================================================================
|
||||
|
||||
@ -1947,8 +1947,8 @@
|
||||
</CcyNtry>
|
||||
<!-- Cardano (ADA) -->
|
||||
<CcyNtry>
|
||||
<CtryNm>Cardano</CtryNm>
|
||||
<CcyNm>Cardano</CcyNm>
|
||||
<CtryNm>Cardano_ADA</CtryNm>
|
||||
<CcyNm>ADA</CcyNm>
|
||||
<Ccy>ada</Ccy>
|
||||
<CcyNbr>null</CcyNbr>
|
||||
<CcyMnrUnts>6</CcyMnrUnts> <!-- 1 ADA = 10^6 Lovelace -->
|
||||
|
||||
@ -12,6 +12,7 @@ import code.consent.ConsentStatus
|
||||
import code.model.dataAccess.BankAccountRouting
|
||||
import code.setup.{APIResponse, DefaultUsers}
|
||||
import com.github.dwickern.macros.NameOf.nameOf
|
||||
import com.openbankproject.commons.model.ErrorMessage
|
||||
import com.openbankproject.commons.model.enums.AccountRoutingScheme
|
||||
import net.liftweb.json.Serialization.write
|
||||
import net.liftweb.mapper.By
|
||||
@ -191,10 +192,8 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit
|
||||
Then("We should get a 200 ")
|
||||
response.code should equal(200)
|
||||
response.body.extract[TransactionsJsonV13].account.iban should not be ("")
|
||||
// response.body.extract[TransactionsJsonV13].transactions.booked.head.length >0 should be (true)
|
||||
response.body.extract[TransactionsJsonV13].transactions.pending.head.nonEmpty should be (true)
|
||||
response.body.extract[TransactionsJsonV13].transactions.booked.nonEmpty should be (true)
|
||||
|
||||
response.body.extract[TransactionsJsonV13].transactions.booked.head.length >0 should be (true)
|
||||
response.body.extract[TransactionsJsonV13].transactions.pending.head.length >0 should be (true)
|
||||
|
||||
val requestGet2 = (V1_3_BG / "accounts" / testAccountId1.value / "transactions").GET <@ (user1) <<? List(("bookingStatus", "booked"))
|
||||
val response2: APIResponse = makeGetRequest(requestGet2)
|
||||
@ -214,6 +213,158 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit
|
||||
}
|
||||
}
|
||||
|
||||
feature(s"BG v1.3 - $getTransactionList - Parameter Validation") {
|
||||
scenario("Authentication User, test failed with invalid bookingStatus parameter", BerlinGroupV1_3, getTransactionList) {
|
||||
val testAccountId = testAccountId1
|
||||
val bankId = APIUtil.defaultBankId
|
||||
grantUserAccessToViewViaEndpoint(
|
||||
bankId,
|
||||
testAccountId.value,
|
||||
resourceUser1.userId,
|
||||
user1,
|
||||
PostViewJsonV400(view_id = Constant.SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID, is_system = true)
|
||||
)
|
||||
|
||||
val requestGetWithInvalidStatus = (V1_3_BG / "accounts" / testAccountId.value / "transactions").GET <@ (user1) <<? List(("bookingStatus", "invalid"))
|
||||
val responseInvalid: APIResponse = makeGetRequest(requestGetWithInvalidStatus)
|
||||
Then("We should get a 400 for invalid bookingStatus")
|
||||
responseInvalid.code should equal(400)
|
||||
responseInvalid.body.extract[ErrorMessagesBG].tppMessages.head.text should include(InvalidUrlParameters)
|
||||
responseInvalid.body.extract[ErrorMessagesBG].tppMessages.head.text should include("bookingStatus parameter must take two one of those values : booked, pending or both!")
|
||||
}
|
||||
|
||||
scenario("Authentication User, test failed with empty bookingStatus parameter", BerlinGroupV1_3, getTransactionList) {
|
||||
val testAccountId = testAccountId1
|
||||
val bankId = APIUtil.defaultBankId
|
||||
grantUserAccessToViewViaEndpoint(
|
||||
bankId,
|
||||
testAccountId.value,
|
||||
resourceUser1.userId,
|
||||
user1,
|
||||
PostViewJsonV400(view_id = Constant.SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID, is_system = true)
|
||||
)
|
||||
|
||||
val requestGetWithEmptyStatus = (V1_3_BG / "accounts" / testAccountId.value / "transactions").GET <@ (user1) <<? List(("bookingStatus", ""))
|
||||
val responseEmpty: APIResponse = makeGetRequest(requestGetWithEmptyStatus)
|
||||
Then("We should get a 400 for empty bookingStatus")
|
||||
responseEmpty.code should equal(400)
|
||||
responseEmpty.body.extract[ErrorMessagesBG].tppMessages.head.text should include(InvalidUrlParameters)
|
||||
responseEmpty.body.extract[ErrorMessagesBG].tppMessages.head.text should include("bookingStatus parameter must take two one of those values : booked, pending or both!")
|
||||
}
|
||||
|
||||
scenario("Authentication User, test failed with case sensitive bookingStatus parameter", BerlinGroupV1_3, getTransactionList) {
|
||||
val testAccountId = testAccountId1
|
||||
val bankId = APIUtil.defaultBankId
|
||||
grantUserAccessToViewViaEndpoint(
|
||||
bankId,
|
||||
testAccountId.value,
|
||||
resourceUser1.userId,
|
||||
user1,
|
||||
PostViewJsonV400(view_id = Constant.SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID, is_system = true)
|
||||
)
|
||||
|
||||
val requestGetWithUpperCaseStatus = (V1_3_BG / "accounts" / testAccountId.value / "transactions").GET <@ (user1) <<? List(("bookingStatus", "BOOKED"))
|
||||
val responseUpperCase: APIResponse = makeGetRequest(requestGetWithUpperCaseStatus)
|
||||
Then("We should get a 400 for case sensitive bookingStatus")
|
||||
responseUpperCase.code should equal(400)
|
||||
responseUpperCase.body.extract[ErrorMessagesBG].tppMessages.head.text should include(InvalidUrlParameters)
|
||||
responseUpperCase.body.extract[ErrorMessagesBG].tppMessages.head.text should include("bookingStatus parameter must take two one of those values : booked, pending or both!")
|
||||
|
||||
val requestGetWithMixedCaseStatus = (V1_3_BG / "accounts" / testAccountId.value / "transactions").GET <@ (user1) <<? List(("bookingStatus", "Booked"))
|
||||
val responseMixedCase: APIResponse = makeGetRequest(requestGetWithMixedCaseStatus)
|
||||
Then("We should get a 400 for mixed case bookingStatus")
|
||||
responseMixedCase.code should equal(400)
|
||||
responseMixedCase.body.extract[ErrorMessagesBG].tppMessages.head.text should include(InvalidUrlParameters)
|
||||
responseMixedCase.body.extract[ErrorMessagesBG].tppMessages.head.text should include("bookingStatus parameter must take two one of those values : booked, pending or both!")
|
||||
}
|
||||
|
||||
scenario("Authentication User, test failed with special characters in bookingStatus parameter", BerlinGroupV1_3, getTransactionList) {
|
||||
val testAccountId = testAccountId1
|
||||
val bankId = APIUtil.defaultBankId
|
||||
grantUserAccessToViewViaEndpoint(
|
||||
bankId,
|
||||
testAccountId.value,
|
||||
resourceUser1.userId,
|
||||
user1,
|
||||
PostViewJsonV400(view_id = Constant.SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID, is_system = true)
|
||||
)
|
||||
|
||||
val invalidBookingStatuses = List("booked!", "pending@", "both#", "booked ", " booked", "booked;", "null", "undefined")
|
||||
|
||||
invalidBookingStatuses.foreach { invalidStatus =>
|
||||
val requestGetWithSpecialChars = (V1_3_BG / "accounts" / testAccountId.value / "transactions").GET <@ (user1) <<? List(("bookingStatus", invalidStatus))
|
||||
val responseSpecialChars: APIResponse = makeGetRequest(requestGetWithSpecialChars)
|
||||
Then(s"We should get a 400 for bookingStatus with special characters: '$invalidStatus'")
|
||||
responseSpecialChars.code should equal(400)
|
||||
responseSpecialChars.body.extract[ErrorMessagesBG].tppMessages.head.text should include(InvalidUrlParameters)
|
||||
responseSpecialChars.body.extract[ErrorMessagesBG].tppMessages.head.text should include("bookingStatus parameter must take two one of those values : booked, pending or both!")
|
||||
}
|
||||
}
|
||||
|
||||
scenario("Authentication User, test missing bookingStatus parameter handling", BerlinGroupV1_3, getTransactionList) {
|
||||
val testAccountId = testAccountId1
|
||||
val bankId = APIUtil.defaultBankId
|
||||
grantUserAccessToViewViaEndpoint(
|
||||
bankId,
|
||||
testAccountId.value,
|
||||
resourceUser1.userId,
|
||||
user1,
|
||||
PostViewJsonV400(view_id = Constant.SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID, is_system = true)
|
||||
)
|
||||
|
||||
// Test without bookingStatus parameter - should fail because it returns empty string which is invalid
|
||||
val requestGetWithoutBookingStatus = (V1_3_BG / "accounts" / testAccountId.value / "transactions").GET <@ (user1)
|
||||
val responseWithoutParam: APIResponse = makeGetRequest(requestGetWithoutBookingStatus)
|
||||
Then("We should get a 400 for missing bookingStatus parameter (treated as empty string)")
|
||||
responseWithoutParam.code should equal(400)
|
||||
responseWithoutParam.body.extract[ErrorMessagesBG].tppMessages.head.text should include(InvalidUrlParameters)
|
||||
responseWithoutParam.body.extract[ErrorMessagesBG].tppMessages.head.text should include("bookingStatus parameter must take two one of those values : booked, pending or both!")
|
||||
}
|
||||
|
||||
scenario("Authentication User, test multiple invalid bookingStatus parameters", BerlinGroupV1_3, getTransactionList) {
|
||||
val testAccountId = testAccountId1
|
||||
val bankId = APIUtil.defaultBankId
|
||||
grantUserAccessToViewViaEndpoint(
|
||||
bankId,
|
||||
testAccountId.value,
|
||||
resourceUser1.userId,
|
||||
user1,
|
||||
PostViewJsonV400(view_id = Constant.SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID, is_system = true)
|
||||
)
|
||||
|
||||
// Test with multiple bookingStatus parameters - only first one should be considered
|
||||
val requestGetWithMultipleParams = (V1_3_BG / "accounts" / testAccountId.value / "transactions").GET <@ (user1) <<? List(("bookingStatus", "invalid"), ("bookingStatus", "booked"))
|
||||
val responseMultipleParams: APIResponse = makeGetRequest(requestGetWithMultipleParams)
|
||||
Then("We should get a 400 because first parameter is invalid")
|
||||
responseMultipleParams.code should equal(400)
|
||||
responseMultipleParams.body.extract[ErrorMessage].message should include(DuplicateQueryParameters)
|
||||
}
|
||||
|
||||
scenario("Authentication User, test URL encoding in bookingStatus parameter", BerlinGroupV1_3, getTransactionList) {
|
||||
val testAccountId = testAccountId1
|
||||
val bankId = APIUtil.defaultBankId
|
||||
grantUserAccessToViewViaEndpoint(
|
||||
bankId,
|
||||
testAccountId.value,
|
||||
resourceUser1.userId,
|
||||
user1,
|
||||
PostViewJsonV400(view_id = Constant.SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID, is_system = true)
|
||||
)
|
||||
|
||||
// Test with URL encoded values that should still be invalid
|
||||
val encodedInvalidStatuses = List("book%65d", "pend%69ng", "bot%68")
|
||||
|
||||
encodedInvalidStatuses.foreach { encodedStatus =>
|
||||
val requestGetWithEncodedStatus = (V1_3_BG / "accounts" / testAccountId.value / "transactions").GET <@ (user1) <<? List(("bookingStatus", encodedStatus))
|
||||
val responseEncoded: APIResponse = makeGetRequest(requestGetWithEncodedStatus)
|
||||
Then(s"We should get a 400 for URL encoded invalid bookingStatus: '$encodedStatus'")
|
||||
responseEncoded.code should equal(400)
|
||||
responseEncoded.body.extract[ErrorMessagesBG].tppMessages.head.text should include(InvalidUrlParameters)
|
||||
responseEncoded.body.extract[ErrorMessagesBG].tppMessages.head.text should include("bookingStatus parameter must take two one of those values : booked, pending or both!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
feature(s"BG v1.3 - $getTransactionDetails") {
|
||||
scenario("Authentication User, test succeed", BerlinGroupV1_3, getTransactionDetails, getTransactionList) {
|
||||
val testAccountId = testAccountId1
|
||||
@ -238,8 +389,7 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit
|
||||
Then("We should get a 200 ")
|
||||
response.code should equal(200)
|
||||
response.body.extract[TransactionsJsonV13].account.iban should not be ("")
|
||||
response.body.extract[TransactionsJsonV13].transactions.pending.head.nonEmpty should be (true)
|
||||
// response.body.extract[TransactionsJsonV13].transactions.pending.length > 0 should be (true)
|
||||
response.body.extract[TransactionsJsonV13].transactions.pending.head.length > 0 should be (true)
|
||||
val transactionId = response.body.extract[TransactionsJsonV13].transactions.pending.head.head.transactionId
|
||||
|
||||
val requestGet2 = (V1_3_BG / "accounts" / testAccountId.value / "transactions" / transactionId).GET <@ (user1)
|
||||
|
||||
@ -0,0 +1,106 @@
|
||||
package code.api.berlin.group.v1_3
|
||||
|
||||
import code.api.berlin.group.v1_3.BgSpecValidation._
|
||||
import code.api.v4_0_0.V400ServerSetup
|
||||
import org.scalatest.Tag
|
||||
|
||||
import java.time.LocalDate
|
||||
import java.util.Date
|
||||
|
||||
class BgSpecValidationTest extends V400ServerSetup {
|
||||
|
||||
// Test tags
|
||||
object File extends Tag("BgSpecValidation.scala")
|
||||
object Function1 extends Tag("validateValidUntil")
|
||||
object Function2 extends Tag("getErrorMessage")
|
||||
object Function3 extends Tag("getDate")
|
||||
object Function4 extends Tag("formatToISODate")
|
||||
|
||||
feature(s"Test function: $Function1 at file $File") {
|
||||
|
||||
scenario("Reject past date", Function1) {
|
||||
When("The client provides a date in the past")
|
||||
val yesterday = LocalDate.now().minusDays(1).toString
|
||||
|
||||
Then("It should be rejected")
|
||||
val error = getErrorMessage(yesterday)
|
||||
error should include("cannot be in the past")
|
||||
}
|
||||
|
||||
scenario("Accept today's date", Function1) {
|
||||
When("The client provides today's date")
|
||||
val today = LocalDate.now().toString
|
||||
|
||||
Then("It should be accepted")
|
||||
val error = getErrorMessage(today)
|
||||
error shouldBe ""
|
||||
}
|
||||
|
||||
scenario("Accept exactly 180 days in the future", Function1) {
|
||||
When("The client provides the maximum allowed date (180 days)")
|
||||
val maxDay = MaxValidDays.toString
|
||||
|
||||
Then("It should be accepted")
|
||||
val error = getErrorMessage(maxDay)
|
||||
error shouldBe ""
|
||||
}
|
||||
|
||||
scenario("Reject date beyond 180 days", Function1) {
|
||||
When("The client provides a date 181 days in the future")
|
||||
val tooFar = MaxValidDays.plusDays(1).toString
|
||||
|
||||
Then("It should be rejected")
|
||||
val error = getErrorMessage(tooFar)
|
||||
error should include("exceeds the maximum allowed period")
|
||||
}
|
||||
|
||||
scenario("Reject invalid date format", Function1) {
|
||||
When("The client provides a date in wrong format")
|
||||
val invalid = "2025/12/31"
|
||||
|
||||
Then("It should be rejected")
|
||||
val error = getErrorMessage(invalid)
|
||||
error should include("invalid")
|
||||
}
|
||||
}
|
||||
|
||||
feature(s"Test function: $Function2 and $Function3 at file $File") {
|
||||
|
||||
scenario("getDate returns valid Date for correct input", Function3) {
|
||||
When("We provide a valid ISO date")
|
||||
val today = LocalDate.now().toString
|
||||
val result = getDate(today)
|
||||
|
||||
Then("It should return a non-null Date")
|
||||
result shouldBe a[Date]
|
||||
}
|
||||
|
||||
scenario("getDate returns null for invalid input", Function3) {
|
||||
When("We provide an invalid date format")
|
||||
val result = getDate("2025/12/31")
|
||||
|
||||
Then("It should return null")
|
||||
result shouldBe null
|
||||
}
|
||||
}
|
||||
|
||||
feature(s"Test function: $Function4 at file $File") {
|
||||
|
||||
scenario("formatToISODate formats a valid Date", Function4) {
|
||||
When("We pass a valid Date object")
|
||||
val today = new Date()
|
||||
val formatted = formatToISODate(today)
|
||||
|
||||
Then("It should return an ISO date string")
|
||||
formatted should fullyMatch regex """\d{4}-\d{2}-\d{2}"""
|
||||
}
|
||||
|
||||
scenario("formatToISODate handles null gracefully", Function4) {
|
||||
When("We pass null")
|
||||
val formatted = formatToISODate(null)
|
||||
|
||||
Then("It should return empty string")
|
||||
formatted shouldBe ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -108,10 +108,10 @@ class JSONFactory_BERLIN_GROUP_1_3Test extends FeatureSpec with Matchers with Gi
|
||||
|
||||
val jsonString: String = compactRender(Extraction.decompose(result))
|
||||
|
||||
jsonString.contains("creditorName") shouldBe true
|
||||
jsonString.contains("creditorAccount") shouldBe true
|
||||
jsonString.contains("debtorName") shouldBe false
|
||||
jsonString.contains("debtorAccount") shouldBe false
|
||||
jsonString.contains("creditorName") shouldBe false
|
||||
jsonString.contains("creditorAccount") shouldBe false
|
||||
jsonString.contains("debtorName") shouldBe true
|
||||
jsonString.contains("debtorAccount") shouldBe true
|
||||
|
||||
println(jsonString)
|
||||
}
|
||||
|
||||
132
obp-api/src/test/scala/code/api/v5_1_0/RateLimitingTest.scala
Normal file
132
obp-api/src/test/scala/code/api/v5_1_0/RateLimitingTest.scala
Normal file
@ -0,0 +1,132 @@
|
||||
/**
|
||||
Open Bank Project - API
|
||||
Copyright (C) 2011-2019, TESOBE GmbH
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Email: contact@tesobe.com
|
||||
TESOBE GmbH
|
||||
Osloerstrasse 16/17
|
||||
Berlin 13359, Germany
|
||||
|
||||
This product includes software developed at
|
||||
TESOBE (http://www.tesobe.com/)
|
||||
*/
|
||||
package code.api.v5_1_0
|
||||
|
||||
import code.api.util.APIUtil.OAuth._
|
||||
import code.api.util.ApiRole
|
||||
import code.api.util.ApiRole.CanReadCallLimits
|
||||
import code.api.util.ErrorMessages.{UserHasMissingRoles, UserNotLoggedIn}
|
||||
import code.api.v4_0_0.CallLimitPostJsonV400
|
||||
import code.api.v5_1_0.OBPAPI5_1_0.Implementations5_1_0
|
||||
import code.consumer.Consumers
|
||||
import code.entitlement.Entitlement
|
||||
import code.setup.PropsReset
|
||||
import com.github.dwickern.macros.NameOf.nameOf
|
||||
import com.openbankproject.commons.model.ErrorMessage
|
||||
import com.openbankproject.commons.util.ApiVersion
|
||||
import org.scalatest.Tag
|
||||
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.{ZoneId, ZonedDateTime}
|
||||
import java.util.Date
|
||||
|
||||
class RateLimitingTest extends V510ServerSetup with PropsReset {
|
||||
|
||||
/**
|
||||
* Test tags
|
||||
* Example: To run tests with tag "getPermissions":
|
||||
* mvn test -D tagsToInclude
|
||||
*
|
||||
* This is made possible by the scalatest maven plugin
|
||||
*/
|
||||
object ApiVersion400 extends Tag(ApiVersion.v4_0_0.toString)
|
||||
object ApiVersion510 extends Tag(ApiVersion.v5_1_0.toString)
|
||||
object ApiCallsLimit extends Tag(nameOf(Implementations5_1_0.getCallsLimit))
|
||||
|
||||
override def beforeEach() = {
|
||||
super.beforeEach()
|
||||
setPropsValues("use_consumer_limits"->"true")
|
||||
setPropsValues("user_consumer_limit_anonymous_access"->"6000")
|
||||
}
|
||||
|
||||
val yesterday = ZonedDateTime.now(ZoneId.of("UTC")).minusDays(1)
|
||||
val tomorrow = ZonedDateTime.now(ZoneId.of("UTC")).plusDays(10)
|
||||
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'")
|
||||
|
||||
val fromDate = Date.from(yesterday.toInstant())
|
||||
val toDate = Date.from(tomorrow.toInstant())
|
||||
|
||||
val callLimitJsonInitial = CallLimitPostJsonV400(
|
||||
from_date = fromDate,
|
||||
to_date = toDate,
|
||||
api_version = None,
|
||||
api_name = None,
|
||||
bank_id = None,
|
||||
per_second_call_limit = "-1",
|
||||
per_minute_call_limit = "-1",
|
||||
per_hour_call_limit = "-1",
|
||||
per_day_call_limit ="-1",
|
||||
per_week_call_limit = "-1",
|
||||
per_month_call_limit = "-1"
|
||||
)
|
||||
val callLimitJsonMonth: CallLimitPostJsonV400 = callLimitJsonInitial.copy(per_month_call_limit = "100")
|
||||
|
||||
|
||||
feature("Rate Limit - " + ApiCallsLimit + " - " + ApiVersion400) {
|
||||
|
||||
scenario("We will try to get calls limit per minute for a Consumer - unauthorized access", ApiCallsLimit, ApiVersion510) {
|
||||
When(s"We make a request $ApiVersion510")
|
||||
val Some((c, _)) = user1
|
||||
val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("")
|
||||
val request510 = (v5_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").GET
|
||||
val response510 = makeGetRequest(request510)
|
||||
Then("We should get a 401")
|
||||
response510.code should equal(401)
|
||||
And("error should be " + UserNotLoggedIn)
|
||||
response510.body.extract[ErrorMessage].message should equal(UserNotLoggedIn)
|
||||
}
|
||||
scenario("We will try to get calls limit per minute without a proper Role " + ApiRole.canReadCallLimits, ApiCallsLimit, ApiVersion510) {
|
||||
When("We make a request v3.1.0 without a Role " + ApiRole.canReadCallLimits)
|
||||
val Some((c, _)) = user1
|
||||
val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("")
|
||||
val request510 = (v5_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").GET <@ (user1)
|
||||
val response510 = makeGetRequest(request510)
|
||||
Then("We should get a 403")
|
||||
response510.code should equal(403)
|
||||
And("error should be " + UserHasMissingRoles + CanReadCallLimits)
|
||||
response510.body.extract[ErrorMessage].message should equal(UserHasMissingRoles + CanReadCallLimits)
|
||||
}
|
||||
scenario("We will try to get calls limit per minute with a proper Role " + ApiRole.canReadCallLimits, ApiCallsLimit, ApiVersion510) {
|
||||
|
||||
When("We make a request v5.1.0 with a Role " + ApiRole.canSetCallLimits)
|
||||
val response01 = setRateLimiting(user1, callLimitJsonMonth)
|
||||
Then("We should get a 200")
|
||||
response01.code should equal(200)
|
||||
|
||||
When(s"We make a request v$ApiVersion510 with a Role " + ApiRole.canReadCallLimits)
|
||||
val Some((c, _)) = user1
|
||||
val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("")
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanReadCallLimits.toString)
|
||||
val request510 = (v5_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").GET <@ (user1)
|
||||
val response510 = makeGetRequest(request510)
|
||||
Then("We should get a 200")
|
||||
response510.code should equal(200)
|
||||
response510.body.extract[CallLimitJson510]
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -11,8 +11,9 @@ import code.api.v1_4_0.JSONFactory1_4_0.TransactionRequestAccountJsonV140
|
||||
import code.api.v2_0_0.{BasicAccountsJSON, TransactionRequestBodyJsonV200}
|
||||
import code.api.v3_0_0.ViewJsonV300
|
||||
import code.api.v3_1_0.{CreateAccountRequestJsonV310, CreateAccountResponseJsonV310, CustomerJsonV310}
|
||||
import code.api.v4_0_0.{AtmJsonV400, BanksJson400, PostAccountAccessJsonV400, PostViewJsonV400, TransactionRequestWithChargeJSON400}
|
||||
import code.api.v4_0_0.{AtmJsonV400, BanksJson400, CallLimitPostJsonV400, PostAccountAccessJsonV400, PostViewJsonV400, TransactionRequestWithChargeJSON400}
|
||||
import code.api.v5_0_0.PostCustomerJsonV500
|
||||
import code.consumer.Consumers
|
||||
import code.entitlement.Entitlement
|
||||
import code.setup.{APIResponse, DefaultUsers, ServerSetupWithTestData}
|
||||
import com.openbankproject.commons.model.{AccountRoutingJsonV121, AmountOfMoneyJsonV121, CreateViewJson}
|
||||
@ -32,6 +33,15 @@ trait V510ServerSetup extends ServerSetupWithTestData with DefaultUsers {
|
||||
def dynamicEndpoint_Request: Req = baseRequest / "obp" / ApiShortVersions.`dynamic-endpoint`.toString
|
||||
def dynamicEntity_Request: Req = baseRequest / "obp" / ApiShortVersions.`dynamic-entity`.toString
|
||||
|
||||
|
||||
def setRateLimiting(consumerAndToken: Option[(Consumer, Token)], putJson: CallLimitPostJsonV400): APIResponse = {
|
||||
val Some((c, _)) = consumerAndToken
|
||||
val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("")
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanSetCallLimits.toString)
|
||||
val request400 = (v4_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@ (consumerAndToken)
|
||||
makePutRequest(request400, write(putJson))
|
||||
}
|
||||
|
||||
def randomBankId : String = {
|
||||
def getBanksInfo : APIResponse = {
|
||||
val request = v5_1_0_Request / "banks"
|
||||
|
||||
@ -14,6 +14,7 @@ import code.transactionrequests.MappedTransactionRequest
|
||||
import code.util.Helper.MdcLoggable
|
||||
import com.openbankproject.commons.model._
|
||||
import com.openbankproject.commons.model.enums.AccountRoutingScheme
|
||||
import com.openbankproject.commons.model.enums._
|
||||
import net.liftweb.common.Box
|
||||
import net.liftweb.mapper.{By, MetaMapper}
|
||||
import net.liftweb.util.Helpers._
|
||||
@ -99,7 +100,7 @@ trait LocalMappedConnectorTestSetup extends TestConnectorSetupWithStandardPermis
|
||||
Entitlement.entitlement.vend.addEntitlement(bankId, userId, roleName)
|
||||
}
|
||||
|
||||
override protected def createTransaction(account: BankAccount, startDate: Date, finishDate: Date) = {
|
||||
override protected def createTransaction(account: BankAccount, startDate: Date, finishDate: Date, isCompleted: Boolean) = {
|
||||
//ugly
|
||||
val mappedBankAccount = account.asInstanceOf[MappedBankAccount]
|
||||
|
||||
@ -109,6 +110,13 @@ trait LocalMappedConnectorTestSetup extends TestConnectorSetupWithStandardPermis
|
||||
|
||||
mappedBankAccount.accountBalance(accountBalanceAfter).save
|
||||
|
||||
// Determine transaction status based on isCompleted parameter
|
||||
val transactionStatus = if (isCompleted) {
|
||||
TransactionRequestStatus.COMPLETED.toString
|
||||
} else {
|
||||
TransactionRequestStatus.INITIATED.toString
|
||||
}
|
||||
|
||||
MappedTransaction.create
|
||||
.bank(account.bankId.value)
|
||||
.account(account.accountId.value)
|
||||
@ -131,6 +139,7 @@ trait LocalMappedConnectorTestSetup extends TestConnectorSetupWithStandardPermis
|
||||
.CPOtherAccountSecondaryRoutingAddress(randomString(5))
|
||||
.CPOtherBankRoutingScheme(randomString(5))
|
||||
.CPOtherBankRoutingAddress(randomString(5))
|
||||
.status(transactionStatus) // Use determined transaction status
|
||||
.saveMe
|
||||
.toTransaction.orNull
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ trait TestConnectorSetup {
|
||||
//TODO: implement these right here using Connector.connector.vend and get rid of specific connector setup files
|
||||
protected def createBank(id : String) : Bank
|
||||
protected def createAccount(bankId: BankId, accountId : AccountId, currency : String) : BankAccount
|
||||
protected def createTransaction(account : BankAccount, startDate : Date, finishDate : Date)
|
||||
protected def createTransaction(account : BankAccount, startDate : Date, finishDate : Date, isCompleted: Boolean) : Transaction
|
||||
protected def createTransactionRequest(account: BankAccount): List[MappedTransactionRequest]
|
||||
protected def updateAccountCurrency(bankId: BankId, accountId : AccountId, currency : String) : BankAccount
|
||||
|
||||
@ -126,7 +126,8 @@ trait TestConnectorSetup {
|
||||
for(i <- 0 until NUM_TRANSACTIONS){
|
||||
val postedDate = InitialDateFactory.date
|
||||
val completedDate = add10Minutes(postedDate)
|
||||
createTransaction(account, postedDate, completedDate)
|
||||
val isCompleted = (i % 2) == 0 // Even indices (0,2,4...) are COMPLETED, odd indices (1,3,5...) are INITIATED
|
||||
createTransaction(account, postedDate, completedDate, isCompleted)
|
||||
}
|
||||
|
||||
//load all transactions for the account to generate the counterparty metadata
|
||||
@ -152,7 +153,7 @@ trait TestConnectorSetup {
|
||||
protected def createPublicView(bankId: BankId, accountId: AccountId) : View
|
||||
protected def createCustomRandomView(bankId: BankId, accountId: AccountId) : View
|
||||
|
||||
protected def setAccountHolder(user: User, bankId : BankId, accountId : AccountId)
|
||||
protected def setAccountHolder(user: User, bankId : BankId, accountId : AccountId) : Unit
|
||||
|
||||
protected def wipeTestData()
|
||||
protected def wipeTestData() : Unit
|
||||
}
|
||||
|
||||
@ -174,14 +174,6 @@ object PemCertificateRole extends OBPEnumeration[PemCertificateRole] {
|
||||
object PSP_IC extends Value
|
||||
object PSP_AI extends Value
|
||||
object PSP_PI extends Value
|
||||
|
||||
def toBerlinGroup(role: String): String = {
|
||||
role match {
|
||||
case item if PSP_AI.toString == item => "AISP"
|
||||
case item if PSP_PI.toString == item => "PISP"
|
||||
case _ => ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed trait UserInvitationPurpose extends EnumValue
|
||||
|
||||
76
zed/.metals-config.json
Normal file
76
zed/.metals-config.json
Normal file
@ -0,0 +1,76 @@
|
||||
{
|
||||
"maven": {
|
||||
"enabled": true
|
||||
},
|
||||
"metals": {
|
||||
"serverVersion": "1.0.0",
|
||||
"javaHome": "/usr/lib/jvm/java-17-openjdk-amd64",
|
||||
"bloopVersion": "2.0.0",
|
||||
"superMethodLensesEnabled": true,
|
||||
"enableSemanticHighlighting": true,
|
||||
"compileOnSave": true,
|
||||
"testUserInterface": "Code Lenses",
|
||||
"inlayHints": {
|
||||
"enabled": true,
|
||||
"hintsInPatternMatch": {
|
||||
"enabled": true
|
||||
},
|
||||
"implicitArguments": {
|
||||
"enabled": true
|
||||
},
|
||||
"implicitConversions": {
|
||||
"enabled": true
|
||||
},
|
||||
"inferredTypes": {
|
||||
"enabled": true
|
||||
},
|
||||
"typeParameters": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"buildTargets": [
|
||||
{
|
||||
"id": "obp-commons",
|
||||
"displayName": "obp-commons",
|
||||
"baseDirectory": "file:///home/marko/Tesobe/GitHub/constantine2nd/OBP-API/obp-commons/",
|
||||
"tags": ["library"],
|
||||
"languageIds": ["scala", "java"],
|
||||
"dependencies": [],
|
||||
"capabilities": {
|
||||
"canCompile": true,
|
||||
"canTest": true,
|
||||
"canRun": false,
|
||||
"canDebug": true
|
||||
},
|
||||
"dataKind": "scala",
|
||||
"data": {
|
||||
"scalaOrganization": "org.scala-lang",
|
||||
"scalaVersion": "2.12.20",
|
||||
"scalaBinaryVersion": "2.12",
|
||||
"platform": "jvm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "obp-api",
|
||||
"displayName": "obp-api",
|
||||
"baseDirectory": "file:///home/marko/Tesobe/GitHub/constantine2nd/OBP-API/obp-api/",
|
||||
"tags": ["application"],
|
||||
"languageIds": ["scala", "java"],
|
||||
"dependencies": ["obp-commons"],
|
||||
"capabilities": {
|
||||
"canCompile": true,
|
||||
"canTest": true,
|
||||
"canRun": true,
|
||||
"canDebug": true
|
||||
},
|
||||
"dataKind": "scala",
|
||||
"data": {
|
||||
"scalaOrganization": "org.scala-lang",
|
||||
"scalaVersion": "2.12.20",
|
||||
"scalaBinaryVersion": "2.12",
|
||||
"platform": "jvm"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
298
zed/README.md
Normal file
298
zed/README.md
Normal file
@ -0,0 +1,298 @@
|
||||
# ZED IDE Setup for OBP-API Development
|
||||
|
||||
> **Complete ZED IDE integration for the Open Bank Project API**
|
||||
|
||||
This folder contains everything needed to set up ZED IDE with full Scala language server support, automated build tasks, and streamlined development workflows for OBP-API.
|
||||
|
||||
## 🚀 Quick Setup (5 minutes)
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Java 17+** (OpenJDK recommended)
|
||||
- **Maven 3.6+**
|
||||
- **ZED IDE** (latest version)
|
||||
|
||||
### Single Setup Script
|
||||
|
||||
```bash
|
||||
cd OBP-API
|
||||
./zed/setup-zed-ide.sh
|
||||
```
|
||||
|
||||
This unified script automatically:
|
||||
|
||||
- ✅ Installs missing dependencies (Coursier, Bloop)
|
||||
- ✅ Compiles the project and resolves dependencies
|
||||
- ✅ Generates dynamic Bloop configurations
|
||||
- ✅ Sets up Metals language server
|
||||
- ✅ Copies ZED configuration files to `.zed/` folder
|
||||
- ✅ Configures build and run tasks
|
||||
- ✅ Sets up manual-only code formatting
|
||||
|
||||
## 📁 What's Included
|
||||
|
||||
```
|
||||
zed/
|
||||
├── README.md # This comprehensive guide
|
||||
├── setup-zed-ide.sh # Single unified setup script
|
||||
├── generate-bloop-config.sh # Dynamic Bloop config generator
|
||||
├── settings.json # ZED IDE settings template
|
||||
├── tasks.json # Pre-configured build/run tasks
|
||||
├── .metals-config.json # Metals language server config
|
||||
└── setup-zed.bat # Windows setup script
|
||||
```
|
||||
|
||||
## ⌨️ Essential Keyboard Shortcuts
|
||||
|
||||
| Action | Linux | macOS/Windows | Purpose |
|
||||
| -------------------- | -------------- | ------------- | ----------------------------- |
|
||||
| **Command Palette** | `Ctrl+Shift+P` | `Cmd+Shift+P` | Access all tasks |
|
||||
| **Go to Definition** | `F12` | `F12` | Navigate to symbol definition |
|
||||
| **Find References** | `Shift+F12` | `Shift+F12` | Find all symbol usages |
|
||||
| **Quick Open File** | `Ctrl+P` | `Cmd+P` | Fast file navigation |
|
||||
| **Format Code** | `Ctrl+Shift+I` | `Cmd+Shift+I` | Auto-format Scala code |
|
||||
| **Symbol Search** | `Ctrl+T` | `Cmd+T` | Search symbols project-wide |
|
||||
|
||||
## 🛠️ Available Development Tasks
|
||||
|
||||
Access via Command Palette (`Ctrl+Shift+P` on Linux, `Cmd+Shift+P` on macOS/Windows) → `"task: spawn"` (Linux) or `"Tasks: Spawn"` (macOS/Windows):
|
||||
|
||||
### Core Development Tasks
|
||||
|
||||
| Task | Purpose | Duration | When to Use |
|
||||
| ---------------------------- | ------------------------ | --------- | ------------------------------------ |
|
||||
| **Quick Build Dependencies** | Build only dependencies | 1-3 min | First step, after dependency changes |
|
||||
| **[1] Run OBP-API Server** | Start development server | 3-5 min | Daily development |
|
||||
| **🔨 Build OBP-API** | Full project build | 2-5 min | After code changes |
|
||||
| **Run Tests** | Execute test suite | 5-15 min | Before commits |
|
||||
| **[3] Compile Only** | Quick syntax check | 30s-1 min | During development |
|
||||
|
||||
### Utility Tasks
|
||||
|
||||
| Task | Purpose |
|
||||
| --------------------------------- | ------------------------- |
|
||||
| **[4] Clean Target Folders** | Remove build artifacts |
|
||||
| **🔄 Continuous Compile (Scala)** | Auto-recompile on changes |
|
||||
| **[2] Test API Root Endpoint** | Verify server status |
|
||||
| **🔧 Kill Server on Port 8080** | Stop stuck processes |
|
||||
| **🔍 Check Dependencies** | Verify Maven dependencies |
|
||||
|
||||
## 🏗️ Development Workflow
|
||||
|
||||
### Daily Development
|
||||
|
||||
1. **Start Development Session**
|
||||
- Linux: `Ctrl+Shift+P` → `"task: spawn"` → `"Quick Build Dependencies"`
|
||||
- macOS: `Cmd+Shift+P` → `"Tasks: Spawn"` → `"Quick Build Dependencies"`
|
||||
|
||||
2. **Start API Server**
|
||||
- Use task `"[1] Run OBP-API Server"`
|
||||
- Server runs on: `http://localhost:8080`
|
||||
- Test endpoint: `http://localhost:8080/obp/v5.1.0/root`
|
||||
|
||||
3. **Code Development**
|
||||
- Edit Scala files in `obp-api/src/main/scala/`
|
||||
- Use `F12` for Go to Definition
|
||||
- Auto-completion with `Ctrl+Space`
|
||||
- Real-time error highlighting
|
||||
- Format code with `Ctrl+Shift+I`
|
||||
|
||||
4. **Testing & Validation**
|
||||
- Quick compile: `"[3] Compile Only"` task
|
||||
- Run tests: `"Run Tests"` task
|
||||
- API testing: `"[2] Test API Root Endpoint"` task
|
||||
|
||||
## 🔧 Configuration Details
|
||||
|
||||
### ZED IDE Settings (`settings.json`)
|
||||
|
||||
- **Format on Save**: DISABLED (manual formatting only - use `Ctrl+Shift+I`)
|
||||
- **Scala LSP**: Optimized Metals configuration
|
||||
- **Maven Integration**: Proper MAVEN_OPTS for Java 17+
|
||||
- **UI Preferences**: One Dark theme, consistent layout
|
||||
- **Inlay Hints**: Enabled for better code understanding
|
||||
|
||||
### Build Tasks (`tasks.json`)
|
||||
|
||||
All tasks include proper environment variables:
|
||||
|
||||
```bash
|
||||
MAVEN_OPTS="-Xss128m --add-opens=java.base/java.util.jar=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED"
|
||||
```
|
||||
|
||||
### Metals LSP (`.metals-config.json`)
|
||||
|
||||
- **Build Tool**: Maven
|
||||
- **Bloop Integration**: Dynamic configuration generation
|
||||
- **Scala Version**: 2.12.20
|
||||
- **Java Target**: Java 11 (compatible with Java 17)
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
| Problem | Symptom | Solution |
|
||||
| ------------------------------- | ------------------------------------ | ------------------------------------------------ |
|
||||
| **Language Server Not Working** | No go-to-definition, no autocomplete | Restart ZED, wait for Metals initialization |
|
||||
| **Compilation Errors** | Red squiggly lines, build failures | Check Problems panel, run "Clean Target Folders" |
|
||||
| **Server Won't Start** | Port 8080 busy | Run "Kill Server on Port 8080" task |
|
||||
| **Out of Memory** | Build fails with heap space error | Already configured in tasks |
|
||||
| **Missing Dependencies** | Import errors | Run "Check Dependencies" task |
|
||||
|
||||
### Recovery Procedures
|
||||
|
||||
1. **Full Reset**:
|
||||
|
||||
```bash
|
||||
./zed/setup-zed-ide.sh # Re-run complete setup
|
||||
```
|
||||
|
||||
2. **Regenerate Bloop Configurations**:
|
||||
|
||||
```bash
|
||||
./zed/generate-bloop-config.sh # Regenerate configs
|
||||
```
|
||||
|
||||
3. **Clean Restart**:
|
||||
- Clean build with "Clean Target Folders" task
|
||||
- Restart ZED IDE
|
||||
- Wait for Metals to reinitialize (2-3 minutes)
|
||||
|
||||
### Platform-Specific Notes
|
||||
|
||||
#### Linux Users
|
||||
|
||||
- Use `"task: spawn"` in command palette (not `"Tasks: Spawn"`)
|
||||
- Ensure proper Java permissions for Maven
|
||||
|
||||
#### macOS/Windows Users
|
||||
|
||||
- Use `"Tasks: Spawn"` in command palette
|
||||
- Windows users can also use `setup-zed.bat`
|
||||
|
||||
## 🌐 API Development
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
OBP-API/
|
||||
├── obp-api/ # Main API application
|
||||
│ └── src/main/scala/ # Scala source code
|
||||
│ └── code/api/ # API endpoint definitions
|
||||
│ ├── v5_1_0/ # Latest API version
|
||||
│ ├── v4_0_0/ # Previous versions
|
||||
│ └── util/ # Utility functions
|
||||
├── obp-commons/ # Shared utilities and models
|
||||
│ └── src/main/scala/ # Common Scala code
|
||||
└── .zed/ # ZED IDE configuration (generated)
|
||||
```
|
||||
|
||||
### Adding New API Endpoints
|
||||
|
||||
1. Navigate to `obp-api/src/main/scala/code/api/v5_1_0/`
|
||||
2. Find appropriate API trait (e.g., `OBPAPI5_1_0.scala`)
|
||||
3. Follow existing endpoint patterns
|
||||
4. Use `F12` to navigate to helper functions
|
||||
5. Test with API test task
|
||||
|
||||
### Testing Endpoints
|
||||
|
||||
```bash
|
||||
# Root API information
|
||||
curl http://localhost:8080/obp/v5.1.0/root
|
||||
|
||||
# Health check
|
||||
curl http://localhost:8080/obp/v5.1.0/config
|
||||
|
||||
# Banks list (requires proper setup)
|
||||
curl http://localhost:8080/obp/v5.1.0/banks
|
||||
```
|
||||
|
||||
## 🎯 Pro Tips
|
||||
|
||||
### Code Navigation
|
||||
|
||||
- **Quick file access**: `Ctrl+P` then type filename
|
||||
- **Symbol search**: `Ctrl+T` then type function/class name
|
||||
- **Project-wide text search**: `Ctrl+Shift+F`
|
||||
|
||||
### Efficiency Shortcuts
|
||||
|
||||
- `Ctrl+/` - Toggle line comment
|
||||
- `Ctrl+D` - Select next occurrence
|
||||
- `Ctrl+Shift+L` - Select all occurrences
|
||||
- `F2` - Rename symbol
|
||||
- `Alt+←/→` - Navigate back/forward
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
- Close unused files to reduce memory usage
|
||||
- Use "Continuous Compile" for faster feedback
|
||||
- Limit test runs to specific modules during development
|
||||
|
||||
## 📚 Additional Resources
|
||||
|
||||
### Documentation
|
||||
|
||||
- **OBP-API Project**: https://github.com/OpenBankProject/OBP-API
|
||||
- **API Documentation**: https://apiexplorer.openbankproject.com
|
||||
- **Community Forums**: https://openbankproject.com
|
||||
|
||||
### Learning Resources
|
||||
|
||||
- **Scala**: https://docs.scala-lang.org/
|
||||
- **Lift Framework**: https://liftweb.net/
|
||||
- **Maven**: https://maven.apache.org/guides/
|
||||
- **ZED IDE**: https://zed.dev/docs
|
||||
|
||||
## 🆘 Getting Help
|
||||
|
||||
### Diagnostic Commands
|
||||
|
||||
```bash
|
||||
# Check Java version
|
||||
java -version
|
||||
|
||||
# Check Maven
|
||||
mvn -version
|
||||
|
||||
# Check Bloop status
|
||||
bloop projects
|
||||
|
||||
# Test compilation
|
||||
bloop compile obp-commons obp-api
|
||||
|
||||
# Check ZED configuration
|
||||
ls -la .zed/
|
||||
```
|
||||
|
||||
### Common Error Messages
|
||||
|
||||
| Error | Cause | Solution |
|
||||
| --------------------------- | ----------------------------- | ----------------------------- |
|
||||
| "Java module system" errors | Java 17+ module restrictions | Already handled in MAVEN_OPTS |
|
||||
| "Port 8080 already in use" | Previous server still running | Use "Kill Server" task |
|
||||
| "Metals not responding" | Language server crashed | Restart ZED IDE |
|
||||
| "Compilation failed" | Dependency issues | Run "Check Dependencies" |
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Getting Started Checklist
|
||||
|
||||
- [ ] Install Java 17+, Maven 3.6+, ZED IDE
|
||||
- [ ] Clone OBP-API repository
|
||||
- [ ] Run `./zed/setup-zed-ide.sh` (single setup script)
|
||||
- [ ] Open project in ZED IDE
|
||||
- [ ] Wait for Metals initialization (2-3 minutes)
|
||||
- [ ] Run "Quick Build Dependencies" task
|
||||
- [ ] Start server with "[1] Run OBP-API Server" task
|
||||
- [ ] Test API at http://localhost:8080/obp/v5.1.0/root
|
||||
- [ ] Try "Go to Definition" (F12) on Scala symbol
|
||||
- [ ] Format code manually with `Ctrl+Shift+I` (auto-format disabled)
|
||||
- [ ] Make a small code change and test compilation
|
||||
|
||||
**Welcome to productive OBP-API development with ZED IDE! 🚀**
|
||||
|
||||
---
|
||||
|
||||
_This setup provides a complete, optimized development environment for the Open Bank Project API using ZED IDE with full Scala language server support._
|
||||
263
zed/generate-bloop-config.sh
Executable file
263
zed/generate-bloop-config.sh
Executable file
@ -0,0 +1,263 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Generate portable Bloop configuration files for OBP-API
|
||||
# This script creates Bloop JSON configurations with proper paths for any system
|
||||
|
||||
set -e
|
||||
|
||||
echo "🔧 Generating Bloop configuration files..."
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Get the project root directory (parent of zed folder)
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
echo "📁 Project root: $PROJECT_ROOT"
|
||||
|
||||
# Check if we're in the zed directory and project structure exists
|
||||
if [[ ! -f "$PROJECT_ROOT/pom.xml" ]] || [[ ! -d "$PROJECT_ROOT/obp-api" ]] || [[ ! -d "$PROJECT_ROOT/obp-commons" ]]; then
|
||||
echo -e "${RED}❌ Error: Could not find OBP-API project structure${NC}"
|
||||
echo "Make sure you're running this from the zed/ folder of the OBP-API project"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Change to project root for Maven operations
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# Detect Java home
|
||||
if [[ -z "$JAVA_HOME" ]]; then
|
||||
JAVA_HOME=$(dirname $(dirname $(readlink -f $(which java))))
|
||||
echo -e "${YELLOW}⚠️ JAVA_HOME not set, detected: $JAVA_HOME${NC}"
|
||||
else
|
||||
echo -e "${GREEN}✅ JAVA_HOME: $JAVA_HOME${NC}"
|
||||
fi
|
||||
|
||||
# Get Maven local repository
|
||||
M2_REPO=$(mvn help:evaluate -Dexpression=settings.localRepository -q -DforceStdout 2>/dev/null || echo "$HOME/.m2/repository")
|
||||
echo "📦 Maven repository: $M2_REPO"
|
||||
|
||||
# Ensure .bloop directory exists in project root
|
||||
mkdir -p "$PROJECT_ROOT/.bloop"
|
||||
|
||||
# Generate obp-commons.json
|
||||
echo "🔨 Generating obp-commons configuration..."
|
||||
cat > "$PROJECT_ROOT/.bloop/obp-commons.json" << EOF
|
||||
{
|
||||
"version": "1.5.5",
|
||||
"project": {
|
||||
"name": "obp-commons",
|
||||
"directory": "${PROJECT_ROOT}/obp-commons",
|
||||
"workspaceDir": "${PROJECT_ROOT}",
|
||||
"sources": [
|
||||
"${PROJECT_ROOT}/obp-commons/src/main/scala",
|
||||
"${PROJECT_ROOT}/obp-commons/src/main/java"
|
||||
],
|
||||
"dependencies": [],
|
||||
"classpath": [
|
||||
"${PROJECT_ROOT}/obp-commons/target/classes",
|
||||
"${M2_REPO}/net/liftweb/lift-common_2.12/3.5.0/lift-common_2.12-3.5.0.jar",
|
||||
"${M2_REPO}/org/scala-lang/scala-library/2.12.12/scala-library-2.12.12.jar",
|
||||
"${M2_REPO}/org/slf4j/slf4j-api/1.7.25/slf4j-api-1.7.25.jar",
|
||||
"${M2_REPO}/org/scala-lang/modules/scala-xml_2.12/1.3.0/scala-xml_2.12-1.3.0.jar",
|
||||
"${M2_REPO}/org/scala-lang/modules/scala-parser-combinators_2.12/1.1.2/scala-parser-combinators_2.12-1.1.2.jar",
|
||||
"${M2_REPO}/net/liftweb/lift-util_2.12/3.5.0/lift-util_2.12-3.5.0.jar",
|
||||
"${M2_REPO}/org/scala-lang/scala-compiler/2.12.12/scala-compiler-2.12.12.jar",
|
||||
"${M2_REPO}/net/liftweb/lift-actor_2.12/3.5.0/lift-actor_2.12-3.5.0.jar",
|
||||
"${M2_REPO}/net/liftweb/lift-markdown_2.12/3.5.0/lift-markdown_2.12-3.5.0.jar",
|
||||
"${M2_REPO}/joda-time/joda-time/2.10/joda-time-2.10.jar",
|
||||
"${M2_REPO}/org/joda/joda-convert/2.1/joda-convert-2.1.jar",
|
||||
"${M2_REPO}/commons-codec/commons-codec/1.11/commons-codec-1.11.jar",
|
||||
"${M2_REPO}/nu/validator/htmlparser/1.4.12/htmlparser-1.4.12.jar",
|
||||
"${M2_REPO}/xerces/xercesImpl/2.11.0/xercesImpl-2.11.0.jar",
|
||||
"${M2_REPO}/xml-apis/xml-apis/1.4.01/xml-apis-1.4.01.jar",
|
||||
"${M2_REPO}/org/mindrot/jbcrypt/0.4/jbcrypt-0.4.jar",
|
||||
"${M2_REPO}/net/liftweb/lift-mapper_2.12/3.5.0/lift-mapper_2.12-3.5.0.jar",
|
||||
"${M2_REPO}/net/liftweb/lift-db_2.12/3.5.0/lift-db_2.12-3.5.0.jar",
|
||||
"${M2_REPO}/net/liftweb/lift-webkit_2.12/3.5.0/lift-webkit_2.12-3.5.0.jar",
|
||||
"${M2_REPO}/commons-fileupload/commons-fileupload/1.3.3/commons-fileupload-1.3.3.jar",
|
||||
"${M2_REPO}/commons-io/commons-io/2.2/commons-io-2.2.jar",
|
||||
"${M2_REPO}/org/mozilla/rhino/1.7.10/rhino-1.7.10.jar",
|
||||
"${M2_REPO}/net/liftweb/lift-proto_2.12/3.5.0/lift-proto_2.12-3.5.0.jar",
|
||||
"${M2_REPO}/org/scala-lang/scala-reflect/2.12.20/scala-reflect-2.12.20.jar",
|
||||
"${M2_REPO}/org/scalatest/scalatest_2.12/3.0.8/scalatest_2.12-3.0.8.jar",
|
||||
"${M2_REPO}/org/scalactic/scalactic_2.12/3.0.8/scalactic_2.12-3.0.8.jar",
|
||||
"${M2_REPO}/net/liftweb/lift-json_2.12/3.5.0/lift-json_2.12-3.5.0.jar",
|
||||
"${M2_REPO}/org/scala-lang/scalap/2.12.12/scalap-2.12.12.jar",
|
||||
"${M2_REPO}/com/thoughtworks/paranamer/paranamer/2.8/paranamer-2.8.jar",
|
||||
"${M2_REPO}/com/alibaba/transmittable-thread-local/2.11.5/transmittable-thread-local-2.11.5.jar",
|
||||
"${M2_REPO}/org/apache/commons/commons-lang3/3.12.0/commons-lang3-3.12.0.jar",
|
||||
"${M2_REPO}/org/apache/commons/commons-text/1.10.0/commons-text-1.10.0.jar",
|
||||
"${M2_REPO}/com/google/guava/guava/32.0.0-jre/guava-32.0.0-jre.jar",
|
||||
"${M2_REPO}/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar",
|
||||
"${M2_REPO}/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar",
|
||||
"${M2_REPO}/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar",
|
||||
"${M2_REPO}/org/checkerframework/checker-qual/3.33.0/checker-qual-3.33.0.jar",
|
||||
"${M2_REPO}/com/google/errorprone/error_prone_annotations/2.18.0/error_prone_annotations-2.18.0.jar",
|
||||
"${M2_REPO}/com/google/j2objc/j2objc-annotations/2.8/j2objc-annotations-2.8.jar"
|
||||
],
|
||||
"out": "${PROJECT_ROOT}/obp-commons/target/classes",
|
||||
"classesDir": "${PROJECT_ROOT}/obp-commons/target/classes",
|
||||
"resources": [
|
||||
"${PROJECT_ROOT}/obp-commons/src/main/resources"
|
||||
],
|
||||
"scala": {
|
||||
"organization": "org.scala-lang",
|
||||
"name": "scala-compiler",
|
||||
"version": "2.12.20",
|
||||
"options": [
|
||||
"-unchecked",
|
||||
"-explaintypes",
|
||||
"-encoding",
|
||||
"UTF-8",
|
||||
"-feature"
|
||||
],
|
||||
"jars": [
|
||||
"${M2_REPO}/org/scala-lang/scala-library/2.12.20/scala-library-2.12.20.jar",
|
||||
"${M2_REPO}/org/scala-lang/scala-compiler/2.12.20/scala-compiler-2.12.20.jar",
|
||||
"${M2_REPO}/org/scala-lang/scala-reflect/2.12.20/scala-reflect-2.12.20.jar"
|
||||
],
|
||||
"analysis": "${PROJECT_ROOT}/obp-commons/target/bloop-bsp-clients-classes/classes-Metals-",
|
||||
"setup": {
|
||||
"order": "mixed",
|
||||
"addLibraryToBootClasspath": true,
|
||||
"addCompilerToClasspath": false,
|
||||
"addExtraJarsToClasspath": false,
|
||||
"manageBootClasspath": true,
|
||||
"filterLibraryFromClasspath": true
|
||||
}
|
||||
},
|
||||
"java": {
|
||||
"options": ["-source", "11", "-target", "11"]
|
||||
},
|
||||
"platform": {
|
||||
"name": "jvm",
|
||||
"config": {
|
||||
"home": "${JAVA_HOME}",
|
||||
"options": []
|
||||
},
|
||||
"mainClass": []
|
||||
},
|
||||
"resolution": {
|
||||
"modules": []
|
||||
},
|
||||
"tags": ["library"]
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Generate obp-api.json
|
||||
echo "🔨 Generating obp-api configuration..."
|
||||
cat > "$PROJECT_ROOT/.bloop/obp-api.json" << EOF
|
||||
{
|
||||
"version": "1.5.5",
|
||||
"project": {
|
||||
"name": "obp-api",
|
||||
"directory": "${PROJECT_ROOT}/obp-api",
|
||||
"workspaceDir": "${PROJECT_ROOT}",
|
||||
"sources": [
|
||||
"${PROJECT_ROOT}/obp-api/src/main/scala",
|
||||
"${PROJECT_ROOT}/obp-api/src/main/java"
|
||||
],
|
||||
"dependencies": ["obp-commons"],
|
||||
"classpath": [
|
||||
"${PROJECT_ROOT}/obp-api/target/classes",
|
||||
"${PROJECT_ROOT}/obp-commons/target/classes",
|
||||
"${M2_REPO}/com/tesobe/obp-commons/1.10.1/obp-commons-1.10.1.jar",
|
||||
"${M2_REPO}/net/liftweb/lift-common_2.12/3.5.0/lift-common_2.12-3.5.0.jar",
|
||||
"${M2_REPO}/org/scala-lang/scala-library/2.12.12/scala-library-2.12.12.jar",
|
||||
"${M2_REPO}/org/slf4j/slf4j-api/1.7.32/slf4j-api-1.7.32.jar",
|
||||
"${M2_REPO}/org/scala-lang/modules/scala-xml_2.12/1.3.0/scala-xml_2.12-1.3.0.jar",
|
||||
"${M2_REPO}/net/liftweb/lift-util_2.12/3.5.0/lift-util_2.12-3.5.0.jar",
|
||||
"${M2_REPO}/org/scala-lang/scala-compiler/2.12.12/scala-compiler-2.12.12.jar",
|
||||
"${M2_REPO}/net/liftweb/lift-mapper_2.12/3.5.0/lift-mapper_2.12-3.5.0.jar",
|
||||
"${M2_REPO}/net/liftweb/lift-json_2.12/3.5.0/lift-json_2.12-3.5.0.jar",
|
||||
"${M2_REPO}/org/scala-lang/scala-reflect/2.12.20/scala-reflect-2.12.20.jar",
|
||||
"${M2_REPO}/net/databinder/dispatch/dispatch-lift-json_2.12/0.13.1/dispatch-lift-json_2.12-0.13.1.jar",
|
||||
"${M2_REPO}/ch/qos/logback/logback-classic/1.2.13/logback-classic-1.2.13.jar",
|
||||
"${M2_REPO}/org/slf4j/log4j-over-slf4j/1.7.26/log4j-over-slf4j-1.7.26.jar",
|
||||
"${M2_REPO}/org/slf4j/slf4j-ext/1.7.26/slf4j-ext-1.7.26.jar",
|
||||
"${M2_REPO}/org/bouncycastle/bcpg-jdk15on/1.70/bcpg-jdk15on-1.70.jar",
|
||||
"${M2_REPO}/org/bouncycastle/bcpkix-jdk15on/1.70/bcpkix-jdk15on-1.70.jar",
|
||||
"${M2_REPO}/org/apache/commons/commons-lang3/3.12.0/commons-lang3-3.12.0.jar",
|
||||
"${M2_REPO}/org/apache/commons/commons-text/1.10.0/commons-text-1.10.0.jar",
|
||||
"${M2_REPO}/com/github/everit-org/json-schema/org.everit.json.schema/1.6.1/org.everit.json.schema-1.6.1.jar"
|
||||
],
|
||||
"out": "${PROJECT_ROOT}/obp-api/target/classes",
|
||||
"classesDir": "${PROJECT_ROOT}/obp-api/target/classes",
|
||||
"resources": [
|
||||
"${PROJECT_ROOT}/obp-api/src/main/resources"
|
||||
],
|
||||
"scala": {
|
||||
"organization": "org.scala-lang",
|
||||
"name": "scala-compiler",
|
||||
"version": "2.12.20",
|
||||
"options": [
|
||||
"-unchecked",
|
||||
"-explaintypes",
|
||||
"-encoding",
|
||||
"UTF-8",
|
||||
"-feature"
|
||||
],
|
||||
"jars": [
|
||||
"${M2_REPO}/org/scala-lang/scala-library/2.12.20/scala-library-2.12.20.jar",
|
||||
"${M2_REPO}/org/scala-lang/scala-compiler/2.12.20/scala-compiler-2.12.20.jar",
|
||||
"${M2_REPO}/org/scala-lang/scala-reflect/2.12.20/scala-reflect-2.12.20.jar"
|
||||
],
|
||||
"analysis": "${PROJECT_ROOT}/obp-api/target/bloop-bsp-clients-classes/classes-Metals-",
|
||||
"setup": {
|
||||
"order": "mixed",
|
||||
"addLibraryToBootClasspath": true,
|
||||
"addCompilerToClasspath": false,
|
||||
"addExtraJarsToClasspath": false,
|
||||
"manageBootClasspath": true,
|
||||
"filterLibraryFromClasspath": true
|
||||
}
|
||||
},
|
||||
"java": {
|
||||
"options": ["-source", "11", "-target", "11"]
|
||||
},
|
||||
"platform": {
|
||||
"name": "jvm",
|
||||
"config": {
|
||||
"home": "${JAVA_HOME}",
|
||||
"options": []
|
||||
},
|
||||
"mainClass": []
|
||||
},
|
||||
"resolution": {
|
||||
"modules": []
|
||||
},
|
||||
"tags": ["application"]
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
echo -e "${GREEN}✅ Generated $PROJECT_ROOT/.bloop/obp-commons.json${NC}"
|
||||
echo -e "${GREEN}✅ Generated $PROJECT_ROOT/.bloop/obp-api.json${NC}"
|
||||
|
||||
# Verify the configurations
|
||||
echo "🔍 Verifying generated configurations..."
|
||||
if command -v bloop &> /dev/null; then
|
||||
if bloop projects | grep -q "obp-api\|obp-commons"; then
|
||||
echo -e "${GREEN}✅ Bloop can detect the projects${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Bloop server may need to be restarted to detect new configurations${NC}"
|
||||
echo "Run: pkill -f bloop && bloop server &"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Bloop not found, skipping verification${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}🎉 Bloop configuration generation complete!${NC}"
|
||||
echo ""
|
||||
echo "📋 Next steps:"
|
||||
echo "1. Restart Bloop server if needed: pkill -f bloop && bloop server &"
|
||||
echo "2. Verify projects are detected: bloop projects"
|
||||
echo "3. Test compilation: bloop compile obp-commons obp-api"
|
||||
echo "4. Open project in Zed IDE for full language server support"
|
||||
echo ""
|
||||
echo -e "${GREEN}Happy coding! 🚀${NC}"
|
||||
80
zed/settings.json
Normal file
80
zed/settings.json
Normal file
@ -0,0 +1,80 @@
|
||||
{
|
||||
"format_on_save": "off",
|
||||
"tab_size": 2,
|
||||
"terminal": {
|
||||
"env": {
|
||||
"MAVEN_OPTS": "-Xss128m --add-opens=java.base/java.util.jar=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED"
|
||||
}
|
||||
},
|
||||
"project_panel": {
|
||||
"dock": "left",
|
||||
"default_width": 300
|
||||
},
|
||||
"outline_panel": {
|
||||
"dock": "right"
|
||||
},
|
||||
"theme": "One Dark",
|
||||
"ui_font_size": 14,
|
||||
"buffer_font_size": 14,
|
||||
"soft_wrap": "editor_width",
|
||||
"show_whitespaces": "selection",
|
||||
"tabs": {
|
||||
"git_status": true,
|
||||
"file_icons": true
|
||||
},
|
||||
"gutter": {
|
||||
"line_numbers": true
|
||||
},
|
||||
"scrollbar": {
|
||||
"show": "auto"
|
||||
},
|
||||
"indent_guides": {
|
||||
"enabled": true
|
||||
},
|
||||
"lsp": {
|
||||
"metals": {
|
||||
"initialization_options": {
|
||||
"compileOnSave": true,
|
||||
"debuggingProvider": true,
|
||||
"decorationProvider": true,
|
||||
"didFocusProvider": true,
|
||||
"doctorProvider": "html",
|
||||
"executeClientCommandProvider": true,
|
||||
"inputBoxProvider": true,
|
||||
"quickPickProvider": true,
|
||||
"renameProvider": true,
|
||||
"statusBarProvider": "on",
|
||||
"treeViewProvider": true,
|
||||
"buildTool": "maven"
|
||||
},
|
||||
"settings": {
|
||||
"metals.ammoniteJvmProperties": ["-Xmx1G"],
|
||||
"metals.buildServer.version": "2.0.0",
|
||||
"metals.javaFormat.eclipseConfigPath": "",
|
||||
"metals.javaFormat.eclipseProfile": "",
|
||||
"metals.superMethodLensesEnabled": true,
|
||||
"metals.testUserInterface": "Code Lenses",
|
||||
"metals.bloopSbtAlreadyInstalled": true,
|
||||
"metals.gradleScript": "",
|
||||
"metals.mavenScript": "",
|
||||
"metals.millScript": "",
|
||||
"metals.sbtScript": "",
|
||||
"metals.scalafmtConfigPath": ".scalafmt.conf",
|
||||
"metals.enableSemanticHighlighting": true,
|
||||
"metals.allowMultilineStringFormatting": true,
|
||||
"metals.inlayHints.enabled": true,
|
||||
"metals.inlayHints.hintsInPatternMatch.enabled": true,
|
||||
"metals.inlayHints.implicitArguments.enabled": true,
|
||||
"metals.inlayHints.implicitConversions.enabled": true,
|
||||
"metals.inlayHints.inferredTypes.enabled": true,
|
||||
"metals.inlayHints.typeParameters.enabled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"languages": {
|
||||
"Scala": {
|
||||
"language_servers": ["metals"],
|
||||
"format_on_save": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
221
zed/setup-zed-ide.sh
Executable file
221
zed/setup-zed-ide.sh
Executable file
@ -0,0 +1,221 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ZED IDE Complete Setup Script for OBP-API
|
||||
# This script provides a unified setup for ZED IDE with full Scala language server support
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Setting up ZED IDE for OBP-API Scala development..."
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Get the project root directory (parent of zed folder)
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
echo "📁 Project root: $PROJECT_ROOT"
|
||||
|
||||
# Check if we're in the zed directory and project structure exists
|
||||
if [[ ! -f "$PROJECT_ROOT/pom.xml" ]] || [[ ! -d "$PROJECT_ROOT/obp-api" ]] || [[ ! -d "$PROJECT_ROOT/obp-commons" ]]; then
|
||||
echo -e "${RED}❌ Error: Could not find OBP-API project structure${NC}"
|
||||
echo "Make sure you're running this from the zed/ folder of the OBP-API project"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Change to project root for Maven operations
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
echo "📁 Working directory: $(pwd)"
|
||||
|
||||
# Check prerequisites
|
||||
echo "🔍 Checking prerequisites..."
|
||||
|
||||
# Check Java
|
||||
if ! command -v java &> /dev/null; then
|
||||
echo -e "${RED}❌ Java not found. Please install Java 11 or 17${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAVA_VERSION=$(java -version 2>&1 | head -1 | cut -d'"' -f2 | cut -d'.' -f1-2)
|
||||
echo -e "${GREEN}✅ Java found: ${JAVA_VERSION}${NC}"
|
||||
|
||||
# Check Maven
|
||||
if ! command -v mvn &> /dev/null; then
|
||||
echo -e "${RED}❌ Maven not found. Please install Maven${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MVN_VERSION=$(mvn -version 2>&1 | head -1 | cut -d' ' -f3)
|
||||
echo -e "${GREEN}✅ Maven found: ${MVN_VERSION}${NC}"
|
||||
|
||||
# Check Coursier
|
||||
if ! command -v cs &> /dev/null; then
|
||||
echo -e "${YELLOW}⚠️ Coursier not found. Installing...${NC}"
|
||||
curl -fL https://github.com/coursier/coursier/releases/latest/download/cs-x86_64-pc-linux.gz | gzip -d > cs
|
||||
chmod +x cs
|
||||
sudo mv cs /usr/local/bin/
|
||||
echo -e "${GREEN}✅ Coursier installed${NC}"
|
||||
else
|
||||
echo -e "${GREEN}✅ Coursier found${NC}"
|
||||
fi
|
||||
|
||||
# Check/Install Bloop
|
||||
if ! command -v bloop &> /dev/null; then
|
||||
echo -e "${YELLOW}⚠️ Bloop not found. Installing...${NC}"
|
||||
cs install bloop
|
||||
echo -e "${GREEN}✅ Bloop installed${NC}"
|
||||
else
|
||||
echo -e "${GREEN}✅ Bloop found: $(bloop about | head -1)${NC}"
|
||||
fi
|
||||
|
||||
# Start Bloop server if not running
|
||||
if ! pgrep -f "bloop.*server" > /dev/null; then
|
||||
echo "🔧 Starting Bloop server..."
|
||||
bloop server &
|
||||
sleep 3
|
||||
echo -e "${GREEN}✅ Bloop server started${NC}"
|
||||
else
|
||||
echo -e "${GREEN}✅ Bloop server already running${NC}"
|
||||
fi
|
||||
|
||||
# Compile the project to ensure dependencies are resolved
|
||||
echo "🔨 Compiling Maven project (this may take a few minutes)..."
|
||||
if mvn compile -q; then
|
||||
echo -e "${GREEN}✅ Maven compilation successful${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Maven compilation failed. Please fix compilation errors first.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy ZED configuration files to project root
|
||||
echo "📋 Setting up ZED IDE configuration..."
|
||||
ZED_DIR="$PROJECT_ROOT/.zed"
|
||||
ZED_SRC_DIR="$PROJECT_ROOT/zed"
|
||||
|
||||
# Create .zed directory if it doesn't exist
|
||||
if [ ! -d "$ZED_DIR" ]; then
|
||||
echo "📁 Creating .zed directory..."
|
||||
mkdir -p "$ZED_DIR"
|
||||
else
|
||||
echo "📁 .zed directory already exists"
|
||||
fi
|
||||
|
||||
# Copy settings.json
|
||||
if [ -f "$ZED_SRC_DIR/settings.json" ]; then
|
||||
echo "⚙️ Copying settings.json..."
|
||||
cp "$ZED_SRC_DIR/settings.json" "$ZED_DIR/settings.json"
|
||||
echo -e "${GREEN}✅ settings.json copied successfully${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Error: settings.json not found in zed folder${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy tasks.json
|
||||
if [ -f "$ZED_SRC_DIR/tasks.json" ]; then
|
||||
echo "📋 Copying tasks.json..."
|
||||
cp "$ZED_SRC_DIR/tasks.json" "$ZED_DIR/tasks.json"
|
||||
echo -e "${GREEN}✅ tasks.json copied successfully${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Error: tasks.json not found in zed folder${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy .metals-config.json if it exists
|
||||
if [[ -f "$ZED_SRC_DIR/.metals-config.json" ]]; then
|
||||
echo "🔧 Copying Metals configuration..."
|
||||
cp "$ZED_SRC_DIR/.metals-config.json" "$PROJECT_ROOT/.metals-config.json"
|
||||
echo -e "${GREEN}✅ Metals configuration copied${NC}"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ ZED configuration files copied to .zed/ folder${NC}"
|
||||
|
||||
# Generate Bloop configuration files dynamically
|
||||
echo "🔧 Generating Bloop configuration files..."
|
||||
if [[ -f "$ZED_SRC_DIR/generate-bloop-config.sh" ]]; then
|
||||
chmod +x "$ZED_SRC_DIR/generate-bloop-config.sh"
|
||||
"$ZED_SRC_DIR/generate-bloop-config.sh"
|
||||
echo -e "${GREEN}✅ Bloop configuration files generated${NC}"
|
||||
else
|
||||
# Fallback: Check if existing configurations are present
|
||||
if [[ -f "$PROJECT_ROOT/.bloop/obp-commons.json" && -f "$PROJECT_ROOT/.bloop/obp-api.json" ]]; then
|
||||
echo -e "${GREEN}✅ Bloop configuration files already exist${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Bloop configuration files missing and generator not found.${NC}"
|
||||
echo "Please ensure .bloop/*.json files exist or run zed/generate-bloop-config.sh manually"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Restart Bloop server to pick up new configurations
|
||||
echo "🔄 Restarting Bloop server to detect new configurations..."
|
||||
pkill -f bloop 2>/dev/null || true
|
||||
sleep 1
|
||||
bloop server &
|
||||
sleep 2
|
||||
|
||||
# Verify Bloop can see projects
|
||||
echo "🔍 Verifying Bloop projects..."
|
||||
BLOOP_PROJECTS=$(bloop projects 2>/dev/null || echo "")
|
||||
if [[ "$BLOOP_PROJECTS" == *"obp-api"* && "$BLOOP_PROJECTS" == *"obp-commons"* ]]; then
|
||||
echo -e "${GREEN}✅ Bloop projects detected:${NC}"
|
||||
echo "$BLOOP_PROJECTS" | sed 's/^/ /'
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Bloop projects not immediately detected. This is normal for fresh setups.${NC}"
|
||||
echo "The configuration should work when you open ZED IDE."
|
||||
fi
|
||||
|
||||
# Test Bloop compilation
|
||||
echo "🧪 Testing Bloop compilation..."
|
||||
if bloop compile obp-commons > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✅ Bloop compilation test successful${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Bloop compilation test failed, but setup is complete. Try restarting ZED IDE.${NC}"
|
||||
fi
|
||||
|
||||
# Check ZED configuration
|
||||
if [[ -f "$PROJECT_ROOT/.zed/settings.json" ]]; then
|
||||
echo -e "${GREEN}✅ ZED configuration found${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ ZED configuration not found in .zed/settings.json${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}🎉 ZED IDE setup completed successfully!${NC}"
|
||||
echo ""
|
||||
echo "Your ZED configuration includes:"
|
||||
echo " • Format on save: DISABLED (manual formatting only - use Ctrl+Shift+I)"
|
||||
echo " • Scala/Metals LSP configuration optimized for OBP-API"
|
||||
echo " • Pre-configured build and run tasks"
|
||||
echo " • Dynamic Bloop configuration for language server support"
|
||||
echo ""
|
||||
echo "📋 Next steps:"
|
||||
echo "1. Open ZED IDE"
|
||||
echo "2. Open the OBP-API project directory in ZED"
|
||||
echo "3. Wait for Metals to initialize (may take a few minutes)"
|
||||
echo "4. Try 'Go to Definition' on a Scala symbol (F12 or Cmd+Click)"
|
||||
echo ""
|
||||
echo "🛠️ Available tasks (access with Cmd/Ctrl + Shift + P → 'task: spawn'):"
|
||||
echo " • [1] Run OBP-API Server - Start development server"
|
||||
echo " • [2] Test API Root Endpoint - Quick health check"
|
||||
echo " • [3] Compile Only - Fast syntax check"
|
||||
echo " • [4] Clean Target Folders - Remove build artifacts"
|
||||
echo " • Quick Build Dependencies - Build deps only (for onboarding)"
|
||||
echo " • Run Tests - Execute full test suite"
|
||||
echo ""
|
||||
echo "💡 Troubleshooting:"
|
||||
echo "• If 'Go to Definition' doesn't work immediately, restart ZED IDE"
|
||||
echo "• Use 'ZED: Reload Window' from the command palette if needed"
|
||||
echo "• Check zed/README.md for comprehensive documentation"
|
||||
echo "• Run './zed/generate-bloop-config.sh' to regenerate configurations if needed"
|
||||
echo ""
|
||||
echo "🔗 Resources:"
|
||||
echo "• Complete ZED setup guide: zed/README.md"
|
||||
echo "• Bloop projects: bloop projects"
|
||||
echo "• Bloop compilation: bloop compile obp-commons obp-api"
|
||||
echo ""
|
||||
echo "Note: The .zed folder is in .gitignore, so you can customize settings"
|
||||
echo " without affecting other developers."
|
||||
echo ""
|
||||
echo -e "${GREEN}Happy coding! 🚀${NC}"
|
||||
64
zed/setup-zed.bat
Normal file
64
zed/setup-zed.bat
Normal file
@ -0,0 +1,64 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM Zed IDE Setup Script for OBP-API (Windows)
|
||||
REM This script copies the recommended Zed configuration to your local .zed folder
|
||||
|
||||
echo 🔧 Setting up Zed IDE configuration for OBP-API...
|
||||
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
set "PROJECT_ROOT=%SCRIPT_DIR%.."
|
||||
set "ZED_DIR=%PROJECT_ROOT%\.zed"
|
||||
|
||||
REM Create .zed directory if it doesn't exist
|
||||
if not exist "%ZED_DIR%" (
|
||||
echo 📁 Creating .zed directory...
|
||||
mkdir "%ZED_DIR%"
|
||||
) else (
|
||||
echo 📁 .zed directory already exists
|
||||
)
|
||||
|
||||
REM Copy settings.json
|
||||
if exist "%SCRIPT_DIR%settings.json" (
|
||||
echo ⚙️ Copying settings.json...
|
||||
copy "%SCRIPT_DIR%settings.json" "%ZED_DIR%\settings.json" >nul
|
||||
if !errorlevel! equ 0 (
|
||||
echo ✅ settings.json copied successfully
|
||||
) else (
|
||||
echo ❌ Error copying settings.json
|
||||
exit /b 1
|
||||
)
|
||||
) else (
|
||||
echo ❌ Error: settings.json not found in zed folder
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Copy tasks.json
|
||||
if exist "%SCRIPT_DIR%tasks.json" (
|
||||
echo 📋 Copying tasks.json...
|
||||
copy "%SCRIPT_DIR%tasks.json" "%ZED_DIR%\tasks.json" >nul
|
||||
if !errorlevel! equ 0 (
|
||||
echo ✅ tasks.json copied successfully
|
||||
) else (
|
||||
echo ❌ Error copying tasks.json
|
||||
exit /b 1
|
||||
)
|
||||
) else (
|
||||
echo ❌ Error: tasks.json not found in zed folder
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 🎉 Zed IDE setup completed successfully!
|
||||
echo.
|
||||
echo Your Zed configuration includes:
|
||||
echo • Format on save: DISABLED (preserves your code formatting)
|
||||
echo • Scala/Metals LSP configuration optimized for OBP-API
|
||||
echo • 9 predefined tasks for building, running, and testing
|
||||
echo.
|
||||
echo To see available tasks in Zed, use: Ctrl + Shift + P → 'task: spawn'
|
||||
echo.
|
||||
echo Note: The .zed folder is in .gitignore, so you can customize settings
|
||||
echo without affecting other developers.
|
||||
|
||||
pause
|
||||
111
zed/tasks.json
Normal file
111
zed/tasks.json
Normal file
@ -0,0 +1,111 @@
|
||||
[
|
||||
{
|
||||
"label": "[1] Run OBP-API Server",
|
||||
"command": "mvn",
|
||||
"args": ["jetty:run", "-pl", "obp-api"],
|
||||
"env": {
|
||||
"MAVEN_OPTS": "-Xss128m --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.math=ALL-UNNAMED --add-opens=java.base/java.util.stream=ALL-UNNAMED --add-opens=java.base/java.util.regex=ALL-UNNAMED"
|
||||
}
|
||||
"use_new_terminal": true,
|
||||
"allow_concurrent_runs": false,
|
||||
"reveal": "always",
|
||||
"tags": ["run", "server"]
|
||||
},
|
||||
{
|
||||
"label": "[2] Test API Root Endpoint",
|
||||
"command": "curl",
|
||||
"args": [
|
||||
"-X",
|
||||
"GET",
|
||||
"http://localhost:8080/obp/v5.1.0/root",
|
||||
"-H",
|
||||
"accept: application/json"
|
||||
],
|
||||
"use_new_terminal": false,
|
||||
"allow_concurrent_runs": true,
|
||||
"reveal": "always",
|
||||
"tags": ["test", "api"]
|
||||
},
|
||||
{
|
||||
"label": "[3] Compile Only",
|
||||
"command": "mvn",
|
||||
"args": ["compile", "-pl", "obp-api"],
|
||||
"env": {
|
||||
"MAVEN_OPTS": "-Xss128m --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.math=ALL-UNNAMED"
|
||||
},
|
||||
"use_new_terminal": false,
|
||||
"allow_concurrent_runs": false,
|
||||
"reveal": "always",
|
||||
"tags": ["compile", "build"]
|
||||
},
|
||||
{
|
||||
"label": "[4] Build OBP-API",
|
||||
"command": "mvn",
|
||||
"args": [
|
||||
"install",
|
||||
"-pl",
|
||||
".,obp-commons",
|
||||
"-am",
|
||||
"-DskipTests",
|
||||
"-Ddependency-check.skip=true"
|
||||
],
|
||||
"env": {
|
||||
"MAVEN_OPTS": "-Xss128m --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.math=ALL-UNNAMED"
|
||||
},
|
||||
"use_new_terminal": false,
|
||||
"allow_concurrent_runs": false,
|
||||
"reveal": "always",
|
||||
"tags": ["build"]
|
||||
},
|
||||
{
|
||||
"label": "[5] Clean Target Folders",
|
||||
"command": "mvn",
|
||||
"args": ["clean"],
|
||||
"use_new_terminal": false,
|
||||
"allow_concurrent_runs": false,
|
||||
"reveal": "always",
|
||||
"tags": ["clean", "build"]
|
||||
},
|
||||
{
|
||||
"label": "[6] Kill OBP-APIServer on Port 8080",
|
||||
"command": "bash",
|
||||
"args": [
|
||||
"-c",
|
||||
"lsof -ti:8080 | xargs kill -9 || echo 'No process found on port 8080'"
|
||||
],
|
||||
"use_new_terminal": false,
|
||||
"allow_concurrent_runs": true,
|
||||
"reveal": "always",
|
||||
"tags": ["utility"]
|
||||
},
|
||||
{
|
||||
"label": "[7] Run Tests",
|
||||
"command": "mvn",
|
||||
"args": ["test", "-pl", "obp-api"],
|
||||
"env": {
|
||||
"MAVEN_OPTS": "-Xss128m --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.math=ALL-UNNAMED"
|
||||
},
|
||||
"use_new_terminal": false,
|
||||
"allow_concurrent_runs": false,
|
||||
"reveal": "always",
|
||||
"tags": ["test"]
|
||||
},
|
||||
{
|
||||
"label": "[8] Maven Validate",
|
||||
"command": "mvn",
|
||||
"args": ["validate"],
|
||||
"use_new_terminal": false,
|
||||
"allow_concurrent_runs": false,
|
||||
"reveal": "always",
|
||||
"tags": ["validate"]
|
||||
},
|
||||
{
|
||||
"label": "[9] Check Dependencies",
|
||||
"command": "mvn",
|
||||
"args": ["dependency:resolve"],
|
||||
"use_new_terminal": false,
|
||||
"allow_concurrent_runs": false,
|
||||
"reveal": "always",
|
||||
"tags": ["dependencies"]
|
||||
}
|
||||
]
|
||||
Loading…
Reference in New Issue
Block a user