diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 8c51d388fd..9ef3a3bfcf 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -37,7 +37,10 @@ connector=star #hikari.connectionTimeout= #hikari.maximumPoolSize= #hikari.idleTimeout= -#hikari.keepaliveTime= +# keepaliveTime must be set below the database/firewall idle TCP timeout to prevent stale-connection +# failures on the first call after a period of inactivity. HikariCP will ping each idle connection +# at this interval (in ms), keeping it alive. Recommended: 30000 (30s) when DB/firewall timeout is ~60s. +#hikari.keepaliveTime=30000 #hikari.maxLifetime= ## if connector = star, then need to set which connectors will be used. For now, obp support rest, akka. diff --git a/obp-api/src/main/scala/bootstrap/liftweb/CustomDBVendor.scala b/obp-api/src/main/scala/bootstrap/liftweb/CustomDBVendor.scala index 0f7c3fbe50..763c7ca4ae 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/CustomDBVendor.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/CustomDBVendor.scala @@ -30,7 +30,7 @@ class CustomDBVendor(driverName: String, 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 keepaliveTime = APIUtil.getPropsAsLongValue("hikari.keepaliveTime", 30000L) val maxLifetime = APIUtil.getPropsAsLongValue("hikari.maxLifetime") if(connectionTimeout.isDefined){ @@ -42,9 +42,7 @@ class CustomDBVendor(driverName: String, if(idleTimeout.isDefined){ config.setIdleTimeout(idleTimeout.head) } - if(keepaliveTime.isDefined){ - config.setKeepaliveTime(keepaliveTime.head) - } + config.setKeepaliveTime(keepaliveTime) if(maxLifetime.isDefined){ config.setMaxLifetime(maxLifetime.head) } diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 0591bd6a1e..294dd08cb4 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -2635,6 +2635,8 @@ object SwaggerDefinitionsJSON { provider_id = providerIdValueExample.value, provider = providerValueExample.value, username = usernameExample.value, + first_name = ExampleValue.firstNameExample.value, + last_name = ExampleValue.lastNameExample.value, entitlements = entitlementJSONs, views = Some(viewsJSON300), agreements = Some(List(userAgreementJson)), @@ -2649,6 +2651,22 @@ object SwaggerDefinitionsJSON { users = List(userInfoJsonV600) ) + lazy val userWithNamesJsonV510 = UserWithNamesJsonV510( + user_id = ExampleValue.userIdExample.value, + email = ExampleValue.emailExample.value, + provider_id = providerIdValueExample.value, + provider = providerValueExample.value, + username = usernameExample.value, + first_name = ExampleValue.firstNameExample.value, + last_name = ExampleValue.lastNameExample.value, + entitlements = entitlementJSONs, + views = Some(viewsJSON300), + agreements = Some(List(userAgreementJson)), + is_deleted = false, + last_marketing_agreement_signed_date = Some(DateWithDayExampleObject), + is_locked = false + ) + lazy val migrationScriptLogJsonV600 = MigrationScriptLogJsonV600( migration_script_log_id = "550e8400-e29b-41d4-a716-446655440000", name = "addUniqueIndexOnResourceUserUserId", diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index 1e8b90773a..e6728b32f5 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -2670,7 +2670,7 @@ trait APIMethods510 { | """.stripMargin, EmptyBody, - userJsonV400, + userWithNamesJsonV510, List($AuthenticatedUserIsRequired, UserHasMissingRoles, UserNotFoundByProviderAndUsername, UnknownError), List(apiTagUser), Some(List(canGetAnyUser)) @@ -2685,8 +2685,14 @@ trait APIMethods510 { } entitlements <- NewStyle.function.getEntitlementsByUserId(user.userId, cc.callContext) isLocked = LoginAttempt.userIsLocked(user.provider, user.name) + authUser = AuthUser.find(By(AuthUser.user, user.userPrimaryKey.value)) } yield { - (JSONFactory400.createUserInfoJSON(user, entitlements, None, isLocked), HttpCode.`200`(cc.callContext)) + (JSONFactory510.createUserWithNamesJSON( + user, + authUser.map(_.firstName.get).getOrElse(""), + authUser.map(_.lastName.get).getOrElse(""), + entitlements, None, isLocked + ), HttpCode.`200`(cc.callContext)) } } } diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala index f1f36add98..54543a4c45 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala @@ -40,8 +40,14 @@ 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.v2_0_0.{EntitlementJSONs, JSONFactory200} +import code.api.v3_0_0.ViewsJSON300 +import code.api.v4_0_0.{EnergySource400, HostedAt400, HostedBy400, UserAgreementJson} import code.api.v5_0_0.PostConsentRequestJsonV500 +import code.entitlement.Entitlement +import code.model.dataAccess.AuthUser +import code.users.UserAgreement +import net.liftweb.mapper.By import code.atmattribute.AtmAttribute import code.atms.Atms.Atm import code.consent.MappedConsent @@ -662,6 +668,22 @@ case class SyncExternalUserJson(user_id: String) case class UserValidatedJson(is_validated: Boolean) +case class UserWithNamesJsonV510( + user_id: String, + email: String, + provider_id: String, + provider: String, + username: String, + first_name: String, + last_name: String, + entitlements: EntitlementJSONs, + views: Option[ViewsJSON300], + agreements: Option[List[UserAgreementJson]], + is_deleted: Boolean, + last_marketing_agreement_signed_date: Option[Date], + is_locked: Boolean +) + case class BankAccountBalanceRequestJsonV510( balance_type: String, @@ -1346,4 +1368,31 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable { } + def createUserWithNamesJSON( + user: User, + firstName: String, + lastName: String, + entitlements: List[Entitlement], + agreements: Option[List[UserAgreement]], + isLocked: Boolean + ): UserWithNamesJsonV510 = { + UserWithNamesJsonV510( + user_id = user.userId, + email = user.emailAddress, + provider_id = user.idGivenByProvider, + provider = stringOrNull(user.provider), + username = stringOrNull(user.name), + first_name = firstName, + last_name = lastName, + entitlements = JSONFactory200.createEntitlementJSONs(entitlements), + views = None, + agreements = agreements.map( + _.map(i => UserAgreementJson(`type` = i.agreementType, text = i.agreementText)) + ), + is_deleted = user.isDeleted.getOrElse(false), + last_marketing_agreement_signed_date = user.lastMarketingAgreementSignedDate, + is_locked = isLocked + ) + } + } diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index e30f8d6f45..24e75d26ec 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -1852,6 +1852,9 @@ trait APIMethods600 { if (agreementList.isEmpty) None else Some(agreementList) } isLocked = LoginAttempt.userIsLocked(user.provider, user.name) + authUser = code.model.dataAccess.AuthUser.find( + By(code.model.dataAccess.AuthUser.user, user.userPrimaryKey.value) + ) // Fetch metrics data for the user userMetrics <- Future { code.metrics.MappedMetric.findAll( @@ -1863,7 +1866,16 @@ trait APIMethods600 { lastActivityDate = userMetrics.headOption.map(_.getDate()) recentOperationIds = userMetrics.map(_.getImplementedByPartialFunction()).distinct.take(5) } yield { - (JSONFactory600.createUserInfoJsonV600(user, entitlements, agreements, isLocked, lastActivityDate, recentOperationIds), HttpCode.`200`(callContext)) + (JSONFactory600.createUserInfoJsonV600( + user, + authUser.map(_.firstName.get).getOrElse(""), + authUser.map(_.lastName.get).getOrElse(""), + entitlements, + agreements, + isLocked, + lastActivityDate, + recentOperationIds + ), HttpCode.`200`(callContext)) } } } diff --git a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala index 5ff232ea5e..2eab8090be 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala @@ -37,8 +37,9 @@ import code.apiproductattribute.ApiProductAttributeTrait import code.featuredapicollection.FeaturedApiCollectionTrait import code.loginattempts.LoginAttempt import code.model.ModeratedBankAccountCore -import code.model.dataAccess.ResourceUser +import code.model.dataAccess.{AuthUser, ResourceUser} import code.users.UserAgreement +import net.liftweb.mapper.By import code.util.Helper.MdcLoggable import com.openbankproject.commons.model.{ AmountOfMoneyJsonV121, @@ -249,6 +250,8 @@ case class UserInfoJsonV600( provider_id: String, provider: String, username: String, + first_name: String, + last_name: String, entitlements: EntitlementJSONs, views: Option[ViewsJSON300], agreements: Option[List[UserAgreementJson]], @@ -1077,6 +1080,8 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable { def createUserInfoJsonV600( user: User, + firstName: String, + lastName: String, entitlements: List[Entitlement], agreements: Option[List[UserAgreement]], isLocked: Boolean, @@ -1089,6 +1094,8 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable { username = stringOrNull(user.name), provider_id = user.idGivenByProvider, provider = stringOrNull(user.provider), + first_name = firstName, + last_name = lastName, entitlements = JSONFactory200.createEntitlementJSONs(entitlements), views = None, agreements = agreements.map( @@ -1111,16 +1118,19 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable { ] ): UsersInfoJsonV600 = { UsersInfoJsonV600( - users.map(t => + users.map { t => + val authUser = AuthUser.find(By(AuthUser.user, t._1.id.get)) createUserInfoJsonV600( t._1, + authUser.map(_.firstName.get).getOrElse(""), + authUser.map(_.lastName.get).getOrElse(""), t._2.getOrElse(Nil), t._3, LoginAttempt.userIsLocked(t._1.provider, t._1.name), None, List.empty ) - ) + } ) } diff --git a/obp-api/src/test/scala/code/api/v5_1_0/UserTest.scala b/obp-api/src/test/scala/code/api/v5_1_0/UserTest.scala index 64878373f3..2ae08eeb39 100644 --- a/obp-api/src/test/scala/code/api/v5_1_0/UserTest.scala +++ b/obp-api/src/test/scala/code/api/v5_1_0/UserTest.scala @@ -4,15 +4,16 @@ import code.api.util.APIUtil.OAuth._ import code.api.util.ApiRole.{CanGetAnyUser, CanGetEntitlementsForAnyUserAtAnyBank, CanValidateUser} import code.api.util.ErrorMessages.{UserHasMissingRoles, AuthenticatedUserIsRequired, attemptedToOpenAnEmptyBox} import code.api.v3_0_0.UserJsonV300 -import code.api.v4_0_0.UserJsonV400 import code.api.v5_1_0.OBPAPI5_1_0.Implementations5_1_0 import code.entitlement.Entitlement import code.model.UserX +import code.model.dataAccess.AuthUser import code.users.Users import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.model.ErrorMessage import com.openbankproject.commons.util.ApiVersion import net.liftweb.json.Serialization.write +import net.liftweb.util.Helpers.randomString import org.scalatest.Tag import java.util.UUID @@ -59,14 +60,37 @@ class UserTest extends V510ServerSetup { When("We make a request v5.1.0") val request400 = (v5_1_0_Request / "users" / "provider"/user.provider / "username" / user.name ).GET <@(user1) val response400 = makeGetRequest(request400) - Then("We get successful response") + Then("We get successful response with first_name and last_name fields") response400.code should equal(200) - response400.body.extract[UserJsonV400] + val json = response400.body.extract[UserWithNamesJsonV510] + json.first_name should equal("") + json.last_name should equal("") Users.users.vend.deleteResourceUser(user.id.get) } } - + feature(s"test $ApiEndpoint1 version $VersionOfApi - first_name and last_name populated from AuthUser") { + scenario("We will call the endpoint with an AuthUser that has first and last name set", ApiEndpoint1, VersionOfApi) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAnyUser.toString) + val username = "user.withnames." + UUID.randomUUID.toString.take(8) + val email = s"$username@example.com" + val user = UserX.createResourceUser(defaultProvider, Some(username), None, Some(username), None, Some(UUID.randomUUID.toString), None).openOrThrowException(attemptedToOpenAnEmptyBox) + val authUser = AuthUser.create + .email(email).username(username).password(randomString(12)) + .validated(true).firstName("Alice").lastName("Smith") + .provider(defaultProvider).user(user.userPrimaryKey.value).saveMe() + When("We make a request v5.1.0") + val request = (v5_1_0_Request / "users" / "provider" / user.provider / "username" / user.name).GET <@(user1) + val response = makeGetRequest(request) + Then("We get first_name and last_name from AuthUser") + response.code should equal(200) + val json = response.body.extract[UserWithNamesJsonV510] + json.first_name should equal("Alice") + json.last_name should equal("Smith") + authUser.delete_! + Users.users.vend.deleteResourceUser(user.id.get) + } + } feature(s"test $ApiEndpoint1 version $VersionOfApi - Authorized access with URL-encoded provider") { scenario("We will call the endpoint with a provider containing special URL characters (colon, slash)", ApiEndpoint1, VersionOfApi) { @@ -81,7 +105,7 @@ class UserTest extends V510ServerSetup { val response = makeGetRequest(request) Then("We get successful response - endpoint correctly URL-decodes the provider") response.code should equal(200) - response.body.extract[UserJsonV400] + response.body.extract[UserWithNamesJsonV510] Users.users.vend.deleteResourceUser(user.id.get) } } diff --git a/obp-api/src/test/scala/code/api/v6_0_0/GetUserByUserIdTest.scala b/obp-api/src/test/scala/code/api/v6_0_0/GetUserByUserIdTest.scala index a984877124..c393da1fd3 100644 --- a/obp-api/src/test/scala/code/api/v6_0_0/GetUserByUserIdTest.scala +++ b/obp-api/src/test/scala/code/api/v6_0_0/GetUserByUserIdTest.scala @@ -57,6 +57,9 @@ class GetUserByUserIdTest extends V600ServerSetup with DefaultUsers { And("The response should contain user details") (response.body \ "user_id").extract[String] should equal(resourceUser1.userId) + And("The response should include first_name and last_name fields") + response.body \ "first_name" should not equal net.liftweb.json.JNothing + response.body \ "last_name" should not equal net.liftweb.json.JNothing } }