diff --git a/obp-api/pom.xml b/obp-api/pom.xml index ed9eaa037..28695365f 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -210,7 +210,7 @@ org.elasticsearch elasticsearch - 6.8.13 + 6.8.17 com.sksamuel.elastic4s diff --git a/obp-api/src/main/resources/ResourceDocs/ResourceDocs-Chinese.json b/obp-api/src/main/resources/ResourceDocs/ResourceDocs-Chinese.json index b7655ca8a..0de8d4327 100644 --- a/obp-api/src/main/resources/ResourceDocs/ResourceDocs-Chinese.json +++ b/obp-api/src/main/resources/ResourceDocs/ResourceDocs-Chinese.json @@ -8446,7 +8446,7 @@ "request_url": "/obp/v4.0.0/management/dynamic_entities/DYNAMIC_ENTITY_ID", "summary": "更新DynamicEntity", "description": "

更新DynamicEntity。

身份验证是强制性的

更新一个DynamicEntity,更新完成后,将更改相应的CRUD端点。

当前的支持文件类型如下:
[字符串,数字,整数,布尔值]

", - "description_markdown": "Update a DynamicEntity.\n\n\nAuthentication is Mandatory\n\nUpdate one DynamicEntity, after update finished, the corresponding CRUD endpoints will be changed.\n\nCurrent support field types as follow:\n[string, number, integer, boolean]\n\n", + "description_markdown": "Update a DynamicEntity.\n\n\nAuthentication is Mandatory\n\nUpdate one DynamicEntity, after update finished, the corresponding CRUD endpoints will be changed.\n\nThe following field types are as supported:\n[string, number, integer, boolean]\n\n", "example_request_body": { "FooBar": { "required": [ @@ -11963,7 +11963,7 @@ "request_url": "/obp/v4.0.0/management/dynamic_entities", "summary": "创建动态实体", "description": "

创建一个DynamicEntity。

身份验证是强制性的

创建一个DynamicEntity,创建成功后,将自动生成相应的CRUD端点

当前的支持文件类型如下:
[字符串,数字,整数,布尔值]

", - "description_markdown": "Create a DynamicEntity.\n\n\nAuthentication is Mandatory\n\nCreate one DynamicEntity, after created success, the corresponding CRUD endpoints will be generated automatically\n\nCurrent support field types as follow:\n[string, number, integer, boolean]\n\n", + "description_markdown": "Create a DynamicEntity.\n\n\nAuthentication is Mandatory\n\nCreate a DynamicEntity. If creation is successful, the corresponding POST, GET, PUT and DELETE (Create, Read, Update, Delete or CRUD for short) endpoints will be generated automatically\n\nThe following field types are as supported:\n[string, number, integer, boolean]\n\n", "example_request_body": { "FooBar": { "required": [ diff --git a/obp-api/src/main/resources/i18n/lift-core.properties b/obp-api/src/main/resources/i18n/lift-core.properties index eceed845a..d9bc9055e 100644 --- a/obp-api/src/main/resources/i18n/lift-core.properties +++ b/obp-api/src/main/resources/i18n/lift-core.properties @@ -372,7 +372,8 @@ invalid.username=Invalid Username: \ 2) Usernames MUST be between 8 and 100 characters long \ 3) Usernames MUST NOT start with _ or . \ 4) Usernames MUST NOT contain __ or ._ or ._ or .. \ -5) Usernames MUST NOT end with _ or . +5) Usernames MUST NOT end with _ or . \ +6) Any valid email address is allowed as the Username your.username.is.not.unique = Your username is not unique. Please enter a different one. # Those 2 messages must have the same output in order to prevent leakage of information diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 72e4feaf5..6ba33300b 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -387,6 +387,9 @@ webui_obp_cli_url = https://github.com/OpenBankProject/OBP-CLI # API Tester URL, change to your instance webui_api_tester_url = https://apitester.openbankproject.com +# API Hola app URL, change to your instance +webui_api_hola_url = # + @@ -420,6 +423,11 @@ webui_sdks_url = https://github.com/OpenBankProject/OBP-API/wiki/OAuth-Client-SD # then OBP-API can show the content to the HomePage `SDK Showcases`. Please check it over the sandbox homepage first. #webui_featured_sdks_external_link = https://static.openbankproject.com/obp/sdks.html + +# the external html page for the FAQ section. the default link is the obp one. Please following the div to modify it. This link should be anonymous access. +# then OBP-API can show the content to the HomePage `FAQs`. Please check it over the sandbox homepage first. +#webui_main_faq_external_link = /main-faq.html + # Text about data in FAQ webui_faq_data_text = We use real data and customer profiles which have been anonymized. @@ -430,7 +438,7 @@ webui_faq_url = https://openbankproject.com/faq/ webui_faq_email = contact@openbankproject.com # Link to support platform -webui_support_platform_url = https://slack.openbankproject.com/ +webui_support_platform_url = https://chat.openbankproject.com # Link to Direct Login glossary on api explorer webui_direct_login_documentation_url = @@ -619,6 +627,16 @@ super_admin_user_ids=USER_ID1,USER_ID2, ## Note: The email address used for login must match one ## registered on OBP localy. # openid_connect.enabled=true +# openid_connect.show_tokens=false +# Response mode +# possible values: query, fragment, form_post, query.jwt, fragment.jwt, form_post.jwt, jwt +# openid_connect.response_mode=form_post +# Response type +# possible values: "code", "id_token", "code id_token" +# openid_connect.response_type=code +# Scope +# possible values: "openid email profile", "openid email", "openid" +# openid_connect.scope=openid email profile # First identity provider # openid_connect_1.button_text = Google # openid_connect_1.client_secret=OYdWujJlU7fFOW_NXzPlDI4T @@ -633,7 +651,7 @@ super_admin_user_ids=USER_ID1,USER_ID2, # openid_connect_2.button_text = name of 2nd provider # openid_connect_2.client_secret=OYdWujJlU7fFOW_NXzPlDI4T # openid_connect_2.client_id=883773244832-s4hi72j0rble0iiivq1gn09k7vvptdci.apps.googleusercontent.com -# openid_connect_2.callback_url=http://127.0.0.1:8080/auth/openid-connect/callback +# openid_connect_2.callback_url=http://127.0.0.1:8080/auth/openid-connect/callback-2 # openid_connect_2.endpoint.authorization=https://accounts.google.com/o/oauth2/v2/auth # openid_connect_2.endpoint.userinfo=https://openidconnect.googleapis.com/v1/userinfo # openid_connect_2.endpoint.token=https://oauth2.googleapis.com/token @@ -646,8 +664,8 @@ consumers_enabled_by_default=true # Autocomplete for login form has to be explicitly set autocomplete_at_login_form_enabled=false -# Skip Auth User Email validation (defaults to true) -#authUser.skipEmailValidation=true +# Skip Auth User Email validation (defaults to false as of 29 June 2021) +#authUser.skipEmailValidation=false # If using Kafka but want to get counterparties from OBP, set this to true #get_counterparties_from_OBP_DB=true @@ -664,11 +682,21 @@ autocomplete_at_login_form_enabled=false # Enable/Disable Gateway communication at all # In case isn't defined default value is false # allow_gateway_login=false +# Define secret used to validate JWT token +# jwt.token_secret=your-at-least-256-bit-secret-token # Define comma separated list of allowed IP addresses # gateway.host=127.0.0.1 -# Define secret used to validate JWT token -# gateway.token_secret=secret -# -------------------------------------- Gateway login -- + + +# -- DAuth -------------------------------------- +# Enable/Disable DAuth communication at all +# In case isn't defined default value is false +allow_dauth=false +# Define public key used to validate JWT token +jwt.public_key_rsa=path-to-the-pem-file +# Define comma separated list of allowed IP addresses +dauth.host=127.0.0.1 +# -------------------------------------- DAuth-- # Disable akka (Remote storage not possible) @@ -787,7 +815,9 @@ featured_apis=elasticSearchWarehouseV300 # If Rest Connector do not get the response in the following seconds, it will throw the error message back. # This props can be omitted, the default value is 59. It should be less than Nginx timeout. # rest2019_connector_timeout = 59 - +# If set it to `true`, it will add the x-sign (SHA256WithRSA) into each the rest connector http calls, +# please add the name of the field for the UserAuthContext and/or link to other documentation.. +#rest_connector_sends_x-sign_header=false # -- Scopes ----------------------------------------------------- @@ -1074,3 +1104,5 @@ webui_developer_user_invitation_email_html_text=\ \ +# List of countries where consent is not required for the collection of personal data +personal_data_collection_consent_country_waiver_list = Austria, Belgium, Bulgaria, Croatia, Republic of Cyprus, Czech Republic, Denmark, Estonia, Finland, France, Germany, Greece, Hungary, Ireland, Italy, Latvia, Lithuania, Luxembourg, Malta, Netherlands, Poland, Portugal, Romania, Slovakia, Slovenia, Spain, Sweden, England, Scotland, Wales, Northern Ireland \ No newline at end of file diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 33830aca6..8ab2c7a62 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -403,13 +403,14 @@ class Boot extends MdcLoggable { enableVersionIfAllowed(ApiVersion.v4_0_0) enableVersionIfAllowed(ApiVersion.b1) - - def enableAPIs: LiftRules#RulesSeq[DispatchPF] = { + def enableOpenIdConnectApis = { // OpenIdConnect endpoint and validator - if(APIUtil.getPropsAsBoolValue("openid_connect.enabled", false)) { + if (APIUtil.getPropsAsBoolValue("openid_connect.enabled", false)) { LiftRules.dispatch.append(OpenIdConnect) } - + } + def enableAPIs: LiftRules#RulesSeq[DispatchPF] = { + //OAuth API call LiftRules.statelessDispatch.append(OAuthHandshake) @@ -424,10 +425,20 @@ class Boot extends MdcLoggable { } APIUtil.getPropsValue("server_mode", "apis,portal") match { - case mode if mode == "portal" => - case mode if mode == "apis" => enableAPIs - case mode if mode.contains("apis") && mode.contains("portal") => enableAPIs - case _ => enableAPIs + // Instance runs as the portal only + case mode if mode == "portal" => // Callback url in case of OpenID Connect MUST be enabled at portal side + enableOpenIdConnectApis + // Instance runs as the APIs only + case mode if mode == "apis" => + enableAPIs + // Instance runs as the portal and APIs as well + // This is default mode + case mode if mode.contains("apis") && mode.contains("portal") => + enableAPIs + enableOpenIdConnectApis + // Failure + case _ => + throw new RuntimeException("The props server_mode`is not properly set. Allowed cases: { server_mode=portal, server_mode=apis, server_mode=apis,portal }") } @@ -510,6 +521,7 @@ class Boot extends MdcLoggable { Menu("Dummy user tokens", "Get Dummy user tokens") / "dummy-user-tokens" >> AuthUser.loginFirst, Menu("Validate OTP", "Validate OTP") / "otp" >> AuthUser.loginFirst, + Menu("User Information", "User Information") / "user-information", Menu("User Invitation", "User Invitation") / "user-invitation", Menu("User Invitation Info", "User Invitation Info") / "user-invitation-info", Menu("User Invitation Invalid", "User Invitation Invalid") / "user-invitation-invalid", diff --git a/obp-api/src/main/scala/code/api/GatewayLogin.scala b/obp-api/src/main/scala/code/api/GatewayLogin.scala index aaf83020e..e66c33160 100755 --- a/obp-api/src/main/scala/code/api/GatewayLogin.scala +++ b/obp-api/src/main/scala/code/api/GatewayLogin.scala @@ -120,10 +120,11 @@ object GatewayLogin extends RestHelper with MdcLoggable { def validateJwtToken(token: String): Box[PayloadOfJwtJSON] = { APIUtil.getPropsAsBoolValue("jwt.use.ssl", false) match { case true => + logger.debug("validateJwtToken says: verifying jwt token with RSA: " + token) val claim = CertificateUtil.decryptJwtWithRsa(token) Box(parse(claim.toString).extractOpt[PayloadOfJwtJSON]) case false => - logger.debug("validateJwtToken says: verifying jwt token: " + token) + logger.debug("validateJwtToken says: verifying jwt token with HmacProtection: " + token) logger.debug(CertificateUtil.verifywtWithHmacProtection(token).toString) CertificateUtil.verifywtWithHmacProtection(token) match { case true => @@ -262,7 +263,8 @@ object GatewayLogin extends RestHelper with MdcLoggable { email = None, userId = None, createdByUserInvitationId = None, - company = None + company = None, + lastMarketingAgreementSignedDate = None ) } match { case Full(u) => @@ -480,7 +482,7 @@ object GatewayLogin extends RestHelper with MdcLoggable { val payload = GatewayLogin.parseJwt(parameters) payload match { case Full(payload) => - val username = getFieldFromPayloadJson(payload, "username") + val username = getFieldFromPayloadJson(payload, "login_user_name") logger.debug("username: " + username) Users.users.vend.getUserByProviderId(provider = gateway, idGivenByProvider = username) case _ => diff --git a/obp-api/src/main/scala/code/api/OAuth2.scala b/obp-api/src/main/scala/code/api/OAuth2.scala index 0ec4c7129..e77309bac 100644 --- a/obp-api/src/main/scala/code/api/OAuth2.scala +++ b/obp-api/src/main/scala/code/api/OAuth2.scala @@ -96,6 +96,8 @@ object OAuth2Login extends RestHelper with MdcLoggable { Google.applyRulesFuture(value, cc) } else if (Yahoo.isIssuer(value)) { Yahoo.applyRulesFuture(value, cc) + } else if (Azure.isIssuer(value)) { + Azure.applyRulesFuture(value, cc) } else { Hydra.applyRulesFuture(value, cc) } @@ -285,7 +287,8 @@ object OAuth2Login extends RestHelper with MdcLoggable { email = getClaim(name = "email", idToken = idToken), userId = None, createdByUserInvitationId = None, - company = None + company = None, + lastMarketingAgreementSignedDate = None ) } } diff --git a/obp-api/src/main/scala/code/api/OBPRestHelper.scala b/obp-api/src/main/scala/code/api/OBPRestHelper.scala index 7357f890e..323278334 100644 --- a/obp-api/src/main/scala/code/api/OBPRestHelper.scala +++ b/obp-api/src/main/scala/code/api/OBPRestHelper.scala @@ -32,11 +32,12 @@ import code.api.Constant._ import code.api.OAuthHandshake._ import code.api.builder.AccountInformationServiceAISApi.APIMethods_AccountInformationServiceAISApi import code.api.util.APIUtil._ -import code.api.util.ErrorMessages.attemptedToOpenAnEmptyBox +import code.api.util.ErrorMessages.{InvalidDAuthHeaderToken, UserIsDeleted, UsernameHasBeenLocked, attemptedToOpenAnEmptyBox} import code.api.util._ import code.api.v3_0_0.APIMethods300 import code.api.v3_1_0.APIMethods310 import code.api.v4_0_0.APIMethods400 +import code.loginattempts.LoginAttempt import code.model.dataAccess.AuthUser import code.util.Helper.MdcLoggable import com.alibaba.ttl.TransmittableThreadLocal @@ -260,7 +261,24 @@ trait OBPRestHelper extends RestHelper with MdcLoggable { } } - def failIfBadAuthorizationHeader(rd: Option[ResourceDoc])(fn: CallContext => Box[JsonResponse]) : JsonResponse = { + def failIfBadAuthorizationHeader(rd: Option[ResourceDoc])(function: CallContext => Box[JsonResponse]) : JsonResponse = { + // Check is it a user deleted or locked + def fn(callContext: CallContext): Box[JsonResponse] = { + callContext.user match { + case Full(u) => // There is a user. Check it. + if(u.isDeleted.getOrElse(false)) { + Failure(UserIsDeleted) // The user is DELETED. + } else { + LoginAttempt.userIsLocked(u.name) match { + case true => Failure(UsernameHasBeenLocked) // The user is LOCKED. + case false => function(callContext) // All good + } + } + case _ => // There is no user. Just forward the result. + function(callContext) + } + } + val authorization = S.request.map(_.header("Authorization")).flatten val directLogin: Box[String] = S.request.map(_.header("DirectLogin")).flatten val body: Box[String] = getRequestBody(S.request) @@ -382,7 +400,45 @@ trait OBPRestHelper extends RestHelper with MdcLoggable { case _ => Failure(ErrorMessages.GatewayLoginUnknownError) } - } else { + } + else if (APIUtil.getPropsAsBoolValue("allow_dauth", false) && hasDAuthHeader(cc.requestHeaders)) { + logger.info("allow_dauth-getRemoteIpAddress: " + remoteIpAddress ) + APIUtil.getPropsValue("dauth.host") match { + case Full(h) if h.split(",").toList.exists(_.equalsIgnoreCase(remoteIpAddress) == true) => // Only addresses from white list can use this feature + val dauthToken = DAuth.getDAuthToken(cc.requestHeaders) + dauthToken match { + case Some(token :: _) => + val payload = DAuth.parseJwt(token) + payload match { + case Full(payload) => + DAuth.getOrCreateResourceUser(payload: String, Some(cc)) match { + case Full((u, callContext)) => // Authentication is successful + val consumer = DAuth.getConsumerByConsumerKey(payload)//TODO, need to verify the key later. + val jwt = DAuth.createJwt(payload) + val callContextUpdated = ApiSession.updateCallContext(DAuthResponseHeader(Some(jwt)), callContext) + fn(callContextUpdated.map( callContext =>callContext.copy(user = Full(u), consumer = consumer)).getOrElse(callContext.getOrElse(cc).copy(user = Full(u), consumer = consumer))) + case Failure(msg, t, c) => Failure(msg, t, c) + case _ => Full(errorJsonResponse(payload)) + } + case Failure(msg, t, c) => + Failure(msg, t, c) + case _ => + Failure(ErrorMessages.DAuthUnknownError) + } + case _ => + Failure(InvalidDAuthHeaderToken) + } + case Full(h) if h.split(",").toList.exists(_.equalsIgnoreCase(remoteIpAddress) == false) => // All other addresses will be rejected + Failure(ErrorMessages.DAuthWhiteListAddresses) + case Empty => + Failure(ErrorMessages.DAuthHostPropertyMissing) // There is no dauth.host in props file + case Failure(msg, t, c) => + Failure(msg, t, c) + case _ => + Failure(ErrorMessages.DAuthUnknownError) + } + } + else { fn(cc) } } 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 5a40b3321..3b091f85d 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 @@ -1,6 +1,7 @@ package code.api.ResourceDocs1_4_0 import java.util.Date + import code.api.Constant._ import code.api.Constant import code.api.UKOpenBanking.v2_0_0.JSONFactory_UKOpenBanking_200 @@ -15,7 +16,7 @@ import code.api.v3_0_0.JSONFactory300.createBranchJsonV300 import code.api.v3_0_0.custom.JSONFactoryCustom300 import code.api.v3_0_0.{LobbyJsonV330, _} import code.api.v3_1_0.{AccountBalanceV310, AccountsBalancesV310Json, BadLoginStatusJson, ContactDetailsJson, CustomerWithAttributesJsonV310, InviteeJson, ObpApiLoopbackJson, PhysicalCardWithAttributesJsonV310, PutUpdateCustomerEmailJsonV310, _} -import code.api.v4_0_0.{BankAttributeBankResponseJsonV400, _} +import code.api.v4_0_0.{BankAttributeBankResponseJsonV400, FastFirehoseAccountsJsonV400, PostHistoricalTransactionAtBankJson, _} import code.api.v3_1_0.{AccountBalanceV310, AccountsBalancesV310Json, BadLoginStatusJson, ContactDetailsJson, InviteeJson, ObpApiLoopbackJson, PhysicalCardWithAttributesJsonV310, PutUpdateCustomerEmailJsonV310, _} import code.branches.Branches.{Branch, DriveUpString, LobbyString} import code.consent.ConsentStatus @@ -33,6 +34,7 @@ import com.openbankproject.commons.model.{UserAuthContextUpdateStatus, ViewBasic import com.openbankproject.commons.util.{ApiVersion, FieldNameApiVersions, ReflectUtils, RequiredArgs, RequiredInfo} import net.liftweb.json import java.net.URLEncoder + import code.endpointMapping.EndpointMappingCommons import scala.collection.immutable.List @@ -50,8 +52,8 @@ object SwaggerDefinitionsJSON { val license = License( - id = "id", - name ="String" + id = licenseIdExample.value, + name = licenseNameExample.value ) val routing = Routing( @@ -1206,8 +1208,8 @@ object SwaggerDefinitionsJSON { hours = "5" ) val licenseJson = LicenseJsonV140( - id = "5", - name = "TESOBE" + id = licenseIdExample.value, + name = licenseNameExample.value ) val metaJson = MetaJsonV140( license = licenseJson @@ -1320,7 +1322,7 @@ object SwaggerDefinitionsJSON { // Internal data examples (none JSON format). // Use transform... to convert these to our various json formats for different API versions - val meta: Meta = Meta(license = License (id = "PDDL", name = "Open Data Commons Public Domain Dedication and License ")) // Note the meta is V140 + val meta: Meta = Meta(license = License (id = licenseIdExample.value, name = licenseNameExample.value)) // Note the meta is V140 val openingTimesV300 =OpeningTimesV300( opening_time = "10:00", closing_time = "18:00") @@ -1701,6 +1703,31 @@ object SwaggerDefinitionsJSON { username = usernameExample.value, entitlements = entitlementJSONs ) + + val userJsonV400 = UserJsonV400( + user_id = ExampleValue.userIdExample.value, + email = ExampleValue.emailExample.value, + provider_id = providerIdValueExample.value, + provider = providerValueExample.value, + username = usernameExample.value, + entitlements = entitlementJSONs, + views = None, + agreements = None, + is_deleted = false, + last_marketing_agreement_signed_date = Some(DateWithDayExampleObject) + ) + val userJsonWithAgreementsV400 = UserJsonV400( + user_id = ExampleValue.userIdExample.value, + email = ExampleValue.emailExample.value, + provider_id = providerIdValueExample.value, + provider = providerValueExample.value, + username = usernameExample.value, + entitlements = entitlementJSONs, + views = None, + agreements = Some(Nil), + is_deleted = false, + last_marketing_agreement_signed_date = Some(DateWithDayExampleObject) + ) val userIdJsonV400 = UserIdJsonV400( user_id = ExampleValue.userIdExample.value ) @@ -1965,6 +1992,9 @@ object SwaggerDefinitionsJSON { val usersJsonV200 = UsersJsonV200( users = List(userJsonV200) ) + val usersJsonV400 = UsersJsonV400( + users = List(userJsonV400) + ) val counterpartiesJSON = CounterpartiesJSON( counterparties = List(coreCounterpartyJSON) @@ -3136,12 +3166,12 @@ object SwaggerDefinitionsJSON { ) val moderatedCoreAccountJsonV300 = ModeratedCoreAccountJsonV300( - id = "5995d6a2-01b3-423c-a173-5481df49bdaf", - bank_id= "String", - label= "String", - number= "String", + id = accountIdExample.value, + bank_id = bankIdExample.value, + label= labelExample.value, + number= numberExample.value, owners = List(userJSONV121), - `type`= "String", + `type`= typeExample.value, balance = amountOfMoneyJsonV121, account_routings = List(accountRoutingJsonV121), account_rules = List(accountRuleJsonV300) @@ -3150,12 +3180,12 @@ object SwaggerDefinitionsJSON { val moderatedCoreAccountsJsonV300 = ModeratedCoreAccountsJsonV300(List(moderatedCoreAccountJsonV300)) val moderatedFirehoseAccountJsonV400 = ModeratedFirehoseAccountJsonV400( - id = "5995d6a2-01b3-423c-a173-5481df49bdaf", - bank_id= "String", - label= "String", - number= "String", + id = accountIdExample.value, + bank_id = bankIdExample.value, + label= labelExample.value, + number= numberExample.value, owners = List(userJSONV121), - product_code = "String", + product_code = productCodeExample.value, balance = amountOfMoneyJsonV121, account_routings = List(accountRoutingJsonV121), account_rules = List(accountRuleJsonV300) @@ -3163,6 +3193,23 @@ object SwaggerDefinitionsJSON { val moderatedFirehoseAccountsJsonV400 = ModeratedFirehoseAccountsJsonV400(List(moderatedFirehoseAccountJsonV400)) + val fastFirehoseAccountJsonV400 = FastFirehoseAccountJsonV400( + id = accountIdExample.value, + bank_id = bankIdExample.value, + label = labelExample.value, + number = numberExample.value, + owners = "user_id:b27327a2-a822-41e5-a909-0150da688939,provider:https://finx22openplatform.fintech-galaxy.com,user_name:synth_user_1_54891", + product_code = productCodeExample.value, + balance = amountOfMoneyJsonV121, + account_routings = "bank_id:bisb.com,account_id:c590e38e-847c-466f-9a62-f2ad67daf106", + account_attributes= "type:INTEGER,code:Loan1,value:0," + + "type:STRING,code:Loan1,value:4421.783" + + ) + + val fastFirehoseAccountsJsonV400 = FastFirehoseAccountsJsonV400( + List(fastFirehoseAccountJsonV400) + ) val aggregateMetricsJSONV300 = AggregateMetricJSON( count = 7076, average_response_time = 65.21, @@ -3474,7 +3521,7 @@ object SwaggerDefinitionsJSON { is_active = Some(true) ) val productAttributeResponseJson = ProductAttributeResponseWithoutBankIdJson( - product_code = "saving1", + product_code = productCodeExample.value, product_attribute_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f", name = "OVERDRAFT_START_DATE", `type` = "DATE_WITH_DAY", @@ -3482,7 +3529,7 @@ object SwaggerDefinitionsJSON { ) val productAttributeResponseJsonV400 = ProductAttributeResponseJsonV400( bank_id = bankIdExample.value, - product_code = "saving1", + product_code = productCodeExample.value, product_attribute_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f", name = "OVERDRAFT_START_DATE", `type` = "DATE_WITH_DAY", @@ -3490,7 +3537,7 @@ object SwaggerDefinitionsJSON { is_active = Some(true) ) val productAttributeResponseWithoutBankIdJsonV400 = ProductAttributeResponseWithoutBankIdJsonV400( - product_code = "saving1", + product_code = productCodeExample.value, product_attribute_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f", name = "OVERDRAFT_START_DATE", `type` = "DATE_WITH_DAY", @@ -3520,7 +3567,7 @@ object SwaggerDefinitionsJSON { value = "2012-04-23" ) val accountAttributeResponseJson = AccountAttributeResponseJson( - product_code = "saving1", + product_code = productCodeExample.value, account_attribute_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f", name = "OVERDRAFT_START_DATE", `type` = "DATE_WITH_DAY", @@ -3541,14 +3588,14 @@ object SwaggerDefinitionsJSON { ) val accountApplicationJson = AccountApplicationJson( - product_code = "saveing1", + product_code = productCodeExample.value, user_id = Some(ExampleValue.userIdExample.value), customer_id = Some(customerIdExample.value) ) val accountApplicationResponseJson = AccountApplicationResponseJson ( account_application_id = "gc23a7e2-7dd2-4bdf-a0b4-ae31232a4763", - product_code = "saveing1", + product_code = productCodeExample.value, user = resourceUserJSON, customer = customerJsonV310, date_of_application = DateWithDayExampleObject, @@ -3562,7 +3609,7 @@ object SwaggerDefinitionsJSON { val productJsonV310 = ProductJsonV310( bank_id = bankIdExample.value, - code = "product_code", + code = productCodeExample.value, parent_product_code = "parent", name = "product name", category = "category", @@ -3579,7 +3626,7 @@ object SwaggerDefinitionsJSON { val productCollectionItemJsonV310 = ProductCollectionItemJsonV310(member_product_code = "A") val productCollectionJsonV310 = ProductCollectionJsonV310( collection_code = "C", - product_code = "D", items = List(productCollectionItemJsonV310, productCollectionItemJsonV310.copy(member_product_code = "B")) + product_code = productCodeExample.value, items = List(productCollectionItemJsonV310, productCollectionItemJsonV310.copy(member_product_code = "B")) ) val productCollectionsJsonV310 = ProductCollectionsJsonV310(product_collection = List(productCollectionJsonV310)) @@ -3765,7 +3812,7 @@ object SwaggerDefinitionsJSON { account_id = accountIdExample.value, user_id = userIdExample.value, label = labelExample.value, - product_code = accountTypeExample.value, + product_code = productCodeExample.value, balance = amountOfMoneyJsonV121, branch_id = branchIdExample.value, account_routings = List(accountRoutingJsonV121), @@ -3801,7 +3848,7 @@ object SwaggerDefinitionsJSON { label = "NoneLabel", number = "123", owners = List(userJSONV121), - product_code = "OBP", + product_code = productCodeExample.value, balance = amountOfMoneyJsonV121, views_available = List(viewJSONV121), bank_id = bankIdExample.value, @@ -3829,6 +3876,16 @@ object SwaggerDefinitionsJSON { completed= DateWithSecondsExampleString, `type`= SANDBOX_TAN.toString, charge_policy= "SHARED" + ) + val postHistoricalTransactionAtBankJson = PostHistoricalTransactionAtBankJson( + from_account_id = "", + to_account_id = "", + value = amountOfMoneyJsonV121, + description = "this is for work", + posted = DateWithSecondsExampleString, + completed= DateWithSecondsExampleString, + `type`= SANDBOX_TAN.toString, + charge_policy= "SHARED" ) val postHistoricalTransactionResponseJson = PostHistoricalTransactionResponseJson( @@ -3972,7 +4029,7 @@ object SwaggerDefinitionsJSON { val createAccountRequestJsonV310 = CreateAccountRequestJsonV310( user_id = userIdExample.value, label = labelExample.value, - product_code = accountTypeExample.value, + product_code = productCodeExample.value, balance = amountOfMoneyJsonV121, branch_id = branchIdExample.value, account_routings = List(accountRoutingJsonV121) @@ -4031,6 +4088,16 @@ object SwaggerDefinitionsJSON { ) val postAccountAccessJsonV400 = PostAccountAccessJsonV400(userIdExample.value, PostViewJsonV400(ExampleValue.viewIdExample.value, true)) + val postCreateUserAccountAccessJsonV400 = PostCreateUserAccountAccessJsonV400( + usernameExample.value, + s"dauth.${providerExample.value}", + List(PostViewJsonV400(viewIdExample.value, isSystemExample.value.toBoolean)) + ) + val postCreateUserWithRolesJsonV400 = PostCreateUserWithRolesJsonV400( + usernameExample.value, + s"dauth.${providerExample.value}", + List(createEntitlementJSON) + ) val revokedJsonV400 = RevokedJsonV400(true) val postRevokeGrantAccountAccessJsonV400 = PostRevokeGrantAccountAccessJsonV400(List("ReadAccountsBasic")) @@ -4198,9 +4265,9 @@ object SwaggerDefinitionsJSON { charge = transactionRequestChargeJsonV200 ) - val postApiCollectionJson400 = PostApiCollectionJson400(apiCollectionNameExample.value, true) + val postApiCollectionJson400 = PostApiCollectionJson400(apiCollectionNameExample.value, true, Some(descriptionExample.value)) - val apiCollectionJson400 = ApiCollectionJson400(apiCollectionIdExample.value, userIdExample.value, apiCollectionNameExample.value, true) + val apiCollectionJson400 = ApiCollectionJson400(apiCollectionIdExample.value, userIdExample.value, apiCollectionNameExample.value, true, descriptionExample.value) val apiCollectionsJson400 = ApiCollectionsJson400(List(apiCollectionJson400)) val postApiCollectionEndpointJson400 = PostApiCollectionEndpointJson400(operationIdExample.value) @@ -4218,7 +4285,7 @@ object SwaggerDefinitionsJSON { requestVerb = requestVerbExample.value, requestUrl = requestUrlExample.value, summary = dynamicResourceDocSummaryExample.value, - description = dynamicResourceDocdescriptionExample.value, + description = dynamicResourceDocDescriptionExample.value, exampleRequestBody = Option(json.parse(exampleRequestBodyExample.value)), successResponseBody = Option(json.parse(successResponseBodyExample.value)), errorResponseBodies = errorResponseBodiesExample.value, @@ -4426,6 +4493,17 @@ object SwaggerDefinitionsJSON { meta = metaJson, ) + val entitlementJsonV400 = EntitlementJsonV400( + entitlement_id = entitlementIdExample.value, + role_name = roleNameExample.value, + bank_id = bankIdExample.value, + user_id = userIdExample.value, + ) + + val entitlementsJsonV400 = EntitlementsJsonV400( + list = List(entitlementJsonV400) + ) + //The common error or success format. //Just some helper format to use in Json diff --git a/obp-api/src/main/scala/code/api/dauth.scala b/obp-api/src/main/scala/code/api/dauth.scala new file mode 100755 index 000000000..e23240428 --- /dev/null +++ b/obp-api/src/main/scala/code/api/dauth.scala @@ -0,0 +1,221 @@ +/** +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 . + +Email: contact@tesobe.com +TESOBE GmbH. +Osloer Strasse 16/17 +Berlin 13359, Germany + + This product includes software developed at + TESOBE (http://www.tesobe.com/) + + */ +package code.api + +import code.api.JSONFactoryDAuth.PayloadOfJwtJSON +import code.api.util._ +import code.consumer.Consumers +import code.model.{Consumer, UserX} +import code.users.Users +import code.util.Helper.MdcLoggable +import com.nimbusds.jwt.JWTClaimsSet +import com.openbankproject.commons.model.User +import net.liftweb.common._ +import net.liftweb.http._ +import net.liftweb.http.rest.RestHelper +import net.liftweb.json._ +import com.openbankproject.commons.ExecutionContext.Implicits.global +import net.liftweb.http.provider.HTTPParam + +import scala.collection.immutable.List +import scala.concurrent.Future + +/** + * This object provides the API calls necessary to + * authenticate users using JSON Web Tokens (http://jwt.io). + */ + + +object JSONFactoryDAuth { + //Never update these values inside the case class + case class PayloadOfJwtJSON( + smart_contract_address: String, + network_name: String, + consumer_key: String, + timestamp: Option[String], + msg_sender: Option[String], + request_id: Option[String] + ) + +} + +object DAuth extends RestHelper with MdcLoggable { + + + def createJwt(payloadAsJsonString: String) : String = { + val smartContractAddress = getFieldFromPayloadJson(payloadAsJsonString, "smart_contract_address") + val networkName = getFieldFromPayloadJson(payloadAsJsonString, "network_name") + val msgSender = getFieldFromPayloadJson(payloadAsJsonString, "msg_sender") + val consumerKey = getFieldFromPayloadJson(payloadAsJsonString, "consumer_key") + val timeStamp = getFieldFromPayloadJson(payloadAsJsonString, "timestamp") + val requestId = getFieldFromPayloadJson(payloadAsJsonString, "request_id") + + val json = JSONFactoryDAuth.PayloadOfJwtJSON( + smart_contract_address = smartContractAddress, + network_name = networkName, + consumer_key = consumerKey, + msg_sender = Some(msgSender), + timestamp = Some(timeStamp), + request_id = Some(requestId) + ) + val jwtPayloadAsJson = compactRender(Extraction.decompose(json)) + val jwtClaims: JWTClaimsSet = JWTClaimsSet.parse(jwtPayloadAsJson) + + APIUtil.getPropsAsBoolValue("jwt.use.ssl", false) match { + case true => + CertificateUtil.encryptJwtWithRsa(jwtClaims) + case false => + CertificateUtil.jwtWithHmacProtection(jwtClaims) + } + } + + def parseJwt(jwt:String): Box[String] = { + logger.debug("parseJwt says jwt.toString is: " + jwt) + logger.debug("parseJwt says: validateJwtToken(jwt) is:" + validateJwtToken(jwt)) + validateJwtToken(jwt) match { + case Full(jwtPayload) => + logger.debug("parseJwt says: Full: " + jwtPayload.toString) + Full(compactRender(Extraction.decompose(jwtPayload))) + case _ => + logger.debug("parseJwt says: Not Full(jwtPayload)") + Failure(ErrorMessages.DAuthJwtTokenIsNotValid) + } + } + + def validateJwtToken(token: String): Box[PayloadOfJwtJSON] = { + APIUtil.getPropsAsBoolValue("jwt.use.ssl", false) match { + case true => + logger.debug("validateJwtToken says: verifying jwt token with RSA: " + token) + val claim = CertificateUtil.decryptJwtWithRsa(token) + Box(parse(claim.toString).extractOpt[PayloadOfJwtJSON]) + case false => + logger.debug("validateJwtToken says: verifying jwt token with HmacProtection: " + token) + logger.debug(JwtUtil.validateJwtWithRsaKey(token).toString) + JwtUtil.validateJwtWithRsaKey(token) match { + case true => + logger.debug("validateJwtToken says: jwt is verified: " + token) + val claim = CertificateUtil.parseJwtWithHmacProtection(token) + logger.debug("validateJwtToken says: this is claim of verified jwt: " + claim.toString()) + Box(parse(claim.toString).extractOpt[PayloadOfJwtJSON]) + case _ => + logger.debug("validateJwtToken says: could not verify jwt") + Failure(ErrorMessages.DAuthJwtTokenIsNotValid) + } + } + } + + // Check if the request (access token or request token) is valid and return a tuple + def getDAuthToken(requestHeaders: List[HTTPParam]) : Option[List[String]] = { + requestHeaders.find(_.name==APIUtil.DAuthHeaderKey).map(_.values) + } + + def getOrCreateResourceUser(jwtPayload: String, callContext: Option[CallContext]) : Box[(User, Option[CallContext])] = { + val userName = getFieldFromPayloadJson(jwtPayload, "smart_contract_address") + val provider = "dauth."+getFieldFromPayloadJson(jwtPayload, "network_name") + logger.debug("login_user_name: " + userName) + for { + tuple <- + UserX.getOrCreateDauthResourceUser(userName, provider) match { + case Full(u) => + Full((u,callContext)) // Return user + case Empty => + Failure(ErrorMessages.DAuthCannotGetOrCreateUser) + case Failure(msg, t, c) => + Failure(msg, t, c) + case _ => + Failure(ErrorMessages.DAuthUnknownError) + } + } yield { + tuple + } + } + def getOrCreateResourceUserFuture(jwtPayload: String, callContext: Option[CallContext]) : Future[Box[(User, Option[CallContext])]] = { + val username = getFieldFromPayloadJson(jwtPayload, "smart_contract_address") + val provider = "dauth."+ getFieldFromPayloadJson(jwtPayload, "network_name") + logger.debug("login_user_name: " + username) + + for { + tuple <- Future { UserX.getOrCreateDauthResourceUser(username, provider)} map { + case (Full(u)) => + Full(u, callContext) // Return user + case (Empty) => + Failure(ErrorMessages.DAuthCannotGetOrCreateUser) + case (Failure(msg, t, c)) => + Failure(msg, t, c) + case _ => + Failure(ErrorMessages.DAuthUnknownError) + } + } yield { + tuple + } + } + + def getConsumerByConsumerKey(jwtPayload: String) : Box[Consumer] = { + val consumeyKey = getFieldFromPayloadJson(jwtPayload, "consumer_key") + Consumers.consumers.vend.getConsumerByConsumerKey(consumeyKey) + } + + private def getFieldFromPayloadJson(payloadAsJsonString: String, fieldName: String) = { + val jwtJson = parse(payloadAsJsonString) // Transform Json string to JsonAST + val v = jwtJson.\(fieldName) + v match { + case JNothing => + "" + case _ => + compactRender(v).replace("\"", "") + } + } + // Try to find errorCode in Json string received from South side and extract to list + // Return list of error codes values + def getErrors(message: String) : List[String] = { + val json = parse(message) removeField { + case JField("backendMessages", _) => true + case _ => false + } + val listOfValues = for { + JArray(objects) <- json + JObject(obj) <- objects + JField("errorCode", JString(fieldName)) <- obj + } yield fieldName + listOfValues + } + + def getUser : Box[User] = { + val token = S.getRequestHeader(APIUtil.DAuthHeaderKey) + val payload = token.map(DAuth.parseJwt).flatten + payload match { + case Full(payload) => + val username = getFieldFromPayloadJson(payload, "smart_contract_address") + val provider = getFieldFromPayloadJson(payload, "network_name") + val providerHardCodePrefixDauth = "dauth."+provider + logger.debug("username: " + username) + Users.users.vend.getUserByProviderId(provider = providerHardCodePrefixDauth, idGivenByProvider = username) + case _ => + None + } + } +} diff --git a/obp-api/src/main/scala/code/api/directlogin.scala b/obp-api/src/main/scala/code/api/directlogin.scala index 3dc004a94..dd2196953 100644 --- a/obp-api/src/main/scala/code/api/directlogin.scala +++ b/obp-api/src/main/scala/code/api/directlogin.scala @@ -113,12 +113,8 @@ object DirectLogin extends RestHelper with MdcLoggable { try { val resourceUser = UserX.findByResourceUserId(userId).openOrThrowException(s"$InvalidDirectLoginParameters can not find the resourceUser!") val authUser = AuthUser.findUserByUsernameLocally(resourceUser.name).openOrThrowException(s"$InvalidDirectLoginParameters can not find the auth user!") - if(!emailDomainToSpaceMappings.isEmpty){ - AuthUser.grantEntitlementsToUseDynamicEndpointsInSpaces(authUser) - } - if(!emailDomainToEntitlementMappings.isEmpty){ - AuthUser.grantEmailDomainEntitlementsToUser(authUser) - } + AuthUser.grantEntitlementsToUseDynamicEndpointsInSpaces(authUser) + AuthUser.grantEmailDomainEntitlementsToUser(authUser) } catch { case e: Throwable => // error handling, found wrong props value as early as possible. this.logger.error(s"directLogin.grantEntitlementsToUseDynamicEndpointsInSpacesInDirectLogin throw exception, details: $e" ); diff --git a/obp-api/src/main/scala/code/api/openidconnect.scala b/obp-api/src/main/scala/code/api/openidconnect.scala index 657302e01..a3b387bec 100644 --- a/obp-api/src/main/scala/code/api/openidconnect.scala +++ b/obp-api/src/main/scala/code/api/openidconnect.scala @@ -101,6 +101,15 @@ object OpenIdConnect extends OBPRestHelper with MdcLoggable { private def callbackUrlCommonCode(identityProvider: Int): JsonResponse = { val (code, state, sessionState) = extractParams(S) + logger.debug("(code, state, sessionState) = " + (code, state, sessionState)) + logger.debug("S.receivedCookies = " + S.receivedCookies) + logger.debug("S.responseCookies = " + S.responseCookies) + logger.debug("server_mode = " + APIUtil.getPropsValue("server_mode")) + + def chainErrorMessage(badObj: Failure, errorMessage: String) = { + val chainedFailure: Failure = badObj ?~! errorMessage + (401, filterMessage(chainedFailure), None) + } val (httpCode, message, authorizationUser) = if (state == sessionState) { exchangeAuthorizationCodeForTokens(code, identityProvider) match { @@ -113,20 +122,31 @@ object OpenIdConnect extends OBPRestHelper with MdcLoggable { case Full(user) => // All good getOrCreateAuthUser(user) match { case Full(authUser) => + // Grant roles according to the props email_domain_to_space_mappings + AuthUser.grantEmailDomainEntitlementsToUser(authUser) + // Grant roles according to the props email_domain_to_space_mappings + AuthUser.grantEntitlementsToUseDynamicEndpointsInSpaces(authUser) + // Consumer getOrCreateConsumer(idToken, user.userId) match { case Full(consumer) => - saveAuthorizationToken(tokenType, accessToken, idToken, refreshToken, scope, expiresIn) match { + saveAuthorizationToken(tokenType, accessToken, idToken, refreshToken, scope, expiresIn, authUser.id.get) match { case Full(token) => (200, "OK", Some(authUser)) - case _ => (401, ErrorMessages.CouldNotHandleOpenIDConnectData + "1", Some(authUser)) + case badObj@Failure(_, _, _) => chainErrorMessage(badObj, ErrorMessages.CouldNotHandleOpenIDConnectData) + case _ => (401, ErrorMessages.CouldNotHandleOpenIDConnectData + "saveAuthorizationToken", Some(authUser)) } - case _ => (401, ErrorMessages.CouldNotHandleOpenIDConnectData + "2", Some(authUser)) + case badObj@Failure(_, _, _) => chainErrorMessage(badObj, ErrorMessages.CouldNotHandleOpenIDConnectData) + case _ => (401, ErrorMessages.CouldNotHandleOpenIDConnectData + "getOrCreateConsumer", Some(authUser)) } - case _ => (401, ErrorMessages.CouldNotHandleOpenIDConnectData + "3", None) + case badObj@Failure(_, _, _) => chainErrorMessage(badObj, ErrorMessages.CouldNotHandleOpenIDConnectData) + case _ => (401, ErrorMessages.CouldNotHandleOpenIDConnectData + "getOrCreateAuthUser", None) } + case badObj@Failure(_, _, _) => chainErrorMessage(badObj, ErrorMessages.CouldNotSaveOpenIDConnectUser) case _ => (401, ErrorMessages.CouldNotSaveOpenIDConnectUser, None) } + case badObj@Failure(_, _, _) => chainErrorMessage(badObj, ErrorMessages.CouldNotValidateIDToken) case _ => (401, ErrorMessages.CouldNotValidateIDToken, None) } + case badObj@Failure(_, _, _) => chainErrorMessage(badObj, ErrorMessages.CouldNotExchangeAuthorizationCodeForTokens) case _ => (401, ErrorMessages.CouldNotExchangeAuthorizationCodeForTokens, None) } } else { @@ -182,11 +202,12 @@ object OpenIdConnect extends OBPRestHelper with MdcLoggable { provider = issuer, providerId = subject, createdByConsentId = None, - name = getClaim(name = "given_name", idToken = idToken).orElse(subject), + name = subject, email = getClaim(name = "email", idToken = idToken), userId = None, createdByUserInvitationId = None, - company = None + company = None, + lastMarketingAgreementSignedDate = None ) } } @@ -219,17 +240,26 @@ object OpenIdConnect extends OBPRestHelper with MdcLoggable { "redirect_uri=" + config.callback_url + "&" + "code=" + authorizationCode + "&" + "grant_type=authorization_code" - val response = fromUrl(String.format("%s", config.token_endpoint), data, "POST") - val tokenResponse = json.parse(response) - for { - idToken <- tryo{(tokenResponse \ "id_token").extractOrElse[String]("")} - accessToken <- tryo{(tokenResponse \ "access_token").extractOrElse[String]("")} - tokenType <- tryo{(tokenResponse \ "token_type").extractOrElse[String]("")} - expiresIn <- tryo{(tokenResponse \ "expires_in").extractOrElse[String]("")} - refreshToken <- tryo{(tokenResponse \ "refresh_token").extractOrElse[String]("")} - scope <- tryo{(tokenResponse \ "scope").extractOrElse[String]("")} - } yield { - (idToken, accessToken, tokenType, expiresIn.toLong, refreshToken, scope) + logger.debug("Request parameters: " + data) + logger.debug("Token endpoint: " + config.token_endpoint) + val response: Box[String] = fromUrl(String.format("%s", config.token_endpoint), data, "POST") + logger.debug("Response: " + response) + response match { + case Full(value) => + val tokenResponse = json.parse(value) + logger.debug("Token response: " + tokenResponse) + for { + idToken <- tryo{(tokenResponse \ "id_token").extractOrElse[String]("")} + accessToken <- tryo{(tokenResponse \ "access_token").extractOrElse[String]("")} + tokenType <- tryo{(tokenResponse \ "token_type").extractOrElse[String]("")} + expiresIn <- tryo{(tokenResponse \ "expires_in").extractOrElse[String]("")} + refreshToken <- tryo{(tokenResponse \ "refresh_token").extractOrElse[String]("")} + scope <- tryo{(tokenResponse \ "scope").extractOrElse[String]("")} + } yield { + (idToken, accessToken, tokenType, expiresIn.toLong, refreshToken, scope) + } + case badObject@Failure(_, _, _) => badObject + case _ => Failure(ErrorMessages.InternalServerError + " - exchangeAuthorizationCodeForTokens") } } @@ -240,7 +270,7 @@ object OpenIdConnect extends OBPRestHelper with MdcLoggable { String.format("%s", config.userinfo_endpoint), "?access_token="+accessToken, "GET" - ) + ).openOrThrowException(ErrorMessages.InternalServerError + " - getUserInfo") ) userResponse match { case response: JValue => Full(response) @@ -272,14 +302,16 @@ object OpenIdConnect extends OBPRestHelper with MdcLoggable { idToken: String, refreshToken: String, scope: String, - expiresIn: Long): Box[OpenIDConnectToken] = { + expiresIn: Long, + authUserPrimaryKey: Long): Box[OpenIDConnectToken] = { val token = TokensOpenIDConnect.tokens.vend.createToken( tokenType = tokenType, accessToken = accessToken, idToken = idToken, refreshToken = refreshToken, scope = scope, - expiresIn = expiresIn + expiresIn = expiresIn, + authUserPrimaryKey = authUserPrimaryKey ) token match { case Full(_) => // All good @@ -293,7 +325,7 @@ object OpenIdConnect extends OBPRestHelper with MdcLoggable { method: String, connectTimeout: Int = 2000, readTimeout: Int = 10000 - ): String = { + ): Box[String] = { var content:String = "" import java.net.URL try { @@ -328,10 +360,13 @@ object OpenIdConnect extends OBPRestHelper with MdcLoggable { val inputStream = connection.getInputStream content = scala.io.Source.fromInputStream(inputStream).mkString if (inputStream != null) inputStream.close() + Full(content) } catch { - case e:Throwable => logger.error(e) + case e:Throwable => + e.printStackTrace() + logger.error(e) + Failure(e.getMessage) } - content } diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index 97487d270..a51b727fc 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -33,6 +33,7 @@ import java.nio.charset.Charset import java.text.{ParsePosition, SimpleDateFormat} import java.util.concurrent.ConcurrentHashMap import java.util.{Calendar, Date, UUID} + import code.UserRefreshes.UserRefreshes import code.accountholders.AccountHolders import code.api.Constant._ @@ -44,12 +45,10 @@ import code.api.util.APIUtil.ResourceDoc.{findPathVariableNames, isPathVariable} import code.api.util.ApiRole.{canCreateProduct, canCreateProductAtAnyBank} import code.api.util.ApiTag.{ResourceDocTag, apiTagBank, apiTagNewStyle} import code.api.util.Glossary.GlossaryItem -import code.api.util.JwsUtil.getJwsHeaderValue -import code.api.util.RateLimitingJson.CallLimit import code.api.v1_2.ErrorMessage import code.api.v2_0_0.CreateEntitlementJSON -import code.api.{DirectLogin, _} import code.api.v4_0_0.dynamic.{DynamicEndpointHelper, DynamicEndpoints, DynamicEntityHelper} +import code.api.{DirectLogin, _} import code.authtypevalidation.AuthenticationTypeValidationProvider import code.bankconnectors.Connector import code.consumer.Consumers @@ -58,20 +57,25 @@ import code.entitlement.Entitlement import code.metrics._ import code.model._ import code.model.dataAccess.AuthUser -import code.ratelimiting.{RateLimiting, RateLimitingDI} import code.sanitycheck.SanityCheck import code.scope.Scope import code.usercustomerlinks.UserCustomerLink -import code.util.{Helper, JsonSchemaUtil} import code.util.Helper.{MdcLoggable, SILENCE_IS_GOLDEN} +import code.util.{Helper, JsonSchemaUtil} import code.views.Views import code.webuiprops.MappedWebUiPropsProvider.getWebUiPropsValue import com.alibaba.ttl.internal.javassist.CannotCompileException import com.github.dwickern.macros.NameOf.{nameOf, nameOfType} +import com.openbankproject.commons.ExecutionContext.Implicits.global import com.openbankproject.commons.model.enums.StrongCustomerAuthentication.SCA import com.openbankproject.commons.model.enums.{PemCertificateRole, StrongCustomerAuthentication} import com.openbankproject.commons.model.{Customer, _} +import com.openbankproject.commons.util.Functions.Implicits._ +import com.openbankproject.commons.util.Functions.Memo +import com.openbankproject.commons.util._ import dispatch.url +import javassist.expr.{ExprEditor, MethodCall} +import javassist.{ClassPool, LoaderClassPath} import net.liftweb.actor.LAFuture import net.liftweb.common.{Empty, _} import net.liftweb.http._ @@ -83,21 +87,14 @@ import net.liftweb.json.JsonAST.{JField, JNothing, JObject, JString, JValue} import net.liftweb.json.JsonParser.ParseException import net.liftweb.json._ import net.liftweb.util.Helpers._ -import net.liftweb.util.{Helpers, LiftFlowOfControlException, Props, StringHelpers, ThreadGlobal} - -import scala.collection.JavaConverters._ -import scala.collection.immutable.{List, Nil} -import scala.collection.mutable.{ArrayBuffer, ListBuffer} -import com.openbankproject.commons.ExecutionContext.Implicits.global -import com.openbankproject.commons.util.{ApiVersion, Functions, JsonAble, ReflectUtils, ScannedApiVersion} -import com.openbankproject.commons.util.Functions.Implicits._ -import com.openbankproject.commons.util.Functions.Memo -import javassist.{ClassPool, LoaderClassPath} -import javassist.expr.{ExprEditor, MethodCall} +import net.liftweb.util._ import org.apache.commons.io.IOUtils import org.apache.commons.lang3.StringUtils +import scala.collection.JavaConverters._ +import scala.collection.immutable.{List, Nil} import scala.collection.mutable +import scala.collection.mutable.{ArrayBuffer, ListBuffer} import scala.concurrent.Future import scala.io.BufferedSource import scala.util.Either @@ -178,6 +175,15 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ def hasAnOAuth2Header(authorization: Box[String]): Boolean = hasHeader("Bearer", authorization) def hasGatewayHeader(authorization: Box[String]) = hasHeader("GatewayLogin", authorization) + + /** + * The value `DAuth` is in the KEY + * DAuth:xxxxx + * + * Other types: the `GatewayLogin` is in the VALUE + * Authorization:GatewayLogin token=xxxx + */ + def hasDAuthHeader(requestHeaders: List[HTTPParam]) = requestHeaders.map(_.name).exists(_ ==DAuthHeaderKey) /** * Helper function which tells us does an "Authorization" request header field has the Type of an authentication scheme @@ -624,7 +630,8 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ /** check the currency ISO code from the ISOCurrencyCodes.xml file */ def isValidCurrencyISOCode(currencyCode: String): Boolean = { - val currencyIsoCodeArray = (CurrencyIsoCodeFromXmlFile \"CcyTbl" \ "CcyNtry" \ "Ccy").map(_.text).mkString(" ").split("\\s+") + // Note: We add BTC bitcoin as XBT (the ISO compliant varient) + val currencyIsoCodeArray = (CurrencyIsoCodeFromXmlFile \"CcyTbl" \ "CcyNtry" \ "Ccy").map(_.text).mkString(" ").split("\\s+") :+ "XBT" currencyIsoCodeArray.contains(currencyCode) } @@ -1659,7 +1666,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ } def buildOperationId(apiVersion: ScannedApiVersion, partialFunctionName: String) = - s"${apiVersion.fullyQualifiedVersion}-$partialFunctionName" + s"${apiVersion.fullyQualifiedVersion}-$partialFunctionName".trim //This is correct: OBPv3.0.0-getCoreAccountById //This is OBPv4_0_0-dynamicEntity_deleteFooBar33 @@ -2242,7 +2249,10 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ None } } - + /** + * Defines DAuth Custom Response Header. + */ + val DAuthHeaderKey = "DAuth" /** * Turn a string of format "FooBar" into snake case "foo_bar" * @@ -2706,69 +2716,62 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ case _ => Future { (Failure(ErrorMessages.GatewayLoginUnknownError), None) } } - } else if(Option(cc).flatMap(_.user).isDefined) { + } // DAuth Login + else if (getPropsAsBoolValue("allow_dauth", false) && hasDAuthHeader(cc.requestHeaders)) { + logger.info("allow_dauth-getRemoteIpAddress: " + remoteIpAddress ) + APIUtil.getPropsValue("dauth.host") match { + case Full(h) if h.split(",").toList.exists(_.equalsIgnoreCase(remoteIpAddress) == true) => // Only addresses from white list can use this feature + val dauthToken = DAuth.getDAuthToken(cc.requestHeaders) + dauthToken match { + case Some(token :: _) => + val payload = DAuth.parseJwt(token) + payload match { + case Full(payload) => + DAuth.getOrCreateResourceUserFuture(payload: String, Some(cc)) map { + case Full((u,callContext)) => // Authentication is successful + val consumer = DAuth.getConsumerByConsumerKey(payload)//TODO, need to verify the key later. + val jwt = DAuth.createJwt(payload) + val callContextUpdated = ApiSession.updateCallContext(DAuthResponseHeader(Some(jwt)), callContext) + (Full(u), callContextUpdated.map(_.copy(consumer=consumer, user = Full(u)))) + case Failure(msg, t, c) => + (Failure(msg, t, c), None) + case _ => + (Failure(payload), None) + } + case Failure(msg, t, c) => + Future { (Failure(msg, t, c), None) } + case _ => + Future { (Failure(ErrorMessages.DAuthUnknownError), None) } + } + case _ => + Future { (Failure(InvalidDAuthHeaderToken), None) } + } + case Full(h) if h.split(",").toList.exists(_.equalsIgnoreCase(remoteIpAddress) == false) => // All other addresses will be rejected + Future { (Failure(ErrorMessages.DAuthWhiteListAddresses), None) } + case Empty => + Future { (Failure(ErrorMessages.DAuthHostPropertyMissing), None) } // There is no dauth.host in props file + case Failure(msg, t, c) => + Future { (Failure(msg, t, c), None) } + case _ => + Future { (Failure(ErrorMessages.DAuthUnknownError), None) } + } + } + else if(Option(cc).flatMap(_.user).isDefined) { Future{(cc.user, Some(cc))} } else { Future { (Empty, Some(cc)) } } + // COMMON POST AUTHENTICATION CODE GOES BELOW + // Check is it a user deleted or locked + val userIsLockedOrDeleted: Future[(Box[User], Option[CallContext])] = AfterApiAuth.checkUserIsDeletedOrLocked(res) + // Check Rate Limiting + val resultWithRateLimiting: Future[(Box[User], Option[CallContext])] = AfterApiAuth.checkRateLimiting(userIsLockedOrDeleted) - /****************************************************************************************************************** - * This block of code needs to update Call Context with Rate Limiting - * Please note that first source is the table RateLimiting and second is the table Consumer - */ - def getRateLimiting(consumerId: String, version: String, name: String): Future[Box[RateLimiting]] = { - RateLimitingUtil.useConsumerLimits match { - case true => RateLimitingDI.rateLimiting.vend.getByConsumerId(consumerId, version, name, Some(new Date())) - case false => Future(Empty) - } - } - val resultWithRateLimiting: Future[(Box[User], Option[CallContext])] = for { - (user, cc) <- res - consumer = cc.flatMap(_.consumer) - version = cc.map(_.implementedInVersion).getOrElse("None") // Calculate apiVersion in case of Rate Limiting - operationId = cc.flatMap(_.operationId) // Unique Identifier of Dynamic Endpoints - // Calculate apiName in case of Rate Limiting - name = cc.flatMap(_.resourceDocument.map(_.partialFunctionName)) // 1st try: function name at resource doc - .orElse(operationId) // 2nd try: In case of Dynamic Endpoint we can only use operationId - .getOrElse("None") // Not found any unique identifier - rateLimiting <- getRateLimiting(consumer.map(_.consumerId.get).getOrElse(""), version, name) - } yield { - val limit: Option[CallLimit] = rateLimiting match { - case Full(rl) => Some(CallLimit( - rl.consumerId, - rl.apiName, - rl.apiVersion, - rl.bankId, - rl.perSecondCallLimit, - rl.perMinuteCallLimit, - rl.perHourCallLimit, - rl.perDayCallLimit, - rl.perWeekCallLimit, - rl.perMonthCallLimit)) - case Empty => - Some(CallLimit( - consumer.map(_.consumerId.get).getOrElse(""), - None, - None, - None, - consumer.map(_.perSecondCallLimit.get).getOrElse(-1), - consumer.map(_.perMinuteCallLimit.get).getOrElse(-1), - consumer.map(_.perHourCallLimit.get).getOrElse(-1), - consumer.map(_.perDayCallLimit.get).getOrElse(-1), - consumer.map(_.perWeekCallLimit.get).getOrElse(-1), - consumer.map(_.perMonthCallLimit.get).getOrElse(-1) - )) - case _ => None - } - (user, cc.map(_.copy(rateLimiting = limit))) - } - /*************************************************************************************************************** */ - - - resultWithRateLimiting map { // Update Call Context + // Update Call Context + resultWithRateLimiting map { x => (x._1, ApiSession.updateCallContext(Spelling(spelling), x._2)) } map { x => (x._1, x._2.map(_.copy(implementedInVersion = implementedInVersion))) @@ -2789,7 +2792,9 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ } } - + + + /** * This Function is used to terminate a Future used in for-comprehension with specific message and code in case that value of Box is not Full. @@ -2989,7 +2994,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ throw new Exception(UnknownError) } } - + def unboxFullAndWrapIntoFuture[T](box: Box[T])(implicit m: Manifest[T]) : Future[T] = { Future { unboxFull(fullBoxOrException(box)) @@ -3848,6 +3853,40 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ } } } + /** + * validate whether current request's query parameters + * @param operationId + * @param callContext + * @return Full(errorResponse) if validate fail + */ + def validateQueryParams(operationId: String, callContext: CallContext): Box[JsonResponse] = { + val queryString: String = if (callContext.url.contains("?")) callContext.url.split("\\?",2)(1) else "" + val queryParams: Array[String] = queryString.split("&").map(_.split("=")(0)) + val queryParamsGrouped: Map[String, Array[String]] = queryParams.groupBy(x => x) + queryParamsGrouped.toList.forall(_._2.size == 1) match { + case true => Empty + case false => + Box.tryo( + createErrorJsonResponse(s"${ErrorMessages.DuplicateQueryParameters}", 400, callContext.correlationId) + ) + } + } + /** + * validate whether current request's header keys + * @param operationId + * @param callContext + * @return Full(errorResponse) if validate fail + */ + def validateRequestHeadersKeys(operationId: String, callContext: CallContext): Box[JsonResponse] = { + val headerKeysGrouped: Map[String, List[HTTPParam]] = callContext.requestHeaders.groupBy(x => x.name) + headerKeysGrouped.toList.forall(_._2.size == 1) match { + case true => Empty + case false => + Box.tryo( + createErrorJsonResponse(s"${ErrorMessages.DuplicateHeaderKeys}", 400, callContext.correlationId) + ) + } + } def createErrorJsonResponse(errorMsg: String, errorCode: Int, correlationId: String): JsonResponse = { import net.liftweb.json.JsonDSL._ @@ -3911,6 +3950,14 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ // validate auth type { case (Some(callContext), operationId) => validateAuthType(operationId, callContext) + }, + // validate query params + { + case (Some(callContext), operationId) => validateQueryParams(operationId, callContext) + }, + // validate request header keys + { + case (Some(callContext), operationId) => validateRequestHeadersKeys(operationId, callContext) } ) diff --git a/obp-api/src/main/scala/code/api/util/AfterApiAuth.scala b/obp-api/src/main/scala/code/api/util/AfterApiAuth.scala new file mode 100644 index 000000000..50cba999c --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/AfterApiAuth.scala @@ -0,0 +1,90 @@ +package code.api.util + +import java.util.Date + +import code.api.util.ErrorMessages.{UserIsDeleted, UsernameHasBeenLocked} +import code.api.util.RateLimitingJson.CallLimit +import code.loginattempts.LoginAttempt +import code.ratelimiting.{RateLimiting, RateLimitingDI} +import com.openbankproject.commons.model.User +import net.liftweb.common.{Box, Empty, Failure, Full} +import com.openbankproject.commons.ExecutionContext.Implicits.global + +import scala.concurrent.Future + + +object AfterApiAuth { + def checkUserIsDeletedOrLocked(res: Future[(Box[User], Option[CallContext])]): Future[(Box[User], Option[CallContext])] = { + for { + (user: Box[User], cc) <- res + } yield { + user match { + case Full(u) => // There is a user. Check it. + if (u.isDeleted.getOrElse(false)) { + (Failure(UserIsDeleted), cc) // The user is DELETED. + } else { + LoginAttempt.userIsLocked(u.name) match { + case true => (Failure(UsernameHasBeenLocked), cc) // The user is LOCKED. + case false => (user, cc) // All good + } + } + case _ => // There is no user. Just forward the result. + (user, cc) + } + } + } + + /** + * This block of code needs to update Call Context with Rate Limiting + * Please note that first source is the table RateLimiting and second is the table Consumer + */ + def checkRateLimiting(userIsLockedOrDeleted: Future[(Box[User], Option[CallContext])]): Future[(Box[User], Option[CallContext])] = { + def getRateLimiting(consumerId: String, version: String, name: String): Future[Box[RateLimiting]] = { + RateLimitingUtil.useConsumerLimits match { + case true => RateLimitingDI.rateLimiting.vend.getByConsumerId(consumerId, version, name, Some(new Date())) + case false => Future(Empty) + } + } + for { + (user, cc) <- userIsLockedOrDeleted + consumer = cc.flatMap(_.consumer) + version = cc.map(_.implementedInVersion).getOrElse("None") // Calculate apiVersion in case of Rate Limiting + operationId = cc.flatMap(_.operationId) // Unique Identifier of Dynamic Endpoints + // Calculate apiName in case of Rate Limiting + name = cc.flatMap(_.resourceDocument.map(_.partialFunctionName)) // 1st try: function name at resource doc + .orElse(operationId) // 2nd try: In case of Dynamic Endpoint we can only use operationId + .getOrElse("None") // Not found any unique identifier + rateLimiting <- getRateLimiting(consumer.map(_.consumerId.get).getOrElse(""), version, name) + } yield { + val limit: Option[CallLimit] = rateLimiting match { + case Full(rl) => Some(CallLimit( + rl.consumerId, + rl.apiName, + rl.apiVersion, + rl.bankId, + rl.perSecondCallLimit, + rl.perMinuteCallLimit, + rl.perHourCallLimit, + rl.perDayCallLimit, + rl.perWeekCallLimit, + rl.perMonthCallLimit)) + case Empty => + Some(CallLimit( + consumer.map(_.consumerId.get).getOrElse(""), + None, + None, + None, + consumer.map(_.perSecondCallLimit.get).getOrElse(-1), + consumer.map(_.perMinuteCallLimit.get).getOrElse(-1), + consumer.map(_.perHourCallLimit.get).getOrElse(-1), + consumer.map(_.perDayCallLimit.get).getOrElse(-1), + consumer.map(_.perWeekCallLimit.get).getOrElse(-1), + consumer.map(_.perMonthCallLimit.get).getOrElse(-1) + )) + case _ => None + } + (user, cc.map(_.copy(rateLimiting = limit))) + } + } + +} diff --git a/obp-api/src/main/scala/code/api/util/ApiPropsWithAlias.scala b/obp-api/src/main/scala/code/api/util/ApiPropsWithAlias.scala index 1492be369..2f662a5d5 100644 --- a/obp-api/src/main/scala/code/api/util/ApiPropsWithAlias.scala +++ b/obp-api/src/main/scala/code/api/util/ApiPropsWithAlias.scala @@ -29,6 +29,10 @@ object ApiPropsWithAlias { name="allow_customer_firehose", alias="allow_firehose_views", defaultValue="false") + def jwtTokenSecret = getValueByNameOrAlias( + name="jwt.token_secret", + alias="gateway.token_secret", + defaultValue="Cannot get your at least 256 bit secret") } object HelperFunctions extends MdcLoggable { diff --git a/obp-api/src/main/scala/code/api/util/ApiRole.scala b/obp-api/src/main/scala/code/api/util/ApiRole.scala index 1becd2472..2dc44a533 100644 --- a/obp-api/src/main/scala/code/api/util/ApiRole.scala +++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala @@ -821,6 +821,9 @@ object ApiRole { case class CanGetBankLevelEndpointTag(requiresBankId: Boolean = true) extends ApiRole lazy val canGetBankLevelEndpointTag = CanGetBankLevelEndpointTag() + case class CanCreateHistoricalTransactionAtBank(requiresBankId: Boolean = true) extends ApiRole + lazy val canCreateHistoricalTransactionAtBank = CanCreateHistoricalTransactionAtBank() + private val dynamicApiRoles = new ConcurrentHashMap[String, ApiRole] private case class DynamicApiRole(role: String, requiresBankId: Boolean = false) extends ApiRole{ diff --git a/obp-api/src/main/scala/code/api/util/ApiSession.scala b/obp-api/src/main/scala/code/api/util/ApiSession.scala index eb84aa7ee..8f2607e77 100644 --- a/obp-api/src/main/scala/code/api/util/ApiSession.scala +++ b/obp-api/src/main/scala/code/api/util/ApiSession.scala @@ -1,10 +1,11 @@ package code.api.util +import code.api.JSONFactoryDAuth import java.util.{Date, UUID} import code.api.JSONFactoryGateway.PayloadOfJwtJSON import code.api.oauth1a.OauthParams._ import code.api.util.APIUtil._ -import code.api.util.AuthenticationType.{Anonymous, DirectLogin, GatewayLogin, OAuth2_OIDC, OAuth2_OIDC_FAPI} +import code.api.util.AuthenticationType.{Anonymous, DirectLogin, GatewayLogin, DAuth, OAuth2_OIDC, OAuth2_OIDC_FAPI} import code.api.util.ErrorMessages.{BankAccountNotFound, UserNotLoggedIn} import code.api.util.RateLimitingJson.CallLimit import code.context.UserAuthContextProvider @@ -25,6 +26,8 @@ import scala.collection.immutable.List case class CallContext( gatewayLoginRequestPayload: Option[PayloadOfJwtJSON] = None, //Never update these values inside the case class !!! gatewayLoginResponseHeader: Option[String] = None, + dauthRequestPayload: Option[JSONFactoryDAuth.PayloadOfJwtJSON] = None, //Never update these values inside the case class !!! + dauthResponseHeader: Option[String] = None, spelling: Option[String] = None, user: Box[User] = Empty, consumer: Box[Consumer] = Empty, @@ -137,6 +140,8 @@ case class CallContext( def authType: AuthenticationType = { if(hasGatewayHeader(authReqHeaderField)) { GatewayLogin + } else if(requestHeaders.exists(_.name==DAuthHeaderKey)) { // DAuth Login + DAuth } else if(has2021DirectLoginHeader(requestHeaders)) { // Direct Login DirectLogin } else if(hasDirectLoginHeader(authReqHeaderField)) { // Direct Login Deprecated @@ -161,6 +166,7 @@ object AuthenticationType extends OBPEnumeration[AuthenticationType]{ override def toString: String = "OAuth1.0a" } object GatewayLogin extends AuthenticationType + object DAuth extends AuthenticationType object OAuth2_OIDC extends AuthenticationType object OAuth2_OIDC_FAPI extends AuthenticationType object Anonymous extends AuthenticationType @@ -193,9 +199,11 @@ case class CallContextLight(gatewayLoginRequestPayload: Option[PayloadOfJwtJSON] `X-Rate-Limit-Reset` : Long = -1 ) -trait GatewayLoginParam -case class GatewayLoginRequestPayload(jwtPayload: Option[PayloadOfJwtJSON]) extends GatewayLoginParam -case class GatewayLoginResponseHeader(jwt: Option[String]) extends GatewayLoginParam +trait LoginParam +case class GatewayLoginRequestPayload(jwtPayload: Option[PayloadOfJwtJSON]) extends LoginParam +case class GatewayLoginResponseHeader(jwt: Option[String]) extends LoginParam +case class DAuthRequestPayload(jwtPayload: Option[JSONFactoryDAuth.PayloadOfJwtJSON]) extends LoginParam +case class DAuthResponseHeader(jwt: Option[String]) extends LoginParam case class Spelling(spelling: Box[String]) @@ -230,17 +238,17 @@ object ApiSession { def updateCallContext(s: Spelling, cnt: Option[CallContext]): Option[CallContext] = { cnt match { case None => - Some(CallContext(gatewayLoginRequestPayload = None, gatewayLoginResponseHeader = None, spelling = s.spelling)) + Some(CallContext(spelling = s.spelling)) //Some fields default value is NONE. case Some(v) => Some(v.copy(spelling = s.spelling)) } } - def updateCallContext(jwt: GatewayLoginParam, cnt: Option[CallContext]): Option[CallContext] = { + def updateCallContext(jwt: LoginParam, cnt: Option[CallContext]): Option[CallContext] = { jwt match { - case GatewayLoginRequestPayload(None) => + case GatewayLoginRequestPayload(None) | DAuthRequestPayload(None) => cnt - case GatewayLoginResponseHeader(None) => + case GatewayLoginResponseHeader(None) | DAuthResponseHeader(None) => cnt case GatewayLoginRequestPayload(Some(jwtPayload)) => cnt match { @@ -256,6 +264,20 @@ object ApiSession { case None => Some(CallContext(gatewayLoginRequestPayload = None, gatewayLoginResponseHeader = Some(j), spelling = None)) } + case DAuthRequestPayload(Some(jwtPayload)) => + cnt match { + case Some(v) => + Some(v.copy(dauthRequestPayload = Some(jwtPayload))) + case None => + Some(CallContext(dauthRequestPayload = Some(jwtPayload), dauthResponseHeader = None, spelling = None)) + } + case DAuthResponseHeader(Some(j)) => + cnt match { + case Some(v) => + Some(v.copy(dauthResponseHeader = Some(j))) + case None => + Some(CallContext(dauthRequestPayload = None, dauthResponseHeader = Some(j), spelling = None)) + } } } diff --git a/obp-api/src/main/scala/code/api/util/ApiTag.scala b/obp-api/src/main/scala/code/api/util/ApiTag.scala index f426ca289..14d135ac3 100644 --- a/obp-api/src/main/scala/code/api/util/ApiTag.scala +++ b/obp-api/src/main/scala/code/api/util/ApiTag.scala @@ -70,16 +70,20 @@ object ApiTag { val apiTagConsent = ResourceDocTag("Consent") val apiTagMethodRouting = ResourceDocTag("Method-Routing") val apiTagWebUiProps = ResourceDocTag("WebUi-Props") - val apiTagEndpointMapping = ResourceDocTag("Endpoint-Mapping-Manage") - val apiTagManageDynamicEndpoint = ResourceDocTag("Dynamic-Endpoint-Manage") - val apiTagManageDynamicEntity = ResourceDocTag("Dynamic-Entity-Manage") - val apiTagDynamicResourceDoc = ResourceDocTag("Dynamic-Resource-Doc-Manage") - val apiTagDynamicMessageDoc = ResourceDocTag("Dynamic-Message-Doc-Manage") + val apiTagEndpointMapping = ResourceDocTag("Endpoint-Mapping") + val apiTagApiCollection = ResourceDocTag("Api-Collection") + + val apiTagDynamicResourceDoc = ResourceDocTag("Dynamic-Resource-Doc") + val apiTagDynamicMessageDoc = ResourceDocTag("Dynamic-Message-Doc") + + val apiTagDAuth = ResourceDocTag("DAuth") val apiTagDynamic = ResourceDocTag("Dynamic") val apiTagDynamicEntity = ResourceDocTag("Dynamic-Entity") + val apiTagManageDynamicEntity = ResourceDocTag("Dynamic-Entity-Manage") val apiTagDynamicEndpoint = ResourceDocTag("Dynamic-Endpoint") + val apiTagManageDynamicEndpoint = ResourceDocTag("Dynamic-Endpoint-Manage") val apiTagJsonSchemaValidation = ResourceDocTag("JSON-Schema-Validation") val apiTagAuthenticationTypeValidation = ResourceDocTag("Authentication-Type-Validation") diff --git a/obp-api/src/main/scala/code/api/util/CertificateUtil.scala b/obp-api/src/main/scala/code/api/util/CertificateUtil.scala index d6cd3bdca..1baea2fee 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateUtil.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateUtil.scala @@ -25,7 +25,7 @@ object CryptoSystem extends Enumeration { object CertificateUtil extends MdcLoggable { // your-at-least-256-bit-secret - val sharedSecret = APIUtil.getPropsValue("gateway.token_secret", "Cannot get your at least 256 bit secret") + val sharedSecret: String = ApiPropsWithAlias.jwtTokenSecret lazy val (publicKey: RSAPublicKey, privateKey: RSAPrivateKey) = APIUtil.getPropsAsBoolValue("jwt.use.ssl", false) match { case true => diff --git a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala index 9cec32129..49ef8d0fb 100644 --- a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala +++ b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala @@ -198,7 +198,8 @@ object Consent { email = email, userId = None, createdByUserInvitationId = None, - company = None + company = None, + lastMarketingAgreementSignedDate = None ) } } diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index 3224f8f7e..36078c94c 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -58,6 +58,8 @@ object ErrorMessages { val InvalidRequestPayload = "OBP-09014: Incorrect request body Format, it should be a valid json that matches Validation rule." val DynamicDataNotFound = "OBP-09015: Dynamic Data not found. Please specify a valid value." + val DuplicateQueryParameters = "OBP-09016: Duplicate Query Parameters are not allowed." + val DuplicateHeaderKeys = "OBP-09017: Duplicate Header Keys are not allowed." // General messages (OBP-10XXX) @@ -87,8 +89,8 @@ object ErrorMessages { val InvalidInBoundMapping = "OBP-10032: Incorrect inBoundMapping Format, it should be a json structure." val invalidIban = "OBP-10033: Invalid IBAN." val InvalidUrlParameters = "OBP-10034: Invalid URL parameters." - val InvalidUri = "OBP-10404: Request Not Found. The server has not found anything matching the Request-URI.Check your URL and the headers. " + - "NOTE: when it is POST or PUT api, the Content-Type must be `application/json`. OBP only support the json format body." + val InvalidUri = "OBP-10404: 404 Not Found. The server could not find the requested URI. Please double check your URL, headers and body. " + + "Note: When you are making a POST or PUT request, the Content-Type header MUST be `application/json`. Note: OBP only supports JSON formatted bodies." val ResourceDoesNotExist = "OBP-10405: Resource does not exist." val InvalidJsonValue = "OBP-10035: Incorrect json value." @@ -152,7 +154,7 @@ object ErrorMessages { val GatewayLoginUnknownError = "OBP-20029: Unknown Gateway login error." val GatewayLoginHostPropertyMissing = "OBP-20030: Property gateway.host is not defined." val GatewayLoginWhiteListAddresses = "OBP-20031: Gateway login can be done only from allowed addresses." - val GatewayLoginJwtTokenIsNotValid = "OBP-20040: The JWT is corrupted/changed during a transport." + val GatewayLoginJwtTokenIsNotValid = "OBP-20040: The Gateway login JWT is corrupted/changed during a transport." val GatewayLoginCannotExtractJwtToken = "OBP-20041: Header, Payload and Signature cannot be extracted from the JWT." val GatewayLoginNoNeedToCallCbs = "OBP-20042: There is no need to call CBS" val GatewayLoginCannotFindUser = "OBP-20043: User cannot be found. Please initiate CBS communication in order to create it." @@ -178,8 +180,20 @@ object ErrorMessages { val FrequencyPerDayError = "OBP-20062: Frequency per day must be greater than 0." val FrequencyPerDayMustBeOneError = "OBP-20063: Frequency per day must be equal to 1 in case of one-off access." + val UserIsDeleted = "OBP-20064: The user is deleted!" + + val DAuthCannotGetOrCreateUser = "OBP-20065: Cannot get or create user during DAuth process." + val DAuthMissingParameters = "OBP-20066: These DAuth parameters are missing: " + val DAuthUnknownError = "OBP-20067: Unknown DAuth login error." + val DAuthHostPropertyMissing = "OBP-20068: Property dauth.host is not defined." + val DAuthWhiteListAddresses = "OBP-20069: DAuth login can be done only from allowed addresses." + val DAuthNoJwtForResponse = "OBP-20070: There is no useful value for JWT." + val DAuthJwtTokenIsNotValid = "OBP-20071: The DAuth JWT is corrupted/changed during a transport." + val InvalidDAuthHeaderToken = "OBP-20072: DAuth Header value should be one single string." val UserNotSuperAdminOrMissRole = "OBP-20101: Current User is not super admin or is missing entitlements: " + val CannotGetOrCreateUser = "OBP-20102: Cannot get or create user." + val InvalidUserProvider = "OBP-20103: Invalid DAuth User Provider." // OAuth 2 val ApplicationNotIdentified = "OBP-20200: The application cannot be identified. " @@ -372,6 +386,8 @@ object ErrorMessages { val InvalidPaymentSystemName = "OBP-30116: Invalid payment system name. The payment system name should only contain 0-9/a-z/A-Z/'-'/'.'/'_', the length should be smaller than 200." val ProductFeeNotFoundById = "OBP-30117: Product Fee not found. Please specify a valid value for PRODUCT_FEE_ID." + val CreateProductFeeError = "OBP-30118: Could not insert the Product Fee." + val UpdateProductFeeError = "OBP-30119: Could not update the Product Fee." val EntitlementIsBankRole = "OBP-30205: This entitlement is a Bank Role. Please set bank_id to a valid bank id." @@ -396,6 +412,7 @@ object ErrorMessages { val EntitlementRequestNotFound = "OBP-30215: EntitlementRequestId not found" val EntitlementAlreadyExists = "OBP-30216: Entitlement already exists for the user." val EntitlementCannotBeDeleted = "OBP-30219: EntitlementId cannot be deleted." + val EntitlementCannotBeGranted = "OBP-30220: Entitlement cannot be granted." val CreateSystemViewError = "OBP-30250: Could not create the system view" val DeleteSystemViewError = "OBP-30251: Could not delete the system view" @@ -486,7 +503,9 @@ object ErrorMessages { val InvalidChargePolicy = "OBP-40013: Invalid Charge Policy. Please specify a valid value for Charge_Policy: SHARED, SENDER or RECEIVER. " val AllowedAttemptsUsedUp = "OBP-40014: Sorry, you've used up your allowed attempts. " val InvalidChallengeType = "OBP-40015: Invalid Challenge Type. Please specify a valid value for CHALLENGE_TYPE, when you create the transaction request." - val InvalidChallengeAnswer = "OBP-40016: Invalid Challenge Answer. Please specify a valid value for answer in Json body. If it is sandbox mode, the answer must be `123`. If it kafka mode, the answer can be got by phone message or other security ways." + val InvalidChallengeAnswer = "OBP-40016: Invalid Challenge Answer. Please specify a valid value for answer in Json body. " + + "If connector = mapped and transactionRequestType_OTP_INSTRUCTION_TRANSPORT = DUMMY and suggested_default_sca_method=DUMMY, the answer must be `123`. " + + "If connector = others, the challenge answer can be got by phone message or other security ways." val InvalidPhoneNumber = "OBP-40017: Invalid Phone Number. Please specify a valid value for PHONE_NUMBER. Eg:+9722398746 " val TransactionRequestsNotEnabled = "OBP-40018: Sorry, Transaction Requests are not enabled in this API instance." val NextChallengePending = s"OBP-40019: Cannot create transaction due to transaction request is in status: ${NEXT_CHALLENGE_PENDING}." @@ -521,6 +540,7 @@ object ErrorMessages { val DynamicMessageDocNotFound = "OBP-40043: DynamicMessageDoc not found, please specify valid DYNAMIC_MESSAGE_DOC_ID. " val DynamicMessageDocDeleteError = "OBP-40044: DynamicMessageDoc can not be deleted. " val DynamicCodeCompileFail = "OBP-40045: The code to do compile is illegal scala code, compilation failed. " + val InvalidOperationId = "OBP-40046: Invalid operation_id, please specify valid operation_id." // Exceptions (OBP-50XXX) val UnknownError = "OBP-50000: Unknown Error." val FutureTimeoutException = "OBP-50001: Future Timeout Exception." @@ -564,6 +584,7 @@ object ErrorMessages { val InvalidConnectorResponseForSaveDoubleEntryBookTransaction = "OBP-50216: The Connector did not return a valid response for saving double-entry transaction." val InvalidConnectorResponseForCancelPayment = "OBP-50217: Connector did not return the transaction we requested." val InvalidConnectorResponseForGetEndpointTags = "OBP-50218: Connector did not return the set of endpoint tags we requested." + val InvalidConnectorResponseForGetBankAccountsWithAttributes = "OBP-50219: Connector did not return the bank accounts we requested." // Adapter Exceptions (OBP-6XXXX) // Reserved for adapter (south of Kafka) messages diff --git a/obp-api/src/main/scala/code/api/util/ExampleValue.scala b/obp-api/src/main/scala/code/api/util/ExampleValue.scala index b82a736a0..fb7044526 100644 --- a/obp-api/src/main/scala/code/api/util/ExampleValue.scala +++ b/obp-api/src/main/scala/code/api/util/ExampleValue.scala @@ -2,9 +2,10 @@ package code.api.util import code.api.util.APIUtil.parseDate -import code.api.util.ErrorMessages.{InvalidJsonFormat, UserHasMissingRoles, UserNotLoggedIn, UnknownError} +import code.api.util.ErrorMessages.{InvalidJsonFormat, UnknownError, UserHasMissingRoles, UserNotLoggedIn} import net.liftweb.json.JsonDSL._ import code.api.util.Glossary.{glossaryItems, makeGlossaryItem} +import code.apicollection.ApiCollection import code.dynamicEntity.{DynamicEntityDefinition, DynamicEntityFooBar, DynamicEntityFullBarFields, DynamicEntityIntTypeExample, DynamicEntityStringTypeExample} import com.openbankproject.commons.model.enums.{CustomerAttributeType, DynamicEntityFieldType} import com.openbankproject.commons.util.ReflectUtils @@ -100,7 +101,13 @@ object ExampleValue { lazy val customerNumberExample = ConnectorField("5987953", s"The human friendly customer identifier that MUST uniquely identify the Customer at the Bank ID. Customer Number is NOT used in URLs.") glossaryItems += makeGlossaryItem("Customer.customerNumber", customerNumberExample) - + + lazy val licenseIdExample = ConnectorField("ODbL-1.0", s"") + glossaryItems += makeGlossaryItem("License.id", licenseIdExample) + + lazy val licenseNameExample = ConnectorField("Open Database License", s"") + glossaryItems += makeGlossaryItem("License.name", licenseNameExample) + lazy val customerAttributeIdExample = ConnectorField("7uy8a7e4-6d02-40e3-a129-0b2bf89de8uh", s"Customer attribute id") glossaryItems += makeGlossaryItem("Customer.attributeId", customerAttributeIdExample) @@ -819,7 +826,7 @@ object ExampleValue { lazy val relatesToKycCheckIdExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) glossaryItems += makeGlossaryItem("relates_to_kyc_check_id", relatesToKycCheckIdExample) - lazy val productCodeExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) + lazy val productCodeExample = ConnectorField("1234", NoDescriptionProvided) glossaryItems += makeGlossaryItem("product_code", productCodeExample) lazy val imageUrlExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) @@ -1194,8 +1201,8 @@ object ExampleValue { lazy val productAttributeIdExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) glossaryItems += makeGlossaryItem("product_attribute_id", productAttributeIdExample) - lazy val isSystemExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) - glossaryItems += makeGlossaryItem("is_system", isSystemExample) + lazy val isSystemExample = ConnectorField("true", "If the view is the system level, then it is true") + glossaryItems += makeGlossaryItem("view.is_system", isSystemExample) lazy val detailsExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) glossaryItems += makeGlossaryItem("details", detailsExample) @@ -1302,7 +1309,7 @@ object ExampleValue { lazy val webUiPropsIdExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) glossaryItems += makeGlossaryItem("web_ui_props_id", webUiPropsIdExample) - lazy val providerExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) + lazy val providerExample = ConnectorField("ETHEREUM","the provider name ") glossaryItems += makeGlossaryItem("provider", providerExample) lazy val canSeePhysicalLocationExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) @@ -1602,7 +1609,7 @@ object ExampleValue { lazy val superFamilyExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) glossaryItems += makeGlossaryItem("super_family", superFamilyExample) - lazy val nameExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) + lazy val nameExample = ConnectorField("ACCOUNT_MANAGEMENT_FEE",NoDescriptionProvided) glossaryItems += makeGlossaryItem("name", nameExample) lazy val productFeeIdExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) @@ -2055,11 +2062,11 @@ object ExampleValue { lazy val indexExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) glossaryItems += makeGlossaryItem("index", indexExample) - lazy val descriptionExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) + lazy val descriptionExample = ConnectorField(s"This an optional field. Maximum length is ${ApiCollection.Description.maxLen}. It can be any characters here.","The human readable description here.") glossaryItems += makeGlossaryItem("description", descriptionExample) - lazy val dynamicResourceDocdescriptionExample = ConnectorField("Create one User", "the description for this endpoint") - glossaryItems += makeGlossaryItem("DynamicResourceDoc.description", dynamicResourceDocdescriptionExample) + lazy val dynamicResourceDocDescriptionExample = ConnectorField("Create one User", "the description for this endpoint") + glossaryItems += makeGlossaryItem("DynamicResourceDoc.description", dynamicResourceDocDescriptionExample) lazy val canDeleteCommentExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) glossaryItems += makeGlossaryItem("can_delete_comment", canDeleteCommentExample) diff --git a/obp-api/src/main/scala/code/api/util/Glossary.scala b/obp-api/src/main/scala/code/api/util/Glossary.scala index d1d947480..978af487b 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -635,10 +635,12 @@ object Glossary extends MdcLoggable { title = "Bank", description = """ - |A Bank (aka Space) represents a financial institution, brand or organisaitonal unit under which resources such as endpoints and entities exist. + |A Bank (aka Space) represents a financial institution, brand or organizational unit under which resources such as endpoints and entities exist. | |Both standard entities (e.g. financial products and bank accounts in the OBP standard) and dynamic entities and endpoints (created by you or your organisation) can exist at the Bank level. | +|For example see [Bank/Space level Dynamic Entities](/?version=OBPv4.0.0&operation_id=OBPv4_0_0-createBankLevelDynamicEntity) and [Bank/Space level Dynamic Endpoints](http://localhost:8082/?version=OBPv4.0.0&operation_id=OBPv4_0_0-createBankLevelDynamicEndpoint) +| |The Bank is important because many Roles can be granted at the Bank level. In this way, it's possible to create segregated or partitioned sets of endpoints and data structures in a single OBP instance. | |A User creating a Bank (if they have the right so to do), automatically gets the Entitlement to grant any Role for that Bank. Thus the creator of a Bank / Space becomes the "god" of that Bank / Space. @@ -1842,10 +1844,10 @@ object Glossary extends MdcLoggable { |# Define comma separated list of allowed IP addresses |# gateway.host=127.0.0.1 |# Define secret used to validate JWT token -|# gateway.token_secret=secret +|# jwt.token_secret=your-at-least-256-bit-secret-token |# -------------------------------------- Gateway login -- |``` -|Please keep in mind that property gateway.token_secret is used to validate JWT token to check it is not changed or corrupted during transport. +|Please keep in mind that property jwt.token_secret is used to validate JWT token to check it is not changed or corrupted during transport. | |### 2) Create / have access to a JWT | @@ -1879,15 +1881,15 @@ object Glossary extends MdcLoggable { | base64UrlEncode(header) + "." + | base64UrlEncode(payload), | -|) secret base64 encoded +|) your-at-least-256-bit-secret-token |``` | |Here is the above example token: | |``` |eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. -|AS8D76F7A89S87D6F7A9SD876FA789SD78F6A7S9D78F6AS79DF87A6S7D9F7A6S7D9F78A6SD798F78679D786S789D78F6A7S9D78F6AS79DF876A7S89DF786AS9D87F69AS7D6FN1bWVyIn0. -|KEuvjv3dmwkOhQ3JJ6dIShK8CG_fd2REApOGn1TRmgU +|eyJsb2dpbl91c2VyX25hbWUiOiJ1c2VybmFtZSIsImlzX2ZpcnN0IjpmYWxzZSwiYXBwX2lkIjoiODVhOTY1ZjAtMGQ1NS00ZTBhLThiMWMtNjQ5YzRiMDFjNGZiIiwiYXBwX25hbWUiOiJHV0wiLCJ0aW1lX3N0YW1wIjoiMjAxOC0wOC0yMFQxNDoxMzo0MFoiLCJjYnNfdG9rZW4iOiJ5b3VyX3Rva2VuIiwiY2JzX2lkIjoieW91cl9jYnNfaWQiLCJzZXNzaW9uX2lkIjoiMTIzNDU2Nzg5In0. +|bfWGWttEEcftiqrb71mE6Xy1tT_I-gmDPgjzvn6kC_k |``` | | @@ -1922,8 +1924,8 @@ object Glossary extends MdcLoggable { | |``` |curl -v -H 'Authorization: GatewayLogin token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. -|AS8D76F7A89S87D6F7A9SD876FA789SD78F6A7S9D78F6AS79DF87A6S7D9F7A6S7D9F78A6SD798F78679D786S789D78F6A7S9D78F6AS79DF876A7S89DF786AS9D87F69AS7D6FN1bWVyIn0. -|KEuvjv3dmwkOhQ3JJ6dIShK8CG_fd2REApOGn1TRmgU" $getServerUrl/obp/v3.0.0/users/current +|eyJsb2dpbl91c2VyX25hbWUiOiJ1c2VybmFtZSIsImlzX2ZpcnN0IjpmYWxzZSwiYXBwX2lkIjoiODVhOTY1ZjAtMGQ1NS00ZTBhLThiMWMtNjQ5YzRiMDFjNGZiIiwiYXBwX25hbWUiOiJHV0wiLCJ0aW1lX3N0YW1wIjoiMjAxOC0wOC0yMFQxNDoxMzo0MFoiLCJjYnNfdG9rZW4iOiJ5b3VyX3Rva2VuIiwiY2JzX2lkIjoieW91cl9jYnNfaWQiLCJzZXNzaW9uX2lkIjoiMTIzNDU2Nzg5In0. +|bfWGWttEEcftiqrb71mE6Xy1tT_I-gmDPgjzvn6kC_k"' $getServerUrl/obp/v3.0.0/users/current |``` | | @@ -1960,12 +1962,12 @@ object Glossary extends MdcLoggable { |``` |import jwt |from datetime import datetime, timezone -|from obp_python.config import obp_api_host |import requests | |env = 'local' |DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ' | +|obp_api_host = 'https://yourhost.com' |payload = { | "login_user_name": "username", | "is_first": False, @@ -1978,7 +1980,7 @@ object Glossary extends MdcLoggable { |} | | -|token = jwt.encode(payload, 'secretsecretsecretstsecretssssss', algorithm='HS256') +|token = jwt.encode(payload, 'your-at-least-256-bit-secret-token', algorithm='HS256').decode("utf-8") |authorization = 'GatewayLogin token="{}"'.format(token) |headers = {'Authorization': authorization} |url = obp_api_host + '/obp/v4.0.0/users/current' @@ -2014,6 +2016,171 @@ object Glossary extends MdcLoggable { """) + val dauthEnabledMessage : String = if (APIUtil.getPropsAsBoolValue("allow_dauth", false)) + {"Note: DAuth is enabled."} else {"Note: *DAuth is NOT enabled on this instance!*"} + + + glossaryItems += GlossaryItem( + title = APIUtil.DAuthHeaderKey, + description = + s""" + |### DAuth Introduction, Setup and Usage +| +| +|DAuth is an experimental authentication mechanism that aims to pin an ethereum or other blockchain Smart Contract to an OBP "User". +| +|In the future, it might be possible to be more specific and pin specific actors (wallets) that are acting within the smart contract, but so far, one smart contract acts on behalf of one User. +| +|Thus, if a smart contract "X" calls the OBP API using the DAuth header, OBP will get or create a user called X and the call will proceed in the context of that User "X". +| +| +|DAuth is invoked by the REST client (caller) including a specific header (see step 3 below) in any OBP REST call. +| +|When OBP receives the DAuth token, it creates or gets a User with a username based on the smart_contract_address and the provider based on the network_name. The combination of username and provider is unique in OBP. +| +|If you are calling OBP-API via an API3 Airnode, the Airnode will take care of constructing the required header. +| +|When OBP detects a DAuth header / token it first checks if the Consumer is allowed to make such a call. OBP will validate the Consumer ip address and signature etc. +| +|Note: The DAuth flow does *not* require an explicit POST like Direct Login to create the token. +| +|Permissions may be assigned to an OBP User at any time, via the UserAuthContext, Views, Entitlements to Roles or Consents. +| +|$dauthEnabledMessage +| +|Note: *The DAuth client is responsible for creating a token which will be trusted by OBP absolutely*! +| +| +|To use DAuth: +| +|### 1) Configure OBP API to accept DAuth. +| +|Set up properties in your props file +| +|``` +|# -- DAuth -------------------------------------- +|# Define secret used to validate JWT token +|# jwt.public_key_rsa=path-to-the-pem-file +|# Enable/Disable DAuth communication at all +|# In case isn't defined default value is false +|# allow_dauth=false +|# Define comma separated list of allowed IP addresses +|# dauth.host=127.0.0.1 +|# -------------------------------------- DAuth-- +|``` +|Please keep in mind that property jwt.public_key_rsa is used to validate JWT token to check it is not changed or corrupted during transport. +| +|### 2) Create / have access to a JWT +| +|The following videos are available: +| * [DAuth in local environment](https://vimeo.com/644315074) +| +|HEADER:ALGORITHM & TOKEN TYPE +| +|``` +|{ +| "alg": "RS256", +| "typ": "JWT" +|} +|``` +|PAYLOAD:DATA +| +|``` +|{ +| "smart_contract_address": "0xe123425E7734CE288F8367e1Bb143E90bb3F051224", +| "network_name": "AIRNODE.TESTNET.ETHEREUM", +| "msg_sender": "0xe12340927f1725E7734CE288F8367e1Bb143E90fhku767", +| "consumer_key": "0x1234a4ec31e89cea54d1f125db7536e874ab4a96b4d4f6438668b6bb10a6adb", +| "timestamp": "2021-11-04T14:13:40Z", +| "request_id": "0Xe876987694328763492876348928736497869273649" +|} +|``` +|VERIFY SIGNATURE +|``` +|RSASHA256( +| base64UrlEncode(header) + "." + +| base64UrlEncode(payload), +| +|) your-RSA-key-pair +|``` +| +|Here is an example token: +| +|``` +|eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlMTIzNDI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyMjQiLCJuZXR3b3JrX25hbWUiOiJFVEhFUkVVTSIsIm1zZ19zZW5kZXIiOiIweGUxMjM0MDkyN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGZoa3U3NjciLCJjb25zdW1lcl9rZXkiOiIweDEyMzRhNGVjMzFlODljZWE1NGQxZjEyNWRiNzUzNmU4NzRhYjRhOTZiNGQ0ZjY0Mzg2NjhiNmJiMTBhNmFkYiIsInRpbWVzdGFtcCI6IjIwMjEtMTEtMDRUMTQ6MTM6NDBaIiwicmVxdWVzdF9pZCI6IjBYZTg3Njk4NzY5NDMyODc2MzQ5Mjg3NjM0ODkyODczNjQ5Nzg2OTI3MzY0OSJ9.XSiQxjEVyCouf7zT8MubEKsbOBZuReGVhnt9uck6z6k +|``` +| +| +| +|### 3) Try a REST call using the header +| +| +|Using your favorite http client: +| +| GET $getServerUrl/obp/v3.0.0/users/current +| +|Body +| +| Leave Empty! +| +| +|Headers: +| +| DAuth: your-jwt-from-step-above +| +|Here is it all together: +| +| GET $getServerUrl/obp/v3.0.0/users/current HTTP/1.1 +| Host: localhost:8080 +| User-Agent: curl/7.47.0 +| Accept: */* +| DAuth: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlMTIzNDI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyMjQiLCJuZXR3b3JrX25hbWUiOiJFVEhFUkVVTSIsIm1zZ19zZW5kZXIiOiIweGUxMjM0MDkyN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGZoa3U3NjciLCJjb25zdW1lcl9rZXkiOiIweDEyMzRhNGVjMzFlODljZWE1NGQxZjEyNWRiNzUzNmU4NzRhYjRhOTZiNGQ0ZjY0Mzg2NjhiNmJiMTBhNmFkYiIsInRpbWVzdGFtcCI6IjIwMjEtMTEtMDRUMTQ6MTM6NDBaIiwicmVxdWVzdF9pZCI6IjBYZTg3Njk4NzY5NDMyODc2MzQ5Mjg3NjM0ODkyODczNjQ5Nzg2OTI3MzY0OSJ9.XSiQxjEVyCouf7zT8MubEKsbOBZuReGVhnt9uck6z6k +| +|CURL example +| +|``` +|curl -v -H 'DAuth: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlMTIzNDI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyMjQiLCJuZXR3b3JrX25hbWUiOiJFVEhFUkVVTSIsIm1zZ19zZW5kZXIiOiIweGUxMjM0MDkyN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGZoa3U3NjciLCJjb25zdW1lcl9rZXkiOiIweDEyMzRhNGVjMzFlODljZWE1NGQxZjEyNWRiNzUzNmU4NzRhYjRhOTZiNGQ0ZjY0Mzg2NjhiNmJiMTBhNmFkYiIsInRpbWVzdGFtcCI6IjIwMjEtMTEtMDRUMTQ6MTM6NDBaIiwicmVxdWVzdF9pZCI6IjBYZTg3Njk4NzY5NDMyODc2MzQ5Mjg3NjM0ODkyODczNjQ5Nzg2OTI3MzY0OSJ9.XSiQxjEVyCouf7zT8MubEKsbOBZuReGVhnt9uck6z6k' $getServerUrl/obp/v3.0.0/users/current +|``` +| +| +|You should receive a response like: +| +|``` +|{ +| "user_id": "4c4d3175-1e5c-4cfd-9b08-dcdc209d8221", +| "email": "", +| "provider_id": "0xe123425E7734CE288F8367e1Bb143E90bb3F051224", +| "provider": "ETHEREUM", +| "username": "0xe123425E7734CE288F8367e1Bb143E90bb3F051224", +| "entitlements": { +| "list": [] +| } +|} +|``` +| +|### Under the hood +| +|The file, dauth.scala handles the DAuth, +| +|We: +| +|``` +|-> Check if Props allow_dauth is true +| -> Check if DAuth header exists +| -> Check if getRemoteIpAddress is OK +| -> Look for "token" +| -> parse the JWT token and getOrCreate the user +| -> get the data of the user +|``` +| +|### More information +| +| Parameter names and values are case sensitive. +| Each parameter MUST NOT appear more than once per request. +| + """) + + glossaryItems += GlossaryItem( title = "SCA (Strong Customer Authentication)", @@ -2150,7 +2317,7 @@ object Glossary extends MdcLoggable { glossaryItems += GlossaryItem( - title = "API Collections", + title = "API Collection", description = s"""An API Collection is a collection of endpoints grouped together for a certain purpose. | |Having read access to a Collection does not constitute execute access on the endpoints in the Collection. @@ -2186,28 +2353,48 @@ object Glossary extends MdcLoggable { glossaryItems += GlossaryItem( - title = "Dynamic Entity", + title = "Dynamic Entity Manage", description = - s"""If you want to create, store and custom data in OBP, you can create "Dynamic Entities". + s""" +| +|Dynamic Entities can be used to store and retrieve custom data objects (think your own tables and fields) in the OBP instance. +| +|You can define your own Dynamic Entities or use Dynamic Entities created by others. +| +|You would use Dynamic Entities if you want to go beyond the OBP standard data model and store custom data structures. Note, if you want to extend the core OBP banking model of Customers, Products, Accounts, Transactions and so on you can also add Custom Attributes to these standard objects. +| +|You would use Dynamic Endpoints if you want to go beyond the standard OBP or other open banking standard APIs. +| +|Dynamic Entities have their own REST APIs so you can easily Create, Read, Update and Delete records. However, you can also connect Dynamic Endpoints with your own API definitions (via Swagger) and so create custom GET endpoints connecting to any combination of Dynamic Entities. +| +|Dynamic Endpoints can retrieve the data of Dynamic Entities so you can effectively create bespoke endpoint / data combinations - at least for GET endpoints - using Dynamic Endpoints, Entities and Endpoint Mapping. +| +|In order to use Dynamic Entities you will need to have the appropriate Entitlements to Create, Read, Update or Delete records in the Dynamic Entity. +| |You define your Dynamic Entities in JSON. | |Fields are typed, have an example value and a (markdown) description. They can also be constrained in size. | |You can also create field "references" to other fields in other Entities. These are like foreign keys to other Dynamic or Static (built in) entities. |In other words, if you create an Entity called X which has a field called A, you can force the values of X.A to match the values of Y.B where Y is another Dynamic Entity or Z.B where Z is a Static (OBP) Entity. +|If you want to add data to an existing Entity, you can create a Dynamic Entity which has a reference field to the existing entity. | |Dynamic Entities can be created at the System level (bank_id is null) - or Bank / Space level (bank_id is not null). You might want to create Bank level Dynamic Entities in order to grant automated roles based on user email domain. | -|Upon successful creation of a Dynamic Entity, OBP automatically: +|When creating a Dynamic Entity, OBP automatically: | -|*Creates Create, Read, Update and Delete endpoints to operate on the Entity so you can insert, get, modify and delete records. -|*Creates Roles to guard the above endpoints. +|* Creates a data structure in the OBP database in which to store the records of the new Entity. +|* Creates a primary key for the Entity which can be used to update and delete the Entity. +|* Creates Create, Read, Update and Delete endpoints to operate on the Entity so you can insert, get, modify and delete records. These CRUD operations are all available over the generated REST endpoints. +|* Creates Roles to guard the above endpoints. | |Following the creation of a Dynamic Entity you will need to grant yourself or others the appropriate roles before you can insert or get records. | -|Each Dynamic Entity gets a dynamicEntityId which uniquely identifies it and the userId which identifies the user who created the Entity. +|The generated Roles required for CRUD operations on a Dynamic Entity are like any other OBP Role i.e. they can be requested, granted, revoked and auto-granted using the API Explorer / API Manager or via REST API. To see the Roles required for a Dynamic Entities endpoints, see the API Explorer for each endpoint concerned. | -|For more information see the endpoints. +|Each Dynamic Entity gets a dynamicEntityId which uniquely identifies it and also the userId which identifies the user who created the Entity. The dynamicEntityId is used to update the definition of the Entity. +| +|To visualise any data contained in Dynamic Entities you could use external BI tools and use the GET endpoints and authenticate using OAuth or Direct Login. | |The following videos are available: | @@ -2216,6 +2403,314 @@ object Glossary extends MdcLoggable { | """.stripMargin) + glossaryItems += GlossaryItem( + title = "Dynamic Endpoint Manage", + description = + s""" +| +|If you want to create endpoints from Swagger / Open API specification files, use Dynamic Endpoints. +| +|We use the term "Dynamic" because these Endpoints persist in the OBP database and are served from real time generated Scala code. +| +|This contrasts to the "Static" endpoints (see the Static glossary item) which are served from static Scala code. +| +|Dynamic endpoints can be changed in real-time and do not require an OBP instance restart. +| +|When you POST a swagger file, all the endpoints defined in the swagger file, will be created in this OBP instance. +| +|You can create a set of endpoints in three different modes: +| +|1) If the *host* field in the Swagger file is set to "dynamic_entity", then you should link the swagger JSON fields to Dynamic Entity fields. To do this use the *Endpoint Mapping* endpoints. +| +|2) If the *host* field in the Swagger file is set to "obp_mock", the Dynamic Endpoints created will return *example responses defined in the swagger file*. +| +|3) If you need to link the responses to external resource, use the *Method Routing* endpoints. +| +| +|Dynamic Endpoints can be created at the System level (bank_id is null) or Bank / Space level (bank_id is NOT null). +|You might want to create Bank level Dynamic Entities in order to grant automated roles based on user email domain. See the OBP-API sample.props.template +| +|Upon the successful creation of each Dynamic Endpoint, OBP will automatically: +| +|*Create a Guard with a named Role on the Endpoint to protect it from unauthorised users. +|*Grant you an Entitlement to the required Role so you can call the endpoint and pass its Guard. +| +|The following videos are available: +| +| * [Introduction to Dynamic Endpoints](https://vimeo.com/426235612) +| * [Features of Dynamic Endpoints](https://vimeo.com/444133309) +| +""".stripMargin) + + glossaryItems += GlossaryItem( + title = "Endpoint Mapping", + description = + s""" + |Endpoint Mapping can be used to map each JSON field in a Dynamic Endpoint to different Dynamic Entity fields. + | + |This document assumes you already have some knowledge of OBP Dynamic Endpoints and Dynamic Entities. + | + |To enable Endpoint Mapping for your Dynamic Endpoints, either set the `host` in the swagger file to "dynamic_entity" upon creation of the Dynamic Endpoints - or update the host using the Update Dynamic Endpoint Host endpoints. + | + |Once the `host` is thus set, you can use the Endpoint Mapping endpoints to map the Dynamic Endpoint fields to Dynamic Entity data. + | + |See the [Create Endpoint Mapping](/index#OBPv4.0.0-createEndpointMapping) JSON body. You will need to know the operation_id in advance and you can prepare the request_mapping and response_mapping objects. You can get the operation ID from the API Explorer or Get Dynamic Endpoints endpoints. + | + |For more details and a walk through, please see the following video: + | + | * [Endpoint Mapping](https://vimeo.com/553369108) + |""".stripMargin) + + glossaryItems += GlossaryItem( + title = "Branch", + description = + s"""The bank branches, it contains the address, location, lobby, drive_up of the Branch. + """.stripMargin) + + glossaryItems += GlossaryItem( + title = "API", + description = + s"""|The terms `API` (Application Programming Interface) and `Endpoint` are used somewhat interchangeably. +| +|However, an API normally refers to a group of Endpoints. +| +|An endpoint has a unique URL path and HTTP verb (GET, POST, PUT, DELETE etc). +| +|When we POST a Swagger file to the Create Endpoint endpoint, we are in fact creating a set of Endpoints that have a common Tag. Tags are used to group Endpoints in the API Explorer and filter the Endpoints in the Resource Doc endpoints. +| +|Endpoints can also be grouped together in Collections. +| +|See also [Endpoint](/glossary#Endpoint) +| + """.stripMargin) + + glossaryItems += GlossaryItem( + title = "Endpoint", + description = + s""" +|The terms `Endpoint` and `API` (Application Programming Interface) are used somewhat interchangeably. However, an Endpoint is a specific URL defined by its path (eg. /obp/v4.0/root) and its http verb (e.g. GET, POST, PUT, DELETE etc). +|Endpoints are like arrows into a system. Like any good computer function, endpoints should expect much and offer little in return. They should fail early and be clear about any reason for failure. In other words each endpoint should have a tight and limited contract with any caller - and especially the outside world! +| +|In OBP, all system endpoints are RESTful - and most Open Banking Standards are RESTful. However, it is possible to create non-RESTful APIs in OBP using the Create Endpoint endpoints. +| +|You can immediately tell if an endpoint is not RESTful by seeing a verb in the URL. For example: +| +|POST /customers is RESTful = GOOD +|POST /create-customer is NOT RESTful (due to the word "create") = BAD +| +|RESTful APIs use resource names in URL paths. You can think of RESTful resources like database tables. You wouldn't name a database table "create-customer", so don't use that in a URL path. +| +|If we consider interacting with a Customers table, we read the data using GET /Customers and write to the table using POST /Customers. This model keeps the names clear and predictable. +|Note that we are only talking about the front end interface here - anything could be happening in the backend - and that is one of the beauties of APIs. For instance GET /Customers could call 5 different databases and 3 XML services in the background. Similarly POST /Customers could insert into various different tables and backend services. The important thing is that the user of the API (The Consumer or Client in OAuth parlance) has a simple and consistent experience. +| +|In OBP, all Endpoints are implemented by `Partial Functions`. A Partial Function is a function which only accepts (and responds) to calls with certain parameter values. In the case of API Endpoints the inputs to the Partial Functions are the URL path and http verb. Note that it would be possible to have different Partial Functions respond even to different query parameters, but for OBP static endpoints at least, we take the approach of URL path + http Verb is handled by one Partial Function. +|Each Partial Function is identified by an Operation ID which uniquely identifies the endpoint in the system. Having an Operation ID allows us to decorate the Endpoint with metadata (e.g. Tags) and surround the Endpoint with behaviour such as JSON Schema Validation. +| +|See also [API](/glossary#API) +| +""".stripMargin) + + + + glossaryItems += GlossaryItem( + title = "API Tag", + description = + s"""All OBP API relevant docs, eg: API configuration, JSON Web Key, Adapter Info, Rate Limiting + """.stripMargin) + + + + glossaryItems += GlossaryItem( + title = "Account Access", + description = + s""" + |Account Access is OBP View system. The Account owners can create the view themselves. + |And they can grant/revoke the view to other users to use their view. + |""".stripMargin) + +// val allTagNames: Set[String] = ApiTag.allDisplayTagNames +// val existingItems: Set[String] = glossaryItems.map(_.title).toSet +// allTagNames.diff(existingItems).map(title => glossaryItems += GlossaryItem(title, title)) + + glossaryItems += GlossaryItem( + title = "Static Endpoint", + description = + s""" +|Static endpoints are served from static Scala source code which is contained in (public) Git repositories. +| +|Static endpoints cover all the OBP API and User management functionality as well as the Open Bank Project banking APIs and other Open Banking standards such as UK Open Banking, Berlin Group and STET etc.. + |In short, Static (standard) endpoints are defined in Git as Scala source code, where as Dynamic (custom) endpoints are defined in the OBP database. + | +|Modifications to Static endpoint core properties such as URLs and response bodies require source code changes and an instance restart. However, JSON Schema Validation and Dynamic Connector changes can be applied in real-time. +""".stripMargin) + + glossaryItems += GlossaryItem( + title = "Message Doc", + description = + s""" +|OBP can communicate with core banking systems (CBS) and other back end services using a "Connector -> Adapter" approach. +| +|The OBP Connector is a core part of the OBP-API and is written in Scala / Java and potentially other JVM languages. +| +|The OBP Connector implements multiple functions / methods in a style that satisfies a particular transport / protocol such as HTTP REST, Akka or Kafka. +| +|An OBP Adapter is a separate software component written in any programming language that responds to requests from the OBP Connector. +| +|Requests are sent by the Connector to the Adapter (or a message queue). +| +|The Adapter must satisfy the Connector method's request for data (or return an error). +| +|"Message Docs" are used to define and document the request / response structure. +| +|Message Docs are visible in the API Explorer. +| +|Message Docs are also available over the Message Doc endpoints. +| +|Each Message Doc relates to one OBP function / method. +| +|The Message Doc includes: +| +| 1) The Name of the internal OBP function / method e.g. getAccountsForUser +| 2) The Outbound Message structure. +| 3) The Inbound Message structure. +| 4) The Connector name which denotes the protocol / transport used (e.g. REST, Akka, Kafka etc) +| 5) Outbound / Inbound Topic +| 6) A list of required Inbound fields +| 7) A list of dependent endpoints. +| +|The perspective is that of the OBP-API Connector i.e. the OBP Connector sends the message Out, and it receives the answer In. +| +|The Outbound message contains several top level data structures: +| +| 1) The outboundAdapterCallContext +| +| This tells the Adapter about the specific REST call that triggered the request and contains the correlationId to uniquely identify the REST call, the consumerId to identify the API Consumer (App) and a generalContext which is a list of key / value pairs that give the Adapter additional custom information about the call. +| +| 2) outboundAdapterAuthInfo +| +|This tells the Adapter about the authenticated User that is making the call including: the userId, the userName, the userAuthContext (a list of key / value pairs that have been validated using SCA (see the UserAuthContext endpoints)) and other optional structures such as linked Customers and Views on Accounts to further identify the User. +| +|3) The body +| +|The body contains named fields that are specific to each Function / Message Doc. +| +|For instance, getTransaction might send the bankId, accountId and transactionId so the Adapter can route the request based on bankId and check User permissions on the AccountId before retrieving a Transaction. +| +|The Inbound message +| +|The Inbound message is the reply or response from the Adapter and has the following structure: +| +|1) The inboundAdapterCallContext +| +|This is generally an echo of the outboundAdapterCallContext so the Connector can double check the target destination of the response. +| +|2) The status +| +|This contains information about status of the response including any errorCode and a list of backendMessages. +| +|3) The data +| +|This contains the named fields and their values which are specific to each Function / Message Doc. +| +| +|The Outbound / Inbound Topics are used for routing in multi OBP instance / Kafka installations. (so OBP nodes only listen only to the correct Topics). +| +|The dependent endpoints are listed to facilitate navigation in the API Explorer so integrators can test endpoints during integration. +| +|Message Docs can be generated automatically using OBP code tools. Thus, it's possible to create custom connectors that follow specific protocol and structural patterns e.g. for message queue X over XML format Y. +| +|""".stripMargin) + + glossaryItems += GlossaryItem( + title = "Method Routing", + description = + s""" + | + | Open Bank Project can have different connectors, to connect difference data sources. + | We support several sources at the moment, eg: databases, rest services, stored procedures and kafka. + | + | If OBP set connector=star, then you can use this method routing to switch the sources. + | And we also provide the fields mapping in side the endpoints. If the fields in the source are different from connector, + | then you can map the fields yourself. + | + | The following videos are available: + | + | *[Method Routing Endpoints](https://vimeo.com/398973130) + | *[Method Routing Endpoints Mapping](https://vimeo.com/404983764) + | + |""".stripMargin) + + glossaryItems += GlossaryItem( + title = "JSON Schema Validation", + description = + s""" + |JSON Schema is "a vocabulary that allows you to annotate and validate JSON documents". + | + |By applying JSON Schema Validation to your endpoints you can constrain POST and PUT request bodies. For example, you can set minimum / maximum lengths of fields and constrain values to certain lists or regular expressions. + | + |See [JSONSchema.org](https://json-schema.org/) for more information about the standard. + | + |Note that Dynamic Entities also use JSON Schema Validation so you don't need to additionally wrap the resulting endpoints with extra JSON Schema Validation but you could do. + | + + | + | We provide the schema validations over the endpoints. + | All the OBP endpoints request/response body fields can be validated by the schema. + | + |The following videos are available: + |* [JSON schema validation of request for Static and Dynamic Endpoints and Entities] (https://vimeo.com/485287014) + |""".stripMargin) + + + glossaryItems += GlossaryItem( + title = "Connector Method", + description = + s""" + | The developer can override all the existing Connector methods on their own. + | This function needs to be used together with the Method Routing. + | when set "connector = internal", then the developer can call their own method body at API level. + | + |eg: Get Banks endpoint, it calls the connector "getBanks" method, then the developers can use these endpoints to modify the business logic in the getBanks method body. + | + | The following videos are available: + |* [Introduction for Connector Method] (https://vimeo.com/507795470) + | + |""".stripMargin) + + + + glossaryItems += GlossaryItem( + title = "Dynamic Resource Doc", + description = + s""" + | The developers can create their own endpoints by this endpoint. + | Need to prepare the obp resource doc format json. + | And all the business logic code can be written in the *method_body* field, it is the encoded scala code. + | + | It is still working in the processing .. + |The following videos are available: + |* [Introduction for dConnector Method] (https://vimeo.com/623381607) + | + |""".stripMargin) + + glossaryItems += GlossaryItem( + title = "Dynamic Message Doc", + description = + s""" + | The developers can create their own scala methods in OBP code. + | These endpoints are designed for extending the current connector methods. + | when you call the dynamic resource doc endpoints, sometimes you need to call internal scala methods, + | which are not existing in OBP code, then you can use these endpoints to prepare them on your own. + | + | And you can use these endpoints to design your own helper methods in OBP code. + | + | It is still working in the processing .. + |The following videos are available: + |* [Introduction for Connector Method] (https://vimeo.com/623317747) + | + |""".stripMargin) + + /////////////////////////////////////////////////////////////////// // NOTE! Some glossary items are generated in ExampleValue.scala diff --git a/obp-api/src/main/scala/code/api/util/HashUtil.scala b/obp-api/src/main/scala/code/api/util/HashUtil.scala index f272994e8..bd8b5836e 100644 --- a/obp-api/src/main/scala/code/api/util/HashUtil.scala +++ b/obp-api/src/main/scala/code/api/util/HashUtil.scala @@ -14,9 +14,10 @@ object HashUtil { def main(args: Array[String]): Unit = { // You can verify hash with command line tool in linux, unix: // $ echo -n "123" | openssl dgst -sha256 - val password = "123" - val hashedPassword = Sha256Hash(password) - println("Password: " + password) - println("Hashed password: " + hashedPassword) + + val plainText = "123" + val hashedText = Sha256Hash(plainText) + println("Password: " + plainText) + println("Hashed password: " + hashedText) } } diff --git a/obp-api/src/main/scala/code/api/util/JwtUtil.scala b/obp-api/src/main/scala/code/api/util/JwtUtil.scala index fa899d189..b82b44cea 100644 --- a/obp-api/src/main/scala/code/api/util/JwtUtil.scala +++ b/obp-api/src/main/scala/code/api/util/JwtUtil.scala @@ -1,11 +1,14 @@ package code.api.util -import java.net.URL +import java.net.{URI, URL} +import java.nio.file.{Files, Paths} import java.text.ParseException +import code.api.util.RSAUtil.logger import code.util.Helper.MdcLoggable import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.crypto.{MACVerifier, RSASSAVerifier} +import com.nimbusds.jose.jwk.{JWK, RSAKey} import com.nimbusds.jose.jwk.source.{JWKSource, RemoteJWKSet} import com.nimbusds.jose.proc.{JWSVerificationKeySelector, SecurityContext} import com.nimbusds.jose.util.{DefaultResourceRetriever, JSONObjectUtils} @@ -235,7 +238,26 @@ object JwtUtil extends MdcLoggable { } } + def validateJwtWithRsaKey(jwtString: String): Boolean = { + val relativePath = APIUtil.getPropsValue("jwt.public_key_rsa", "") + val basePath = this.getClass.getResource("/").toString .replaceFirst("target[/\\\\].*$", "") + val filePath = new URI(s"${basePath}$relativePath").getPath + val publicKey = getPublicRsaKeyFromFile(filePath) + val signedJWT = SignedJWT.parse(jwtString) + val verifier = new RSASSAVerifier(publicKey) + signedJWT.verify(verifier) + } + + def getPublicRsaKeyFromFile(path: String): RSAKey = { + val pathOfFile = Paths.get(path) + val pemEncodedRSAPubliceKey = Files.readAllLines(pathOfFile).toArray.toList.mkString("\n") + logger.debug(pemEncodedRSAPubliceKey) + // Parse PEM-encoded key to RSA public / private JWK + val jwk: JWK = JWK.parseFromPEMEncodedObjects(pemEncodedRSAPubliceKey); + logger.debug(s"Key $path is private: " + jwk.isPrivate) + jwk.toPublicJWK.toRSAKey + } def main(args: Array[String]): Unit = { diff --git a/obp-api/src/main/scala/code/api/util/NewStyle.scala b/obp-api/src/main/scala/code/api/util/NewStyle.scala index f2a844196..8603ff1de 100644 --- a/obp-api/src/main/scala/code/api/util/NewStyle.scala +++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala @@ -36,7 +36,7 @@ import code.apicollection.{ApiCollectionTrait, MappedApiCollectionsProvider} import code.model.dataAccess.{AuthUser, BankAccountRouting} import code.standingorders.StandingOrderTrait import code.usercustomerlinks.UserCustomerLink -import code.users.{UserInvitation, UserInvitationProvider, Users} +import code.users.{UserAgreement, UserAgreementProvider, UserInvitation, UserInvitationProvider, Users} import code.util.Helper import com.openbankproject.commons.util.{ApiVersion, JsonUtils} import code.views.Views @@ -408,6 +408,12 @@ object NewStyle { } } + def getBankAccountsWithAttributes(bankId: BankId, queryParams: List[OBPQueryParam], callContext: Option[CallContext]): OBPReturnType[List[FastFirehoseAccount]] = { + Connector.connector.vend.getBankAccountsWithAttributes(bankId, queryParams, callContext) map { i => + (unboxFullOrFail(i._1, callContext,s"$InvalidConnectorResponseForGetBankAccountsWithAttributes", 400 ), i._2) + } + } + def getBankAccountBalances(bankIdAccountId: BankIdAccountId, callContext: Option[CallContext]): OBPReturnType[AccountBalances] = { Connector.connector.vend.getBankAccountBalances(bankIdAccountId: BankIdAccountId, callContext: Option[CallContext]) map { i => (unboxFullOrFail(i._1, callContext,s"$InvalidConnectorResponseForGetBankAccounts", 400 ), i._2) @@ -824,6 +830,9 @@ object NewStyle { connectorEmptyResponse(_, callContext) } } + def getAgreementByUserId(userId: String, agreementType: String, callContext: Option[CallContext]): Future[Box[UserAgreement]] = { + Future(UserAgreementProvider.userAgreementProvider.vend.getUserAgreement(userId, agreementType)) + } def getEntitlementsByBankId(bankId: String, callContext: Option[CallContext]): Future[List[Entitlement]] = { Entitlement.entitlement.vend.getEntitlementsByBankId(bankId) map { @@ -1025,6 +1034,12 @@ object NewStyle { } } + def getOrCreateResourceUser(username: String, provider: String, callContext: Option[CallContext]): OBPReturnType[User] = { + Future { UserX.getOrCreateDauthResourceUser(username, provider).map(user =>(user, callContext))} map { + unboxFullOrFail(_, callContext, s"$CannotGetOrCreateUser Current USERName($username) PROVIDER ($provider)", 404) + } + } + def createTransactionRequestv210( u: User, viewId: ViewId, @@ -3064,12 +3079,14 @@ object NewStyle { userId: String, apiCollectionName: String, isSharable: Boolean, + description: String, callContext: Option[CallContext] ) : OBPReturnType[ApiCollectionTrait] = { Future(MappedApiCollectionsProvider.createApiCollection( userId: String, apiCollectionName: String, - isSharable: Boolean) + isSharable: Boolean, + description: String) ) map { i => (unboxFullOrFail(i, callContext, CreateApiCollectionError), callContext) } diff --git a/obp-api/src/main/scala/code/api/util/RSAUtil.scala b/obp-api/src/main/scala/code/api/util/RSAUtil.scala index e8baabb9d..c3d6d6276 100644 --- a/obp-api/src/main/scala/code/api/util/RSAUtil.scala +++ b/obp-api/src/main/scala/code/api/util/RSAUtil.scala @@ -1,8 +1,16 @@ package code.api.util +import java.nio.file.{Files, Paths} +import java.security.Signature import code.api.util.CertificateUtil.{privateKey, publicKey} import code.util.Helper.MdcLoggable +import com.nimbusds.jose.crypto.RSASSASigner +import com.nimbusds.jose.jwk.JWK +import com.nimbusds.jose.{JWSAlgorithm, JWSHeader, JWSObject, Payload} import javax.crypto.Cipher +import net.liftweb.util.SecurityHelpers +import net.liftweb.util.SecurityHelpers.base64EncodeURLSafe +import java.time.Instant object RSAUtil extends MdcLoggable { @@ -16,20 +24,90 @@ object RSAUtil extends MdcLoggable { Base64.encodeBase64String(res) } def decrypt(encrypted: String): String = { - import org.apache.commons.codec.binary.Base64 import javax.crypto.Cipher + import org.apache.commons.codec.binary.Base64 val bytes = Base64.decodeBase64(encrypted) val cipher = Cipher.getInstance(cryptoSystem) cipher.init(Cipher.DECRYPT_MODE, privateKey) new String(cipher.doFinal(bytes), "utf-8") } + def computeHash(input: String): String = SecurityHelpers.hash256(input) + def computeHexHash(input: String): String = { + SecurityHelpers.hexDigest256(input.getBytes("UTF-8")) + } + + def signWithRsa256(payload: String, jwk: JWK): String = { + // Prepare JWS object with simple string as a payload + val jwsObject = new JWSObject( + new JWSHeader.Builder(JWSAlgorithm.RS256).build, + new Payload(payload) + ) + + val rsaSigner = new RSASSASigner(jwk.toRSAKey) + // Compute the RSA signature + jwsObject.sign(rsaSigner) + + // To serialize to compact form, produces something like + // eyJhbGciOiJSUzI1NiJ9.SW4gUlNBIHdlIHRydXN0IQ.IRMQENi4nJyp4er2L + // mZq3ivwoAjqa1uUkSBKFIX7ATndFF5ivnt-m8uApHO4kfIFOrW7w2Ezmlg3Qd + // maXlS9DhN0nUk_hGI3amEjkKd0BWYCB8vfUbUv0XGjQip78AI4z1PrFRNidm7 + // -jPDm5Iq0SZnjKjCNS5Q15fokXZc8u0A + val s = jwsObject.serialize + s + } + + def computeXSign(input: String, jwk: JWK) = { + logger.debug("Input: " + input) + logger.debug("Hash: " + computeHash(input)) + logger.debug("HEX hash: " + computeHexHash(input)) + // Compute the signature + val data = input.getBytes("UTF8") + val sig = Signature.getInstance("SHA256WithRSA") + sig.initSign(jwk.toRSAKey.toPrivateKey) + sig.update(data) + val signatureBytes = sig.sign + val xSign = base64EncodeURLSafe(signatureBytes) + logger.debug("x-sign: " + xSign) + xSign + } + + def getPrivateKeyFromFile(path: String): JWK = { + val pathOfFile = Paths.get(path) + val pemEncodedRSAPrivateKey = Files.readAllLines(pathOfFile).toArray.toList.mkString("\n") + logger.debug(pemEncodedRSAPrivateKey) + // Parse PEM-encoded key to RSA public / private JWK + val jwk: JWK = JWK.parseFromPEMEncodedObjects(pemEncodedRSAPrivateKey); + logger.debug("Key is private: " + jwk.isPrivate) + jwk + } + + def getPrivateKeyFromString(privateKeyValue: String): JWK = { + val pemEncodedRSAPrivateKey = privateKeyValue + logger.debug(privateKeyValue) + // Parse PEM-encoded key to RSA public / private JWK + val jwk: JWK = JWK.parseFromPEMEncodedObjects(pemEncodedRSAPrivateKey); + logger.debug("Key is private: " + jwk.isPrivate) + jwk + } + def main(args: Array[String]): Unit = { - val db = "jdbc:postgresql://localhost:5432/obp_mapped?user=obp&password=f" + val randomString = """G!y"k9GHD$D""" + val db = "jdbc:postgresql://localhost:5432/obp_mapped?user=obp&password=%s".format(randomString) val res = encrypt(db) println("db.url: " + db) println("encrypt: " + res) println("decrypt: " + decrypt(res)) + + val timestamp = Instant.now.getEpochSecond + val uri = "https://api.qredo.network/api/v1/p/company" + val body = """{"name":"Tesobe GmbH","city":"Berlin","country":"DE","domain":"tesobe.com","ref":"9827feec-4eae-4e80-bda3-daa7c3b97ad1"}""" + val inputMessage = s"""${timestamp}${uri}${body}""" + val privateKey = getPrivateKeyFromFile("obp-api/src/test/resources/cert/private.pem") + computeXSign(inputMessage, privateKey) + logger.debug("timestamp: " + timestamp) + + } } diff --git a/obp-api/src/main/scala/code/api/util/SecureRandomUtil.scala b/obp-api/src/main/scala/code/api/util/SecureRandomUtil.scala index 095cc9c93..a11b8c2e1 100644 --- a/obp-api/src/main/scala/code/api/util/SecureRandomUtil.scala +++ b/obp-api/src/main/scala/code/api/util/SecureRandomUtil.scala @@ -1,5 +1,6 @@ package code.api.util +import java.math.BigInteger import java.security.SecureRandom /** @@ -15,4 +16,8 @@ object SecureRandomUtil { // Obtains random numbers from the underlying native OS. // No assertions are made as to the blocking nature of generating these numbers. val csprng = SecureRandom.getInstance("NativePRNG") + + def alphanumeric(nrChars: Int = 24): String = { + new BigInteger(nrChars * 5, csprng).toString(32) + } } diff --git a/obp-api/src/main/scala/code/api/util/migration/Migration.scala b/obp-api/src/main/scala/code/api/util/migration/Migration.scala index 660a93130..0bc5a7c58 100644 --- a/obp-api/src/main/scala/code/api/util/migration/Migration.scala +++ b/obp-api/src/main/scala/code/api/util/migration/Migration.scala @@ -84,6 +84,10 @@ object Migration extends MdcLoggable { populateTheFieldIsActiveAtProductAttribute(startedBeforeSchemifier) alterColumnUsernameProviderFirstnameAndLastnameAtAuthUser(startedBeforeSchemifier) alterColumnEmailAtResourceUser(startedBeforeSchemifier) + alterColumnNameAtProductFee(startedBeforeSchemifier) + addFastFirehoseAccountsView(startedBeforeSchemifier) + addFastFirehoseAccountsMaterializedView(startedBeforeSchemifier) + alterUserAuthContextColumnKeyAndValueLength(startedBeforeSchemifier) } private def dummyScript(): Boolean = { @@ -283,6 +287,52 @@ object Migration extends MdcLoggable { } } } + private def alterColumnNameAtProductFee(startedBeforeSchemifier: Boolean): Boolean = { + if(startedBeforeSchemifier == true) { + logger.warn(s"Migration.database.alterColumnNameAtProductFee(true) cannot be run before Schemifier.") + true + } else { + val name = nameOf(alterColumnNameAtProductFee(startedBeforeSchemifier)) + runOnce(name) { + MigrationOfProductFee.alterColumnProductFeeName(name) + } + } + } + private def addFastFirehoseAccountsView(startedBeforeSchemifier: Boolean): Boolean = { + if(startedBeforeSchemifier == true) { + logger.warn(s"Migration.database.addfastFirehoseAccountsView(true) cannot be run before Schemifier.") + true + } else { + val name = nameOf(addFastFirehoseAccountsView(startedBeforeSchemifier)) + runOnce(name) { + MigrationOfFastFireHoseView.addFastFireHoseView(name) + } + } + } + + private def addFastFirehoseAccountsMaterializedView(startedBeforeSchemifier: Boolean): Boolean = { + if(startedBeforeSchemifier == true) { + logger.warn(s"Migration.database.addfastFirehoseAccountsMaterializedView(true) cannot be run before Schemifier.") + true + } else { + val name = nameOf(addFastFirehoseAccountsMaterializedView(startedBeforeSchemifier)) + runOnce(name) { + MigrationOfFastFireHoseMaterializedView.addFastFireHoseMaterializedView(name) + } + } + } + + private def alterUserAuthContextColumnKeyAndValueLength(startedBeforeSchemifier: Boolean): Boolean = { + if(startedBeforeSchemifier == true) { + logger.warn(s"Migration.database.alterUserAuthContextColumnKeyAndValueLength(true) cannot be run before Schemifier.") + true + } else { + val name = nameOf(alterUserAuthContextColumnKeyAndValueLength(startedBeforeSchemifier)) + runOnce(name) { + MigrationOfUserAuthContextFieldLength.alterColumnKeyAndValueLength(name) + } + } + } } diff --git a/obp-api/src/main/scala/code/api/util/migration/MigrationOfFastFireHoseMaterializedView.scala b/obp-api/src/main/scala/code/api/util/migration/MigrationOfFastFireHoseMaterializedView.scala new file mode 100644 index 000000000..8184288b7 --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/migration/MigrationOfFastFireHoseMaterializedView.scala @@ -0,0 +1,108 @@ +package code.api.util.migration + +import code.api.util.APIUtil +import code.api.util.migration.Migration.{DbFunction, saveLog} +import code.productfee.ProductFee +import net.liftweb.common.Full +import net.liftweb.mapper.{DB, Schemifier} +import net.liftweb.util.DefaultConnectionIdentifier + +import java.time.format.DateTimeFormatter +import java.time.{ZoneId, ZonedDateTime} + +object MigrationOfFastFireHoseMaterializedView { + + val oneDayAgo = ZonedDateTime.now(ZoneId.of("UTC")).minusDays(1) + val oneYearInFuture = ZonedDateTime.now(ZoneId.of("UTC")).plusYears(1) + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'") + + def addFastFireHoseMaterializedView(name: String): Boolean = { + DbFunction.tableExists(ProductFee, (DB.use(DefaultConnectionIdentifier){ conn => conn})) match { + case true => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + var isSuccessful = false + + def migrationSql(isMaterializedView:Boolean) =s""" + |CREATE ${if(isMaterializedView) "MATERIALIZED" else ""} VIEW mv_fast_firehose_accounts AS select + | mappedbankaccount.theaccountid as account_id, + | mappedbankaccount.bank as bank_id, + | mappedbankaccount.accountlabel as account_label, + | mappedbankaccount.accountnumber as account_number, + | (select + | string_agg( + | 'user_id:' + | || resourceuser.userid_ + | ||',provider:' + | ||resourceuser.provider_ + | ||',user_name:' + | ||resourceuser.name_, + | ',') as owners + | from resourceuser + | where + | resourceuser.id = mapperaccountholders.user_c + | ), + | mappedbankaccount.kind as kind, + | mappedbankaccount.accountcurrency as account_currency , + | mappedbankaccount.accountbalance as account_balance, + | (select + | string_agg( + | 'bank_id:' + | ||bankaccountrouting.bankid + | ||',account_id:' + | ||bankaccountrouting.accountid, + | ',' + | ) as account_routings + | from bankaccountrouting + | where + | bankaccountrouting.accountid = mappedbankaccount.theaccountid + | ), + | (select + | string_agg( + | 'type:' + | || mappedaccountattribute.mtype + | ||',code:' + | ||mappedaccountattribute.mcode + | ||',value:' + | ||mappedaccountattribute.mvalue, + | ',') as account_attributes + | from mappedaccountattribute + | where + | mappedaccountattribute.maccountid = mappedbankaccount.theaccountid + | ) + |from mappedbankaccount + | LEFT JOIN mapperaccountholders + | ON (mappedbankaccount.bank = mapperaccountholders.accountbankpermalink and mappedbankaccount.theaccountid = mapperaccountholders.accountpermalink); + |""".stripMargin + val executedSql = + DbFunction.maybeWrite(true, Schemifier.infoF _, DB.use(DefaultConnectionIdentifier){ conn => conn}) { + APIUtil.getPropsValue("db.driver") openOr("org.h2.Driver") match { + case value if value.contains("org.h2.Driver") => + () => migrationSql(false)//Note: H2 database, do not support the MATERIALIZED view + case _ => + () => migrationSql(true) + } + } + + val endDate = System.currentTimeMillis() + val comment: String = + s"""Executed SQL: + |$executedSql + |""".stripMargin + isSuccessful = true + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + + case false => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + val isSuccessful = false + val endDate = System.currentTimeMillis() + val comment: String = + s"""${ProductFee._dbTableNameLC} table does not exist""".stripMargin + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + } + } + +} \ No newline at end of file diff --git a/obp-api/src/main/scala/code/api/util/migration/MigrationOfFastFireHoseView.scala b/obp-api/src/main/scala/code/api/util/migration/MigrationOfFastFireHoseView.scala new file mode 100644 index 000000000..90c97f55f --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/migration/MigrationOfFastFireHoseView.scala @@ -0,0 +1,102 @@ +package code.api.util.migration + +import java.time.format.DateTimeFormatter +import java.time.{ZoneId, ZonedDateTime} +import code.api.util.APIUtil +import code.api.util.migration.Migration.{DbFunction, saveLog} +import code.productfee.ProductFee +import net.liftweb.common.Full +import net.liftweb.mapper.{DB, Schemifier} +import net.liftweb.util.DefaultConnectionIdentifier + +object MigrationOfFastFireHoseView { + + val oneDayAgo = ZonedDateTime.now(ZoneId.of("UTC")).minusDays(1) + val oneYearInFuture = ZonedDateTime.now(ZoneId.of("UTC")).plusYears(1) + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'") + + def addFastFireHoseView(name: String): Boolean = { + DbFunction.tableExists(ProductFee, (DB.use(DefaultConnectionIdentifier){ conn => conn})) match { + case true => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + var isSuccessful = false + + val executedSql = + DbFunction.maybeWrite(true, Schemifier.infoF _, DB.use(DefaultConnectionIdentifier){ conn => conn}) { + () => + """ + |CREATE VIEW v_fast_firehose_accounts AS select + | mappedbankaccount.theaccountid as account_id, + | mappedbankaccount.bank as bank_id, + | mappedbankaccount.accountlabel as account_label, + | mappedbankaccount.accountnumber as account_number, + | (select + | string_agg( + | 'user_id:' + | || resourceuser.userid_ + | ||',provider:' + | ||resourceuser.provider_ + | ||',user_name:' + | ||resourceuser.name_, + | ',') as owners + | from resourceuser + | where + | resourceuser.id = mapperaccountholders.user_c + | ), + | mappedbankaccount.kind as kind, + | mappedbankaccount.accountcurrency as account_currency , + | mappedbankaccount.accountbalance as account_balance, + | (select + | string_agg( + | 'bank_id:' + | ||bankaccountrouting.bankid + | ||',account_id:' + | ||bankaccountrouting.accountid, + | ',' + | ) as account_routings + | from bankaccountrouting + | where + | bankaccountrouting.accountid = mappedbankaccount.theaccountid + | ), + | (select + | string_agg( + | 'type:' + | || mappedaccountattribute.mtype + | ||',code:' + | ||mappedaccountattribute.mcode + | ||',value:' + | ||mappedaccountattribute.mvalue, + | ',') as account_attributes + | from mappedaccountattribute + | where + | mappedaccountattribute.maccountid = mappedbankaccount.theaccountid + | ) + |from mappedbankaccount + | LEFT JOIN mapperaccountholders + | ON (mappedbankaccount.bank = mapperaccountholders.accountbankpermalink and mappedbankaccount.theaccountid = mapperaccountholders.accountpermalink); + |""".stripMargin + } + + val endDate = System.currentTimeMillis() + val comment: String = + s"""Executed SQL: + |$executedSql + |""".stripMargin + isSuccessful = true + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + + case false => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + val isSuccessful = false + val endDate = System.currentTimeMillis() + val comment: String = + s"""${ProductFee._dbTableNameLC} table does not exist""".stripMargin + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + } + } + +} \ No newline at end of file diff --git a/obp-api/src/main/scala/code/api/util/migration/MigrationOfProductFee.scala b/obp-api/src/main/scala/code/api/util/migration/MigrationOfProductFee.scala new file mode 100644 index 000000000..bbd589050 --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/migration/MigrationOfProductFee.scala @@ -0,0 +1,63 @@ +package code.api.util.migration + +import java.time.format.DateTimeFormatter +import java.time.{ZoneId, ZonedDateTime} +import code.api.util.APIUtil +import code.api.util.migration.Migration.{DbFunction, saveLog} +import code.productfee.ProductFee +import net.liftweb.common.Full +import net.liftweb.mapper.{DB, Schemifier} +import net.liftweb.util.DefaultConnectionIdentifier + +object MigrationOfProductFee { + + val oneDayAgo = ZonedDateTime.now(ZoneId.of("UTC")).minusDays(1) + val oneYearInFuture = ZonedDateTime.now(ZoneId.of("UTC")).plusYears(1) + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'") + + def alterColumnProductFeeName(name: String): Boolean = { + DbFunction.tableExists(ProductFee, (DB.use(DefaultConnectionIdentifier){ conn => conn})) match { + case true => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + var isSuccessful = false + + val executedSql = + DbFunction.maybeWrite(true, Schemifier.infoF _, DB.use(DefaultConnectionIdentifier){ conn => conn}) { + APIUtil.getPropsValue("db.driver") match { + case Full(value) if value.contains("com.microsoft.sqlserver.jdbc.SQLServerDriver") => + () => + """ + |ALTER TABLE ProductFee ALTER COLUMN name varchar(100); + |""".stripMargin + case _ => + () => + """ + |ALTER TABLE ProductFee ALTER COLUMN name type varchar(100); + |""".stripMargin + } + + } + + val endDate = System.currentTimeMillis() + val comment: String = + s"""Executed SQL: + |$executedSql + |""".stripMargin + isSuccessful = true + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + + case false => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + val isSuccessful = false + val endDate = System.currentTimeMillis() + val comment: String = + s"""${ProductFee._dbTableNameLC} table does not exist""".stripMargin + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + } + } + +} diff --git a/obp-api/src/main/scala/code/api/util/migration/MigrationOfUserAuthContextFieldLength.scala b/obp-api/src/main/scala/code/api/util/migration/MigrationOfUserAuthContextFieldLength.scala new file mode 100644 index 000000000..d38aa41a1 --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/migration/MigrationOfUserAuthContextFieldLength.scala @@ -0,0 +1,63 @@ +package code.api.util.migration + +import java.time.format.DateTimeFormatter +import java.time.{ZoneId, ZonedDateTime} +import code.api.util.APIUtil +import code.api.util.migration.Migration.{DbFunction, saveLog} +import code.context.MappedUserAuthContext +import net.liftweb.common.Full +import net.liftweb.mapper.{DB, Schemifier} +import net.liftweb.util.DefaultConnectionIdentifier + +object MigrationOfUserAuthContextFieldLength { + + val oneDayAgo = ZonedDateTime.now(ZoneId.of("UTC")).minusDays(1) + val oneYearInFuture = ZonedDateTime.now(ZoneId.of("UTC")).plusYears(1) + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'") + + def alterColumnKeyAndValueLength(name: String): Boolean = { + DbFunction.tableExists(MappedUserAuthContext, (DB.use(DefaultConnectionIdentifier){ conn => conn})) match { + case true => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + var isSuccessful = false + + val executedSql = + DbFunction.maybeWrite(true, Schemifier.infoF _, DB.use(DefaultConnectionIdentifier){ conn => conn}) { + APIUtil.getPropsValue("db.driver") match { + case Full(value) if value.contains("com.microsoft.sqlserver.jdbc.SQLServerDriver") => + () => + """ + |ALTER TABLE MappedUserAuthContext ALTER COLUMN mKey varchar(4000); + |ALTER TABLE MappedUserAuthContext ALTER COLUMN mValue varchar(4000); + |""".stripMargin + case _ => + () => + """ + |ALTER TABLE MappedUserAuthContext ALTER COLUMN mKey type varchar(4000); + |ALTER TABLE MappedUserAuthContext ALTER COLUMN mValue type varchar(4000); + |""".stripMargin + } + } + + val endDate = System.currentTimeMillis() + val comment: String = + s"""Executed SQL: + |$executedSql + |""".stripMargin + isSuccessful = true + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + + case false => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + val isSuccessful = false + val endDate = System.currentTimeMillis() + val comment: String = + s"""${MappedUserAuthContext._dbTableNameLC} table does not exist""".stripMargin + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + } + } +} \ No newline at end of file diff --git a/obp-api/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/obp-api/src/main/scala/code/api/v1_2_1/APIMethods121.scala index 3e45426b8..2e5adbe69 100644 --- a/obp-api/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/obp-api/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -2401,7 +2401,7 @@ trait APIMethods121 { "getTransactionNarrative", "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/narrative", - "Get narrative", + "Get a Transaction Narrative", """Returns the account owner description of the transaction [moderated](#1_2_1-getViewsForBankAccount) by the view. | |Authentication via OAuth is required if the view is not public.""", @@ -2437,7 +2437,7 @@ trait APIMethods121 { "addTransactionNarrative", "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/narrative", - "Add narrative", + "Add a Transaction Narrative", s"""Creates a description of the transaction TRANSACTION_ID. | |Note: Unlike other items of metadata, there is only one "narrative" per transaction accross all views. @@ -2481,7 +2481,7 @@ trait APIMethods121 { "updateTransactionNarrative", "PUT", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/narrative", - "Update narrative", + "Update a Transaction Narrative", """Updates the description of the transaction TRANSACTION_ID. | |Authentication via OAuth is required if the view is not public.""", @@ -2519,7 +2519,7 @@ trait APIMethods121 { "deleteTransactionNarrative", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/narrative", - "Delete narrative", + "Delete a Transaction Narrative", """Deletes the description of the transaction TRANSACTION_ID. | |Authentication via OAuth is required if the view is not public.""", @@ -2556,7 +2556,7 @@ trait APIMethods121 { "getCommentsForViewOnTransaction", "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/comments", - "Get comments", + "Get Transaction Comments", """Returns the transaction TRANSACTION_ID comments made on a [view](#1_2_1-getViewsForBankAccount) (VIEW_ID). | |Authentication via OAuth is required if the view is not public.""", @@ -2593,7 +2593,7 @@ trait APIMethods121 { "addCommentForViewOnTransaction", "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/comments", - "Add comment", + "Add a Transaction Comment", """Posts a comment about a transaction TRANSACTION_ID on a [view](#1_2_1-getViewsForBankAccount) VIEW_ID. | |${authenticationRequiredMessage(false)} @@ -2639,7 +2639,7 @@ trait APIMethods121 { "deleteCommentForViewOnTransaction", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/comments/COMMENT_ID", - "Delete comment", + "Delete a Transaction Comment", """Delete the comment COMMENT_ID about the transaction TRANSACTION_ID made on [view](#1_2_1-getViewsForBankAccount). | |Authentication via OAuth is required. The user must either have owner privileges for this account, or must be the user that posted the comment.""", @@ -2677,7 +2677,7 @@ trait APIMethods121 { "getTagsForViewOnTransaction", "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/tags", - "Get tags", + "Get Transaction Tags", """Returns the transaction TRANSACTION_ID tags made on a [view](#1_2_1-getViewsForBankAccount) (VIEW_ID). Authentication via OAuth is required if the view is not public.""", emptyObjectJson, @@ -2713,7 +2713,7 @@ trait APIMethods121 { "addTagForViewOnTransaction", "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/tags", - "Add a tag", + "Add a Transaction Tag", s"""Posts a tag about a transaction TRANSACTION_ID on a [view](#1_2_1-getViewsForBankAccount) VIEW_ID. | |${authenticationRequiredMessage(true)} @@ -2759,7 +2759,7 @@ trait APIMethods121 { "deleteTagForViewOnTransaction", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/tags/TAG_ID", - "Delete a tag", + "Delete a Transaction Tag", """Deletes the tag TAG_ID about the transaction TRANSACTION_ID made on [view](#1_2_1-getViewsForBankAccount). |Authentication via OAuth is required. The user must either have owner privileges for this account, |or must be the user that posted the tag. @@ -2796,7 +2796,7 @@ trait APIMethods121 { "getImagesForViewOnTransaction", "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/images", - "Get images", + "Get Transaction Images", """Returns the transaction TRANSACTION_ID images made on a [view](#1_2_1-getViewsForBankAccount) (VIEW_ID). Authentication via OAuth is required if the view is not public.""", emptyObjectJson, @@ -2832,7 +2832,7 @@ trait APIMethods121 { "addImageForViewOnTransaction", "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/images", - "Add an image", + "Add a Transaction Image", s"""Posts an image about a transaction TRANSACTION_ID on a [view](#1_2_1-getViewsForBankAccount) VIEW_ID. | |${authenticationRequiredMessage(true) } @@ -2878,7 +2878,7 @@ trait APIMethods121 { "deleteImageForViewOnTransaction", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/images/IMAGE_ID", - "Delete an image", + "Delete a Transaction Image", """Deletes the image IMAGE_ID about the transaction TRANSACTION_ID made on [view](#1_2_1-getViewsForBankAccount). | |Authentication via OAuth is required. The user must either have owner privileges for this account, or must be the user that posted the image.""", @@ -2919,7 +2919,7 @@ trait APIMethods121 { "getWhereTagForViewOnTransaction", "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/where", - "Get where tag", + "Get a Transaction where Tag", """Returns the "where" Geo tag added to the transaction TRANSACTION_ID made on a [view](#1_2_1-getViewsForBankAccount) (VIEW_ID). |It represents the location where the transaction has been initiated. | @@ -2956,7 +2956,7 @@ trait APIMethods121 { "addWhereTagForViewOnTransaction", "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/where", - "Add where tag", + "Add a Transaction where Tag", s"""Creates a "where" Geo tag on a transaction TRANSACTION_ID in a [view](#1_2_1-getViewsForBankAccount). | |${authenticationRequiredMessage(true)} @@ -3002,7 +3002,7 @@ trait APIMethods121 { "updateWhereTagForViewOnTransaction", "PUT", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/where", - "Update where tag", + "Update a Transaction where Tag", s"""Updates the "where" Geo tag on a transaction TRANSACTION_ID in a [view](#1_2_1-getViewsForBankAccount). | |${authenticationRequiredMessage(true)} @@ -3048,7 +3048,7 @@ trait APIMethods121 { "deleteWhereTagForViewOnTransaction", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/where", - "Delete where tag", + "Delete a Transaction Tag", s"""Deletes the where tag of the transaction TRANSACTION_ID made on [view](#1_2_1-getViewsForBankAccount). | |${authenticationRequiredMessage(true)} diff --git a/obp-api/src/main/scala/code/api/v2_2_0/APIMethods220.scala b/obp-api/src/main/scala/code/api/v2_2_0/APIMethods220.scala index ab31a4e38..8e4969bbe 100644 --- a/obp-api/src/main/scala/code/api/v2_2_0/APIMethods220.scala +++ b/obp-api/src/main/scala/code/api/v2_2_0/APIMethods220.scala @@ -1,7 +1,6 @@ package code.api.v2_2_0 import java.util.Date - import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ import code.api.util.APIUtil._ import code.api.util.ApiRole.{canCreateBranch, _} @@ -14,6 +13,7 @@ import code.api.v2_1_0._ import code.api.v2_2_0.JSONFactory220.transformV220ToBranch import code.bankconnectors._ import code.consumer.Consumers +import code.entitlement.Entitlement import code.fx.{MappedFXRate, fx} import code.metadata.counterparties.{Counterparties, MappedCounterparty} import code.metrics.ConnectorMetricsProvider @@ -455,6 +455,23 @@ trait APIMethods220 { bank.bank_routing.scheme, bank.bank_routing.address ) + entitlements <- Entitlement.entitlement.vend.getEntitlementsByUserId(u.userId) + + entitlementsByBank = entitlements.filter(_.bankId==bank.id) + _ <- entitlementsByBank.filter(_.roleName == CanCreateEntitlementAtOneBank.toString()).size > 0 match { + case true => + // Already has entitlement + Full() + case false => + Full(Entitlement.entitlement.vend.addEntitlement(bank.id, u.userId, CanCreateEntitlementAtOneBank.toString())) + } + _ <- entitlementsByBank.filter(_.roleName == CanReadDynamicResourceDocsAtOneBank.toString()).size > 0 match { + case true => + // Already has entitlement + Full() + case false => + Full(Entitlement.entitlement.vend.addEntitlement(bank.id, u.userId, CanReadDynamicResourceDocsAtOneBank.toString())) + } } yield { val json = JSONFactory220.createBankJSON(success) createdJsonResponse(Extraction.decompose(json)) diff --git a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala index 2fa88b736..a33571670 100644 --- a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala +++ b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala @@ -1450,7 +1450,6 @@ trait APIMethods310 { List( UserNotLoggedIn, UserHasMissingRoles, - CreateUserAuthContextError, UnknownError ), List(apiTagUser, apiTagNewStyle), diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 8283d6434..28507dcdb 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -5,7 +5,7 @@ import code.DynamicEndpoint.DynamicEndpointSwagger import code.accountattribute.AccountAttributeX import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ import code.api.util.APIUtil.{fullBoxOrException, _} -import code.api.util.ApiRole._ +import code.api.util.ApiRole.{canCreateEntitlementAtAnyBank, _} import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ import code.api.util.ExampleValue._ @@ -19,7 +19,7 @@ import code.api.v1_2_1.{JSONFactory, PostTransactionTagJSON} import code.api.v1_4_0.JSONFactory1_4_0 import code.api.v1_4_0.JSONFactory1_4_0.TransactionRequestAccountJsonV140 import code.api.v2_0_0.OBPAPI2_0_0.Implementations2_0_0 -import code.api.v2_0_0.{EntitlementJSONs, JSONFactory200} +import code.api.v2_0_0.{CreateEntitlementJSON, EntitlementJSONs, JSONFactory200} import code.api.v2_1_0._ import code.api.v3_0_0.JSONFactory300 import code.api.v3_1_0._ @@ -41,7 +41,7 @@ import code.endpointMapping.EndpointMappingCommons import code.entitlement.Entitlement import code.metadata.counterparties.{Counterparties, MappedCounterparty} import code.metadata.tags.Tags -import code.model.dataAccess.{AuthUser, BankAccountCreation} +import code.model.dataAccess.{AuthUser, BankAccountCreation, ResourceUser} import code.model.{toUserExtended, _} import code.ratelimiting.RateLimitingDI import code.snippet.{WebUIPlaceholder, WebUITemplate} @@ -51,7 +51,7 @@ import code.transactionrequests.TransactionRequests.TransactionChallengeTypes._ import code.transactionrequests.TransactionRequests.TransactionRequestTypes import code.transactionrequests.TransactionRequests.TransactionRequestTypes.{apply => _, _} import code.userlocks.UserLocksProvider -import code.users.Users +import code.users.{UserAgreement, Users} import code.util.Helper.booleanToFuture import code.util.{Helper, JsonSchemaUtil} import code.validation.JsonValidation @@ -73,14 +73,17 @@ import net.liftweb.json.JsonDSL._ import net.liftweb.json.Serialization.write import net.liftweb.json.{compactRender, prettyRender, _} import net.liftweb.mapper.By -import net.liftweb.util.Helpers.now +import net.liftweb.util.Helpers.{now, tryo} import net.liftweb.util.Mailer.{From, PlainMailBodyType, Subject, To, XHTMLMailBodyType} import net.liftweb.util.{Helpers, Mailer, StringHelpers} import org.apache.commons.collections4.CollectionUtils import org.apache.commons.lang3.StringUtils - import java.net.URLEncoder +import java.text.SimpleDateFormat import java.util.{Calendar, Date} + +import code.api.util.Glossary.getGlossaryItem + import scala.collection.immutable.{List, Nil} import scala.collection.mutable.ArrayBuffer import scala.concurrent.Future @@ -274,7 +277,7 @@ trait APIMethods400 { |* Website""", EmptyBody, bankJson400, - List(UserNotLoggedIn, UnknownError, BankNotFound), + List(UnknownError, BankNotFound), apiTagBank :: apiTagPSD2AIS :: apiTagPsd2 :: apiTagNewStyle :: Nil ) @@ -1179,19 +1182,32 @@ trait APIMethods400 { |4) `answer` : must be `123` in case that Strong Customer Authentication method for OTP challenge is dummy. | For instance: SANDBOX_TAN_OTP_INSTRUCTION_TRANSPORT=dummy | Possible values are dummy,email and sms - | In kafka mode, the answer can be got by phone message or other security ways. + | In kafka mode, the answer can be got by phone message or other SCA methods. | - |In case 1 person needs to answer security challenge we have next flow of state of an `transaction request`: + |Note that each Transaction Request Type can have its own OTP_INSTRUCTION_TRANSPORT method. + |OTP_INSTRUCTION_TRANSPORT methods are set in Props. See sample.props.template for instructions. + | + |Single or Multiple authorisations + | + |OBP allows single or multi party authorisations. + | + |Single party authorisation: + | + |In the case that only one person needs to authorise i.e. answer a security challenge we have the following change of state of a `transaction request`: | INITIATED => COMPLETED - |In case n persons needs to answer security challenge we have next flow of state of an `transaction request`: + | + | + |Multiparty authorisation: + | + |In the case that multiple parties (n persons) need to authorise a transaction request i.e. answer security challenges, we have the followings state flow for a `transaction request`: | INITIATED => NEXT_CHALLENGE_PENDING => ... => NEXT_CHALLENGE_PENDING => COMPLETED | - |The security challenge is bound to a user i.e. in case of right answer and the user is different than expected one the challenge will fail. + |The security challenge is bound to a user i.e. in the case of a correct answer but the user is different than expected the challenge will fail. | |Rule for calculating number of security challenges: - |If product Account attribute REQUIRED_CHALLENGE_ANSWERS=N then create N challenges + |If Product Account attribute REQUIRED_CHALLENGE_ANSWERS=N then create N challenges |(one for every user that has a View where permission "can_add_transaction_request_to_any_account"=true) - |In case REQUIRED_CHALLENGE_ANSWERS is not defined as an account attribute default value is 1. + |In the case REQUIRED_CHALLENGE_ANSWERS is not defined as an account attribute, the default number of security challenges created is one. | """.stripMargin, challengeAnswerJson400, @@ -1803,15 +1819,15 @@ trait APIMethods400 { | |${authenticationRequiredMessage(true)} | - |Create one DynamicEntity, after created success, the corresponding CRUD endpoints will be generated automatically + |Create a DynamicEntity. If creation is successful, the corresponding POST, GET, PUT and DELETE (Create, Read, Update, Delete or CRUD for short) endpoints will be generated automatically | - |Current support field types as follow: + |The following field types are as supported: |${DynamicEntityFieldType.values.map(_.toString).mkString("[", ", ", ", reference]")} | - |${DynamicEntityFieldType.DATE_WITH_DAY} format: ${DynamicEntityFieldType.DATE_WITH_DAY.dateFormat} + |The ${DynamicEntityFieldType.DATE_WITH_DAY} format is: ${DynamicEntityFieldType.DATE_WITH_DAY.dateFormat} | - |Value of reference type is corresponding ids, please look at the following examples. - |Current supporting reference types and corresponding examples as follow: + |Reference types are like foreign keys and composite foreign keys are supported. The value you need to supply as the (composite) foreign key is a UUID (or several UUIDs in the case of a composite key) that match value in another Entity.. + |See the following list of currently available reference types and examples of how to construct key values correctly. Note: As more Dynamic Entities are created on this instance, this list will grow: |``` |${ReferenceType.referenceTypeAndExample.mkString("\n")} |``` @@ -1849,15 +1865,15 @@ trait APIMethods400 { | |${authenticationRequiredMessage(true)} | - |Create one DynamicEntity, after created success, the corresponding CRUD endpoints will be generated automatically + |Create a DynamicEntity. If creation is successful, the corresponding POST, GET, PUT and DELETE (Create, Read, Update, Delete or CRUD for short) endpoints will be generated automatically | - |Current support field types as follow: + |The following field types are as supported: |${DynamicEntityFieldType.values.map(_.toString).mkString("[", ", ", ", reference]")} | - |${DynamicEntityFieldType.DATE_WITH_DAY} format: ${DynamicEntityFieldType.DATE_WITH_DAY.dateFormat} + |The ${DynamicEntityFieldType.DATE_WITH_DAY} format is: ${DynamicEntityFieldType.DATE_WITH_DAY.dateFormat} | - |Value of reference type is corresponding ids, please look at the following examples. - |Current supporting reference types and corresponding examples as follow: + |Reference types are like foreign keys and composite foreign keys are supported. The value you need to supply as the (composite) foreign key is a UUID (or several UUIDs in the case of a composite key) that match value in another Entity.. + |The following list shows all the possible reference types in the system with corresponding examples values so you can see how to construct each reference type value. |``` |${ReferenceType.referenceTypeAndExample.mkString("\n")} |``` @@ -1919,13 +1935,13 @@ trait APIMethods400 { | |Update one DynamicEntity, after update finished, the corresponding CRUD endpoints will be changed. | - |Current support field types as follow: + |The following field types are as supported: |${DynamicEntityFieldType.values.map(_.toString).mkString("[", ", ", ", reference]")} | |${DynamicEntityFieldType.DATE_WITH_DAY} format: ${DynamicEntityFieldType.DATE_WITH_DAY.dateFormat} | - |Value of reference type is corresponding ids, please look at the following examples. - |Current supporting reference types and corresponding examples as follow: + |Reference types are like foreign keys and composite foreign keys are supported. The value you need to supply as the (composite) foreign key is a UUID (or several UUIDs in the case of a composite key) that match value in another Entity.. + |The following list shows all the possible reference types in the system with corresponding examples values so you can see how to construct each reference type value. |``` |${ReferenceType.referenceTypeAndExample.mkString("\n")} |``` @@ -1961,13 +1977,13 @@ trait APIMethods400 { | |Update one DynamicEntity, after update finished, the corresponding CRUD endpoints will be changed. | - |Current support field types as follow: + |The following field types are as supported: |${DynamicEntityFieldType.values.map(_.toString).mkString("[", ", ", ", reference]")} | |${DynamicEntityFieldType.DATE_WITH_DAY} format: ${DynamicEntityFieldType.DATE_WITH_DAY.dateFormat} | - |Value of reference type is corresponding ids, please look at the following examples. - |Current supporting reference types and corresponding examples as follow: + |Reference types are like foreign keys and composite foreign keys are supported. The value you need to supply as the (composite) foreign key is a UUID (or several UUIDs in the case of a composite key) that match value in another Entity.. + |The following list shows all the possible reference types in the system with corresponding examples values so you can see how to construct each reference type value. |``` |${ReferenceType.referenceTypeAndExample.mkString("\n")} |``` @@ -2110,8 +2126,8 @@ trait APIMethods400 { | |${DynamicEntityFieldType.DATE_WITH_DAY} format: ${DynamicEntityFieldType.DATE_WITH_DAY.dateFormat} | - |Value of reference type is corresponding ids, please look at the following examples. - |Current supporting reference types and corresponding examples as follow: + |Reference types are like foreign keys and composite foreign keys are supported. The value you need to supply as the (composite) foreign key is a UUID (or several UUIDs in the case of a composite key) that match value in another Entity.. + |The following list shows all the possible reference types in the system with corresponding examples values so you can see how to construct each reference type value. |``` |${ReferenceType.referenceTypeAndExample.mkString("\n")} |``` @@ -2721,7 +2737,99 @@ trait APIMethods400 { } } } - + staticResourceDocs += ResourceDoc( + createUserWithRoles, + implementedInApiVersion, + nameOf(createUserWithRoles), + "POST", + "/user-entitlements", + "Create (DAuth) User with Roles", + s""" + |This endpoint is used as part of the DAuth solution to grant Entitlements for Roles to a smart contract on the blockchain. + | + |Put the smart contract address in username + | + |For provider use "dauth" + | + |This endpoint will create the User with username and provider if the User does not already exist. + | + |Then it will create Entitlements i.e. grant Roles to the User. + | + |Entitlements are used to grant System or Bank level roles to Users. (For Account level privileges, see Views) + | + |i.e. Entitlements are used to create / consume system or bank level resources where as views / account access are used to consume / create customer level resources. + | + |For a System level Role (.e.g CanGetAnyUser), set bank_id to an empty string i.e. "bank_id":"" + | + |For a Bank level Role (e.g. CanCreateAccount), set bank_id to a valid value e.g. "bank_id":"my-bank-id" + | + |Note: The Roles actually granted will depend on the Roles that the calling user has. + | + |If you try to grant Entitlements to a user that already exist (duplicate entitilements) you will get an error. + | + |For information about DAuth see below: + | + |${getGlossaryItem("DAuth")} + | + |""", + postCreateUserWithRolesJsonV400, + entitlementsJsonV400, + List( + UserNotLoggedIn, + InvalidJsonFormat, + IncorrectRoleName, + EntitlementIsBankRole, + EntitlementIsSystemRole, + EntitlementAlreadyExists, + InvalidUserProvider, + UnknownError + ), + List(apiTagRole, apiTagEntitlement, apiTagUser, apiTagNewStyle, apiTagDAuth)) + + lazy val createUserWithRoles: OBPEndpoint = { + case "user-entitlements" :: Nil JsonPost json -> _ => { + cc => + for { + (Full(loggedInUser), callContext) <- authenticatedAccess(cc) + failMsg = s"$InvalidJsonFormat The Json body should be the $PostCreateUserWithRolesJsonV400 " + postedData <- NewStyle.function.tryons(failMsg, 400, callContext) { + json.extract[PostCreateUserWithRolesJsonV400] + } + + //provider must start with dauth., can not create other provider users. + _ <- Helper.booleanToFuture(s"$InvalidUserProvider The user.provider must be start with 'dauth.'", cc=Some(cc)) { + postedData.provider.startsWith("dauth.") + } + + //check the system role bankId is Empty, but bank level role need bankId + _ <- checkRoleBankIdMappings(callContext, postedData) + + _ <- checkRolesBankIdExsiting(callContext, postedData) + + _ <- checkRolesName(callContext, postedData) + + canCreateEntitlementAtAnyBankRole = Entitlement.entitlement.vend.getEntitlement("", loggedInUser.userId, canCreateEntitlementAtAnyBank.toString()) + + (targetUser, callContext) <- NewStyle.function.getOrCreateResourceUser(postedData.username, postedData.provider, callContext) + + _ <- if (canCreateEntitlementAtAnyBankRole.isDefined) { + //If the loggedIn User has `CanCreateEntitlementAtAnyBankRole` role, then we can grant all the requestRoles to the requestUser. + //But we must check if the requestUser already has the requestRoles or not. + assertTargetUserLacksRoles(targetUser.userId, postedData.roles,callContext) + } else { + //If the loggedIn user does not have the `CanCreateEntitlementAtAnyBankRole` role, we can only grant the roles which the loggedIn user have. + //So we need to check if the requestRoles are beyond the current loggedIn user has. + assertUserCanGrantRoles(loggedInUser.userId, postedData.roles, callContext) + } + + addedEntitlements <- addEntitlementsToUser(targetUser.userId, postedData, callContext) + + } yield { + (JSONFactory400.createEntitlementJSONs(addedEntitlements), HttpCode.`201`(callContext)) + } + } + } + staticResourceDocs += ResourceDoc( getEntitlements, @@ -2735,7 +2843,7 @@ trait APIMethods400 { | """.stripMargin, EmptyBody, - entitlementJSONs, + entitlementsJsonV400, List($UserNotLoggedIn, UserHasMissingRoles, UnknownError), List(apiTagRole, apiTagEntitlement, apiTagUser, apiTagNewStyle), Some(List(canGetEntitlementsForAnyUserAtAnyBank))) @@ -2772,7 +2880,7 @@ trait APIMethods400 { | """.stripMargin, EmptyBody, - entitlementJSONs, + entitlementsJsonV400, List($UserNotLoggedIn, UserHasMissingRoles, UnknownError), List(apiTagRole, apiTagEntitlement, apiTagUser, apiTagNewStyle), Some(List(canGetEntitlementsForOneBank,canGetEntitlementsForAnyBank))) @@ -3302,6 +3410,49 @@ trait APIMethods400 { } } + staticResourceDocs += ResourceDoc( + getFastFirehoseAccountsAtOneBank, + implementedInApiVersion, + nameOf(getFastFirehoseAccountsAtOneBank), + "GET", + "/management/banks/BANK_ID/fast-firehose/accounts", + "Get Fast Firehose Accounts at Bank", + s""" + | + |This endpoint allows bulk access to accounts. + | + |optional pagination parameters for filter with accounts + |${urlParametersDocument(true, false)} + | + |${authenticationRequiredMessage(true)} + | + |""".stripMargin, + EmptyBody, + fastFirehoseAccountsJsonV400, + List($BankNotFound), + List(apiTagAccount, apiTagAccountFirehose, apiTagFirehoseData, apiTagNewStyle), + Some(List(canUseAccountFirehoseAtAnyBank, ApiRole.canUseAccountFirehose)) + ) + + lazy val getFastFirehoseAccountsAtOneBank : OBPEndpoint = { + //get private accounts for all banks + case "management":: "banks" :: BankId(bankId):: "fast-firehose" :: "accounts" :: Nil JsonGet req => { + cc => + for { + (Full(u), bank, callContext) <- SS.userBank + _ <- Helper.booleanToFuture(failMsg = AccountFirehoseNotAllowedOnThisInstance, cc=cc.callContext) { + allowAccountFirehose + } + allowedParams = List("limit", "offset", "sort_direction") + httpParams <- NewStyle.function.extractHttpParamsFromUrl(cc.url) + obpQueryParams <- NewStyle.function.createObpParams(httpParams, allowedParams, callContext) + (firehoseAccounts, callContext) <- NewStyle.function.getBankAccountsWithAttributes(bankId, obpQueryParams, cc.callContext) + } yield { + (JSONFactory400.createFirehoseBankAccountJSON(firehoseAccounts), HttpCode.`200`(callContext)) + } + } + } + staticResourceDocs += ResourceDoc( getCustomersByCustomerPhoneNumber, @@ -3384,7 +3535,7 @@ trait APIMethods400 { | """.stripMargin, EmptyBody, - usersJsonV200, + userJsonWithAgreementsV400, List(UserNotLoggedIn, UserHasMissingRoles, UserNotFoundById, UnknownError), List(apiTagUser, apiTagNewStyle), Some(List(canGetAnyUser))) @@ -3394,14 +3545,125 @@ trait APIMethods400 { case "users" :: "user_id" :: userId :: Nil JsonGet _ => { cc => for { - (Full(u), callContext) <- authenticatedAccess(cc) - _ <- NewStyle.function.hasEntitlement("", u.userId, ApiRole.canGetAnyUser, callContext) user <- Users.users.vend.getUserByUserIdFuture(userId) map { - x => unboxFullOrFail(x, callContext, s"$UserNotFoundByUserId Current UserId($userId)") + x => unboxFullOrFail(x, cc.callContext, s"$UserNotFoundByUserId Current UserId($userId)") } - entitlements <- NewStyle.function.getEntitlementsByUserId(user.userId, callContext) + entitlements <- NewStyle.function.getEntitlementsByUserId(user.userId, cc.callContext) + acceptMarketingInfo <- NewStyle.function.getAgreementByUserId(user.userId, "accept_marketing_info", cc.callContext) + termsAndConditions <- NewStyle.function.getAgreementByUserId(user.userId, "terms_and_conditions", cc.callContext) + privacyConditions <- NewStyle.function.getAgreementByUserId(user.userId, "privacy_conditions", cc.callContext) } yield { - (JSONFactory400.createUserInfoJSON(user, entitlements), HttpCode.`200`(callContext)) + val agreements = acceptMarketingInfo.toList ::: termsAndConditions.toList ::: privacyConditions.toList + (JSONFactory400.createUserInfoJSON(user, entitlements, Some(agreements)), HttpCode.`200`(cc.callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + getUserByUsername, + implementedInApiVersion, + nameOf(getUserByUsername), + "GET", + "/users/username/USERNAME", + "Get User by USERNAME", + s"""Get user by USERNAME + | + |${authenticationRequiredMessage(true)} + | + |CanGetAnyUser entitlement is required, + | + """.stripMargin, + emptyObjectJson, + userJsonV400, + List($UserNotLoggedIn, UserHasMissingRoles, UserNotFoundByUsername, UnknownError), + List(apiTagUser, apiTagNewStyle), + Some(List(canGetAnyUser))) + + + lazy val getUserByUsername: OBPEndpoint = { + case "users" :: "username" :: username :: Nil JsonGet _ => { + cc => + for { + user <- Users.users.vend.getUserByUserNameFuture(username) map { + x => unboxFullOrFail(x, cc.callContext, UserNotFoundByUsername, 404) + } + entitlements <- NewStyle.function.getEntitlementsByUserId(user.userId, cc.callContext) + } yield { + (JSONFactory400.createUserInfoJSON(user, entitlements, None), HttpCode.`200`(cc.callContext)) + } + } + } + + + staticResourceDocs += ResourceDoc( + getUsersByEmail, + implementedInApiVersion, + nameOf(getUsersByEmail), + "GET", + "/users/email/EMAIL/terminator", + "Get Users by Email Address", + s"""Get users by email address + | + |${authenticationRequiredMessage(true)} + |CanGetAnyUser entitlement is required, + | + """.stripMargin, + emptyObjectJson, + usersJsonV400, + List($UserNotLoggedIn, UserHasMissingRoles, UserNotFoundByEmail, UnknownError), + List(apiTagUser, apiTagNewStyle), + Some(List(canGetAnyUser))) + + + lazy val getUsersByEmail: OBPEndpoint = { + case "users" :: "email" :: email :: "terminator" :: Nil JsonGet _ => { + cc => + for { + users <- Users.users.vend.getUsersByEmail(email) + } yield { + (JSONFactory400.createUsersJson(users), HttpCode.`200`(cc.callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + getUsers, + implementedInApiVersion, + nameOf(getUsers), + "GET", + "/users", + "Get all Users", + s"""Get all users + | + |${authenticationRequiredMessage(true)} + | + |CanGetAnyUser entitlement is required, + | + |${urlParametersDocument(false, false)} + |* locked_status (if null ignore) + | + """.stripMargin, + emptyObjectJson, + usersJsonV400, + List( + $UserNotLoggedIn, + UserHasMissingRoles, + UnknownError + ), + List(apiTagUser, apiTagNewStyle), + Some(List(canGetAnyUser))) + + lazy val getUsers: OBPEndpoint = { + case "users" :: Nil JsonGet _ => { + cc => + for { + httpParams <- NewStyle.function.extractHttpParamsFromUrl(cc.url) + obpQueryParams <- createQueriesByHttpParamsFuture(httpParams) map { + x => unboxFullOrFail(x, cc.callContext, InvalidFilterParameterFormat) + } + users <- Users.users.vend.getUsers(obpQueryParams) + } yield { + (JSONFactory400.createUsersJson(users), HttpCode.`200`(cc.callContext)) } } } @@ -3680,6 +3942,13 @@ trait APIMethods400 { case false => Future(Entitlement.entitlement.vend.addEntitlement(bank.id, cc.userId, CanCreateEntitlementAtOneBank.toString())) } + _ <- entitlementsByBank.filter(_.roleName == CanReadDynamicResourceDocsAtOneBank.toString()).size > 0 match { + case true => + // Already has entitlement + Future() + case false => + Future(Entitlement.entitlement.vend.addEntitlement(bank.id, cc.userId, CanReadDynamicResourceDocsAtOneBank.toString())) + } } yield { (JSONFactory400.createBankJSON400(success), HttpCode.`201`(callContext)) } @@ -3986,14 +4255,8 @@ trait APIMethods400 { } _ <- NewStyle.function.canGrantAccessToView(bankId, accountId, cc.loggedInUser, cc.callContext) (user, callContext) <- NewStyle.function.findByUserId(postJson.user_id, cc.callContext) - view <- postJson.view.is_system match { - case true => NewStyle.function.systemView(ViewId(postJson.view.view_id), callContext) - case false => NewStyle.function.customView(ViewId(postJson.view.view_id), BankIdAccountId(bankId, accountId), callContext) - } - addedView <- postJson.view.is_system match { - case true => NewStyle.function.grantAccessToSystemView(bankId, accountId, view, user, callContext) - case false => NewStyle.function.grantAccessToCustomView(view, user, callContext) - } + view <- getView(bankId, accountId, postJson.view, callContext) + addedView <- createAccountAccessToUser(bankId, accountId, user, view, callContext) } yield { val viewJson = JSONFactory300.createViewJSON(addedView) (viewJson, HttpCode.`201`(callContext)) @@ -4001,6 +4264,64 @@ trait APIMethods400 { } } + staticResourceDocs += ResourceDoc( + createUserWithAccountAccess, + implementedInApiVersion, + nameOf(createUserWithAccountAccess), + "POST", + "/banks/BANK_ID/accounts/ACCOUNT_ID/user-account-access", + "Create (DAuth) User with Account Access", + s"""This endpoint is used as part of the DAuth solution to grant access to account and transaction data to a smart contract on the blockchain. + | + |Put the smart contract address in username + | + |For provider use "dauth" + | + |This endpoint will create the (DAuth) User with username and provider if the User does not already exist. + | + |${authenticationRequiredMessage(true)} and the logged in user needs to be account holder. + | + |For information about DAuth see below: + | + |${getGlossaryItem("DAuth")} + | + |""", + postCreateUserAccountAccessJsonV400, + List(viewJsonV300), + List( + $UserNotLoggedIn, + UserMissOwnerViewOrNotAccountHolder, + InvalidJsonFormat, + SystemViewNotFound, + ViewNotFound, + CannotGrantAccountAccess, + UnknownError + ), + List(apiTagAccountAccess, apiTagView, apiTagAccount, apiTagUser, apiTagOwnerRequired, apiTagDAuth, apiTagNewStyle)) + + lazy val createUserWithAccountAccess : OBPEndpoint = { + case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "user-account-access" :: Nil JsonPost json -> _ => { + cc => + val failMsg = s"$InvalidJsonFormat The Json body should be the $PostCreateUserAccountAccessJsonV400 " + for { + postJson <- NewStyle.function.tryons(failMsg, 400, cc.callContext) { + json.extract[PostCreateUserAccountAccessJsonV400] + } + //provider must start with dauth., can not create other provider users. + _ <- Helper.booleanToFuture(s"$InvalidUserProvider The user.provider must be start with 'dauth.'", cc=Some(cc)) { + postJson.provider.startsWith("dauth.") + } + + _ <- NewStyle.function.canGrantAccessToView(bankId, accountId, cc.loggedInUser, cc.callContext) + (targetUser, callContext) <- NewStyle.function.getOrCreateResourceUser(postJson.username, postJson.provider, cc.callContext) + views <- getViews(bankId, accountId, postJson, callContext) + addedView <- createAccountAccessesToUser(bankId, accountId, targetUser, views, callContext) + } yield { + val viewsJson = addedView.map(JSONFactory300.createViewJSON(_)) + (viewsJson, HttpCode.`201`(callContext)) + } + } + } staticResourceDocs += ResourceDoc( revokeUserAccessToView, @@ -4547,6 +4868,113 @@ trait APIMethods400 { } } + staticResourceDocs += ResourceDoc( + createHistoricalTransactionAtBank, + implementedInApiVersion, + nameOf(createHistoricalTransactionAtBank), + "POST", + "/banks/BANK_ID/management/historical/transactions", + "Create Historical Transactions ", + s""" + |Create historical transactions at one Bank + | + |Use this endpoint to create transactions between any two accounts at the same bank. + |From account and to account must be at the same bank. + |Example: + |{ + | "from_account_id": "1ca8a7e4-6d02-48e3-a029-0b2bf89de9f0", + | "to_account_id": "2ca8a7e4-6d02-48e3-a029-0b2bf89de9f0", + | "value": { + | "currency": "GBP", + | "amount": "10" + | }, + | "description": "this is for work", + | "posted": "2017-09-19T02:31:05Z", + | "completed": "2017-09-19T02:31:05Z", + | "type": "SANDBOX_TAN", + | "charge_policy": "SHARED" + |} + | + |This call is experimental. + """.stripMargin, + postHistoricalTransactionAtBankJson, + postHistoricalTransactionResponseJson, + List( + InvalidJsonFormat, + BankNotFound, + AccountNotFound, + CounterpartyNotFoundByCounterpartyId, + InvalidNumber, + NotPositiveAmount, + InvalidTransactionRequestCurrency, + UnknownError + ), + List(apiTagTransactionRequest, apiTagNewStyle), + Some(List(canCreateHistoricalTransactionAtBank)) + ) + + + lazy val createHistoricalTransactionAtBank : OBPEndpoint = { + case "banks" :: BankId(bankId) :: "management" :: "historical" :: "transactions" :: Nil JsonPost json -> _ => { + cc => + for { + (Full(u), callContext) <- authenticatedAccess(cc) + _ <- NewStyle.function.hasEntitlement(bankId.value, u.userId, ApiRole.canCreateHistoricalTransactionAtBank, callContext) + + // Check the input JSON format, here is just check the common parts of all four types + transDetailsJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostHistoricalTransactionJson ", 400, callContext) { + json.extract[PostHistoricalTransactionAtBankJson] + } + (fromAccount, callContext) <- NewStyle.function.checkBankAccountExists(bankId, AccountId(transDetailsJson.from_account_id), callContext) + (toAccount, callContext) <- NewStyle.function.checkBankAccountExists(bankId, AccountId(transDetailsJson.to_account_id), callContext) + amountNumber <- NewStyle.function.tryons(s"$InvalidNumber Current input is ${transDetailsJson.value.amount} ", 400, callContext) { + BigDecimal(transDetailsJson.value.amount) + } + _ <- Helper.booleanToFuture(s"${NotPositiveAmount} Current input is: '${amountNumber}'", cc=callContext) { + amountNumber > BigDecimal("0") + } + posted <- NewStyle.function.tryons(s"$InvalidDateFormat Current `posted` field is ${transDetailsJson.posted}. Please use this format ${DateWithSecondsFormat.toPattern}! ", 400, callContext) { + new SimpleDateFormat(DateWithSeconds).parse(transDetailsJson.posted) + } + completed <- NewStyle.function.tryons(s"$InvalidDateFormat Current `completed` field is ${transDetailsJson.completed}. Please use this format ${DateWithSecondsFormat.toPattern}! ", 400, callContext) { + new SimpleDateFormat(DateWithSeconds).parse(transDetailsJson.completed) + } + // Prevent default value for transaction request type (at least). + _ <- Helper.booleanToFuture(s"${InvalidISOCurrencyCode} Current input is: '${transDetailsJson.value.currency}'", cc=callContext) { + isValidCurrencyISOCode(transDetailsJson.value.currency) + } + amountOfMoneyJson = AmountOfMoneyJsonV121(transDetailsJson.value.currency, transDetailsJson.value.amount) + chargePolicy = transDetailsJson.charge_policy + //There is no constraint for the type at the moment + transactionType = transDetailsJson.`type` + (transactionId, callContext) <- NewStyle.function.makeHistoricalPayment( + fromAccount, + toAccount, + posted, + completed, + amountNumber, + transDetailsJson.value.currency, + transDetailsJson.description, + transactionType, + chargePolicy, + callContext + ) + } yield { + (JSONFactory400.createPostHistoricalTransactionResponseJson( + bankId, + transactionId, + fromAccount.accountId, + toAccount.accountId, + value= amountOfMoneyJson, + description = transDetailsJson.description, + posted, + completed, + transactionRequestType = transactionType, + chargePolicy =transDetailsJson.charge_policy), HttpCode.`201`(callContext)) + } + } + } + staticResourceDocs += ResourceDoc( getTransactionRequest, @@ -7656,6 +8084,7 @@ trait APIMethods400 { cc.userId, postJson.api_collection_name, postJson.is_sharable, + postJson.description.getOrElse(""), Some(cc) ) } yield { @@ -7926,6 +8355,10 @@ trait APIMethods400 { postJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostApiCollectionEndpointJson400", 400, cc.callContext) { json.extract[PostApiCollectionEndpointJson400] } + _ <- Helper.booleanToFuture(failMsg = s"$InvalidOperationId Current OPERATION_ID(${postJson.operation_id})", cc=Some(cc)) { + (DynamicEntityHelper.doc ++ DynamicEndpointHelper.doc ++ DynamicEndpoints.dynamicResourceDocs ++ OBPAPI4_0_0.allResourceDocs).toList + .find(_.operationId==postJson.operation_id).isDefined + } (apiCollection, callContext) <- NewStyle.function.getApiCollectionByUserIdAndCollectionName(cc.userId, apiCollectionName, Some(cc)) apiCollectionEndpoint <- Future{MappedApiCollectionEndpointsProvider.getApiCollectionEndpointByApiCollectionIdAndOperationId(apiCollection.apiCollectionId, postJson.operation_id)} _ <- Helper.booleanToFuture(failMsg = s"$ApiCollectionEndpointAlreadyExisting Current OPERATION_ID(${postJson.operation_id}) is already in API_COLLECTION_NAME($apiCollectionName) ", cc=callContext) { @@ -7941,6 +8374,56 @@ trait APIMethods400 { } } } + staticResourceDocs += ResourceDoc( + createMyApiCollectionEndpointById, + implementedInApiVersion, + nameOf(createMyApiCollectionEndpointById), + "POST", + "/my/api-collection-ids/API_COLLECTION_ID/api-collection-endpoints", + "Create My Api Collection Endpoint By Id", + s"""Create Api Collection Endpoint By Id. + | + |${Glossary.getGlossaryItem("API Collections")} + | + |${authenticationRequiredMessage(true)} + | + |""".stripMargin, + postApiCollectionEndpointJson400, + apiCollectionEndpointJson400, + List( + $UserNotLoggedIn, + InvalidJsonFormat, + UnknownError + ), + List(apiTagApiCollection, apiTagNewStyle) + ) + + lazy val createMyApiCollectionEndpointById: OBPEndpoint = { + case "my" :: "api-collection-ids" :: apiCollectioId :: "api-collection-endpoints" :: Nil JsonPost json -> _ => { + cc => + for { + postJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostApiCollectionEndpointJson400", 400, cc.callContext) { + json.extract[PostApiCollectionEndpointJson400] + } + _ <- Helper.booleanToFuture(failMsg = s"$InvalidOperationId Current OPERATION_ID(${postJson.operation_id})", cc=Some(cc)) { + (DynamicEntityHelper.doc ++ DynamicEndpointHelper.doc ++ DynamicEndpoints.dynamicResourceDocs ++ OBPAPI4_0_0.allResourceDocs).toList + .find(_.operationId==postJson.operation_id).isDefined + } + (apiCollection, callContext) <- NewStyle.function.getApiCollectionById(apiCollectioId, Some(cc)) + apiCollectionEndpoint <- Future{MappedApiCollectionEndpointsProvider.getApiCollectionEndpointByApiCollectionIdAndOperationId(apiCollection.apiCollectionId, postJson.operation_id)} + _ <- Helper.booleanToFuture(failMsg = s"$ApiCollectionEndpointAlreadyExisting Current OPERATION_ID(${postJson.operation_id}) is already in API_COLLECTION_ID($apiCollectioId) ", cc=callContext) { + apiCollectionEndpoint.isEmpty + } + (apiCollectionEndpoint, callContext) <- NewStyle.function.createApiCollectionEndpoint( + apiCollection.apiCollectionId, + postJson.operation_id, + callContext + ) + } yield { + (JSONFactory400.createApiCollectionEndpointJsonV400(apiCollectionEndpoint), HttpCode.`201`(callContext)) + } + } + } staticResourceDocs += ResourceDoc( getMyApiCollectionEndpoint, @@ -8041,6 +8524,39 @@ trait APIMethods400 { (JSONFactory400.createApiCollectionEndpointsJsonV400(apiCollectionEndpoints), HttpCode.`200`(callContext)) } } + } + + staticResourceDocs += ResourceDoc( + getMyApiCollectionEndpointsById, + implementedInApiVersion, + nameOf(getMyApiCollectionEndpointsById), + "GET", + "/my/api-collection-ids/API_COLLECTION_ID/api-collection-endpoints", + "Get My Api Collection Endpoints By Id", + s"""Get Api Collection Endpoints By API_COLLECTION_ID. + | + |${authenticationRequiredMessage(true)} + |""".stripMargin, + EmptyBody, + apiCollectionEndpointsJson400, + List( + $UserNotLoggedIn, + UserNotFoundByUserId, + UnknownError + ), + List(apiTagApiCollection, apiTagNewStyle) + ) + + lazy val getMyApiCollectionEndpointsById: OBPEndpoint = { + case "my" :: "api-collection-ids" :: apiCollectionId :: "api-collection-endpoints":: Nil JsonGet _ => { + cc => + for { + (apiCollection, callContext) <- NewStyle.function.getApiCollectionById(apiCollectionId, Some(cc) ) + (apiCollectionEndpoints, callContext) <- NewStyle.function.getApiCollectionEndpoints(apiCollection.apiCollectionId, callContext) + } yield { + (JSONFactory400.createApiCollectionEndpointsJsonV400(apiCollectionEndpoints), HttpCode.`200`(callContext)) + } + } } staticResourceDocs += ResourceDoc( @@ -8053,7 +8569,7 @@ trait APIMethods400 { s"""${Glossary.getGlossaryItem("API Collections")} | | - |Delete Api Collection Endpoint By Id + |Delete Api Collection Endpoint By OPERATION_ID | |${authenticationRequiredMessage(true)} | @@ -8081,6 +8597,80 @@ trait APIMethods400 { } } + staticResourceDocs += ResourceDoc( + deleteMyApiCollectionEndpointByOperationId, + implementedInApiVersion, + nameOf(deleteMyApiCollectionEndpointByOperationId), + "DELETE", + "/my/api-collection-ids/API_COLLECTION_ID/api-collection-endpoints/OPERATION_ID", + "Delete My Api Collection Endpoint By Id", + s"""${Glossary.getGlossaryItem("API Collections")} + | + |Delete Api Collection Endpoint By OPERATION_ID + | + |${authenticationRequiredMessage(true)} + | + |""", + EmptyBody, + Full(true), + List( + $UserNotLoggedIn, + UserNotFoundByUserId, + UnknownError + ), + List(apiTagApiCollection, apiTagNewStyle) + ) + + lazy val deleteMyApiCollectionEndpointByOperationId : OBPEndpoint = { + case "my" :: "api-collection-ids" :: apiCollectionId :: "api-collection-endpoints" :: operationId :: Nil JsonDelete _ => { + cc => + for { + (apiCollection, callContext) <- NewStyle.function.getApiCollectionById(apiCollectionId, Some(cc) ) + (apiCollectionEndpoint, callContext) <- NewStyle.function.getApiCollectionEndpointByApiCollectionIdAndOperationId(apiCollection.apiCollectionId, operationId, callContext) + (deleted, callContext) <- NewStyle.function.deleteApiCollectionEndpointById(apiCollectionEndpoint.apiCollectionEndpointId, callContext) + } yield { + (Full(deleted), HttpCode.`204`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + deleteMyApiCollectionEndpointById, + implementedInApiVersion, + nameOf(deleteMyApiCollectionEndpointById), + "DELETE", + "/my/api-collection-ids/API_COLLECTION_ID/api-collection-endpoint-ids/API_COLLECTION_ENDPOINT_ID", + "Delete My Api Collection Endpoint By Id", + s"""${Glossary.getGlossaryItem("API Collections")} + |Delete Api Collection Endpoint + |Delete Api Collection Endpoint By Id + | + |${authenticationRequiredMessage(true)} + | + |""", + EmptyBody, + Full(true), + List( + $UserNotLoggedIn, + UserNotFoundByUserId, + UnknownError + ), + List(apiTagApiCollection, apiTagNewStyle) + ) + + lazy val deleteMyApiCollectionEndpointById : OBPEndpoint = { + case "my" :: "api-collection-ids" :: apiCollectionId :: "api-collection-endpoint-ids" :: apiCollectionEndpointId :: Nil JsonDelete _ => { + cc => + for { + (apiCollection, callContext) <- NewStyle.function.getApiCollectionById(apiCollectionId, Some(cc) ) + (apiCollectionEndpoint, callContext) <- NewStyle.function.getApiCollectionEndpointById(apiCollectionEndpointId, callContext) + (deleted, callContext) <- NewStyle.function.deleteApiCollectionEndpointById(apiCollectionEndpoint.apiCollectionEndpointId, callContext) + } yield { + (Full(deleted), HttpCode.`204`(callContext)) + } + } + } + staticResourceDocs += ResourceDoc( createJsonSchemaValidation, implementedInApiVersion, @@ -10305,13 +10895,13 @@ trait APIMethods400 { |* Name |* Code |* Parent Product Code - |* Category - |* Family - |* Super Family |* More info URL |* Description |* Terms and Conditions - |* License the data under this endpoint is released under + |* Description + |* Meta + |* Attributes + |* Fees | |${authenticationRequiredMessage(!getProductsIsPublic)}""".stripMargin, EmptyBody, @@ -10349,6 +10939,117 @@ trait APIMethods400 { } + private def checkRoleBankIdExsiting(callContext: Option[CallContext], entitlement: CreateEntitlementJSON) = { + Helper.booleanToFuture(failMsg = s"$BankNotFound Current BANK_ID (${entitlement.bank_id})", cc=callContext) { + entitlement.bank_id.nonEmpty == false || BankX(BankId(entitlement.bank_id), callContext).map(_._1).isEmpty == false + } + } + + private def checkRolesBankIdExsiting(callContext: Option[CallContext], postedData: PostCreateUserWithRolesJsonV400) = { + Future.sequence(postedData.roles.map(checkRoleBankIdExsiting(callContext,_))) + } + + private def addEntitlementToUser(userId:String, entitlement: CreateEntitlementJSON, callContext: Option[CallContext]) = { + Future(Entitlement.entitlement.vend.addEntitlement(entitlement.bank_id, userId, entitlement.role_name)) map { unboxFull(_) } + } + + private def addEntitlementsToUser(userId:String, postedData: PostCreateUserWithRolesJsonV400, callContext: Option[CallContext]) = { + Future.sequence(postedData.roles.distinct.map(addEntitlementToUser(userId, _, callContext))) + } + + /** + * This method will check all the roles the request user already has and the request roles: + * It will find the roles the requestUser already have, then show the error to the developer. + * (We can not grant the same roles to the request user twice) + */ + private def assertTargetUserLacksRoles(userId:String, requestedEntitlements: List[CreateEntitlementJSON], callContext: Option[CallContext]) = { + //1st: get all the entitlements for the user: + val userEntitlements = Entitlement.entitlement.vend.getEntitlementsByUserId(userId) + val userRoles = userEntitlements.map(_.map(entitlement => (entitlement.roleName, entitlement.bankId))).getOrElse(List.empty[(String,String)]).toSet + + val targetRoles = requestedEntitlements.map(entitlement => (entitlement.role_name, entitlement.bank_id)).toSet + + //2rd: find the duplicated ones: + val duplicatedRoles = userRoles.filter(targetRoles) + + //3rd: We can not grant the roles again, so we show the error to the developer. + if(duplicatedRoles.size >0){ + val errorMessages = s"$EntitlementAlreadyExists user_id($userId) ${duplicatedRoles.mkString(",")}" + Helper.booleanToFuture(errorMessages, cc=callContext) {false} + }else + Future.successful(Full()) + } + + /** + * This method will check all the roles the loggedIn user already has and the request roles: + * It will find the not existing roles from the loggedIn user --> we will show the error to the developer + * (We can only grant the roles which the loggedIn User has to the requestUser) + */ + private def assertUserCanGrantRoles(userId:String, requestedEntitlements: List[CreateEntitlementJSON], callContext: Option[CallContext]) = { + //1st: get all the entitlements for the user: + val userEntitlements = Entitlement.entitlement.vend.getEntitlementsByUserId(userId) + val userRoles = userEntitlements.map(_.map(entitlement => (entitlement.roleName, entitlement.bankId))).getOrElse(List.empty[(String,String)]).toSet + + val targetRoles = requestedEntitlements.map(entitlement => (entitlement.role_name, entitlement.bank_id)).toSet + + //2rd: find the roles which the loggedIn user does not have, + val roleLacking = targetRoles.filterNot(userRoles) + + if(roleLacking.size >0){ + val errorMessages = s"$EntitlementCannotBeGranted user_id($userId). The login user does not have the following roles: ${roleLacking.mkString(",")}" + Helper.booleanToFuture(errorMessages, cc=callContext) {false} + }else + Future.successful(Full()) + } + + private def checkRoleBankIdMapping(callContext: Option[CallContext], entitlement: CreateEntitlementJSON) = { + Helper.booleanToFuture(failMsg = if (ApiRole.valueOf(entitlement.role_name).requiresBankId) EntitlementIsBankRole else EntitlementIsSystemRole, cc = callContext) { + ApiRole.valueOf(entitlement.role_name).requiresBankId == entitlement.bank_id.nonEmpty + } + } + + private def checkRoleBankIdMappings(callContext: Option[CallContext], postedData: PostCreateUserWithRolesJsonV400) = { + Future.sequence(postedData.roles.map(checkRoleBankIdMapping(callContext,_))) + } + + private def checkRoleName(callContext: Option[CallContext], entitlement: CreateEntitlementJSON) = { + Future{ + tryo { + valueOf(entitlement.role_name) + } + } map { + val msg = IncorrectRoleName + entitlement.role_name + ". Possible roles are " + ApiRole.availableRoles.sorted.mkString(", ") + x => unboxFullOrFail(x, callContext, msg) + } + } + + private def checkRolesName(callContext: Option[CallContext], postJsonBody: PostCreateUserWithRolesJsonV400) = { + Future.sequence(postJsonBody.roles.map(checkRoleName(callContext,_))) + } + + private def createAccountAccessToUser(bankId: BankId, accountId: AccountId, user: User, view: View, callContext: Option[CallContext]) = { + view.isSystem match { + case true => NewStyle.function.grantAccessToSystemView(bankId, accountId, view, user, callContext) + case false => NewStyle.function.grantAccessToCustomView(view, user, callContext) + } + } + private def createAccountAccessesToUser(bankId: BankId, accountId: AccountId, user: User, views: List[View], callContext: Option[CallContext]) = { + Future.sequence(views.map(view => + createAccountAccessToUser(bankId: BankId, accountId: AccountId, user: User, view, callContext: Option[CallContext]) + )) + } + + private def getView(bankId: BankId, accountId: AccountId, postView: PostViewJsonV400, callContext: Option[CallContext]) = { + postView.is_system match { + case true => NewStyle.function.systemView(ViewId(postView.view_id), callContext) + case false => NewStyle.function.customView(ViewId(postView.view_id), BankIdAccountId(bankId, accountId), callContext) + } + } + + private def getViews(bankId: BankId, accountId: AccountId, postJson: PostCreateUserAccountAccessJsonV400, callContext: Option[CallContext]) = { + Future.sequence(postJson.views.map(view => getView(bankId: BankId, accountId: AccountId, view: PostViewJsonV400, callContext: Option[CallContext]))) + } + private def createDynamicEndpointMethod(bankId: Option[String], json: JValue, cc: CallContext) = { for { (postedJson, openAPI) <- NewStyle.function.tryons(InvalidJsonFormat, 400, cc.callContext) { diff --git a/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala b/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala index a3ad2eace..957f5898c 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala @@ -28,32 +28,33 @@ package code.api.v4_0_0 import java.text.SimpleDateFormat import java.util.Date - import code.api.attributedefinition.AttributeDefinition import code.api.util.APIUtil import code.api.util.APIUtil.{DateWithDay, DateWithSeconds, stringOptionOrNull, stringOrNull} import code.api.v1_2_1.JSONFactory.{createAmountOfMoneyJSON, createOwnersJSON} import code.api.v1_2_1.{BankRoutingJsonV121, JSONFactory, UserJSONV121, ViewJSONV121} import code.api.v1_4_0.JSONFactory1_4_0.{LocationJsonV140, MetaJsonV140, TransactionRequestAccountJsonV140, transformToLocationFromV140, transformToMetaFromV140} -import code.api.v2_0_0.{EntitlementJSONs, JSONFactory200, TransactionRequestChargeJsonV200} +import code.api.v2_0_0.JSONFactory200.UserJsonV200 +import code.api.v2_0_0.{CreateEntitlementJSON, EntitlementJSONs, JSONFactory200, TransactionRequestChargeJsonV200} import code.api.v2_1_0.{IbanJson, JSONFactory210, PostCounterpartyBespokeJson, ResourceUserJSON} import code.api.v2_2_0.CounterpartyMetadataJson import code.api.v3_0_0.JSONFactory300._ import code.api.v3_0_0._ import code.api.v3_1_0.JSONFactory310.{createAccountAttributeJson, createProductAttributesJson} -import code.api.v3_1_0.{AccountAttributeResponseJson, ProductAttributeResponseWithoutBankIdJson, RedisCallLimitJson} +import code.api.v3_1_0.{AccountAttributeResponseJson, PostHistoricalTransactionResponseJson, ProductAttributeResponseWithoutBankIdJson, RedisCallLimitJson} import code.apicollection.ApiCollectionTrait import code.apicollectionendpoint.ApiCollectionEndpointTrait import code.atms.Atms.Atm import code.bankattribute.BankAttribute import code.consent.MappedConsent import code.entitlement.Entitlement +import code.model.dataAccess.ResourceUser import code.model.{Consumer, ModeratedBankAccount, ModeratedBankAccountCore} import code.ratelimiting.RateLimiting import code.standingorders.StandingOrderTrait import code.transactionrequests.TransactionRequests.TransactionChallengeTypes import code.userlocks.UserLocks -import code.users.UserInvitation +import code.users.{UserAgreement, UserInvitation} import com.openbankproject.commons.model.{DirectDebitTrait, ProductFeeTrait, _} import net.liftweb.common.{Box, Full} import net.liftweb.json.JValue @@ -126,6 +127,31 @@ case class TransactionRequestWithChargeJSON400( challenges: List[ChallengeJsonV400], charge : TransactionRequestChargeJsonV200 ) +case class PostHistoricalTransactionAtBankJson( + from_account_id: String, + to_account_id: String, + value: AmountOfMoneyJsonV121, + description: String, + posted: String, + completed: String, + `type`: String, + charge_policy: String + ) +case class HistoricalTransactionAccountJsonV400( + bank_id: String, + account_id : String + ) +case class PostHistoricalTransactionResponseJsonV400( + transaction_id: String, + from: HistoricalTransactionAccountJsonV400, + to: HistoricalTransactionAccountJsonV400, + value: AmountOfMoneyJsonV121, + description: String, + posted: Date, + completed: Date, + transaction_request_type: String, + charge_policy: String + ) case class PostResetPasswordUrlJsonV400(username: String, email: String, user_id: String) case class ResetPasswordUrlJsonV400(reset_password_url: String) @@ -200,6 +226,22 @@ case class ModeratedFirehoseAccountsJsonV400( accounts: List[ModeratedFirehoseAccountJsonV400] ) +case class FastFirehoseAccountJsonV400( + id: String, + bank_id: String, + label: String, + number: String, + owners: String, + product_code: String, + balance: AmountOfMoneyJsonV121, + account_routings: String , + account_attributes: String +) + +case class FastFirehoseAccountsJsonV400( + accounts: List[FastFirehoseAccountJsonV400] +) + case class ModeratedAccountJSON400( id : String, label : String, @@ -312,6 +354,8 @@ case class StandingOrderJsonV400(standing_order_id: String, active: Boolean) case class PostViewJsonV400(view_id: String, is_system: Boolean) case class PostAccountAccessJsonV400(user_id: String, view: PostViewJsonV400) +case class PostCreateUserAccountAccessJsonV400(username: String, provider:String, views: List[PostViewJsonV400]) +case class PostCreateUserWithRolesJsonV400(username: String, provider:String, roles: List[CreateEntitlementJSON]) case class PostRevokeGrantAccountAccessJsonV400(views: List[String]) case class RevokedJsonV400(revoked: Boolean) @@ -637,7 +681,8 @@ case class ApiCollectionJson400 ( api_collection_id: String, user_id: String, api_collection_name: String, - is_sharable: Boolean + is_sharable: Boolean, + description: String ) case class ApiCollectionsJson400 ( api_collections: List[ApiCollectionJson400] @@ -645,7 +690,8 @@ case class ApiCollectionsJson400 ( case class PostApiCollectionJson400( api_collection_name: String, - is_sharable: Boolean + is_sharable: Boolean, + description: Option[String] ) case class ApiCollectionEndpointJson400 ( @@ -887,6 +933,7 @@ case class AtmJsonV400 ( case class AtmsJsonV400(atms : List[AtmJsonV400]) +case class UserAgreementJson(`type`: String, text: String) case class UserJsonV400( user_id: String, email : String, @@ -895,12 +942,15 @@ case class UserJsonV400( username : String, entitlements : EntitlementJSONs, views: Option[ViewsJSON300], - is_deleted: Boolean + agreements: Option[List[UserAgreementJson]], + is_deleted: Boolean, + last_marketing_agreement_signed_date: Option[Date] ) +case class UsersJsonV400(users: List[UserJsonV400]) object JSONFactory400 { - def createUserInfoJSON(user : User, entitlements: List[Entitlement]) : UserJsonV400 = { + def createUserInfoJSON(user : User, entitlements: List[Entitlement], agreements: Option[List[UserAgreement]]) : UserJsonV400 = { UserJsonV400( user_id = user.userId, email = user.emailAddress, @@ -909,7 +959,23 @@ object JSONFactory400 { provider = stringOrNull(user.provider), entitlements = JSONFactory200.createEntitlementJSONs(entitlements), views = None, - is_deleted = user.isDeleted.getOrElse(false) + 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 + ) + } + + def createUsersJson(users : List[(ResourceUser, Box[List[Entitlement]], Option[List[UserAgreement]])]) : UsersJsonV400 = { + UsersJsonV400( + users.map(t => + createUserInfoJSON( + t._1, + t._2.getOrElse(Nil), + t._3 + ) + ) ) } @@ -1127,6 +1193,24 @@ object JSONFactory400 { ) ) } + def createFirehoseBankAccountJSON(firehoseAccounts : List[FastFirehoseAccount]) : FastFirehoseAccountsJsonV400 = { + FastFirehoseAccountsJsonV400( + firehoseAccounts.map( + account => + FastFirehoseAccountJsonV400( + account.id, + account.bankId, + account.label, + account.number, + account.owners, + account.productCode, + AmountOfMoneyJsonV121(account.balance.currency, account.balance.amount), + account.accountRoutings, + account.accountAttributes + ) + ) + ) + } def createEntitlementJSONs(entitlements: List[Entitlement]): EntitlementsJsonV400 = { @@ -1402,6 +1486,7 @@ object JSONFactory400 { apiCollection.userId, apiCollection.apiCollectionName, apiCollection.isSharable, + apiCollection.description ) } def createIbanCheckerJson(iban: IbanChecker): IbanCheckerJsonV400 = { @@ -1671,5 +1756,37 @@ object JSONFactory400 { )))) ) } + + + + def createPostHistoricalTransactionResponseJson( + bankId: BankId, + transactionId: TransactionId, + fromAccountId: AccountId, + toAccountId: AccountId, + value: AmountOfMoneyJsonV121, + description: String, + posted: Date, + completed: Date, + transactionRequestType: String, + chargePolicy: String + ) : PostHistoricalTransactionResponseJsonV400 = { + PostHistoricalTransactionResponseJsonV400( + transaction_id = transactionId.value, + from = HistoricalTransactionAccountJsonV400(bankId.value, fromAccountId.value), + to = HistoricalTransactionAccountJsonV400(bankId.value, toAccountId.value), + value: AmountOfMoneyJsonV121, + description: String, + posted: Date, + completed: Date, + transaction_request_type = transactionRequestType, + chargePolicy: String + ) + } + + + + + } diff --git a/obp-api/src/main/scala/code/api/v4_0_0/dynamic/DynamicEndpoints.scala b/obp-api/src/main/scala/code/api/v4_0_0/dynamic/DynamicEndpoints.scala index 719b4d17f..828a92971 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/dynamic/DynamicEndpoints.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/dynamic/DynamicEndpoints.scala @@ -1,6 +1,6 @@ package code.api.v4_0_0.dynamic -import code.api.util.APIUtil.{BooleanBody, DoubleBody, EmptyBody, LongBody, OBPEndpoint, PrimaryDataBody, ResourceDoc, StringBody} +import code.api.util.APIUtil.{BooleanBody, DoubleBody, EmptyBody, LongBody, OBPEndpoint, PrimaryDataBody, ResourceDoc, StringBody, getDisabledEndpointOperationIds} import code.api.util.{CallContext, DynamicUtil} import code.api.v4_0_0.dynamic.practise.{DynamicEndpointCodeGenerator, PractiseEndpointGroup} import net.liftweb.common.{Box, Failure, Full} @@ -15,7 +15,14 @@ import scala.util.control.Breaks.{break, breakable} object DynamicEndpoints { //TODO, better put all other dynamic endpoints into this list. eg: dynamicEntityEndpoints, dynamicSwaggerDocsEndpoints .... - private val endpointGroups: List[EndpointGroup] = PractiseEndpointGroup :: DynamicResourceDocsEndpointGroup :: Nil + val disabledEndpointOperationIds = getDisabledEndpointOperationIds + + private val endpointGroups: List[EndpointGroup] = + if(disabledEndpointOperationIds.contains("OBPv4.0.0-test-dynamic-resource-doc")) { + DynamicResourceDocsEndpointGroup :: Nil + }else{ + PractiseEndpointGroup :: DynamicResourceDocsEndpointGroup :: Nil + } /** * this will find dynamic endpoint by request. diff --git a/obp-api/src/main/scala/code/apicollection/ApiCollection.scala b/obp-api/src/main/scala/code/apicollection/ApiCollection.scala index 84e8c793d..35405ac02 100644 --- a/obp-api/src/main/scala/code/apicollection/ApiCollection.scala +++ b/obp-api/src/main/scala/code/apicollection/ApiCollection.scala @@ -10,11 +10,13 @@ class ApiCollection extends ApiCollectionTrait with LongKeyedMapper[ApiCollectio object UserId extends MappedString(this, 100) object ApiCollectionName extends MappedString(this, 100) object IsSharable extends MappedBoolean(this) + object Description extends MappedString(this, 2000) override def apiCollectionId: String = ApiCollectionId.get override def userId: String = UserId.get override def apiCollectionName: String = ApiCollectionName.get override def isSharable: Boolean = IsSharable.get + override def description: String = Description.get } object ApiCollection extends ApiCollection with LongKeyedMetaMapper[ApiCollection] { @@ -26,4 +28,5 @@ trait ApiCollectionTrait { def userId: String def apiCollectionName: String def isSharable: Boolean + def description: String } \ No newline at end of file diff --git a/obp-api/src/main/scala/code/apicollection/ApiCollectionsProvider.scala b/obp-api/src/main/scala/code/apicollection/ApiCollectionsProvider.scala index 64465df42..1d0cdea12 100644 --- a/obp-api/src/main/scala/code/apicollection/ApiCollectionsProvider.scala +++ b/obp-api/src/main/scala/code/apicollection/ApiCollectionsProvider.scala @@ -9,7 +9,8 @@ trait ApiCollectionsProvider { def createApiCollection( userId: String, apiCollectionName: String, - isSharable: Boolean + isSharable: Boolean, + description: String ): Box[ApiCollectionTrait] def getApiCollectionById( @@ -36,7 +37,8 @@ object MappedApiCollectionsProvider extends MdcLoggable with ApiCollectionsProvi override def createApiCollection( userId: String, apiCollectionName: String, - isSharable: Boolean + isSharable: Boolean, + description: String ): Box[ApiCollectionTrait] = tryo ( ApiCollection @@ -44,6 +46,7 @@ object MappedApiCollectionsProvider extends MdcLoggable with ApiCollectionsProvi .UserId(userId) .ApiCollectionName(apiCollectionName) .IsSharable(isSharable) + .Description(description) .saveMe() ) diff --git a/obp-api/src/main/scala/code/atms/Atms.scala b/obp-api/src/main/scala/code/atms/Atms.scala index 8c5f6ac4a..dede15ea6 100644 --- a/obp-api/src/main/scala/code/atms/Atms.scala +++ b/obp-api/src/main/scala/code/atms/Atms.scala @@ -92,7 +92,7 @@ trait AtmsProvider { getAtmsFromProvider(bankId,queryParams) match { case Some(atms) => { val atmsWithLicense = for { - branch <- atms if branch.meta.license.name.size > 3 && branch.meta.license.name.size > 3 + branch <- atms if branch.meta.license.name.size > 3 } yield branch Option(atmsWithLicense) } diff --git a/obp-api/src/main/scala/code/bankconnectors/Connector.scala b/obp-api/src/main/scala/code/bankconnectors/Connector.scala index 892cced4d..56c71d1e3 100644 --- a/obp-api/src/main/scala/code/bankconnectors/Connector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/Connector.scala @@ -2,7 +2,6 @@ package code.bankconnectors import java.util.Date import java.util.UUID.randomUUID - import _root_.akka.http.scaladsl.model.HttpMethod import code.accountholders.{AccountHolders, MapperAccountHolders} import code.api.attributedefinition.AttributeDefinition @@ -13,8 +12,10 @@ import code.api.util.ErrorMessages._ import code.api.util._ import code.api.v1_4_0.JSONFactory1_4_0.TransactionRequestAccountJsonV140 import code.api.v2_1_0._ +import code.api.v4_0_0.ModeratedFirehoseAccountsJsonV400 import code.api.{APIFailure, APIFailureNewStyle} import code.bankattribute.BankAttribute +import code.bankconnectors.LocalMappedConnector.setUnimplementedError import code.bankconnectors.akka.AkkaConnector_vDec2018 import code.bankconnectors.rest.RestConnector_vMar2019 import code.bankconnectors.storedprocedure.StoredProcedureConnector_vDec2019 @@ -45,6 +46,7 @@ import com.openbankproject.commons.util.Functions.lazyValue import com.openbankproject.commons.util.{JsonUtils, ReflectUtils} import com.tesobe.CacheKeyFromArguments import net.liftweb.common._ +import net.liftweb.http.provider.HTTPParam import net.liftweb.json import net.liftweb.json.{Formats, JObject, JValue} import net.liftweb.mapper.By @@ -503,6 +505,10 @@ trait Connector extends MdcLoggable { def getCoreBankAccounts(bankIdAccountIds: List[BankIdAccountId], callContext: Option[CallContext]) : Future[Box[(List[CoreAccount], Option[CallContext])]]= Future{Failure(setUnimplementedError)} + + def getBankAccountsWithAttributes(bankId: BankId, queryParams: List[OBPQueryParam], callContext: Option[CallContext]): OBPReturnType[Box[List[FastFirehoseAccount]]] = + Future{(Failure(setUnimplementedError), callContext)} + def getBankSettlementAccounts(bankId: BankId, callContext: Option[CallContext]): OBPReturnType[Box[List[BankAccount]]] = Future{(Failure(setUnimplementedError), callContext)} def getBankAccountsHeldLegacy(bankIdAccountIds: List[BankIdAccountId], callContext: Option[CallContext]) : Box[List[AccountHeld]]= Failure(setUnimplementedError) diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index 438c4135b..a3721b72f 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -2,7 +2,6 @@ package code.bankconnectors import java.util.Date import java.util.UUID.randomUUID - import _root_.akka.http.scaladsl.model.HttpMethod import code.DynamicData.DynamicDataProvider import code.DynamicEndpoint.{DynamicEndpointProvider, DynamicEndpointT} @@ -48,6 +47,7 @@ import code.metadata.narrative.Narrative import code.metadata.tags.Tags import code.metadata.transactionimages.TransactionImages import code.metadata.wheretags.WhereTags +import code.metrics.MappedMetric import code.model._ import code.model.dataAccess.AuthUser.findUserByUsernameLocally import code.model.dataAccess._ @@ -95,6 +95,9 @@ import org.iban4j.{CountryCode, IbanFormat} import org.mindrot.jbcrypt.BCrypt import scalacache.ScalaCache import scalacache.guava.GuavaCache +import scalikejdbc.{ConnectionPool, ConnectionPoolSettings, MultipleConnectionPoolContext} +import scalikejdbc.DB.CPContext +import scalikejdbc.{DB => scalikeDB, _} import scala.collection.immutable.{List, Nil} import scala.concurrent._ @@ -799,6 +802,76 @@ object LocalMappedConnector extends Connector with MdcLoggable { } } + private lazy val getDbConnectionParameters: (String, String, String) = { + val dbUrl = APIUtil.getPropsValue("db.url") openOr "jdbc:h2:mem:OBPTest;DB_CLOSE_DELAY=-1" + val username = dbUrl.split(";").filter(_.contains("user")).toList.headOption.map(_.split("=")(1)) + val password = dbUrl.split(";").filter(_.contains("password")).toList.headOption.map(_.split("=")(1)) + val dbUser = APIUtil.getPropsValue("db.user").orElse(username) + val dbPassword = APIUtil.getPropsValue("db.password").orElse(password) + (dbUrl, dbUser.getOrElse(""), dbPassword.getOrElse("")) + } + + /** + * this connection pool context corresponding db.url in default.props + */ + implicit lazy val context: CPContext = { + val settings = ConnectionPoolSettings( + initialSize = 5, + maxSize = 20, + connectionTimeoutMillis = 3000L, + validationQuery = "select 1", + connectionPoolFactoryName = "commons-dbcp2" + ) + val (dbUrl, user, password) = getDbConnectionParameters + val dbName = "DB_NAME" // corresponding props db.url DB + ConnectionPool.add(dbName, dbUrl, user, password, settings) + val connectionPool = ConnectionPool.get(dbName) + MultipleConnectionPoolContext(ConnectionPool.DEFAULT_NAME -> connectionPool) + } + + + override def getBankAccountsWithAttributes(bankId: BankId, queryParams: List[OBPQueryParam], callContext: Option[CallContext]): OBPReturnType[Box[List[FastFirehoseAccount]]] = + Future{ + val limit = queryParams.collect { case OBPLimit(value) => value }.headOption.getOrElse(50) + val offset = queryParams.collect { case OBPOffset(value) => value }.headOption.getOrElse(0) + val orderBy = queryParams.collect { + case OBPOrdering(_, OBPDescending) => "DESC" + }.headOption.getOrElse("ASC") + + val ordering = if (orderBy =="DESC" ) sqls"DESC" else sqls"ASC" + + val firehoseAccounts = { + scalikeDB readOnly { implicit session => + val sqlResult = sql""" + select * from mv_fast_firehose_accounts + WHERE mv_fast_firehose_accounts.bank_id = ${bankId.value} + ORDER BY mv_fast_firehose_accounts.account_id $ordering + LIMIT $limit + OFFSET $offset + """.stripMargin + .map( + rs => // Map result to case class + FastFirehoseAccount( + id = rs.stringOpt(1).map(_.toString).getOrElse(null), + bankId= rs.stringOpt(2).map(_.toString).getOrElse(null), + label= rs.stringOpt(3).map(_.toString).getOrElse(null), + number = rs.stringOpt(4).map(_.toString).getOrElse(null), + owners = rs.stringOpt(5).map(_.toString).getOrElse(null), + productCode = rs.stringOpt(6).map(_.toString).getOrElse(null), + balance = AmountOfMoney( + currency = rs.stringOpt(7).map(_.toString).getOrElse(null), + amount = rs.stringOpt(8).map(_.toString).getOrElse(null) + ), + accountRoutings = rs.stringOpt(9).map(_.toString).getOrElse(null), + accountAttributes = rs.stringOpt(10).map(_.toString).getOrElse(null) + ) + ).list().apply() + sqlResult + } + } + (Full(firehoseAccounts), callContext) + } + override def getBankSettlementAccounts(bankId: BankId, callContext: Option[CallContext]): OBPReturnType[Box[List[BankAccount]]] = { Future { Full { diff --git a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala index 0e9d6b9eb..c412085f6 100644 --- a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala +++ b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala @@ -37,10 +37,12 @@ import code.api.cache.Caching import code.api.util.APIUtil.{AdapterImplementation, MessageDoc, OBPReturnType, saveConnectorMetric, _} import code.api.util.ErrorMessages._ import code.api.util.ExampleValue._ +import code.api.util.RSAUtil.{computeXSign, getPrivateKeyFromString} import code.api.util.{APIUtil, CallContext, OBPQueryParam} import code.api.v4_0_0.dynamic.MockResponseHolder import code.bankconnectors._ import code.bankconnectors.vJune2017.AuthInfo +import code.context.UserAuthContextProvider import code.customer.internalMapping.MappedCustomerIdMappingProvider import code.kafka.KafkaHelper import code.model.dataAccess.internalMapping.MappedAccountIdMappingProvider @@ -63,6 +65,7 @@ import net.liftweb.json.{JValue, _} import net.liftweb.util.Helpers.tryo import org.apache.commons.lang3.StringUtils +import java.time.Instant import scala.collection.immutable.List import scala.collection.mutable.ArrayBuffer import scala.concurrent.duration._ @@ -6522,7 +6525,7 @@ trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable } val jsonToSend = if(jValue == JNothing) "" else compactRender(jValue) - val request = prepareHttpRequest(paramUrl, method, HttpProtocol("HTTP/1.1"), jsonToSend).withHeaders(callContext) + val request = prepareHttpRequest(paramUrl, method, HttpProtocol("HTTP/1.1"), jsonToSend).withHeaders(buildHeaders(paramUrl,jsonToSend,callContext)) logger.debug(s"RestConnector_vMar2019 request is : $request") val responseFuture = makeHttpRequest(request) @@ -6572,15 +6575,37 @@ trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable //In RestConnector, we use the headers to propagate the parameters to Adapter. The parameters come from the CallContext.outboundAdapterAuthInfo.userAuthContext //We can set them from UserOauthContext or the http request headers. - private[this] implicit def buildHeaders(callContext: Option[CallContext]): List[HttpHeader] = { + private[this] def buildHeaders( + uri: String, + entityJsonString: String, + callContext: Option[CallContext] + ): List[HttpHeader] = { - val generalContext = callContext.flatMap(_.toOutboundAdapterCallContext.generalContext).getOrElse(List.empty[BasicGeneralContext]) + val needSignatureHead = APIUtil.getPropsAsBoolValue("rest_connector_sends_x-sign_header", false) + val generalContext = callContext.map(createBasicUserAuthContextJsonFromCallContext(_)).getOrElse(List.empty[BasicGeneralContext]) val headersFromGeneralContext = generalContext.map(generalContext => RawHeader(generalContext.key,generalContext.value)) - val basicUserAuthContexts: List[BasicUserAuthContext] = callContext.flatMap(_.toOutboundAdapterCallContext.outboundAdapterAuthInfo.flatMap(_.userAuthContext)).getOrElse(List.empty[BasicUserAuthContext]) - val headersFromUserAuthContext = basicUserAuthContexts.map(userAuthContext => RawHeader(userAuthContext.key,userAuthContext.value)) + val basicUserAuthContexts = UserAuthContextProvider.userAuthContextProvider.vend.getUserAuthContextsBox(callContext.map(_.userId).getOrElse("")).getOrElse(List.empty[UserAuthContext]) + val headersFromUserAuthContext = basicUserAuthContexts.filterNot(_.key == "private-key").map(userAuthContext =>RawHeader(userAuthContext.key,userAuthContext.value)) - headersFromGeneralContext++headersFromUserAuthContext + val timeStamp = Instant.now.getEpochSecond.toString + logger.debug(s"x-timestamp: $timeStamp") + + val extraHeaders = if(needSignatureHead){ + val inputMessage = s"""${timeStamp}${uri}${entityJsonString}""" + val privateKeyValue = basicUserAuthContexts.find(_.key =="private-key").map(_.value).getOrElse("") + val privateKey = getPrivateKeyFromString(privateKeyValue) + val xSign = computeXSign(inputMessage, privateKey) + logger.debug(s"x-sign: $xSign") + List(RawHeader("x-timestamp",timeStamp),RawHeader("x-sign",xSign)) + } else { + List(RawHeader("x-timestamp",timeStamp)) + } + val headers = headersFromUserAuthContext++extraHeaders++headersFromGeneralContext + + logger.debug(s"obp headers: ${headers}") + + headers } @@ -6686,7 +6711,7 @@ trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable compactRender(builtJson) case _ => net.liftweb.json.Serialization.write(outBound) } - val request = prepareHttpRequest(url, method, HttpProtocol("HTTP/1.1"), outBoundJson).withHeaders(callContext) + val request = prepareHttpRequest(url, method, HttpProtocol("HTTP/1.1"), outBoundJson).withHeaders(buildHeaders(url, outBoundJson, callContext)) logger.debug(s"RestConnector_vMar2019 request is : $request") val responseFuture = makeHttpRequest(request) responseFuture.map { diff --git a/obp-api/src/main/scala/code/bankconnectors/vJune2017/KafkaMappedConnector_vJune2017.scala b/obp-api/src/main/scala/code/bankconnectors/vJune2017/KafkaMappedConnector_vJune2017.scala index 76ccbec7a..7a2cba025 100644 --- a/obp-api/src/main/scala/code/bankconnectors/vJune2017/KafkaMappedConnector_vJune2017.scala +++ b/obp-api/src/main/scala/code/bankconnectors/vJune2017/KafkaMappedConnector_vJune2017.scala @@ -1037,8 +1037,6 @@ trait KafkaMappedConnector_vJune2017 extends Connector with KafkaHelper with Mdc } yield { (transaction,callContext) } - case Full((data,status,callContext)) if (status.errorCode!="") => - Failure("INTERNAL-"+ status.errorCode+". + CoreBank-Status:"+ status.backendMessages) case Empty => Failure(ErrorMessages.InvalidConnectorResponse, Empty, Empty) case Failure(msg, e, c) => diff --git a/obp-api/src/main/scala/code/examplething/Thing.scala b/obp-api/src/main/scala/code/examplething/Thing.scala index 870928099..fda82f985 100644 --- a/obp-api/src/main/scala/code/examplething/Thing.scala +++ b/obp-api/src/main/scala/code/examplething/Thing.scala @@ -10,15 +10,15 @@ import net.liftweb.util.SimpleInjector object Thing extends SimpleInjector { val thingProvider = new Inject(buildOne _) {} - // def buildOne: ThingProvider = MappedThingProvider + def buildOne: ThingProvider = MappedThingProvider - - // This determines the provider we use - def buildOne: ThingProvider = - APIUtil.getPropsValue("provider.thing").openOr("mapped") match { - case "mapped" => MappedThingProvider - case _ => MappedThingProvider - } + //If you set props `provider.thing`, you can set to different providers +// // This determines the provider we use +// def buildOne: ThingProvider = +// APIUtil.getPropsValue("provider.thing").openOr("mapped") match { +// case "mapped" => MappedThingProvider +// case _ => MappedThingProvider +// } } diff --git a/obp-api/src/main/scala/code/fx/fx.scala b/obp-api/src/main/scala/code/fx/fx.scala index 4db9fd784..d5935bdfd 100644 --- a/obp-api/src/main/scala/code/fx/fx.scala +++ b/obp-api/src/main/scala/code/fx/fx.scala @@ -27,21 +27,33 @@ object fx extends MdcLoggable { // Make this easier //get data from : http://www.xe.com/de/currencyconverter/convert/?Amount=1&From=AUD&To=EUR - val fallbackExchangeRates = { + // Currently There are 14 currencies with 14 mappings etc. + // We don't actually need to store the Same:Same currency (1:1) but it makes editing the map less error prone in terms of data entry! + // So keep the Map balanced. + // If get compile error with type mismatch; + // found : AnyVal + // required: Double + // check the map is complete for all combinations - (We could use getOrElse (1.0) (double) as defaults in calling function but then we risk having missing values below) + // and make sure to sure explicit doubles e.g. 1.0 rather than 1 !! + + // Note: If you add a non ISO standard currency below, you will also need to add it also to isValidCurrencyISOCode otherwise FX endpoints etc will fail. + + val fallbackExchangeRates: Map[String, Map[String, Double]] = { Map( - "GBP" -> Map("EUR" -> 1.16278, "USD" -> 1.24930, "JPY" -> 141.373, "AED" -> 4.58882, "INR" -> 84.0950, "KRW" -> 1433.92, "XAF" -> 762.826, "JOD" -> 0.936707, "ILS" -> 4.70020, "AUD" -> 1.63992 ,"HKD" -> 10.1468, "MXN" -> 29.2420), - "EUR" -> Map("GBP" -> 0.860011, "USD" -> 1.07428, "JPY" -> 121.567, "AED" -> 3.94594, "INR" -> 72.3136, "KRW" -> 1233.03, "XAF" -> 655.957, "JOD" -> 0.838098, "ILS" -> 4.20494, "AUD" -> 1.49707 ,"HKD" -> 8.88926, "MXN" -> 26.0359), - "USD" -> Map("GBP" -> 0.800446, "EUR" -> 0.930886, "JPY" -> 113.161, "AED" -> 3.67310, "INR" -> 67.3135, "KRW" -> 1147.78, "XAF" -> 610.601, "JOD" -> 0.708659, "ILS" -> 3.55495, "AUD" -> 1.27347 ,"HKD" -> 7.84766, "MXN" -> 21.7480), - "JPY" -> Map("GBP" -> 0.00707350, "EUR" -> 0.00822592, "USD" -> 0.00883695, "AED" -> 0.0324590, "INR" -> 0.594846, "KRW" -> 10.1428, "XAF" -> 5.39585, "JOD" -> 0.00639777, "ILS" -> 0.0320926, "AUD" -> 0.0114819 ,"HKD" -> 0.0709891, "MXN" -> 0.2053), - "AED" -> Map("GBP" -> 0.217921, "EUR" -> 0.253425, "USD" -> 0.272250, "JPY" -> 30.8081, "INR" -> 18.3255, "KRW" -> 312.482, "XAF" -> 166.236, "AED" -> 0.192964, "ILS" -> 0.968033, "AUD" -> 0.346779 ,"HKD" -> 2.13685, "MXN" -> 5.9217), - "INR" -> Map("GBP" -> 0.0118913, "EUR" -> 0.0138287, "USD" -> 0.0148559, "JPY" -> 1.68111, "AED" -> 0.0545671, "KRW" -> 17.0512, "XAF" -> 9.07101, "JOD" -> 0.0110959 , "ILS" -> 0.0556764, "AUD" -> 0.0198319 ,"HKD" -> 0.109972, "MXN" -> 0.2983), - "KRW" -> Map("GBP" -> 0.000697389, "EUR" -> 0.000811008, "USD" -> 0.000871250, "JPY" -> 0.0985917, "AED" -> 0.00320019, "INR" -> 0.0586469, "XAF" -> 0.531986, "JOD" -> 0.000630634, "ILS" -> 0.00316552,"AUD" -> 0.00111694,"HKD" -> 0.00697233, "MXN" -> 0.0183), - "XAF" -> Map("GBP" -> 0.00131092, "EUR" -> 0.00152449, "USD" -> 0.00163773, "JPY" -> 0.185328, "AED" -> 0.00601555, "INR" -> 0.110241, "KRW" -> 1.87975, "JOD" -> 0.00127784, "ILS" -> 0.00641333,"AUD" -> 0.00228226,"HKD" -> 0.0135503, "MXN" -> 0.0396 ), - "JOD" -> Map("GBP" -> 1.06757, "EUR" -> 0.237707, "USD" -> 1.41112, "JPY" -> 156.304, "AED" -> 5.18231, "INR" -> 90.1236, "KRW" -> 1585.68, "XAF" -> 782.572, "ILS" -> 5.02018, "AUD" -> 1.63992 ,"HKD" -> 11.0687, "MXN" -> 30.8336), - "ILS" -> Map("GBP" -> 0.212763, "EUR" -> 1.19318, "USD" -> 0.281298, "JPY" -> 31.1599, "AED" -> 1.03302, "INR" -> 17.9609, "KRW" -> 315.903, "XAF" -> 155.925, "JOD" -> 0.199196, "AUD" -> 0.352661 ,"HKD" -> 2.16985, "MXN" -> 6.4871), - "AUD" -> Map("GBP" -> 0.609788, "EUR" -> 0.667969, "USD" -> 0.785256, "JPY" -> 87.0936, "AED" -> 2.88368, "INR" -> 50.4238, "KRW" -> 895.304, "XAF" -> 438.162, "JOD" -> 0.556152, "ILS" -> 2.83558 ,"HKD" -> 5.61346 , "MXN" -> 16.0826), - "HKD" -> Map("GBP" -> 0.0985443, "EUR" -> 0.112495, "USD" -> 0.127427, "JPY" -> 14.0867, "AED" -> 0.467977, "INR" -> 9.09325, "KRW" -> 143.424, "XAF" -> 73.8049, "JOD" -> 0.0903452, "ILS" -> 0.460862 ,"AUD" -> 0.178137, "MXN" -> 2.8067), - "MXN" -> Map("GBP" -> 0.0341, "EUR" -> 0.0384, "USD" -> 0.0459, "JPY" -> 4.8687, "AED" -> 0.1688, "INR" -> 3.3513, "KRW" -> 54.4512, "XAF" -> 25.1890, "JOD" -> 0.0324, "ILS" -> 0.1541 , "AUD" -> 0.0621, "HKD" -> 0.3562 ) + "GBP" -> Map("GBP" -> 1.0, "EUR" -> 1.16278, "USD" -> 1.24930, "JPY" -> 141.373, "AED" -> 4.58882, "INR" -> 84.0950, "KRW" -> 1433.92, "XAF" -> 762.826, "JOD" -> 0.936707, "ILS" -> 4.70020, "AUD" -> 1.63992, "HKD" -> 10.1468, "MXN" -> 29.2420, "XBT" -> 0.000022756409956), + "EUR" -> Map("GBP" -> 0.860011, "EUR" -> 1.0, "USD" -> 1.07428, "JPY" -> 121.567, "AED" -> 3.94594, "INR" -> 72.3136, "KRW" -> 1233.03, "XAF" -> 655.957, "JOD" -> 0.838098, "ILS" -> 4.20494, "AUD" -> 1.49707, "HKD" -> 8.88926, "MXN" -> 26.0359, "XBT" -> 0.000019087905636), + "USD" -> Map("GBP" -> 0.800446, "EUR" -> 0.930886, "USD" -> 1.0, "JPY" -> 113.161, "AED" -> 3.67310, "INR" -> 67.3135, "KRW" -> 1147.78, "XAF" -> 610.601, "JOD" -> 0.708659, "ILS" -> 3.55495, "AUD" -> 1.27347, "HKD" -> 7.84766, "MXN" -> 21.7480, "XBT" -> 0.0000169154), + "JPY" -> Map("GBP" -> 0.00707350, "EUR" -> 0.00822592, "USD" -> 0.00883695, "JPY" -> 1.0, "AED" -> 0.0324590, "INR" -> 0.594846, "KRW" -> 10.1428, "XAF" -> 5.39585, "JOD" -> 0.00639777, "ILS" -> 0.0320926, "AUD" -> 0.0114819, "HKD" -> 0.0709891, "MXN" -> 0.2053, "XBT" -> 0.000000147171931), + "AED" -> Map("GBP" -> 0.217921, "EUR" -> 0.253425, "USD" -> 0.272250, "JPY" -> 30.8081, "AED" -> 1.0, "INR" -> 18.3255, "KRW" -> 312.482, "XAF" -> 166.236, "JOD" -> 0.1930565, "ILS" -> 0.968033, "AUD" -> 0.346779, "HKD" -> 2.13685, "MXN" -> 5.9217, "XBT" -> 0.000004603349217), + "INR" -> Map("GBP" -> 0.0118913, "EUR" -> 0.0138287, "USD" -> 0.0148559, "JPY" -> 1.68111, "AED" -> 0.0545671, "INR" -> 1.0, "KRW" -> 17.0512, "XAF" -> 9.07101, "JOD" -> 0.0110959, "ILS" -> 0.0556764, "AUD" -> 0.0198319, "HKD" -> 0.109972, "MXN" -> 0.2983, "XBT" -> 0.00000022689396), + "KRW" -> Map("GBP" -> 0.000697389, "EUR" -> 0.000811008, "USD" -> 0.000871250, "JPY" -> 0.0985917, "AED" -> 0.00320019, "INR" -> 0.0586469, "KRW" -> 1.0, "XAF" -> 0.531986, "JOD" -> 0.000630634, "ILS" -> 0.00316552, "AUD" -> 0.00111694,"HKD" -> 0.00697233,"MXN" -> 0.0183, "XBT" -> 0.000000014234725), + "XAF" -> Map("GBP" -> 0.00131092, "EUR" -> 0.00152449, "USD" -> 0.00163773, "JPY" -> 0.185328, "AED" -> 0.00601555, "INR" -> 0.110241, "KRW" -> 1.87975, "XAF" -> 1.0, "JOD" -> 0.00127784, "ILS" -> 0.00641333, "AUD" -> 0.00228226,"HKD" -> 0.0135503, "MXN" -> 0.0396, "XBT" -> 0.000000029074795), + "JOD" -> Map("GBP" -> 1.06757, "EUR" -> 0.237707, "USD" -> 1.41112, "JPY" -> 156.304, "AED" -> 5.18231, "INR" -> 90.1236, "KRW" -> 1585.68, "XAF" -> 782.572, "JOD" -> 1.0, "ILS" -> 5.02018, "AUD" -> 1.63992, "HKD" -> 11.0687, "MXN" -> 30.8336, "XBT" -> 0.000023803244006), + "ILS" -> Map("GBP" -> 0.212763, "EUR" -> 1.19318, "USD" -> 0.281298, "JPY" -> 31.1599, "AED" -> 1.03302, "INR" -> 17.9609, "KRW" -> 315.903, "XAF" -> 155.925, "JOD" -> 0.199196, "ILS" -> 1.0, "AUD" -> 0.352661, "HKD" -> 2.16985, "MXN" -> 6.4871, "XBT" -> 0.000005452272147), + "AUD" -> Map("GBP" -> 0.609788, "EUR" -> 0.667969, "USD" -> 0.785256, "JPY" -> 87.0936, "AED" -> 2.88368, "INR" -> 50.4238, "KRW" -> 895.304, "XAF" -> 438.162, "JOD" -> 0.556152, "ILS" -> 2.83558, "AUD" -> 1.0, "HKD" -> 5.61346, "MXN" -> 16.0826, "XBT" -> 0.000012284055924), + "HKD" -> Map("GBP" -> 0.0985443, "EUR" -> 0.112495, "USD" -> 0.127427, "JPY" -> 14.0867, "AED" -> 0.467977, "INR" -> 9.09325, "KRW" -> 143.424, "XAF" -> 73.8049, "JOD" -> 0.0903452, "ILS" -> 0.460862, "AUD" -> 0.178137, "HKD" -> 1.0, "MXN" -> 2.8067, "XBT" -> 0.000002164242461), + "MXN" -> Map("GBP" -> 0.0341, "EUR" -> 0.0384, "USD" -> 0.0459, "JPY" -> 4.8687, "AED" -> 0.1688, "INR" -> 3.3513, "KRW" -> 54.4512, "XAF" -> 25.1890, "JOD" -> 0.0324, "ILS" -> 0.1541, "AUD" -> 0.0621, "HKD" -> 0.3562, "MXN" -> 1.0, "XBT" -> 0.00000081112586), + "XBT" -> Map("GBP" -> 44188.118, "EUR" -> 52436.431, "USD" -> 59245.918, "JPY" -> 6805170.8, "AED" -> 217414.47, "INR" -> 4407607.74,"KRW" -> 70131575, "XAF" -> 34353824, "JOD" -> 41960.111, "ILS" -> 182981.21, "AUD" -> 81168.603, "HKD" -> 460448.90, "MXN" -> 1230503.30,"XBT" -> 1.0) ) } @@ -104,7 +116,6 @@ object fx extends MdcLoggable { } else { //logger.debug(s"fromAmount is $fromAmount, toCurrency is ${toCurrency}") val rate: Option[Double] = try { - // Get the translated name out of the map Some(fallbackExchangeRates.get(fromCurrency).get(toCurrency)) } catch { diff --git a/obp-api/src/main/scala/code/model/BankingData.scala b/obp-api/src/main/scala/code/model/BankingData.scala index 07374a734..f6d0a66b2 100644 --- a/obp-api/src/main/scala/code/model/BankingData.scala +++ b/obp-api/src/main/scala/code/model/BankingData.scala @@ -199,6 +199,7 @@ case class BankAccountExtended(val bankAccount: BankAccount) extends MdcLoggable val createdByConsentId = None val createdByUserInvitationId = None val isDeleted = None + val lastMarketingAgreementSignedDate = None }) } else { accountHolders diff --git a/obp-api/src/main/scala/code/model/User.scala b/obp-api/src/main/scala/code/model/User.scala index 316cc2106..0e9e82c19 100644 --- a/obp-api/src/main/scala/code/model/User.scala +++ b/obp-api/src/main/scala/code/model/User.scala @@ -131,7 +131,7 @@ object UserX { } def createResourceUser(provider: String, providerId: Option[String], createdByConsentId: Option[String], name: Option[String], email: Option[String], userId: Option[String], company: Option[String]) = { - Users.users.vend.createResourceUser(provider, providerId, createdByConsentId, name, email, userId, None, company) + Users.users.vend.createResourceUser(provider, providerId, createdByConsentId, name, email, userId, None, company, None) } def createUnsavedResourceUser(provider: String, providerId: Option[String], name: Option[String], email: Option[String], userId: Option[String]) = { @@ -141,6 +141,22 @@ object UserX { def saveResourceUser(ru: ResourceUser) = { Users.users.vend.saveResourceUser(ru) } + + def getOrCreateDauthResourceUser(username: String, provider: String) = { + findByUserName(username).or( //first try to find the user by userId + Users.users.vend.createResourceUser( // Otherwise create a new user + provider = provider, + providerId = Some(username), + None, + name = Some(username), + email = None, + userId = None, + createdByUserInvitationId = None, + company = None, + lastMarketingAgreementSignedDate = None + ) + ) + } //def bulkDeleteAllResourceUsers(): Box[Boolean] = { // Users.users.vend.bulkDeleteAllResourceUsers() diff --git a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala index 43f140839..d888ab655 100644 --- a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala +++ b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala @@ -28,7 +28,7 @@ package code.model.dataAccess import code.UserRefreshes.UserRefreshes import code.accountholders.AccountHolders -import code.api.util.APIUtil.{hasAnOAuthHeader, validatePasswordOnCreation, logger, _} +import code.api.util.APIUtil.{hasAnOAuthHeader, logger, validatePasswordOnCreation, _} import code.api.util.ErrorMessages._ import code.api.util._ import code.api.v4_0_0.dynamic.DynamicEndpointHelper @@ -37,6 +37,7 @@ import code.bankconnectors.Connector import code.context.UserAuthContextProvider import code.entitlement.Entitlement import code.loginattempts.LoginAttempt +import code.token.TokensOpenIDConnect import code.users.Users import code.util.Helper import code.util.Helper.MdcLoggable @@ -139,6 +140,7 @@ class AuthUser extends MegaProtoUser[AuthUser] with MdcLoggable { } /** + * Username is a valid email address or the regex below: * Regex to validate a username * * ^(?=.{8,100}$)(?![_.])(?!.*[_.]{2})[a-zA-Z0-9._]+(? String)(e: String) = e match { case null => List(FieldError(this, Text(msg))) case e if e.trim.isEmpty => List(FieldError(this, Text(msg))) + case e if emailRegex.findFirstMatchIn(e).isDefined => Nil // Email is valid username case e if usernameRegex.findFirstMatchIn(e).isDefined => Nil case _ => List(FieldError(this, Text(msg))) } @@ -484,13 +487,26 @@ import net.liftweb.util.Helpers._ */ def getCurrentUserUsername: String = { getCurrentUser match { - case Full(user) if user.provider.contains("google") => user.emailAddress - case Full(user) if user.provider.contains("yahoo") => user.emailAddress + case Full(user) if user.provider.contains("google") && !user.emailAddress.isEmpty => user.emailAddress + case Full(user) if user.provider.contains("yahoo") && !user.emailAddress.isEmpty => user.emailAddress + case Full(user) if user.provider.contains("microsoft") && !user.emailAddress.isEmpty => user.emailAddress case Full(user) => user.name case _ => "" //TODO need more error handling for different user cases } } + def getIDTokenOfCurrentUser(): String = { + if(APIUtil.getPropsAsBoolValue("openid_connect.show_tokens", false)) { + AuthUser.currentUser match { + case Full(authUser) => + TokensOpenIDConnect.tokens.vend.getOpenIDConnectTokenByAuthUser(authUser.id.get).map(_.idToken).getOrElse("") + case _ => "" + } + } else { + "This information is not allowed at this instance." + } + } + /** * get current user.userId * Note: 1.resourceuser has two ids: id(Long) and userid_(String), @@ -1138,43 +1154,45 @@ def restoreSomeSessions(): Unit = { } def grantEntitlementsToUseDynamicEndpointsInSpaces(user: AuthUser) = { - val createdByProcess = "grantEntitlementsToUseDynamicEndpointsInSpaces" - val userId = user.user.obj.map(_.userId).getOrElse("") + if(emailDomainToSpaceMappings.nonEmpty) { + val createdByProcess = "grantEntitlementsToUseDynamicEndpointsInSpaces" + val userId = user.user.obj.map(_.userId).getOrElse("") - // user's already auto granted entitlements. - val entitlementsGrantedByThisProcess = Entitlement.entitlement.vend.getEntitlementsByUserId(userId) - .map(_.filter(role => role.createdByProcess == createdByProcess)) - .getOrElse(Nil) + // user's already auto granted entitlements. + val entitlementsGrantedByThisProcess = Entitlement.entitlement.vend.getEntitlementsByUserId(userId) + .map(_.filter(role => role.createdByProcess == createdByProcess)) + .getOrElse(Nil) - def alreadyHasEntitlement(role:ApiRole, bankId: String): Boolean = - entitlementsGrantedByThisProcess.exists(entitlement => entitlement.roleName == role.toString() && entitlement.bankId == bankId) + def alreadyHasEntitlement(role:ApiRole, bankId: String): Boolean = + entitlementsGrantedByThisProcess.exists(entitlement => entitlement.roleName == role.toString() && entitlement.bankId == bankId) - //call mySpaces --> get BankIds --> listOfRolesToUseAllDynamicEndpointsAOneBank (at each bank)--> Grant roles (for each role) - val allCurrentDynamicRoleToBankIdPairs: List[(ApiRole, String)] = for { - BankId(bankId) <- mySpaces(user: AuthUser) - role <- DynamicEndpointHelper.listOfRolesToUseAllDynamicEndpointsAOneBank(Some(bankId)) - } yield { - if (!alreadyHasEntitlement(role, bankId)) { - Entitlement.entitlement.vend.addEntitlement(bankId, userId, role.toString, createdByProcess) + //call mySpaces --> get BankIds --> listOfRolesToUseAllDynamicEndpointsAOneBank (at each bank)--> Grant roles (for each role) + val allCurrentDynamicRoleToBankIdPairs: List[(ApiRole, String)] = for { + BankId(bankId) <- mySpaces(user: AuthUser) + role <- DynamicEndpointHelper.listOfRolesToUseAllDynamicEndpointsAOneBank(Some(bankId)) + } yield { + if (!alreadyHasEntitlement(role, bankId)) { + Entitlement.entitlement.vend.addEntitlement(bankId, userId, role.toString, createdByProcess) + } + + role -> bankId } - role -> bankId - } + // if user's auto granted entitlement invalid, delete it. + // invalid happens when some dynamic endpoints are removed, so the entitlements linked to the deleted dynamic endpoints are invalid. + for { + grantedEntitlement <- entitlementsGrantedByThisProcess + grantedEntitlementRoleName = grantedEntitlement.roleName + grantedEntitlementBankId = grantedEntitlement.bankId + } { + val isInValidEntitlement = !allCurrentDynamicRoleToBankIdPairs.exists { roleToBankIdPair => + val(role, roleBankId) = roleToBankIdPair + role.toString() == grantedEntitlementRoleName && roleBankId == grantedEntitlementBankId + } - // if user's auto granted entitlement invalid, delete it. - // invalid happens when some dynamic endpoints are removed, so the entitlements linked to the deleted dynamic endpoints are invalid. - for { - grantedEntitlement <- entitlementsGrantedByThisProcess - grantedEntitlementRoleName = grantedEntitlement.roleName - grantedEntitlementBankId = grantedEntitlement.bankId - } { - val isInValidEntitlement = !allCurrentDynamicRoleToBankIdPairs.exists { roleToBankIdPair => - val(role, roleBankId) = roleToBankIdPair - role.toString() == grantedEntitlementRoleName && roleBankId == grantedEntitlementBankId - } - - if(isInValidEntitlement) { - Entitlement.entitlement.vend.deleteEntitlement(Full(grantedEntitlement)) + if(isInValidEntitlement) { + Entitlement.entitlement.vend.deleteEntitlement(Full(grantedEntitlement)) + } } } } diff --git a/obp-api/src/main/scala/code/model/dataAccess/ResourceUser.scala b/obp-api/src/main/scala/code/model/dataAccess/ResourceUser.scala index 6c8d72159..30d5d8a20 100644 --- a/obp-api/src/main/scala/code/model/dataAccess/ResourceUser.scala +++ b/obp-api/src/main/scala/code/model/dataAccess/ResourceUser.scala @@ -26,6 +26,8 @@ TESOBE (http://www.tesobe.com/) */ package code.model.dataAccess +import java.util.Date + import code.api.util.APIUtil import code.util.MappedUUID import com.openbankproject.commons.model.{User, UserPrimaryKey} @@ -81,6 +83,7 @@ class ResourceUser extends LongKeyedMapper[ResourceUser] with User with ManyToMa object IsDeleted extends MappedBoolean(this) { override def defaultValue = false } + object LastMarketingAgreementSignedDate extends MappedDate(this) def emailAddress = { val e = email.get @@ -109,6 +112,7 @@ class ResourceUser extends LongKeyedMapper[ResourceUser] with User with ManyToMa override def createdByConsentId = if(CreatedByConsentId.get == null) None else if (CreatedByConsentId.get.isEmpty) None else Some(CreatedByConsentId.get) //null --> None override def createdByUserInvitationId = if(CreatedByUserInvitationId.get == null) None else if (CreatedByUserInvitationId.get.isEmpty) None else Some(CreatedByUserInvitationId.get) //null --> None override def isDeleted: Option[Boolean] = if(IsDeleted.jdbcFriendly(IsDeleted.calcFieldName) == null) None else Some(IsDeleted.get) // null --> None + override def lastMarketingAgreementSignedDate: Option[Date] = if(IsDeleted.jdbcFriendly(LastMarketingAgreementSignedDate.calcFieldName) == null) None else Some(LastMarketingAgreementSignedDate.get) // null --> None } object ResourceUser extends ResourceUser with LongKeyedMetaMapper[ResourceUser]{ diff --git a/obp-api/src/main/scala/code/productfee/MappedProductFeeProvider.scala b/obp-api/src/main/scala/code/productfee/MappedProductFeeProvider.scala index 00b5f20e1..8c5e01438 100644 --- a/obp-api/src/main/scala/code/productfee/MappedProductFeeProvider.scala +++ b/obp-api/src/main/scala/code/productfee/MappedProductFeeProvider.scala @@ -1,6 +1,7 @@ package code.productfee import code.api.util.APIUtil +import code.api.util.ErrorMessages.{CreateProductFeeError, UpdateProductFeeError} import code.util.UUIDString import com.openbankproject.commons.model.{BankId, ProductCode, ProductFeeTrait} import net.liftweb.common.{Box, Empty, Full} @@ -54,12 +55,12 @@ object MappedProductFeeProvider extends ProductFeeProvider { .Frequency(frequency) .Type(`type`) .saveMe() - } + } ?~! s"$UpdateProductFeeError" case _ => Empty } } case None => Future { - Full { + tryo { ProductFee .create .ProductFeeId(APIUtil.generateUUID) @@ -73,7 +74,7 @@ object MappedProductFeeProvider extends ProductFeeProvider { .Frequency(frequency) .Type(`type`) .saveMe() - } + } ?~! s"$CreateProductFeeError" } } } @@ -95,7 +96,7 @@ class ProductFee extends ProductFeeTrait with LongKeyedMapper[ProductFee] with I object ProductFeeId extends UUIDString(this) - object Name extends MappedString(this, 50) + object Name extends MappedString(this, 100) object IsActive extends MappedBoolean(this) { override def defaultValue = true diff --git a/obp-api/src/main/scala/code/products/Products.scala b/obp-api/src/main/scala/code/products/Products.scala index e902f8f1d..cef006aa2 100644 --- a/obp-api/src/main/scala/code/products/Products.scala +++ b/obp-api/src/main/scala/code/products/Products.scala @@ -43,7 +43,7 @@ trait ProductsProvider { case Some(products) => { val productsWithLicense = for { // Only return products that have a license set unless its for an admin view - product <- products if (adminView || (product.meta.license.name.size > 3 && product.meta.license.name.size > 3)) + product <- products if (adminView || (product.meta.license.name.size > 3)) } yield product Option(productsWithLicense) } diff --git a/obp-api/src/main/scala/code/remotedata/RemotedataUserAgreement.scala b/obp-api/src/main/scala/code/remotedata/RemotedataUserAgreement.scala index 9960f49a7..bc872e22c 100644 --- a/obp-api/src/main/scala/code/remotedata/RemotedataUserAgreement.scala +++ b/obp-api/src/main/scala/code/remotedata/RemotedataUserAgreement.scala @@ -16,4 +16,7 @@ object RemotedataUserAgreement extends ObpActorInit with UserAgreementProvider { def createUserAgreement(userId: String, agreementType: String, agreementText: String): Box[UserAgreement] = getValueFromFuture( (actor ? cc.createOrUpdateUserAgreement(userId, agreementType, agreementText)).mapTo[Box[UserAgreement]] ) + def getUserAgreement(userId: String, agreementType: String): Box[UserAgreement] = getValueFromFuture( + (actor ? cc.getUserAgreement(userId, agreementType)).mapTo[Box[UserAgreement]] + ) } diff --git a/obp-api/src/main/scala/code/remotedata/RemotedataUserAgreementActor.scala b/obp-api/src/main/scala/code/remotedata/RemotedataUserAgreementActor.scala index cac3ddcb7..0967f7670 100644 --- a/obp-api/src/main/scala/code/remotedata/RemotedataUserAgreementActor.scala +++ b/obp-api/src/main/scala/code/remotedata/RemotedataUserAgreementActor.scala @@ -20,6 +20,10 @@ class RemotedataUserAgreementActor extends Actor with ObpActorHelper with MdcLog logger.debug(s"createUserAgreement($userId, $agreementType, $agreementText)") sender ! (mapper.createUserAgreement(userId, agreementType, agreementText)) + case cc.getUserAgreement(userId: String, agreementType: String) => + logger.debug(s"getUserAgreement($userId, $agreementType)") + sender ! (mapper.getUserAgreement(userId, agreementType)) + case message => logger.warn("[AKKA ACTOR ERROR - REQUEST NOT RECOGNIZED] " + message) } diff --git a/obp-api/src/main/scala/code/remotedata/RemotedataUsers.scala b/obp-api/src/main/scala/code/remotedata/RemotedataUsers.scala index faa3b2a6c..3b28059fb 100644 --- a/obp-api/src/main/scala/code/remotedata/RemotedataUsers.scala +++ b/obp-api/src/main/scala/code/remotedata/RemotedataUsers.scala @@ -1,11 +1,13 @@ package code.remotedata +import java.util.Date + import akka.pattern.ask import code.actorsystem.ObpActorInit import code.api.util.OBPQueryParam import code.entitlement.Entitlement import code.model.dataAccess.ResourceUser -import code.users.{RemotedataUsersCaseClasses, Users} +import code.users.{RemotedataUsersCaseClasses, UserAgreement, Users} import com.openbankproject.commons.model.{User, UserPrimaryKey} import net.liftweb.common.Box @@ -61,6 +63,9 @@ object RemotedataUsers extends ObpActorInit with Users { def getUserByEmailFuture(email : String) : Future[List[(ResourceUser, Box[List[Entitlement]])]] = (actor ? cc.getUserByEmailFuture(email)).mapTo[List[(ResourceUser, Box[List[Entitlement]])]] + + def getUsersByEmail(email : String) : Future[List[(ResourceUser, Box[List[Entitlement]], Option[List[UserAgreement]])]] = + (actor ? cc.getUsersByEmail(email)).mapTo[List[(ResourceUser, Box[List[Entitlement]], Option[List[UserAgreement]])]] def getAllUsers() : Box[List[ResourceUser]] = getValueFromFuture( (actor ? cc.getAllUsers()).mapTo[Box[List[ResourceUser]]] @@ -70,9 +75,14 @@ object RemotedataUsers extends ObpActorInit with Users { val res = (actor ? cc.getAllUsersF(queryParams)) res.mapTo[List[(ResourceUser, Box[List[Entitlement]])]] } + + def getUsers(queryParams: List[OBPQueryParam]): Future[List[(ResourceUser, Box[List[Entitlement]], Option[List[UserAgreement]])]] = { + val res = (actor ? cc.getUsers(queryParams)) + res.mapTo[List[(ResourceUser, Box[List[Entitlement]], Option[List[UserAgreement]])]] + } - def createResourceUser(provider: String, providerId: Option[String], createdByConsentId: Option[String], name: Option[String], email: Option[String], userId: Option[String], createdByUserInvitationId: Option[String], company: Option[String]) : Box[ResourceUser] = getValueFromFuture( - (actor ? cc.createResourceUser(provider, providerId, createdByConsentId, name, email, userId, createdByUserInvitationId, company)).mapTo[Box[ResourceUser]] + def createResourceUser(provider: String, providerId: Option[String], createdByConsentId: Option[String], name: Option[String], email: Option[String], userId: Option[String], createdByUserInvitationId: Option[String], company: Option[String], lastMarketingAgreementSignedDate: Option[Date]) : Box[ResourceUser] = getValueFromFuture( + (actor ? cc.createResourceUser(provider, providerId, createdByConsentId, name, email, userId, createdByUserInvitationId, company, lastMarketingAgreementSignedDate)).mapTo[Box[ResourceUser]] ) def createUnsavedResourceUser(provider: String, providerId: Option[String], name: Option[String], email: Option[String], userId: Option[String]) : Box[ResourceUser] = getValueFromFuture( diff --git a/obp-api/src/main/scala/code/remotedata/RemotedataUsersActor.scala b/obp-api/src/main/scala/code/remotedata/RemotedataUsersActor.scala index a088959ed..5ed768852 100644 --- a/obp-api/src/main/scala/code/remotedata/RemotedataUsersActor.scala +++ b/obp-api/src/main/scala/code/remotedata/RemotedataUsersActor.scala @@ -1,5 +1,7 @@ package code.remotedata +import java.util.Date + import akka.actor.Actor import akka.pattern.pipe import code.actorsystem.ObpActorHelper @@ -70,6 +72,10 @@ class RemotedataUsersActor extends Actor with ObpActorHelper with MdcLoggable { case cc.getUserByEmailFuture(email: String) => logger.debug("getUserByEmailFuture(" + email +")") sender ! (mapper.getUserByEmailF(email)) + + case cc.getUsersByEmail(email: String) => + logger.debug("getUsersByEmail(" + email +")") + mapper.getUsersByEmail(email) pipeTo sender case cc.getAllUsers() => logger.debug("getAllUsers()") @@ -78,10 +84,14 @@ class RemotedataUsersActor extends Actor with ObpActorHelper with MdcLoggable { case cc.getAllUsersF(queryParams: List[OBPQueryParam]) => logger.debug(s"getAllUsersF(queryParams: ($queryParams))") mapper.getAllUsersF(queryParams) pipeTo sender + + case cc.getUsers(queryParams: List[OBPQueryParam]) => + logger.debug(s"getUsers(queryParams: ($queryParams))") + mapper.getUsers(queryParams) pipeTo sender - case cc.createResourceUser(provider: String, providerId: Option[String], createdByConsentId: Option[String], name: Option[String], email: Option[String], userId: Option[String], createdByUserInvitationId: Option[String], company: Option[String]) => - logger.debug("createResourceUser(" + provider + ", " + providerId.getOrElse("None") + ", " + name.getOrElse("None") + ", " + email.getOrElse("None") + ", " + userId.getOrElse("None") + ", " + createdByUserInvitationId.getOrElse("None") + ", " + company.getOrElse("None") + ")") - sender ! (mapper.createResourceUser(provider, providerId, createdByConsentId, name, email, userId, createdByUserInvitationId, company)) + case cc.createResourceUser(provider: String, providerId: Option[String], createdByConsentId: Option[String], name: Option[String], email: Option[String], userId: Option[String], createdByUserInvitationId: Option[String], company: Option[String], lastMarketingAgreementSignedDate: Option[Date]) => + logger.debug("createResourceUser(" + provider + ", " + providerId.getOrElse("None") + ", " + name.getOrElse("None") + ", " + email.getOrElse("None") + ", " + userId.getOrElse("None") + ", " + createdByUserInvitationId.getOrElse("None") + ", " + company.getOrElse("None") + ", " + lastMarketingAgreementSignedDate.getOrElse("None") + ")") + sender ! (mapper.createResourceUser(provider, providerId, createdByConsentId, name, email, userId, createdByUserInvitationId, company, lastMarketingAgreementSignedDate)) case cc.createUnsavedResourceUser(provider: String, providerId: Option[String], name: Option[String], email: Option[String], userId: Option[String]) => logger.debug("createUnsavedResourceUser(" + provider + ", " + providerId.getOrElse("None") + ", " + name.getOrElse("None") + ", " + email.getOrElse("None") + ", " + userId.getOrElse("None") + ")") diff --git a/obp-api/src/main/scala/code/snippet/OpenidConnectInvoke.scala b/obp-api/src/main/scala/code/snippet/OpenidConnectInvoke.scala index 9429a8088..cd08a06ef 100644 --- a/obp-api/src/main/scala/code/snippet/OpenidConnectInvoke.scala +++ b/obp-api/src/main/scala/code/snippet/OpenidConnectInvoke.scala @@ -3,9 +3,12 @@ package code.snippet import java.net.URI import code.api.OpenIdConnectConfig +import code.api.util.APIUtil +import com.nimbusds.jwt.JWT import com.nimbusds.oauth2.sdk.id.{ClientID, State} -import com.nimbusds.oauth2.sdk.{ResponseType, Scope} -import com.nimbusds.openid.connect.sdk.{AuthenticationRequest, Nonce} +import com.nimbusds.oauth2.sdk.pkce.{CodeChallenge, CodeChallengeMethod} +import com.nimbusds.oauth2.sdk.{ResponseMode, ResponseType, Scope} +import com.nimbusds.openid.connect.sdk.{AuthenticationRequest, Display, Nonce, OIDCClaimsRequest, OIDCScopeValue, Prompt} import net.liftweb.common.{Box, Empty, Full, Loggable} import net.liftweb.http.js.{JsCmd, JsCmds} import net.liftweb.http.{S, SHtml, SessionVar} @@ -36,17 +39,61 @@ object OpenidConnectInvoke extends Loggable { // Generate nonce val nonce = new Nonce() + val responseMode = APIUtil.getPropsValue("openid_connect.response_mode", "form_post") match { + case "query" => ResponseMode.QUERY + case "fragment" => ResponseMode.FRAGMENT + case "form_post" => ResponseMode.FORM_POST + case "query.jwt" => ResponseMode.QUERY_JWT + case "fragment.jwt" => ResponseMode.FRAGMENT_JWT + case "form_post.jwt" => ResponseMode.FORM_POST_JWT + case "jwt" => ResponseMode.JWT + case _ => ResponseMode.FORM_POST + } + val responseType = APIUtil.getPropsValue("openid_connect.response_type", "code") match { + case "code" => new ResponseType("code") + case "id_token" => new ResponseType("id_token") + case "code id_token" => new ResponseType("code", "id_token") + case _ => new ResponseType("code") + } + val scope = APIUtil.getPropsValue("openid_connect.scope", "openid email profile") match { + case "openid email profile" => + val scope: Scope = new Scope(); + scope.add(OIDCScopeValue.OPENID); + scope.add(OIDCScopeValue.EMAIL); + scope.add(OIDCScopeValue.PROFILE); + scope + case "openid email" => + val scope: Scope = new Scope(); + scope.add(OIDCScopeValue.OPENID); + scope.add(OIDCScopeValue.EMAIL); + scope + case "openid" => + val scope: Scope = new Scope(); + scope.add(OIDCScopeValue.OPENID); + scope + case _ => + val scope: Scope = new Scope(); + scope.add(OIDCScopeValue.OPENID); + scope.add(OIDCScopeValue.EMAIL); + scope.add(OIDCScopeValue.PROFILE); + scope + } + // Compose the request (in code flow) - val req = - new AuthenticationRequest( - new URI(OpenIdConnectConfig.get(identityProvider).authorization_endpoint), - new ResponseType("code"), - Scope.parse("openid email profile"), - clientID, - callback, - state, - nonce - ) + val req = new AuthenticationRequest( + new URI(OpenIdConnectConfig.get(identityProvider).authorization_endpoint), + responseType, + responseMode, + scope, + clientID, + callback, + state, + nonce, + null, null, -1, null, null, + null, null, null, null.asInstanceOf[OIDCClaimsRequest], null, + null, null, + null, null, + null, false, null) OpenIDConnectSessionState.set(Full(state)) val accessType = if (OpenIdConnectConfig.get(identityProvider).access_type_offline) "&access_type=offline" else "" val redirectTo = req.toHTTPRequest.getURL() + "?" + req.toHTTPRequest.getQuery() + accessType diff --git a/obp-api/src/main/scala/code/snippet/UserInformation.scala b/obp-api/src/main/scala/code/snippet/UserInformation.scala new file mode 100644 index 000000000..0bec120d0 --- /dev/null +++ b/obp-api/src/main/scala/code/snippet/UserInformation.scala @@ -0,0 +1,66 @@ +/** +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 . + +Email: contact@tesobe.com +TESOBE GmbH. +Osloer Strasse 16/17 +Berlin 13359, Germany + +This product includes software developed at +TESOBE (http://www.tesobe.com/) + + */ +package code.snippet + +import code.api.util.ErrorMessages.attemptedToOpenAnEmptyBox +import code.model.dataAccess.AuthUser +import code.util.Helper.MdcLoggable +import com.openbankproject.commons.model.User +import net.liftweb.http.{RequestVar, SHtml} +import net.liftweb.util.CssSel +import net.liftweb.util.Helpers._ + +import scala.xml.NodeSeq + +class UserInformation extends MdcLoggable { + + private object idTokenVar extends RequestVar("") + private object providerVar extends RequestVar("") + private object devEmailVar extends RequestVar("") + private object usernameVar extends RequestVar("") + + def show: CssSel = { + if(!AuthUser.loggedIn_?) { + "*" #> NodeSeq.Empty + } else if (AuthUser.getCurrentUser.isEmpty) { + "*" #> NodeSeq.Empty + } else { + val user: User = AuthUser.getCurrentUser.openOrThrowException(attemptedToOpenAnEmptyBox) + usernameVar.set(user.name) + devEmailVar.set(user.emailAddress) + providerVar.set(user.provider) + idTokenVar.set(AuthUser.getIDTokenOfCurrentUser) + "form" #> { + "#user-info-username" #> SHtml.text(usernameVar, usernameVar(_)) & + "#user-info-provider" #> SHtml.text(providerVar.is, providerVar(_)) & + "#user-info-email" #> SHtml.text(devEmailVar, devEmailVar(_)) & + "#user-info-id-token" #> SHtml.text(idTokenVar, idTokenVar(_)) + } & "#register-consumer-success" #> "" + } + } + +} diff --git a/obp-api/src/main/scala/code/snippet/UserInvitation.scala b/obp-api/src/main/scala/code/snippet/UserInvitation.scala index 94b18e002..5f393d155 100644 --- a/obp-api/src/main/scala/code/snippet/UserInvitation.scala +++ b/obp-api/src/main/scala/code/snippet/UserInvitation.scala @@ -27,8 +27,9 @@ TESOBE (http://www.tesobe.com/) package code.snippet import java.time.{Duration, ZoneId, ZoneOffset, ZonedDateTime} +import java.util.Date -import code.api.util.APIUtil +import code.api.util.{APIUtil, SecureRandomUtil} import code.model.dataAccess.{AuthUser, ResourceUser} import code.users import code.users.{UserAgreementProvider, UserInvitationProvider, Users} @@ -36,9 +37,9 @@ import code.util.Helper import code.util.Helper.MdcLoggable import code.webuiprops.MappedWebUiPropsProvider.getWebUiPropsValue import com.openbankproject.commons.model.User -import net.liftweb.common.{Box, Full} +import net.liftweb.common.{Box, Empty, Failure, Full} import net.liftweb.http.{RequestVar, S, SHtml} -import net.liftweb.util.CssSel +import net.liftweb.util.{CssSel, Helpers} import net.liftweb.util.Helpers._ import scala.collection.immutable.List @@ -53,6 +54,8 @@ class UserInvitation extends MdcLoggable { private object usernameVar extends RequestVar("") private object termsCheckboxVar extends RequestVar(false) private object marketingInfoCheckboxVar extends RequestVar(false) + private object consentForCollectingCheckboxVar extends RequestVar(false) + private object consentForCollectingMandatoryCheckboxVar extends RequestVar(true) private object privacyCheckboxVar extends RequestVar(false) val ttl = APIUtil.getPropsAsLongValue("user_invitation.ttl.seconds", 86400) @@ -61,6 +64,7 @@ class UserInvitation extends MdcLoggable { val privacyConditionsValue: String = getWebUiPropsValue("webui_privacy_policy", "") val termsAndConditionsValue: String = getWebUiPropsValue("webui_terms_and_conditions", "") val termsAndConditionsCheckboxValue: String = getWebUiPropsValue("webui_post_user_invitation_terms_and_conditions_checkbox_value", "I agree to the above Developer Terms and Conditions") + val personalDataCollectionConsentCountryWaiverList = getWebUiPropsValue("personal_data_collection_consent_country_waiver_list", "").split(",").toList.map(_.trim) def registerForm: CssSel = { @@ -76,7 +80,12 @@ class UserInvitation extends MdcLoggable { countryVar.set(userInvitation.map(_.country).getOrElse("None")) // Propose the username only for the first time. In case an end user manually change it we must not override it. if(usernameVar.isEmpty) usernameVar.set(firstNameVar.is.toLowerCase + "." + lastNameVar.is.toLowerCase()) - + if(personalDataCollectionConsentCountryWaiverList.exists(_.toLowerCase == countryVar.is.toLowerCase) == true) { + consentForCollectingMandatoryCheckboxVar.set(false) + } else { + consentForCollectingMandatoryCheckboxVar.set(true) + } + def submitButtonDefense(): Unit = { val verifyingTime = ZonedDateTime.now(ZoneOffset.UTC) val createdAt = userInvitation.map(_.createdAt.get).getOrElse(time(239932800)) @@ -91,39 +100,48 @@ class UserInvitation extends MdcLoggable { else if(Users.users.vend.getUserByUserName(usernameVar.is).isDefined) showErrorsForUsername() else if(privacyCheckboxVar.is == false) showErrorsForPrivacyConditions() else if(termsCheckboxVar.is == false) showErrorsForTermsAndConditions() + else if(personalDataCollectionConsentCountryWaiverList.exists(_.toLowerCase == countryVar.is.toLowerCase) == false && consentForCollectingCheckboxVar.is == false) showErrorsForConsentForCollectingPersonalData() else { // Resource User table createResourceUser( provider = "OBP-User-Invitation", providerId = Some(usernameVar.is), - name = Some(firstNameVar.is + " " + lastNameVar.is), + name = Some(usernameVar.is), email = Some(email), userInvitationId = userInvitation.map(_.userInvitationId).toOption, - company = userInvitation.map(_.company).toOption + company = userInvitation.map(_.company).toOption, + lastMarketingAgreementSignedDate = if(marketingInfoCheckboxVar.is) Some(new Date()) else None ).map{ u => // AuthUser table - createAuthUser(user = u, firstName = firstNameVar.is, lastName = lastNameVar.is, password = "") - // Use Agreement table - UserAgreementProvider.userAgreementProvider.vend.createOrUpdateUserAgreement( - u.userId, "privacy_conditions", privacyConditionsValue) - UserAgreementProvider.userAgreementProvider.vend.createOrUpdateUserAgreement( - u.userId, "terms_and_conditions", termsAndConditionsValue) - UserAgreementProvider.userAgreementProvider.vend.createOrUpdateUserAgreement( - u.userId, "accept_marketing_info", marketingInfoCheckboxVar.is.toString) - // Set the status of the user invitation to "FINISHED" - UserInvitationProvider.userInvitationProvider.vend.updateStatusOfUserInvitation(userInvitation.map(_.userInvitationId).getOrElse(""), "FINISHED") - // Set a new password - val resetLink = AuthUser.passwordResetUrl(u.idGivenByProvider, u.emailAddress, u.userId) + "?action=set" - S.redirectTo(resetLink) + createAuthUser(user = u, firstName = firstNameVar.is, lastName = lastNameVar.is) match { + case Failure(msg,_,_) => + Users.users.vend.deleteResourceUser(u.id.get) + showError(msg) + case _ => + // User Agreement table + UserAgreementProvider.userAgreementProvider.vend.createOrUpdateUserAgreement( + u.userId, "privacy_conditions", privacyConditionsValue) + UserAgreementProvider.userAgreementProvider.vend.createOrUpdateUserAgreement( + u.userId, "terms_and_conditions", termsAndConditionsValue) + UserAgreementProvider.userAgreementProvider.vend.createOrUpdateUserAgreement( + u.userId, "accept_marketing_info", marketingInfoCheckboxVar.is.toString) + UserAgreementProvider.userAgreementProvider.vend.createOrUpdateUserAgreement( + u.userId, "consent_for_collecting_personal_data", consentForCollectingCheckboxVar.is.toString) + // Set the status of the user invitation to "FINISHED" + UserInvitationProvider.userInvitationProvider.vend.updateStatusOfUserInvitation(userInvitation.map(_.userInvitationId).getOrElse(""), "FINISHED") + // Set a new password + val resetLink = AuthUser.passwordResetUrl(u.idGivenByProvider, u.emailAddress, u.userId) + "?action=set" + S.redirectTo(resetLink) + } } } } def showError(usernameError: String) = { - S.error("register-consumer-errors", usernameError) + S.error("data-area-errors", usernameError) register & - "#register-consumer-errors *" #> { + "#data-area-errors *" #> { ".error *" #> List(usernameError).map({ e => ".errorContent *" #> e @@ -149,6 +167,9 @@ class UserInvitation extends MdcLoggable { def showErrorsForPrivacyConditions() = { showError(Helper.i18n("privacy.conditions.are.not.selected")) } + def showErrorsForConsentForCollectingPersonalData() = { + showError(Helper.i18n("consent.to.collect.personal.data.is.not.selected")) + } def register = { "form" #> { @@ -161,9 +182,11 @@ class UserInvitation extends MdcLoggable { "#privacy_checkbox" #> SHtml.checkbox(privacyCheckboxVar, privacyCheckboxVar(_)) & "#terms_checkbox" #> SHtml.checkbox(termsCheckboxVar, termsCheckboxVar(_)) & "#marketing_info_checkbox" #> SHtml.checkbox(marketingInfoCheckboxVar, marketingInfoCheckboxVar(_)) & + "#consent_for_collecting_checkbox" #> SHtml.checkbox(consentForCollectingCheckboxVar, consentForCollectingCheckboxVar(_), "id" -> "consent_for_collecting_checkbox") & + "#consent_for_collecting_mandatory" #> SHtml.checkbox(consentForCollectingMandatoryCheckboxVar, consentForCollectingMandatoryCheckboxVar(_), "id" -> "consent_for_collecting_mandatory", "hidden" -> "true") & "type=submit" #> SHtml.submit(s"$registrationConsumerButtonValue", () => submitButtonDefense) } & - "#register-consumer-success" #> "" + "#data-area-success" #> "" } userInvitation match { case Full(payload) if payload.status == "CREATED" => // All good @@ -181,18 +204,24 @@ class UserInvitation extends MdcLoggable { register } - private def createAuthUser(user: User, firstName: String, lastName: String, password: String): Box[AuthUser] = tryo { + private def createAuthUser(user: User, firstName: String, lastName: String): Box[AuthUser] = { val newUser = AuthUser.create .firstName(firstName) .lastName(lastName) .email(user.emailAddress) .user(user.userPrimaryKey.value) - .username(user.idGivenByProvider) + .username(user.name) .provider(user.provider) - .password(password) + .password(SecureRandomUtil.alphanumeric(10)) .validated(true) - // Save the user - newUser.saveMe() + newUser.validate match { + case Nil => + // Save the user + Full(newUser.saveMe()) + case xs => S.error(xs) + Failure(xs.map(i => i.msg).mkString(";")) + } + } private def createResourceUser(provider: String, @@ -200,7 +229,8 @@ class UserInvitation extends MdcLoggable { name: Option[String], email: Option[String], userInvitationId: Option[String], - company: Option[String] + company: Option[String], + lastMarketingAgreementSignedDate: Option[Date], ): Box[ResourceUser] = { Users.users.vend.createResourceUser( provider = provider, @@ -210,7 +240,8 @@ class UserInvitation extends MdcLoggable { email = email, userId = None, createdByUserInvitationId = userInvitationId, - company = company + company = company, + lastMarketingAgreementSignedDate = lastMarketingAgreementSignedDate ) } diff --git a/obp-api/src/main/scala/code/snippet/WebUI.scala b/obp-api/src/main/scala/code/snippet/WebUI.scala index a60c57fe9..a3581422d 100644 --- a/obp-api/src/main/scala/code/snippet/WebUI.scala +++ b/obp-api/src/main/scala/code/snippet/WebUI.scala @@ -142,7 +142,23 @@ class WebUI extends MdcLoggable{ "#main-showcases *" #> scala.xml.Unparsed(sdksHtmlContent) } + val mainFaqHtmlLink = getWebUiPropsValue("webui_main_faq_external_link","") + + val mainFaqHtmlContent = try{ + if (mainFaqHtmlLink.isEmpty)//If the webui_featured_sdks_external_link is not set, we will read the internal sdks.html file instead. + LiftRules.getResource("/main-faq.html").map{ url => + Source.fromURL(url, "UTF-8").mkString + }.openOrThrowException("Please check the content of this file: src/main/webapp/main-faq.html") + else + Source.fromURL(mainFaqHtmlLink, "UTF-8").mkString + }catch { + case _ : Throwable => "

FAQs is wrong, please check the props `webui_main_faq_external_link`

" + } + // webui_featured_sdks_external_link props, we can set the sdks here. check the `SDK Showcases` in Homepage, and you can see all the sdks. + def mainFaqHtml: CssSel = { + "#main-faq *" #> scala.xml.Unparsed(mainFaqHtmlContent) + } val brandString = activeBrand match { case Some(v) => s"&brand=$v" @@ -177,6 +193,10 @@ class WebUI extends MdcLoggable{ def apiTesterLink: CssSel = { ".api-tester-link a [href]" #> scala.xml.Unparsed(getWebUiPropsValue("webui_api_tester_url", "")) } + // Link to Hola app + def apiHolaLink: CssSel = { + ".api-hola-link a [href]" #> scala.xml.Unparsed(getWebUiPropsValue("webui_api_hola_url", "#")) + } // Link to API def apiLink: CssSel = { @@ -325,7 +345,7 @@ class WebUI extends MdcLoggable{ // Support platform link def supportPlatformLink: CssSel = { - val supportplatformlink = scala.xml.Unparsed(getWebUiPropsValue("webui_support_platform_url", "https://slack.openbankproject.com/")) + val supportplatformlink = scala.xml.Unparsed(getWebUiPropsValue("webui_support_platform_url", "https://chat.openbankproject.com")) ".support-platform-link a [href]" #> supportplatformlink & ".support-platform-link a *" #> supportplatformlink.toString().replace("https://","").replace("http://", "") } diff --git a/obp-api/src/main/scala/code/token/MappedOpenIDConnectToken.scala b/obp-api/src/main/scala/code/token/MappedOpenIDConnectToken.scala index cf651db72..dcf1a73eb 100644 --- a/obp-api/src/main/scala/code/token/MappedOpenIDConnectToken.scala +++ b/obp-api/src/main/scala/code/token/MappedOpenIDConnectToken.scala @@ -1,5 +1,7 @@ package code.token +import java.util.Date + import net.liftweb.common.Box import net.liftweb.mapper._ @@ -9,7 +11,8 @@ object MappedOpenIDConnectTokensProvider extends OpenIDConnectTokensProvider { idToken: String, refreshToken: String, scope: String, - expiresIn: Long): Box[OpenIDConnectToken] = Box.tryo { + expiresIn: Long, + authUserPrimaryKey: Long): Box[OpenIDConnectToken] = Box.tryo { OpenIDConnectToken.create .TokenType(tokenType.toString()) .AccessToken(accessToken) @@ -17,8 +20,12 @@ object MappedOpenIDConnectTokensProvider extends OpenIDConnectTokensProvider { .RefreshToken(refreshToken) .Scope(scope) .ExpiresIn(expiresIn) + .AuthUserPrimaryKey(authUserPrimaryKey) .saveMe() } + def getOpenIDConnectTokenByAuthUser(authUserPrimaryKey: Long) = + OpenIDConnectToken.findAll(By(OpenIDConnectToken.AuthUserPrimaryKey, authUserPrimaryKey)) + .sortBy(_.createdAt.get)(Ordering[Date].reverse).headOption } @@ -31,6 +38,7 @@ class OpenIDConnectToken extends OpenIDConnectTokenTrait with LongKeyedMapper[Op object Scope extends MappedString(this, 250) object TokenType extends MappedString(this, 250) object ExpiresIn extends MappedLong(this) + object AuthUserPrimaryKey extends MappedLong(this) override def accessToken: String = AccessToken.get override def idToken: String = IDToken.get @@ -38,6 +46,7 @@ class OpenIDConnectToken extends OpenIDConnectTokenTrait with LongKeyedMapper[Op override def scope: String = Scope.get override def tokenType: String = TokenType.get override def expiresIn: Long = ExpiresIn.get + override def authUserPrimaryKey: Long = AuthUserPrimaryKey.get } diff --git a/obp-api/src/main/scala/code/token/OpenIDConnectTokenProvider.scala b/obp-api/src/main/scala/code/token/OpenIDConnectTokenProvider.scala index efd6bffa3..8b99c7f80 100644 --- a/obp-api/src/main/scala/code/token/OpenIDConnectTokenProvider.scala +++ b/obp-api/src/main/scala/code/token/OpenIDConnectTokenProvider.scala @@ -14,7 +14,10 @@ trait OpenIDConnectTokensProvider { idToken: String, refreshToken: String, scope: String, - expiresIn: Long): Box[OpenIDConnectToken] + expiresIn: Long, + authUserPrimaryKey: Long): Box[OpenIDConnectToken] + + def getOpenIDConnectTokenByAuthUser(authUserPrimaryKey: Long): Box[OpenIDConnectToken] } trait OpenIDConnectTokenTrait { @@ -24,4 +27,5 @@ trait OpenIDConnectTokenTrait { def scope: String def tokenType: String def expiresIn: Long + def authUserPrimaryKey: Long } diff --git a/obp-api/src/main/scala/code/users/LiftUsers.scala b/obp-api/src/main/scala/code/users/LiftUsers.scala index 99c1168e0..73e759889 100644 --- a/obp-api/src/main/scala/code/users/LiftUsers.scala +++ b/obp-api/src/main/scala/code/users/LiftUsers.scala @@ -1,5 +1,7 @@ package code.users +import java.util.Date + import code.api.util._ import code.entitlement.Entitlement import code.loginattempts.LoginAttempt.maxBadLoginAttempts @@ -60,7 +62,8 @@ object LiftUsers extends Users with MdcLoggable{ email = email, userId = None, createdByUserInvitationId = None, - company = None + company = None, + lastMarketingAgreementSignedDate = None ) (newUser, true) } @@ -111,6 +114,25 @@ object LiftUsers extends Users with MdcLoggable{ (user, Entitlement.entitlement.vend.getEntitlementsByUserId(user.userId).map(_.sortWith(_.roleName < _.roleName))) } } + + override def getUsersByEmail(email: String): Future[List[(ResourceUser, Box[List[Entitlement]], Option[List[UserAgreement]])]] = Future { + val users = ResourceUser.findAll(By(ResourceUser.email, email)) + for { + user <- users + } yield { + val entitlements = Entitlement.entitlement.vend.getEntitlementsByUserId(user.userId).map(_.sortWith(_.roleName < _.roleName)) + // val agreements = getUserAgreements(user) + (user, entitlements, None) + } + } + + private def getUserAgreements(user: ResourceUser) = { + val acceptMarketingInfo = UserAgreementProvider.userAgreementProvider.vend.getUserAgreement(user.userId, "accept_marketing_info") + val termsAndConditions = UserAgreementProvider.userAgreementProvider.vend.getUserAgreement(user.userId, "terms_and_conditions") + val privacyConditions = UserAgreementProvider.userAgreementProvider.vend.getUserAgreement(user.userId, "privacy_conditions") + val agreements = acceptMarketingInfo.toList ::: termsAndConditions.toList ::: privacyConditions.toList + agreements + } override def getUserByEmailFuture(email: String): Future[List[(ResourceUser, Box[List[Entitlement]])]] = { Future { @@ -123,11 +145,21 @@ object LiftUsers extends Users with MdcLoggable{ } override def getAllUsersF(queryParams: List[OBPQueryParam]): Future[List[(ResourceUser, Box[List[Entitlement]])]] = { - + Future { + for { + user <- getUsersCommon(queryParams) + } yield { + (user, Entitlement.entitlement.vend.getEntitlementsByUserId(user.userId).map(_.sortWith(_.roleName < _.roleName))) + } + } + } + + + private def getUsersCommon(queryParams: List[OBPQueryParam]) = { val limit = queryParams.collect { case OBPLimit(value) => MaxRows[ResourceUser](value) }.headOption val offset: Option[StartAt[ResourceUser]] = queryParams.collect { case OBPOffset(value) => StartAt[ResourceUser](value) }.headOption val locked: Option[String] = queryParams.collect { case OBPLockedStatus(value) => value }.headOption - val deleted = queryParams.collect { + val deleted = queryParams.collect { case OBPIsDeleted(value) if value == true => // ?is_deleted=true By(ResourceUser.IsDeleted, true) case OBPIsDeleted(value) if value == false => // ?is_deleted=false @@ -135,13 +167,14 @@ object LiftUsers extends Users with MdcLoggable{ }.headOption.orElse( Some(By(ResourceUser.IsDeleted, false)) // There is no query parameter "is_deleted" ) - + val optionalParams: Seq[QueryParam[ResourceUser]] = Seq(limit.toSeq, offset.toSeq, deleted.toSeq).flatten - + def getAllResourceUsers(): List[ResourceUser] = ResourceUser.findAll(optionalParams: _*) + val showUsers: List[ResourceUser] = locked.map(_.toLowerCase()) match { case Some("active") => - val lockedUsers: immutable.Seq[MappedBadLoginAttempt] = + val lockedUsers: immutable.Seq[MappedBadLoginAttempt] = MappedBadLoginAttempt.findAll( By_>(MappedBadLoginAttempt.mBadAttemptsSinceLastSuccessOrReset, maxBadLoginAttempts.toInt) ) @@ -157,11 +190,17 @@ object LiftUsers extends Users with MdcLoggable{ case _ => getAllResourceUsers() } + showUsers + } + + override def getUsers(queryParams: List[OBPQueryParam]): Future[List[(ResourceUser, Box[List[Entitlement]], Option[List[UserAgreement]])]] = { Future { for { - user <- showUsers + user <- getUsersCommon(queryParams) } yield { - (user, Entitlement.entitlement.vend.getEntitlementsByUserId(user.userId).map(_.sortWith(_.roleName < _.roleName))) + val entitlements = Entitlement.entitlement.vend.getEntitlementsByUserId(user.userId).map(_.sortWith(_.roleName < _.roleName)) + // val agreements = getUserAgreements(user) + (user, entitlements, None) } } } @@ -174,7 +213,8 @@ object LiftUsers extends Users with MdcLoggable{ email: Option[String], userId: Option[String], createdByUserInvitationId: Option[String], - company: Option[String]): Box[ResourceUser] = { + company: Option[String], + lastMarketingAgreementSignedDate: Option[Date]): Box[ResourceUser] = { val ru = ResourceUser.create ru.provider_(provider) providerId match { @@ -205,6 +245,10 @@ object LiftUsers extends Users with MdcLoggable{ case Some(v) => ru.Company(v) case None => } + lastMarketingAgreementSignedDate match { + case Some(v) => ru.LastMarketingAgreementSignedDate(v) + case None => + } Full(ru.saveMe()) } diff --git a/obp-api/src/main/scala/code/users/UserAgreement.scala b/obp-api/src/main/scala/code/users/UserAgreement.scala index 9dc3439bd..19a643279 100644 --- a/obp-api/src/main/scala/code/users/UserAgreement.scala +++ b/obp-api/src/main/scala/code/users/UserAgreement.scala @@ -43,6 +43,12 @@ object MappedUserAgreementProvider extends UserAgreementProvider { .saveMe() ) } + override def getUserAgreement(userId: String, agreementType: String): Box[UserAgreement] = { + UserAgreement.findAll( + By(UserAgreement.UserId, userId), + By(UserAgreement.AgreementType, agreementType) + ).sortBy(_.Date.get)(Ordering[Date].reverse).headOption + } } class UserAgreement extends UserAgreementTrait with LongKeyedMapper[UserAgreement] with IdPK with CreatedUpdated { @@ -64,6 +70,7 @@ class UserAgreement extends UserAgreementTrait with LongKeyedMapper[UserAgreemen override def agreementType: String = AgreementType.get override def agreementText: String = AgreementText.get override def agreementHash: String = AgreementHash.get + override def date: Date = Date.get } object UserAgreement extends UserAgreement with LongKeyedMetaMapper[UserAgreement] { diff --git a/obp-api/src/main/scala/code/users/UserAgreementProvider.scala b/obp-api/src/main/scala/code/users/UserAgreementProvider.scala index 85d57dbb2..30c9a049e 100644 --- a/obp-api/src/main/scala/code/users/UserAgreementProvider.scala +++ b/obp-api/src/main/scala/code/users/UserAgreementProvider.scala @@ -1,5 +1,7 @@ package code.users +import java.util.Date + import code.api.util.APIUtil import code.remotedata.RemotedataUserAgreement import net.liftweb.common.Box @@ -21,11 +23,13 @@ object UserAgreementProvider extends SimpleInjector { trait UserAgreementProvider { def createOrUpdateUserAgreement(userId: String, agreementType: String, agreementText: String): Box[UserAgreement] def createUserAgreement(userId: String, agreementType: String, agreementText: String): Box[UserAgreement] + def getUserAgreement(userId: String, agreementType: String): Box[UserAgreement] } class RemotedataUserAgreementProviderCaseClass { case class createOrUpdateUserAgreement(userId: String, agreementType: String, agreementText: String) case class createUserAgreement(userId: String, agreementType: String, agreementText: String) + case class getUserAgreement(userId: String, agreementType: String) } object RemotedataUserAgreementProviderCaseClass extends RemotedataUserAgreementProviderCaseClass @@ -36,4 +40,5 @@ trait UserAgreementTrait { def agreementType: String def agreementText: String def agreementHash: String + def date: Date } \ No newline at end of file diff --git a/obp-api/src/main/scala/code/users/Users.scala b/obp-api/src/main/scala/code/users/Users.scala index 771d37a78..d6b446199 100644 --- a/obp-api/src/main/scala/code/users/Users.scala +++ b/obp-api/src/main/scala/code/users/Users.scala @@ -1,5 +1,7 @@ package code.users +import java.util.Date + import code.api.util.{APIUtil, OBPQueryParam} import code.entitlement.Entitlement import code.model.dataAccess.ResourceUser @@ -46,12 +48,23 @@ trait Users { def getUserByEmail(email: String) : Box[List[ResourceUser]] def getUserByEmailFuture(email: String) : Future[List[(ResourceUser, Box[List[Entitlement]])]] + def getUsersByEmail(email: String) : Future[List[(ResourceUser, Box[List[Entitlement]], Option[List[UserAgreement]])]] def getAllUsers() : Box[List[ResourceUser]] def getAllUsersF(queryParams: List[OBPQueryParam]) : Future[List[(ResourceUser, Box[List[Entitlement]])]] - def createResourceUser(provider: String, providerId: Option[String], createdByConsentId: Option[String], name: Option[String], email: Option[String], userId: Option[String], createdByUserInvitationId: Option[String], company: Option[String]) : Box[ResourceUser] + def getUsers(queryParams: List[OBPQueryParam]): Future[List[(ResourceUser, Box[List[Entitlement]], Option[List[UserAgreement]])]] + + def createResourceUser(provider: String, + providerId: Option[String], + createdByConsentId: Option[String], + name: Option[String], + email: Option[String], + userId: Option[String], + createdByUserInvitationId: Option[String], + company: Option[String], + lastMarketingAgreementSignedDate: Option[Date]) : Box[ResourceUser] def createUnsavedResourceUser(provider: String, providerId: Option[String], name: Option[String], email: Option[String], userId: Option[String]) : Box[ResourceUser] @@ -78,9 +91,11 @@ class RemotedataUsersCaseClasses { case class getUserByUserNameFuture(userName : String) case class getUserByEmail(email : String) case class getUserByEmailFuture(email : String) + case class getUsersByEmail(email : String) case class getAllUsers() case class getAllUsersF(queryParams: List[OBPQueryParam]) - case class createResourceUser(provider: String, providerId: Option[String],createdByConsentId: Option[String], name: Option[String], email: Option[String], userId: Option[String], createdByUserInvitationId: Option[String], company: Option[String]) + case class getUsers(queryParams: List[OBPQueryParam]) + case class createResourceUser(provider: String, providerId: Option[String],createdByConsentId: Option[String], name: Option[String], email: Option[String], userId: Option[String], createdByUserInvitationId: Option[String], company: Option[String], lastMarketingAgreementSignedDate: Option[Date]) case class createUnsavedResourceUser(provider: String, providerId: Option[String], name: Option[String], email: Option[String], userId: Option[String]) case class saveResourceUser(resourceUser: ResourceUser) case class deleteResourceUser(userId: Long) diff --git a/obp-api/src/main/scala/code/yearlycustomercharges/MappedYearlyChargeProvider.scala b/obp-api/src/main/scala/code/yearlycustomercharges/MappedYearlyChargeProvider.scala index 8e47ba47c..592d9997d 100644 --- a/obp-api/src/main/scala/code/yearlycustomercharges/MappedYearlyChargeProvider.scala +++ b/obp-api/src/main/scala/code/yearlycustomercharges/MappedYearlyChargeProvider.scala @@ -1,76 +1,76 @@ -package code.yearlycustomercharges - -import code.util.UUIDString -import com.openbankproject.commons.model.{BankId, CustomerId} -import net.liftweb.common.Box -import net.liftweb.mapper._ - - - -object MappedYearlyChargeProvider extends YearlyChargeProvider { - -// override protected def getYearlyChargeFromProvider(thingId: YearlyChargeId): Option[YearlyCharge] = -// MappedYearlyCharge.find(By(MappedYearlyCharge.thingId_, thingId.value)) - - override protected def getYearlyChargesFromProvider(bankId: BankId, customerId: CustomerId, year: Int): Option[List[YearlyCharge]] = { - Some(MappedYearlyCharge.findAll(By(MappedYearlyCharge.bankId_, bankId.value), By(MappedYearlyCharge.customerId_, customerId.value))) - } -} - -class MappedYearlyCharge extends YearlyCharge with LongKeyedMapper[MappedYearlyCharge] with IdPK { - - override def getSingleton = MappedYearlyCharge - - object bankId_ extends UUIDString(this) - object customerId_ extends UUIDString(this) - - object year_ extends MappedInt(this) - - - - //override def yearlyChargeId: YearlyChargeId = YearlyChargeId(id.get) - override def year: Int = year_.get - - - // override def getSingleton = MappedYearlyCustomerCharge - // - // WIP - // object mCustomerNumber extends MappedString(this,123) - // - // object mYear extends MappedInt(this) - // - // object mCategoryId extends UUIDString(this) - // object mForcastIndictor extends MappedString(this,123) - // object mTypeId extends MappedString(this,123) - // object mNatureId extends UUIDString(this) - // - // - // object mCharge_Currency extends MappedString(this,3) - // object mCharge_Amount extends MappedString(this,32) - // - // object mUpdateDate extends MappedDateTime(this) - // - // - // //override def bankId: String = mBankId.get - // override def customerId: String = mCustomerId.get // id.toString - // override def customerNumber: String = mCustomerNumber.get - // override def year: Integer = mYear.get - // - // override def categoryId: String = mCategoryId.get - // override def forcastIndictor: String = mForcastIndictor.get - // override def typeId: String = mTypeId.get - // override def natureId : String = mNatureId.get - // - // override def charge: AmountOfMoney = AmountOfMoney(mCharge_Currency.get, mCharge_Amount.get) - // override def updateDate : Date = mUpdateDate.get - - - - -} - - -object MappedYearlyCharge extends MappedYearlyCharge with LongKeyedMetaMapper[MappedYearlyCharge] { - override def dbIndexes = UniqueIndex(bankId_, customerId_, year_) :: Index(bankId_) :: super.dbIndexes -} - +//package code.yearlycustomercharges +// +//import code.util.UUIDString +//import com.openbankproject.commons.model.{BankId, CustomerId} +//import net.liftweb.common.Box +//import net.liftweb.mapper._ +// +// +// +//object MappedYearlyChargeProvider extends YearlyChargeProvider { +// +//// override protected def getYearlyChargeFromProvider(thingId: YearlyChargeId): Option[YearlyCharge] = +//// MappedYearlyCharge.find(By(MappedYearlyCharge.thingId_, thingId.value)) +// +// override protected def getYearlyChargesFromProvider(bankId: BankId, customerId: CustomerId, year: Int): Option[List[YearlyCharge]] = { +// Some(MappedYearlyCharge.findAll(By(MappedYearlyCharge.bankId_, bankId.value), By(MappedYearlyCharge.customerId_, customerId.value))) +// } +//} +// +//class MappedYearlyCharge extends YearlyCharge with LongKeyedMapper[MappedYearlyCharge] with IdPK { +// +// override def getSingleton = MappedYearlyCharge +// +// object bankId_ extends UUIDString(this) +// object customerId_ extends UUIDString(this) +// +// object year_ extends MappedInt(this) +// +// +// +// //override def yearlyChargeId: YearlyChargeId = YearlyChargeId(id.get) +// override def year: Int = year_.get +// +// +// // override def getSingleton = MappedYearlyCustomerCharge +// // +// // WIP +// // object mCustomerNumber extends MappedString(this,123) +// // +// // object mYear extends MappedInt(this) +// // +// // object mCategoryId extends UUIDString(this) +// // object mForcastIndictor extends MappedString(this,123) +// // object mTypeId extends MappedString(this,123) +// // object mNatureId extends UUIDString(this) +// // +// // +// // object mCharge_Currency extends MappedString(this,3) +// // object mCharge_Amount extends MappedString(this,32) +// // +// // object mUpdateDate extends MappedDateTime(this) +// // +// // +// // //override def bankId: String = mBankId.get +// // override def customerId: String = mCustomerId.get // id.toString +// // override def customerNumber: String = mCustomerNumber.get +// // override def year: Integer = mYear.get +// // +// // override def categoryId: String = mCategoryId.get +// // override def forcastIndictor: String = mForcastIndictor.get +// // override def typeId: String = mTypeId.get +// // override def natureId : String = mNatureId.get +// // +// // override def charge: AmountOfMoney = AmountOfMoney(mCharge_Currency.get, mCharge_Amount.get) +// // override def updateDate : Date = mUpdateDate.get +// +// +// +// +//} +// +// +//object MappedYearlyCharge extends MappedYearlyCharge with LongKeyedMetaMapper[MappedYearlyCharge] { +// override def dbIndexes = UniqueIndex(bankId_, customerId_, year_) :: Index(bankId_) :: super.dbIndexes +//} +// diff --git a/obp-api/src/main/scala/code/yearlycustomercharges/YearlyCharge.scala b/obp-api/src/main/scala/code/yearlycustomercharges/YearlyCharge.scala index 986b81e42..92d55fe54 100644 --- a/obp-api/src/main/scala/code/yearlycustomercharges/YearlyCharge.scala +++ b/obp-api/src/main/scala/code/yearlycustomercharges/YearlyCharge.scala @@ -1,71 +1,71 @@ -package code.yearlycustomercharges - -import code.api.util.APIUtil -import com.openbankproject.commons.model.{BankId, CustomerId} -import net.liftweb.common.Logger -import net.liftweb.util.SimpleInjector - -object YearlyCharge extends SimpleInjector { - - val yearlyChargeProvider = new Inject(buildOne _) {} - - - // This determines the provider we use - def buildOne: YearlyChargeProvider = - APIUtil.getPropsValue("provider.thing").openOr("mapped") match { - case "mapped" => MappedYearlyChargeProvider - case _ => MappedYearlyChargeProvider - } - -} - -case class YearlyChargeId(value : String) - -// WIP -trait YearlyCharge { - def year : Int - - // def customerNumber : String - // - // - // def categoryId : String - // def forcastIndictor : String - // def typeId : String - // def natureId : String - // def charge : AmountOfMoney - // def updateDate : Date - - - -} - - - - -trait YearlyChargeProvider { - - private val logger = Logger(classOf[YearlyChargeProvider]) - - - /* - Common logic for returning or changing Things - Datasource implementation details are in Thing provider - */ - final def getYearlyCharges(bankId : BankId, customerId: CustomerId, year: Int) : Option[List[YearlyCharge]] = { - getYearlyChargesFromProvider(bankId, customerId, year) match { - case Some(things) => { - - val certainThings = for { - thing <- things // if filter etc. if need be - } yield thing - Option(certainThings) - } - case None => None - } - } - - - protected def getYearlyChargesFromProvider(bank : BankId, customerId: CustomerId, year: Int) : Option[List[YearlyCharge]] - -} - +//package code.yearlycustomercharges +// +//import code.api.util.APIUtil +//import com.openbankproject.commons.model.{BankId, CustomerId} +//import net.liftweb.common.Logger +//import net.liftweb.util.SimpleInjector +// +//object YearlyCharge extends SimpleInjector { +// +// val yearlyChargeProvider = new Inject(buildOne _) {} +// +// +// // This determines the provider we use +// def buildOne: YearlyChargeProvider = +// APIUtil.getPropsValue("provider.thing").openOr("mapped") match { +// case "mapped" => MappedYearlyChargeProvider +// case _ => MappedYearlyChargeProvider +// } +// +//} +// +//case class YearlyChargeId(value : String) +// +//// WIP +//trait YearlyCharge { +// def year : Int +// +// // def customerNumber : String +// // +// // +// // def categoryId : String +// // def forcastIndictor : String +// // def typeId : String +// // def natureId : String +// // def charge : AmountOfMoney +// // def updateDate : Date +// +// +// +//} +// +// +// +// +//trait YearlyChargeProvider { +// +// private val logger = Logger(classOf[YearlyChargeProvider]) +// +// +// /* +// Common logic for returning or changing Things +// Datasource implementation details are in Thing provider +// */ +// final def getYearlyCharges(bankId : BankId, customerId: CustomerId, year: Int) : Option[List[YearlyCharge]] = { +// getYearlyChargesFromProvider(bankId, customerId, year) match { +// case Some(things) => { +// +// val certainThings = for { +// thing <- things // if filter etc. if need be +// } yield thing +// Option(certainThings) +// } +// case None => None +// } +// } +// +// +// protected def getYearlyChargesFromProvider(bank : BankId, customerId: CustomerId, year: Int) : Option[List[YearlyCharge]] +// +//} +// diff --git a/obp-api/src/main/scripts/sql/cre_views.sql b/obp-api/src/main/scripts/sql/cre_views.sql index 82dcc1c46..16cdd14ef 100644 --- a/obp-api/src/main/scripts/sql/cre_views.sql +++ b/obp-api/src/main/scripts/sql/cre_views.sql @@ -1,6 +1,14 @@ drop view v_resource_user cascade; -create or replace view v_resource_user as select userid_ resource_user_id, name_ username, email, id numeric_resource_user_id, provider_ provider, providerid provider_id from resourceuser; +create or replace view v_resource_user as +select +userid_ resource_user_id, +name_ username, +email, +id numeric_resource_user_id, +provider_ provider, +providerid provider_id +from resourceuser; drop view v_auth_user cascade; create view v_auth_user as diff --git a/obp-api/src/main/webapp/consumer-registration.html b/obp-api/src/main/webapp/consumer-registration.html index 0bdd9878a..68641846d 100644 --- a/obp-api/src/main/webapp/consumer-registration.html +++ b/obp-api/src/main/webapp/consumer-registration.html @@ -104,8 +104,8 @@ Berlin 13359, Germany
The signing algorithm name of request object and client_assertion. - Reference 6.1. Passing a Request Object by Value - and 9. Client Authentication + Reference 6.1. Passing a Request Object by Value + and 9. Client Authentication
@@ -125,7 +125,7 @@ Berlin 13359, Germany
@@ -137,8 +137,8 @@ Berlin 13359, Germany
- Content of jwks_uri. jwks_uri and jwks should not both have value at the same time. - Reference 10.1.1. Rotation of Asymmetric Signing Keys + Content of jwks_uri. jwks_uri and jwks should not both have value at the same time. + Reference 10.1.1. Rotation of Asymmetric Signing Keys
@@ -217,7 +217,7 @@ Berlin 13359, Germany
Dummy Users' Direct Login Tokens
- +
Direct Login Endpoint
diff --git a/obp-api/src/main/webapp/index.html b/obp-api/src/main/webapp/index.html index 30660d88d..4a5944c90 100644 --- a/obp-api/src/main/webapp/index.html +++ b/obp-api/src/main/webapp/index.html @@ -255,166 +255,7 @@ Berlin 13359, Germany
-
-

FAQs

- -
-
- -
-
- -
-
- -
-
- -
- -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
+
@@ -447,9 +288,9 @@ Berlin 13359, Germany
@@ -484,6 +325,8 @@ Berlin 13359, Germany href="">OBP CLI API Tester + Hola diff --git a/obp-api/src/main/webapp/main-faq.html b/obp-api/src/main/webapp/main-faq.html new file mode 100644 index 000000000..769160174 --- /dev/null +++ b/obp-api/src/main/webapp/main-faq.html @@ -0,0 +1,160 @@ +

FAQs

+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
diff --git a/obp-api/src/main/webapp/media/css/api-documentation-content.css b/obp-api/src/main/webapp/media/css/api-documentation-content.css index 0f87dab5a..1c0dffa6e 100644 --- a/obp-api/src/main/webapp/media/css/api-documentation-content.css +++ b/obp-api/src/main/webapp/media/css/api-documentation-content.css @@ -48,7 +48,7 @@ font-size: 16px; line-height: 24px; font-weight:normal; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; /*paragraph-height: 12px*/ } @@ -58,12 +58,12 @@ line-height: 24px; font-weight:normal; margin: 8px 0; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; /*paragraph-height: 12px*/ } .container #api_documentation_content a{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; diff --git a/obp-api/src/main/webapp/media/css/authorise.css b/obp-api/src/main/webapp/media/css/authorise.css index 60a886e0a..28fe6ef16 100644 --- a/obp-api/src/main/webapp/media/css/authorise.css +++ b/obp-api/src/main/webapp/media/css/authorise.css @@ -5,7 +5,7 @@ } #authorise h1 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 28px; color: #333333; line-height: 36px; @@ -18,7 +18,7 @@ #login-form-password{ margin-bottom: 8px; margin-top: 32px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -31,7 +31,7 @@ float: right; height: 44px; width: 92px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; text-align: center; line-height: 24px; @@ -73,7 +73,7 @@ } #authorise #authorise-recover-password a, #authorise #oauth-authorise-recover-password a{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 14px; color: #333333; letter-spacing: 0; @@ -84,7 +84,7 @@ #authorise .login-or { margin-top: 30px; float: left; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 18px; color: #929292; letter-spacing: 0; @@ -102,7 +102,7 @@ color: red; } #authorise #thanks-h1 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 28px; color: #333333; letter-spacing: 0; @@ -110,7 +110,7 @@ } #authorise #thanks-detail{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -142,7 +142,7 @@ #authorise #login-form-username-error, #authorise #login-form-password-error{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 14px; color: #333333; line-height: 20px; diff --git a/obp-api/src/main/webapp/media/css/cookies-consent.css b/obp-api/src/main/webapp/media/css/cookies-consent.css index 6ce328364..5a865c0af 100644 --- a/obp-api/src/main/webapp/media/css/cookies-consent.css +++ b/obp-api/src/main/webapp/media/css/cookies-consent.css @@ -1,6 +1,6 @@ #cookies-consent { background: #252525; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; display:none; text-align: center; @@ -17,7 +17,7 @@ position: absolute; left: 100px; top: 32px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #FFFFFF; line-height: 24px; @@ -30,7 +30,7 @@ position: absolute; left: 100px; top: 80px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #FFFFFF; text-align: center; diff --git a/obp-api/src/main/webapp/media/css/data-area.css b/obp-api/src/main/webapp/media/css/data-area.css new file mode 100644 index 000000000..e729672e7 --- /dev/null +++ b/obp-api/src/main/webapp/media/css/data-area.css @@ -0,0 +1,151 @@ +#data-area { + background-color: #53C4EF; + padding: 20px 10px; + color: white; + margin: 30px 0 90px; +} + +#data-area #data-area-explanation, +#data-area form, +#data-area-errors, +#data-area-success { + max-width: 610px; + margin: 0 auto; + color: #333333; +} +#data-area-errors span{ + color: black; +} +#data-area h1 { + font-family: Roboto-Light,sans-serif; + font-size: 28px; + color: #333333; + letter-spacing: 0; + line-height: 36px; + text-align: left; + margin-bottom: 32px; + font-weight: normal; +} + +#data-area #data-area-explanation p:nth-child(2) { + font-family: Roboto-Light,sans-serif; + font-size: 22px; + color: #333333; + letter-spacing: 0; + line-height: 31px; + margin-bottom: 8px; + font-weight: normal; + text-align: left; +} + +#data-area #data-area-explanation p:last-child{ + margin-top: 8px; + font-family: Roboto-Regular,sans-serif; + font-size: 14px; + color: #333333; + letter-spacing: 0; + line-height: 20px; + margin-bottom: 32px; +} + +#data-area-input form label{ + margin-bottom: 8px; + margin-top: 17px; + font-family: Roboto-Regular,sans-serif; + font-size: 16px; + color: #333333; + line-height: 24px; +} + +#data-area-input form select{ + height: 44px; + border: 1px solid #767676; + -webkit-appearance: none; + -webkit-border-radius: 0px; +} + +#data-area #data-area-explanation { + margin-top: 32px; + margin-bottom: 20px; + color: #333333; + padding: 0 15px; +} + +#data-area textarea { + height: 96px; + border: 1px solid #767676; + border-radius: 0px; +} +#data-area #data-area-errors { + margin-bottom: 20px; +} +#data-area #data-area-success { + margin-top: 30px; +} + +#data-area #data-area-success h1 { + padding-left: 15px; +} + +#data-area #data-area-success #data-area-success-message{ + font-family: Roboto-Light,sans-serif; + font-size: 22px; + color: #333333; + letter-spacing: 0; + line-height: 31px; + padding-left: 15px; +} + + +#data-area #data-area-success p { + font-family: Roboto-Light,sans-serif; + font-size: 22px; + color: #333333; + letter-spacing: 0; + margin-bottom: 44px; + line-height: 31px; + padding-left: 15px; +} + +#data-area-success a{ + text-decoration:underline; +} + + +#data-area-success span, +#data-area-success a{ + font-family: Roboto-Regular,sans-serif; + font-size: 16px; + color: #333333; + line-height: 24px; +} +#data-area #data-area-success .row { + margin-bottom: 20px; +} +#data-area #data-area-success .row div:nth-child(1) { + font-family: Roboto-Medium,sans-serif; + font-size: 16px; + color: #333333; + line-height: 24px; +} +#data-area-input form .btn-danger{ + margin-left: 0; + margin-top: 33px; +} + +#data-area-input #data-area-errors-div{ + margin-top: 8px; +} + +#data-area-input #data-area-errors{ + font-family: Roboto-Regular,sans-serif; + font-size: 14px; + color: #333333; + line-height: 20px; + background-image: url(/media/images/icons/status_error_onlight.svg); + background-repeat:no-repeat; + background-position: left 12px; + background-size: 18px 15.70px; + padding-top: 8px; + padding-left: 26px; +} \ No newline at end of file diff --git a/obp-api/src/main/webapp/media/css/fonts.css b/obp-api/src/main/webapp/media/css/fonts.css index d95c71b25..3d35bb34a 100644 --- a/obp-api/src/main/webapp/media/css/fonts.css +++ b/obp-api/src/main/webapp/media/css/fonts.css @@ -1,17 +1,17 @@ /*https://fonts.google.com/specimen/Roboto?selection.family=Roboto&sidebar.open=true*/ /*We use the Roboto font as the OBP default font*/ @font-face { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; src: url(../font/Roboto-Light.ttf); } @font-face { - font-family: Roboto-Medium; + font-family: Roboto-Medium,sans-serif; src: url(../font/Roboto-Medium.ttf); } @font-face { - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; src: url(../font/Roboto-Regular.ttf); } diff --git a/obp-api/src/main/webapp/media/css/footer.css b/obp-api/src/main/webapp/media/css/footer.css index f6069c310..738c9cb51 100644 --- a/obp-api/src/main/webapp/media/css/footer.css +++ b/obp-api/src/main/webapp/media/css/footer.css @@ -9,7 +9,7 @@ footer a,span, footer a:hover, footer a:focus, span:hover { - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 14px; color: #FFFFFF; letter-spacing: 0; diff --git a/obp-api/src/main/webapp/media/css/get-started.css b/obp-api/src/main/webapp/media/css/get-started.css index 45ab63d64..3e22c1ee7 100644 --- a/obp-api/src/main/webapp/media/css/get-started.css +++ b/obp-api/src/main/webapp/media/css/get-started.css @@ -28,13 +28,13 @@ margin-bottom: 17px; } #main-get-started .main-get-started-text p{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; } #main-get-started .main-get-started-text p a{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -64,7 +64,7 @@ } #main-get-started .btn-primary a{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #FFFFFF; text-align: center; diff --git a/obp-api/src/main/webapp/media/css/main-apis.css b/obp-api/src/main/webapp/media/css/main-apis.css index 1270314f6..db29c59f4 100644 --- a/obp-api/src/main/webapp/media/css/main-apis.css +++ b/obp-api/src/main/webapp/media/css/main-apis.css @@ -30,7 +30,7 @@ margin-left: 7.2px; } #main-apis .main-apis-text p{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -59,7 +59,7 @@ background-color: #EDEDED; } #main-apis .btn-secondary a{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; text-align: center; diff --git a/obp-api/src/main/webapp/media/css/main-faq.css b/obp-api/src/main/webapp/media/css/main-faq.css index cb07ec399..5a07aa450 100644 --- a/obp-api/src/main/webapp/media/css/main-faq.css +++ b/obp-api/src/main/webapp/media/css/main-faq.css @@ -17,7 +17,7 @@ } #main-faq h3 { - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -35,7 +35,7 @@ } #main-faq p, #main-faq ol{ margin-top: 18px; - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -44,7 +44,7 @@ #main-faq div a{ margin-top: 10px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -57,7 +57,7 @@ padding: 0; border: 1px solid transparent; background-color: #FFFFFF; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; diff --git a/obp-api/src/main/webapp/media/css/main-showcases.css b/obp-api/src/main/webapp/media/css/main-showcases.css index 06e5122c6..27b4fa9cc 100644 --- a/obp-api/src/main/webapp/media/css/main-showcases.css +++ b/obp-api/src/main/webapp/media/css/main-showcases.css @@ -16,7 +16,7 @@ display: inline-block; margin: 30px 40px; width: 300px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; } @@ -24,7 +24,7 @@ #main-showcases p { color: white; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; line-height: 24px; } diff --git a/obp-api/src/main/webapp/media/css/main-start.css b/obp-api/src/main/webapp/media/css/main-start.css index 474ff9ea2..37ab3844c 100644 --- a/obp-api/src/main/webapp/media/css/main-start.css +++ b/obp-api/src/main/webapp/media/css/main-start.css @@ -28,7 +28,7 @@ padding: 64px 0; } #main-start #main-start_building .btn-default a{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; text-align: center; diff --git a/obp-api/src/main/webapp/media/css/main-support.css b/obp-api/src/main/webapp/media/css/main-support.css index 39b2b4b9e..8652c2db5 100644 --- a/obp-api/src/main/webapp/media/css/main-support.css +++ b/obp-api/src/main/webapp/media/css/main-support.css @@ -14,7 +14,7 @@ margin-bottom: 9px ; } .main-support-item a { - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; diff --git a/obp-api/src/main/webapp/media/css/nav.css b/obp-api/src/main/webapp/media/css/nav.css index 420a0707e..435129e14 100644 --- a/obp-api/src/main/webapp/media/css/nav.css +++ b/obp-api/src/main/webapp/media/css/nav.css @@ -25,7 +25,7 @@ nav .navbar-collapse.collapse { } .navbar-default .navbar-nav > li > a { background-color: white; - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 18px; color: #333333; line-height: 31px; @@ -53,7 +53,7 @@ nav .navbar-collapse.collapse { margin-right: 0; } .navbar-default .navbar-right .navitem p #register-link { - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -79,7 +79,7 @@ nav .navbar-collapse.collapse { .navbar-default .navbar-right .navbar-btn #loggedIn-username { margin-right: 24px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -130,7 +130,7 @@ nav .navbar-collapse.collapse { } #small-screen-navbar #small-nav-log-on-button .login{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 12px; color: #FFFFFF; letter-spacing: 0; @@ -157,7 +157,7 @@ nav .navbar-collapse.collapse { .sidebar a { text-decoration: none; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; diff --git a/obp-api/src/main/webapp/media/css/obp-toastr.css b/obp-api/src/main/webapp/media/css/obp-toastr.css index e167758ef..79f73a5db 100644 --- a/obp-api/src/main/webapp/media/css/obp-toastr.css +++ b/obp-api/src/main/webapp/media/css/obp-toastr.css @@ -26,7 +26,7 @@ #toast-container .toast-title { float: left; font-weight: bold; - font-family: Roboto-Medium; + font-family: Roboto-Medium,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -43,7 +43,7 @@ } #toast-container .toast-message { - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; diff --git a/obp-api/src/main/webapp/media/css/recover-password.css b/obp-api/src/main/webapp/media/css/recover-password.css index bf24d8a7f..d3ba071c8 100644 --- a/obp-api/src/main/webapp/media/css/recover-password.css +++ b/obp-api/src/main/webapp/media/css/recover-password.css @@ -1,6 +1,6 @@ #recover-password h1 { margin-top: 52px; - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 35px; color: #333333; text-align: center; @@ -23,7 +23,7 @@ border: 0; height: 44px; width: 92px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; text-align: center; line-height: 24px; @@ -50,7 +50,6 @@ margin-top: 20px; display: block; width: 100%; - height: 34px; padding: 6px 12px; font-size: 14px; line-height: 1.42857143; diff --git a/obp-api/src/main/webapp/media/css/register-consumer.css b/obp-api/src/main/webapp/media/css/register-consumer.css index c4f5dafa4..476560036 100644 --- a/obp-api/src/main/webapp/media/css/register-consumer.css +++ b/obp-api/src/main/webapp/media/css/register-consumer.css @@ -17,7 +17,7 @@ color: black; } #register-consumer h1 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 28px; color: #333333; letter-spacing: 0; @@ -28,7 +28,7 @@ } #register-consumer #register-consumer-explanation p:nth-child(2) { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 22px; color: #333333; letter-spacing: 0; @@ -40,7 +40,7 @@ #register-consumer #register-consumer-explanation p:last-child{ margin-top: 8px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 14px; color: #333333; letter-spacing: 0; @@ -51,7 +51,7 @@ #register-consumer-input form label{ margin-bottom: 8px; margin-top: 17px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -88,7 +88,7 @@ } #register-consumer #register-consumer-success #register-consumer-success-message{ - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 22px; color: #333333; letter-spacing: 0; @@ -98,7 +98,7 @@ #register-consumer #register-consumer-success p { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 22px; color: #333333; letter-spacing: 0; @@ -114,7 +114,7 @@ #register-consumer-success span, #register-consumer-success a{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -123,7 +123,7 @@ margin-bottom: 20px; } #register-consumer #register-consumer-success .row div:nth-child(1) { - font-family: Roboto-Medium; + font-family: Roboto-Medium,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -144,7 +144,7 @@ #register-consumer-input #consumer-registration-app-description-error, #register-consumer-input #consumer-registration-app-developer-error, #register-consumer-input #register-consumer-errors{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 14px; color: #333333; line-height: 20px; @@ -157,7 +157,7 @@ } #dummy-user-tokens .col-xs-12:last-child{ - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 20px; color: #333333; line-height: 24px; diff --git a/obp-api/src/main/webapp/media/css/reset.css b/obp-api/src/main/webapp/media/css/reset.css index 1fb79bec2..805a1106a 100644 --- a/obp-api/src/main/webapp/media/css/reset.css +++ b/obp-api/src/main/webapp/media/css/reset.css @@ -22,7 +22,7 @@ time, mark, audio, video { font: inherit; font-size: 100%; vertical-align: baseline; - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; } /* HTML5 display-role reset for older browsers */ article, aside, details, figcaption, figure, diff --git a/obp-api/src/main/webapp/media/css/responsive.css b/obp-api/src/main/webapp/media/css/responsive.css index 9d5037105..bd94bc6cc 100644 --- a/obp-api/src/main/webapp/media/css/responsive.css +++ b/obp-api/src/main/webapp/media/css/responsive.css @@ -55,7 +55,7 @@ display: block; } h1 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 33px; line-height: 40px; color: #333333; @@ -64,21 +64,21 @@ } h2 { font-size: 28px; - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; line-height: 36px; color: #333333; font-weight: lighter; text-align: center; } h3 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 23px; color: #333333; line-height: 30px; font-weight: lighter; } h4 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 19px; color: #333333; line-height: 27px; @@ -204,7 +204,7 @@ @media only screen and (min-width: 760px) and (max-width: 959px) { h1 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 33px; line-height: 40px; color: #333333; @@ -213,21 +213,21 @@ } h2 { font-size: 28px; - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; line-height: 36px; color: #333333; font-weight: lighter; text-align: center; } h3 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 23px; color: #333333; line-height: 15px; font-weight: lighter; } h4 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 19px; color: #333333; line-height: 27px; @@ -319,7 +319,7 @@ @media only screen and (min-width: 960px) and (max-width: 1279px) { h1 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 44px; color: #333333; letter-spacing: 0; @@ -327,7 +327,7 @@ } h2 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 35px; color: #333333; text-align: center; @@ -335,7 +335,7 @@ } h3 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 28px; color: #333333; line-height: 36px; @@ -387,7 +387,7 @@ @media only screen and (min-width: 1280px) { h1 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 44px; color: #333333; letter-spacing: 0; @@ -395,7 +395,7 @@ } h2 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 35px; color: #333333; text-align: center; @@ -403,7 +403,7 @@ } h3 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 28px; color: #333333; line-height: 36px; diff --git a/obp-api/src/main/webapp/media/css/signup.css b/obp-api/src/main/webapp/media/css/signup.css index 35f2bb396..9ecba169b 100644 --- a/obp-api/src/main/webapp/media/css/signup.css +++ b/obp-api/src/main/webapp/media/css/signup.css @@ -10,7 +10,7 @@ } #signup form h1 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 28px; color: #333333; letter-spacing: 0; @@ -19,7 +19,7 @@ } #signup form span{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -32,7 +32,7 @@ #signup form #repeat-password{ margin-top: 32px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -43,7 +43,7 @@ } #signup form label{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -138,7 +138,7 @@ font-size: 16px; } #signup-agree-terms #signup-agree-terms-content { - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; letter-spacing: 0; @@ -149,9 +149,8 @@ } #signup #signup-submit input { - color: #FFF; background-color: #53C4EF; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #FFFFFF; text-align: center; @@ -223,7 +222,7 @@ margin-bottom: 0px; } #signup-legal-notice{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 14px; color: #333333; letter-spacing: 0; diff --git a/obp-api/src/main/webapp/media/css/website.css b/obp-api/src/main/webapp/media/css/website.css index 0bd4ea9b1..8eace0157 100644 --- a/obp-api/src/main/webapp/media/css/website.css +++ b/obp-api/src/main/webapp/media/css/website.css @@ -8,6 +8,7 @@ @import url(/media/css/main-start.css); @import url(/media/css/authorise.css); @import url(/media/css/register-consumer.css); +@import url(/media/css/data-area.css); @import url(/media/css/signup.css); @import url(/media/css/recover-password.css); @import url(/media/css/main-showcases.css); @@ -18,7 +19,7 @@ @import url(/media/css/fonts.css); html { - font-family: "Roboto-Regular"; + font-family: Roboto-Regular,sans-serif; height: 100% } @@ -30,7 +31,7 @@ body { } a { - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; color: #333333; text-decoration: none; } @@ -132,7 +133,7 @@ header #lift__noticesContainer__ { } .btn-danger { - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #FFFFFF; text-align: center; @@ -163,8 +164,7 @@ header #lift__noticesContainer__ { padding-bottom: 11px; padding-left: 20px; padding-right: 20px; - font-family: Roboto-Regular; - font-size: 16px; + font-family: Roboto-Regular,sans-serif; color: #333333; text-align: center; line-height: 24px; @@ -212,7 +212,7 @@ input[type="text"]{ } .select2-results__option { padding: 6px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -223,7 +223,7 @@ input[type="text"]{ } .select2-container--default .select2-selection--single .select2-selection__rendered { - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -274,7 +274,7 @@ input[type="text"]{ } body { - font-family: "Roboto-Light"; + font-family: Roboto-Light,sans-serif; } header #header-decoration, @@ -284,6 +284,7 @@ header #header-decoration, #signup, #recover-password, #register-consumer, +#data-area , #create-account, .navbar-default .navbar-toggle:hover, .navbar-default .navbar-toggle:focus, @@ -298,7 +299,6 @@ header #header-decoration, min-width: 43%; padding: 6%; padding-top: 6%; - padding-right: 6%; padding-bottom: 6%; padding-left: 6%; padding-right: 10%; @@ -424,9 +424,8 @@ input{ #add-user-auth-context-update-request-div #identifier-error .error, #confirm-user-auth-context-update-request-div #otp-value-error .error{ - color: black; background-color: white; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; diff --git a/obp-api/src/main/webapp/media/js/select2.js b/obp-api/src/main/webapp/media/js/select2.js deleted file mode 100644 index 7e1eabc58..000000000 --- a/obp-api/src/main/webapp/media/js/select2.js +++ /dev/null @@ -1,6153 +0,0 @@ -/*! - * Select2 4.1.0-beta.1 - * https://select2.github.io - * - * Released under the MIT license - * https://github.com/select2/select2/blob/master/LICENSE.md - */ -;(function (factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['jquery'], factory); - } else if (typeof module === 'object' && module.exports) { - // Node/CommonJS - module.exports = function (root, jQuery) { - if (jQuery === undefined) { - // require('jQuery') returns a factory that requires window to - // build a jQuery instance, we normalize how we use modules - // that require this pattern but the window provided is a noop - // if it's defined (how jquery works) - if (typeof window !== 'undefined') { - jQuery = require('jquery'); - } - else { - jQuery = require('jquery')(root); - } - } - factory(jQuery); - return jQuery; - }; - } else { - // Browser globals - factory(jQuery); - } -} (function (jQuery) { - // This is needed so we can catch the AMD loader configuration and use it - // The inner file should be wrapped (by `banner.start.js`) in a function that - // returns the AMD loader references. - var S2 =(function () { - // Restore the Select2 AMD loader so it can be used - // Needed mostly in the language files, where the loader is not inserted - if (jQuery && jQuery.fn && jQuery.fn.select2 && jQuery.fn.select2.amd) { - var S2 = jQuery.fn.select2.amd; - } - var S2;(function () { if (!S2 || !S2.requirejs) { - if (!S2) { S2 = {}; } else { require = S2; } - /** - * @license almond 0.3.3 Copyright jQuery Foundation and other contributors. - * Released under MIT license, http://github.com/requirejs/almond/LICENSE - */ -//Going sloppy to avoid 'use strict' string cost, but strict practices should -//be followed. - /*global setTimeout: false */ - - var requirejs, require, define; - (function (undef) { - var main, req, makeMap, handlers, - defined = {}, - waiting = {}, - config = {}, - defining = {}, - hasOwn = Object.prototype.hasOwnProperty, - aps = [].slice, - jsSuffixRegExp = /\.js$/; - - function hasProp(obj, prop) { - return hasOwn.call(obj, prop); - } - - /** - * Given a relative module name, like ./something, normalize it to - * a real name that can be mapped to a path. - * @param {String} name the relative name - * @param {String} baseName a real name that the name arg is relative - * to. - * @returns {String} normalized name - */ - function normalize(name, baseName) { - var nameParts, nameSegment, mapValue, foundMap, lastIndex, - foundI, foundStarMap, starI, i, j, part, normalizedBaseParts, - baseParts = baseName && baseName.split("/"), - map = config.map, - starMap = (map && map['*']) || {}; - - //Adjust any relative paths. - if (name) { - name = name.split('/'); - lastIndex = name.length - 1; - - // If wanting node ID compatibility, strip .js from end - // of IDs. Have to do this here, and not in nameToUrl - // because node allows either .js or non .js to map - // to same file. - if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) { - name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, ''); - } - - // Starts with a '.' so need the baseName - if (name[0].charAt(0) === '.' && baseParts) { - //Convert baseName to array, and lop off the last part, - //so that . matches that 'directory' and not name of the baseName's - //module. For instance, baseName of 'one/two/three', maps to - //'one/two/three.js', but we want the directory, 'one/two' for - //this normalization. - normalizedBaseParts = baseParts.slice(0, baseParts.length - 1); - name = normalizedBaseParts.concat(name); - } - - //start trimDots - for (i = 0; i < name.length; i++) { - part = name[i]; - if (part === '.') { - name.splice(i, 1); - i -= 1; - } else if (part === '..') { - // If at the start, or previous value is still .., - // keep them so that when converted to a path it may - // still work when converted to a path, even though - // as an ID it is less than ideal. In larger point - // releases, may be better to just kick out an error. - if (i === 0 || (i === 1 && name[2] === '..') || name[i - 1] === '..') { - - } else if (i > 0) { - name.splice(i - 1, 2); - i -= 2; - } - } - } - //end trimDots - - name = name.join('/'); - } - - //Apply map config if available. - if ((baseParts || starMap) && map) { - nameParts = name.split('/'); - - for (i = nameParts.length; i > 0; i -= 1) { - nameSegment = nameParts.slice(0, i).join("/"); - - if (baseParts) { - //Find the longest baseName segment match in the config. - //So, do joins on the biggest to smallest lengths of baseParts. - for (j = baseParts.length; j > 0; j -= 1) { - mapValue = map[baseParts.slice(0, j).join('/')]; - - //baseName segment has config, find if it has one for - //this name. - if (mapValue) { - mapValue = mapValue[nameSegment]; - if (mapValue) { - //Match, update name to the new value. - foundMap = mapValue; - foundI = i; - break; - } - } - } - } - - if (foundMap) { - break; - } - - //Check for a star map match, but just hold on to it, - //if there is a shorter segment match later in a matching - //config, then favor over this star map. - if (!foundStarMap && starMap && starMap[nameSegment]) { - foundStarMap = starMap[nameSegment]; - starI = i; - } - } - - if (!foundMap && foundStarMap) { - foundMap = foundStarMap; - foundI = starI; - } - - if (foundMap) { - nameParts.splice(0, foundI, foundMap); - name = nameParts.join('/'); - } - } - - return name; - } - - function makeRequire(relName, forceSync) { - return function () { - //A version of a require function that passes a moduleName - //value for items that may need to - //look up paths relative to the moduleName - var args = aps.call(arguments, 0); - - //If first arg is not require('string'), and there is only - //one arg, it is the array form without a callback. Insert - //a null so that the following concat is correct. - if (typeof args[0] !== 'string' && args.length === 1) { - args.push(null); - } - return req.apply(undef, args.concat([relName, forceSync])); - }; - } - - function makeNormalize(relName) { - return function (name) { - return normalize(name, relName); - }; - } - - function makeLoad(depName) { - return function (value) { - defined[depName] = value; - }; - } - - function callDep(name) { - if (hasProp(waiting, name)) { - var args = waiting[name]; - delete waiting[name]; - defining[name] = true; - main.apply(undef, args); - } - - if (!hasProp(defined, name) && !hasProp(defining, name)) { - throw new Error('No ' + name); - } - return defined[name]; - } - - //Turns a plugin!resource to [plugin, resource] - //with the plugin being undefined if the name - //did not have a plugin prefix. - function splitPrefix(name) { - var prefix, - index = name ? name.indexOf('!') : -1; - if (index > -1) { - prefix = name.substring(0, index); - name = name.substring(index + 1, name.length); - } - return [prefix, name]; - } - - //Creates a parts array for a relName where first part is plugin ID, - //second part is resource ID. Assumes relName has already been normalized. - function makeRelParts(relName) { - return relName ? splitPrefix(relName) : []; - } - - /** - * Makes a name map, normalizing the name, and using a plugin - * for normalization if necessary. Grabs a ref to plugin - * too, as an optimization. - */ - makeMap = function (name, relParts) { - var plugin, - parts = splitPrefix(name), - prefix = parts[0], - relResourceName = relParts[1]; - - name = parts[1]; - - if (prefix) { - prefix = normalize(prefix, relResourceName); - plugin = callDep(prefix); - } - - //Normalize according - if (prefix) { - if (plugin && plugin.normalize) { - name = plugin.normalize(name, makeNormalize(relResourceName)); - } else { - name = normalize(name, relResourceName); - } - } else { - name = normalize(name, relResourceName); - parts = splitPrefix(name); - prefix = parts[0]; - name = parts[1]; - if (prefix) { - plugin = callDep(prefix); - } - } - - //Using ridiculous property names for space reasons - return { - f: prefix ? prefix + '!' + name : name, //fullName - n: name, - pr: prefix, - p: plugin - }; - }; - - function makeConfig(name) { - return function () { - return (config && config.config && config.config[name]) || {}; - }; - } - - handlers = { - require: function (name) { - return makeRequire(name); - }, - exports: function (name) { - var e = defined[name]; - if (typeof e !== 'undefined') { - return e; - } else { - return (defined[name] = {}); - } - }, - module: function (name) { - return { - id: name, - uri: '', - exports: defined[name], - config: makeConfig(name) - }; - } - }; - - main = function (name, deps, callback, relName) { - var cjsModule, depName, ret, map, i, relParts, - args = [], - callbackType = typeof callback, - usingExports; - - //Use name if no relName - relName = relName || name; - relParts = makeRelParts(relName); - - //Call the callback to define the module, if necessary. - if (callbackType === 'undefined' || callbackType === 'function') { - //Pull out the defined dependencies and pass the ordered - //values to the callback. - //Default to [require, exports, module] if no deps - deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps; - for (i = 0; i < deps.length; i += 1) { - map = makeMap(deps[i], relParts); - depName = map.f; - - //Fast path CommonJS standard dependencies. - if (depName === "require") { - args[i] = handlers.require(name); - } else if (depName === "exports") { - //CommonJS module spec 1.1 - args[i] = handlers.exports(name); - usingExports = true; - } else if (depName === "module") { - //CommonJS module spec 1.1 - cjsModule = args[i] = handlers.module(name); - } else if (hasProp(defined, depName) || - hasProp(waiting, depName) || - hasProp(defining, depName)) { - args[i] = callDep(depName); - } else if (map.p) { - map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {}); - args[i] = defined[depName]; - } else { - throw new Error(name + ' missing ' + depName); - } - } - - ret = callback ? callback.apply(defined[name], args) : undefined; - - if (name) { - //If setting exports via "module" is in play, - //favor that over return value and exports. After that, - //favor a non-undefined return value over exports use. - if (cjsModule && cjsModule.exports !== undef && - cjsModule.exports !== defined[name]) { - defined[name] = cjsModule.exports; - } else if (ret !== undef || !usingExports) { - //Use the return value from the function. - defined[name] = ret; - } - } - } else if (name) { - //May just be an object definition for the module. Only - //worry about defining if have a module name. - defined[name] = callback; - } - }; - - requirejs = require = req = function (deps, callback, relName, forceSync, alt) { - if (typeof deps === "string") { - if (handlers[deps]) { - //callback in this case is really relName - return handlers[deps](callback); - } - //Just return the module wanted. In this scenario, the - //deps arg is the module name, and second arg (if passed) - //is just the relName. - //Normalize module name, if it contains . or .. - return callDep(makeMap(deps, makeRelParts(callback)).f); - } else if (!deps.splice) { - //deps is a config object, not an array. - config = deps; - if (config.deps) { - req(config.deps, config.callback); - } - if (!callback) { - return; - } - - if (callback.splice) { - //callback is an array, which means it is a dependency list. - //Adjust args if there are dependencies - deps = callback; - callback = relName; - relName = null; - } else { - deps = undef; - } - } - - //Support require(['a']) - callback = callback || function () {}; - - //If relName is a function, it is an errback handler, - //so remove it. - if (typeof relName === 'function') { - relName = forceSync; - forceSync = alt; - } - - //Simulate async callback; - if (forceSync) { - main(undef, deps, callback, relName); - } else { - //Using a non-zero value because of concern for what old browsers - //do, and latest browsers "upgrade" to 4 if lower value is used: - //http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#dom-windowtimers-settimeout: - //If want a value immediately, use require('id') instead -- something - //that works in almond on the global level, but not guaranteed and - //unlikely to work in other AMD implementations. - setTimeout(function () { - main(undef, deps, callback, relName); - }, 4); - } - - return req; - }; - - /** - * Just drops the config on the floor, but returns req in case - * the config return value is used. - */ - req.config = function (cfg) { - return req(cfg); - }; - - /** - * Expose module registry for debugging and tooling - */ - requirejs._defined = defined; - - define = function (name, deps, callback) { - if (typeof name !== 'string') { - throw new Error('See almond README: incorrect module build, no module name'); - } - - //This module may not have dependencies - if (!deps.splice) { - //deps is not an array, so probably means - //an object literal or factory function for - //the value. Adjust args. - callback = deps; - deps = []; - } - - if (!hasProp(defined, name) && !hasProp(waiting, name)) { - waiting[name] = [name, deps, callback]; - } - }; - - define.amd = { - jQuery: true - }; - }()); - - S2.requirejs = requirejs;S2.require = require;S2.define = define; - } - }()); - S2.define("almond", function(){}); - - /* global jQuery:false, $:false */ - S2.define('jquery',[],function () { - var _$ = jQuery || $; - - if (_$ == null && console && console.error) { - console.error( - 'Select2: An instance of jQuery or a jQuery-compatible library was not ' + - 'found. Make sure that you are including jQuery before Select2 on your ' + - 'web page.' - ); - } - - return _$; - }); - - S2.define('select2/utils',[ - 'jquery' - ], function ($) { - var Utils = {}; - - Utils.Extend = function (ChildClass, SuperClass) { - var __hasProp = {}.hasOwnProperty; - - function BaseConstructor () { - this.constructor = ChildClass; - } - - for (var key in SuperClass) { - if (__hasProp.call(SuperClass, key)) { - ChildClass[key] = SuperClass[key]; - } - } - - BaseConstructor.prototype = SuperClass.prototype; - ChildClass.prototype = new BaseConstructor(); - ChildClass.__super__ = SuperClass.prototype; - - return ChildClass; - }; - - function getMethods (theClass) { - var proto = theClass.prototype; - - var methods = []; - - for (var methodName in proto) { - var m = proto[methodName]; - - if (typeof m !== 'function') { - continue; - } - - if (methodName === 'constructor') { - continue; - } - - methods.push(methodName); - } - - return methods; - } - - Utils.Decorate = function (SuperClass, DecoratorClass) { - var decoratedMethods = getMethods(DecoratorClass); - var superMethods = getMethods(SuperClass); - - function DecoratedClass () { - var unshift = Array.prototype.unshift; - - var argCount = DecoratorClass.prototype.constructor.length; - - var calledConstructor = SuperClass.prototype.constructor; - - if (argCount > 0) { - unshift.call(arguments, SuperClass.prototype.constructor); - - calledConstructor = DecoratorClass.prototype.constructor; - } - - calledConstructor.apply(this, arguments); - } - - DecoratorClass.displayName = SuperClass.displayName; - - function ctr () { - this.constructor = DecoratedClass; - } - - DecoratedClass.prototype = new ctr(); - - for (var m = 0; m < superMethods.length; m++) { - var superMethod = superMethods[m]; - - DecoratedClass.prototype[superMethod] = - SuperClass.prototype[superMethod]; - } - - var calledMethod = function (methodName) { - // Stub out the original method if it's not decorating an actual method - var originalMethod = function () {}; - - if (methodName in DecoratedClass.prototype) { - originalMethod = DecoratedClass.prototype[methodName]; - } - - var decoratedMethod = DecoratorClass.prototype[methodName]; - - return function () { - var unshift = Array.prototype.unshift; - - unshift.call(arguments, originalMethod); - - return decoratedMethod.apply(this, arguments); - }; - }; - - for (var d = 0; d < decoratedMethods.length; d++) { - var decoratedMethod = decoratedMethods[d]; - - DecoratedClass.prototype[decoratedMethod] = calledMethod(decoratedMethod); - } - - return DecoratedClass; - }; - - var Observable = function () { - this.listeners = {}; - }; - - Observable.prototype.on = function (event, callback) { - this.listeners = this.listeners || {}; - - if (event in this.listeners) { - this.listeners[event].push(callback); - } else { - this.listeners[event] = [callback]; - } - }; - - Observable.prototype.trigger = function (event) { - var slice = Array.prototype.slice; - var params = slice.call(arguments, 1); - - this.listeners = this.listeners || {}; - - // Params should always come in as an array - if (params == null) { - params = []; - } - - // If there are no arguments to the event, use a temporary object - if (params.length === 0) { - params.push({}); - } - - // Set the `_type` of the first object to the event - params[0]._type = event; - - if (event in this.listeners) { - this.invoke(this.listeners[event], slice.call(arguments, 1)); - } - - if ('*' in this.listeners) { - this.invoke(this.listeners['*'], arguments); - } - }; - - Observable.prototype.invoke = function (listeners, params) { - for (var i = 0, len = listeners.length; i < len; i++) { - listeners[i].apply(this, params); - } - }; - - Utils.Observable = Observable; - - Utils.generateChars = function (length) { - var chars = ''; - - for (var i = 0; i < length; i++) { - var randomChar = Math.floor(Math.random() * 36); - chars += randomChar.toString(36); - } - - return chars; - }; - - Utils.bind = function (func, context) { - return function () { - func.apply(context, arguments); - }; - }; - - Utils._convertData = function (data) { - for (var originalKey in data) { - var keys = originalKey.split('-'); - - var dataLevel = data; - - if (keys.length === 1) { - continue; - } - - for (var k = 0; k < keys.length; k++) { - var key = keys[k]; - - // Lowercase the first letter - // By default, dash-separated becomes camelCase - key = key.substring(0, 1).toLowerCase() + key.substring(1); - - if (!(key in dataLevel)) { - dataLevel[key] = {}; - } - - if (k == keys.length - 1) { - dataLevel[key] = data[originalKey]; - } - - dataLevel = dataLevel[key]; - } - - delete data[originalKey]; - } - - return data; - }; - - Utils.hasScroll = function (index, el) { - // Adapted from the function created by @ShadowScripter - // and adapted by @BillBarry on the Stack Exchange Code Review website. - // The original code can be found at - // http://codereview.stackexchange.com/q/13338 - // and was designed to be used with the Sizzle selector engine. - - var $el = $(el); - var overflowX = el.style.overflowX; - var overflowY = el.style.overflowY; - - //Check both x and y declarations - if (overflowX === overflowY && - (overflowY === 'hidden' || overflowY === 'visible')) { - return false; - } - - if (overflowX === 'scroll' || overflowY === 'scroll') { - return true; - } - - return ($el.innerHeight() < el.scrollHeight || - $el.innerWidth() < el.scrollWidth); - }; - - Utils.escapeMarkup = function (markup) { - var replaceMap = { - '\\': '\', - '&': '&', - '<': '<', - '>': '>', - '"': '"', - '\'': ''', - '/': '/' - }; - - // Do not try to escape the markup if it's not a string - if (typeof markup !== 'string') { - return markup; - } - - return String(markup).replace(/[&<>"'\/\\]/g, function (match) { - return replaceMap[match]; - }); - }; - - // Cache objects in Utils.__cache instead of $.data (see #4346) - Utils.__cache = {}; - - var id = 0; - Utils.GetUniqueElementId = function (element) { - // Get a unique element Id. If element has no id, - // creates a new unique number, stores it in the id - // attribute and returns the new id with a prefix. - // If an id already exists, it simply returns it with a prefix. - - var select2Id = element.getAttribute('data-select2-id'); - - if (select2Id != null) { - return select2Id; - } - - // If element has id, use it. - if (element.id) { - select2Id = 'select2-data-' + element.id; - } else { - select2Id = 'select2-data-' + (++id).toString() + - '-' + Utils.generateChars(4); - } - - element.setAttribute('data-select2-id', select2Id); - - return select2Id; - }; - - Utils.StoreData = function (element, name, value) { - // Stores an item in the cache for a specified element. - // name is the cache key. - var id = Utils.GetUniqueElementId(element); - if (!Utils.__cache[id]) { - Utils.__cache[id] = {}; - } - - Utils.__cache[id][name] = value; - }; - - Utils.GetData = function (element, name) { - // Retrieves a value from the cache by its key (name) - // name is optional. If no name specified, return - // all cache items for the specified element. - // and for a specified element. - var id = Utils.GetUniqueElementId(element); - if (name) { - if (Utils.__cache[id]) { - if (Utils.__cache[id][name] != null) { - return Utils.__cache[id][name]; - } - return $(element).data(name); // Fallback to HTML5 data attribs. - } - return $(element).data(name); // Fallback to HTML5 data attribs. - } else { - return Utils.__cache[id]; - } - }; - - Utils.RemoveData = function (element) { - // Removes all cached items for a specified element. - var id = Utils.GetUniqueElementId(element); - if (Utils.__cache[id] != null) { - delete Utils.__cache[id]; - } - - element.removeAttribute('data-select2-id'); - }; - - Utils.copyNonInternalCssClasses = function (dest, src) { - var classes; - - var destinationClasses = dest.getAttribute('class').trim().split(/\s+/); - - destinationClasses = destinationClasses.filter(function (clazz) { - // Save all Select2 classes - return clazz.indexOf('select2-') === 0; - }); - - var sourceClasses = src.getAttribute('class').trim().split(/\s+/); - - sourceClasses = sourceClasses.filter(function (clazz) { - // Only copy non-Select2 classes - return clazz.indexOf('select2-') !== 0; - }); - - var replacements = destinationClasses.concat(sourceClasses); - - dest.setAttribute('class', replacements.join(' ')); - }; - - return Utils; - }); - - S2.define('select2/results',[ - 'jquery', - './utils' - ], function ($, Utils) { - function Results ($element, options, dataAdapter) { - this.$element = $element; - this.data = dataAdapter; - this.options = options; - - Results.__super__.constructor.call(this); - } - - Utils.Extend(Results, Utils.Observable); - - Results.prototype.render = function () { - var $results = $( - '
    ' - ); - - if (this.options.get('multiple')) { - $results.attr('aria-multiselectable', 'true'); - } - - this.$results = $results; - - return $results; - }; - - Results.prototype.clear = function () { - this.$results.empty(); - }; - - Results.prototype.displayMessage = function (params) { - var escapeMarkup = this.options.get('escapeMarkup'); - - this.clear(); - this.hideLoading(); - - var $message = $( - '' - ); - - var message = this.options.get('translations').get(params.message); - - $message.append( - escapeMarkup( - message(params.args) - ) - ); - - $message[0].className += ' select2-results__message'; - - this.$results.append($message); - }; - - Results.prototype.hideMessages = function () { - this.$results.find('.select2-results__message').remove(); - }; - - Results.prototype.append = function (data) { - this.hideLoading(); - - var $options = []; - - if (data.results == null || data.results.length === 0) { - if (this.$results.children().length === 0) { - this.trigger('results:message', { - message: 'noResults' - }); - } - - return; - } - - data.results = this.sort(data.results); - - for (var d = 0; d < data.results.length; d++) { - var item = data.results[d]; - - var $option = this.option(item); - - $options.push($option); - } - - this.$results.append($options); - }; - - Results.prototype.position = function ($results, $dropdown) { - var $resultsContainer = $dropdown.find('.select2-results'); - $resultsContainer.append($results); - }; - - Results.prototype.sort = function (data) { - var sorter = this.options.get('sorter'); - - return sorter(data); - }; - - Results.prototype.highlightFirstItem = function () { - var $options = this.$results - .find('.select2-results__option--selectable'); - - var $selected = $options.filter('.select2-results__option--selected'); - - // Check if there are any selected options - if ($selected.length > 0) { - // If there are selected options, highlight the first - $selected.first().trigger('mouseenter'); - } else { - // If there are no selected options, highlight the first option - // in the dropdown - $options.first().trigger('mouseenter'); - } - - this.ensureHighlightVisible(); - }; - - Results.prototype.setClasses = function () { - var self = this; - - this.data.current(function (selected) { - var selectedIds = selected.map(function (s) { - return s.id.toString(); - }); - - var $options = self.$results - .find('.select2-results__option--selectable'); - - $options.each(function () { - var $option = $(this); - - var item = Utils.GetData(this, 'data'); - - // id needs to be converted to a string when comparing - var id = '' + item.id; - - if ((item.element != null && item.element.selected) || - (item.element == null && selectedIds.indexOf(id) > -1)) { - this.classList.add('select2-results__option--selected'); - $option.attr('aria-selected', 'true'); - } else { - this.classList.remove('select2-results__option--selected'); - $option.attr('aria-selected', 'false'); - } - }); - - }); - }; - - Results.prototype.showLoading = function (params) { - this.hideLoading(); - - var loadingMore = this.options.get('translations').get('searching'); - - var loading = { - disabled: true, - loading: true, - text: loadingMore(params) - }; - var $loading = this.option(loading); - $loading.className += ' loading-results'; - - this.$results.prepend($loading); - }; - - Results.prototype.hideLoading = function () { - this.$results.find('.loading-results').remove(); - }; - - Results.prototype.option = function (data) { - var option = document.createElement('li'); - option.classList.add('select2-results__option'); - option.classList.add('select2-results__option--selectable'); - - var attrs = { - 'role': 'option' - }; - - var matches = window.Element.prototype.matches || - window.Element.prototype.msMatchesSelector || - window.Element.prototype.webkitMatchesSelector; - - if ((data.element != null && matches.call(data.element, ':disabled')) || - (data.element == null && data.disabled)) { - attrs['aria-disabled'] = 'true'; - - option.classList.remove('select2-results__option--selectable'); - option.classList.add('select2-results__option--disabled'); - } - - if (data.id == null) { - option.classList.remove('select2-results__option--selectable'); - } - - if (data._resultId != null) { - option.id = data._resultId; - } - - if (data.title) { - option.title = data.title; - } - - if (data.children) { - attrs.role = 'group'; - attrs['aria-label'] = data.text; - - option.classList.remove('select2-results__option--selectable'); - option.classList.add('select2-results__option--group'); - } - - for (var attr in attrs) { - var val = attrs[attr]; - - option.setAttribute(attr, val); - } - - if (data.children) { - var $option = $(option); - - var label = document.createElement('strong'); - label.className = 'select2-results__group'; - - this.template(data, label); - - var $children = []; - - for (var c = 0; c < data.children.length; c++) { - var child = data.children[c]; - - var $child = this.option(child); - - $children.push($child); - } - - var $childrenContainer = $('
      ', { - 'class': 'select2-results__options select2-results__options--nested' - }); - - $childrenContainer.append($children); - - $option.append(label); - $option.append($childrenContainer); - } else { - this.template(data, option); - } - - Utils.StoreData(option, 'data', data); - - return option; - }; - - Results.prototype.bind = function (container, $container) { - var self = this; - - var id = container.id + '-results'; - - this.$results.attr('id', id); - - container.on('results:all', function (params) { - self.clear(); - self.append(params.data); - - if (container.isOpen()) { - self.setClasses(); - self.highlightFirstItem(); - } - }); - - container.on('results:append', function (params) { - self.append(params.data); - - if (container.isOpen()) { - self.setClasses(); - } - }); - - container.on('query', function (params) { - self.hideMessages(); - self.showLoading(params); - }); - - container.on('select', function () { - if (!container.isOpen()) { - return; - } - - self.setClasses(); - - if (self.options.get('scrollAfterSelect')) { - self.highlightFirstItem(); - } - }); - - container.on('unselect', function () { - if (!container.isOpen()) { - return; - } - - self.setClasses(); - - if (self.options.get('scrollAfterSelect')) { - self.highlightFirstItem(); - } - }); - - container.on('open', function () { - // When the dropdown is open, aria-expended="true" - self.$results.attr('aria-expanded', 'true'); - self.$results.attr('aria-hidden', 'false'); - - self.setClasses(); - self.ensureHighlightVisible(); - }); - - container.on('close', function () { - // When the dropdown is closed, aria-expended="false" - self.$results.attr('aria-expanded', 'false'); - self.$results.attr('aria-hidden', 'true'); - self.$results.removeAttr('aria-activedescendant'); - }); - - container.on('results:toggle', function () { - var $highlighted = self.getHighlightedResults(); - - if ($highlighted.length === 0) { - return; - } - - $highlighted.trigger('mouseup'); - }); - - container.on('results:select', function () { - var $highlighted = self.getHighlightedResults(); - - if ($highlighted.length === 0) { - return; - } - - var data = Utils.GetData($highlighted[0], 'data'); - - if ($highlighted.hasClass('select2-results__option--selected')) { - self.trigger('close', {}); - } else { - self.trigger('select', { - data: data - }); - } - }); - - container.on('results:previous', function () { - var $highlighted = self.getHighlightedResults(); - - var $options = self.$results.find('.select2-results__option--selectable'); - - var currentIndex = $options.index($highlighted); - - // If we are already at the top, don't move further - // If no options, currentIndex will be -1 - if (currentIndex <= 0) { - return; - } - - var nextIndex = currentIndex - 1; - - // If none are highlighted, highlight the first - if ($highlighted.length === 0) { - nextIndex = 0; - } - - var $next = $options.eq(nextIndex); - - $next.trigger('mouseenter'); - - var currentOffset = self.$results.offset().top; - var nextTop = $next.offset().top; - var nextOffset = self.$results.scrollTop() + (nextTop - currentOffset); - - if (nextIndex === 0) { - self.$results.scrollTop(0); - } else if (nextTop - currentOffset < 0) { - self.$results.scrollTop(nextOffset); - } - }); - - container.on('results:next', function () { - var $highlighted = self.getHighlightedResults(); - - var $options = self.$results.find('.select2-results__option--selectable'); - - var currentIndex = $options.index($highlighted); - - var nextIndex = currentIndex + 1; - - // If we are at the last option, stay there - if (nextIndex >= $options.length) { - return; - } - - var $next = $options.eq(nextIndex); - - $next.trigger('mouseenter'); - - var currentOffset = self.$results.offset().top + - self.$results.outerHeight(false); - var nextBottom = $next.offset().top + $next.outerHeight(false); - var nextOffset = self.$results.scrollTop() + nextBottom - currentOffset; - - if (nextIndex === 0) { - self.$results.scrollTop(0); - } else if (nextBottom > currentOffset) { - self.$results.scrollTop(nextOffset); - } - }); - - container.on('results:focus', function (params) { - params.element[0].classList.add('select2-results__option--highlighted'); - params.element[0].setAttribute('aria-selected', 'true'); - }); - - container.on('results:message', function (params) { - self.displayMessage(params); - }); - - if ($.fn.mousewheel) { - this.$results.on('mousewheel', function (e) { - var top = self.$results.scrollTop(); - - var bottom = self.$results.get(0).scrollHeight - top + e.deltaY; - - var isAtTop = e.deltaY > 0 && top - e.deltaY <= 0; - var isAtBottom = e.deltaY < 0 && bottom <= self.$results.height(); - - if (isAtTop) { - self.$results.scrollTop(0); - - e.preventDefault(); - e.stopPropagation(); - } else if (isAtBottom) { - self.$results.scrollTop( - self.$results.get(0).scrollHeight - self.$results.height() - ); - - e.preventDefault(); - e.stopPropagation(); - } - }); - } - - this.$results.on('mouseup', '.select2-results__option--selectable', - function (evt) { - var $this = $(this); - - var data = Utils.GetData(this, 'data'); - - if ($this.hasClass('select2-results__option--selected')) { - if (self.options.get('multiple')) { - self.trigger('unselect', { - originalEvent: evt, - data: data - }); - } else { - self.trigger('close', {}); - } - - return; - } - - self.trigger('select', { - originalEvent: evt, - data: data - }); - }); - - this.$results.on('mouseenter', '.select2-results__option--selectable', - function (evt) { - var data = Utils.GetData(this, 'data'); - - self.getHighlightedResults() - .removeClass('select2-results__option--highlighted') - .attr('aria-selected', 'false'); - - self.trigger('results:focus', { - data: data, - element: $(this) - }); - }); - }; - - Results.prototype.getHighlightedResults = function () { - var $highlighted = this.$results - .find('.select2-results__option--highlighted'); - - return $highlighted; - }; - - Results.prototype.destroy = function () { - this.$results.remove(); - }; - - Results.prototype.ensureHighlightVisible = function () { - var $highlighted = this.getHighlightedResults(); - - if ($highlighted.length === 0) { - return; - } - - var $options = this.$results.find('.select2-results__option--selectable'); - - var currentIndex = $options.index($highlighted); - - var currentOffset = this.$results.offset().top; - var nextTop = $highlighted.offset().top; - var nextOffset = this.$results.scrollTop() + (nextTop - currentOffset); - - var offsetDelta = nextTop - currentOffset; - nextOffset -= $highlighted.outerHeight(false) * 2; - - if (currentIndex <= 2) { - this.$results.scrollTop(0); - } else if (offsetDelta > this.$results.outerHeight() || offsetDelta < 0) { - this.$results.scrollTop(nextOffset); - } - }; - - Results.prototype.template = function (result, container) { - var template = this.options.get('templateResult'); - var escapeMarkup = this.options.get('escapeMarkup'); - - var content = template(result, container); - - if (content == null) { - container.style.display = 'none'; - } else if (typeof content === 'string') { - container.innerHTML = escapeMarkup(content); - } else { - $(container).append(content); - } - }; - - return Results; - }); - - S2.define('select2/keys',[ - - ], function () { - var KEYS = { - BACKSPACE: 8, - TAB: 9, - ENTER: 13, - SHIFT: 16, - CTRL: 17, - ALT: 18, - ESC: 27, - SPACE: 32, - PAGE_UP: 33, - PAGE_DOWN: 34, - END: 35, - HOME: 36, - LEFT: 37, - UP: 38, - RIGHT: 39, - DOWN: 40, - DELETE: 46 - }; - - return KEYS; - }); - - S2.define('select2/selection/base',[ - 'jquery', - '../utils', - '../keys' - ], function ($, Utils, KEYS) { - function BaseSelection ($element, options) { - this.$element = $element; - this.options = options; - - BaseSelection.__super__.constructor.call(this); - } - - Utils.Extend(BaseSelection, Utils.Observable); - - BaseSelection.prototype.render = function () { - var $selection = $( - '' - ); - - this._tabindex = 0; - - if (Utils.GetData(this.$element[0], 'old-tabindex') != null) { - this._tabindex = Utils.GetData(this.$element[0], 'old-tabindex'); - } else if (this.$element.attr('tabindex') != null) { - this._tabindex = this.$element.attr('tabindex'); - } - - $selection.attr('title', this.$element.attr('title')); - $selection.attr('tabindex', this._tabindex); - $selection.attr('aria-disabled', 'false'); - - this.$selection = $selection; - - return $selection; - }; - - BaseSelection.prototype.bind = function (container, $container) { - var self = this; - - var resultsId = container.id + '-results'; - - this.container = container; - - this.$selection.on('focus', function (evt) { - self.trigger('focus', evt); - }); - - this.$selection.on('blur', function (evt) { - self._handleBlur(evt); - }); - - this.$selection.on('keydown', function (evt) { - self.trigger('keypress', evt); - - if (evt.which === KEYS.SPACE) { - evt.preventDefault(); - } - }); - - container.on('results:focus', function (params) { - self.$selection.attr('aria-activedescendant', params.data._resultId); - }); - - container.on('selection:update', function (params) { - self.update(params.data); - }); - - container.on('open', function () { - // When the dropdown is open, aria-expanded="true" - self.$selection.attr('aria-expanded', 'true'); - self.$selection.attr('aria-owns', resultsId); - - self._attachCloseHandler(container); - }); - - container.on('close', function () { - // When the dropdown is closed, aria-expanded="false" - self.$selection.attr('aria-expanded', 'false'); - self.$selection.removeAttr('aria-activedescendant'); - self.$selection.removeAttr('aria-owns'); - - self.$selection.trigger('focus'); - - self._detachCloseHandler(container); - }); - - container.on('enable', function () { - self.$selection.attr('tabindex', self._tabindex); - self.$selection.attr('aria-disabled', 'false'); - }); - - container.on('disable', function () { - self.$selection.attr('tabindex', '-1'); - self.$selection.attr('aria-disabled', 'true'); - }); - }; - - BaseSelection.prototype._handleBlur = function (evt) { - var self = this; - - // This needs to be delayed as the active element is the body when the tab - // key is pressed, possibly along with others. - window.setTimeout(function () { - // Don't trigger `blur` if the focus is still in the selection - if ( - (document.activeElement == self.$selection[0]) || - ($.contains(self.$selection[0], document.activeElement)) - ) { - return; - } - - self.trigger('blur', evt); - }, 1); - }; - - BaseSelection.prototype._attachCloseHandler = function (container) { - - $(document.body).on('mousedown.select2.' + container.id, function (e) { - var $target = $(e.target); - - var $select = $target.closest('.select2'); - - var $all = $('.select2.select2-container--open'); - - $all.each(function () { - if (this == $select[0]) { - return; - } - - var $element = Utils.GetData(this, 'element'); - - $element.select2('close'); - }); - }); - }; - - BaseSelection.prototype._detachCloseHandler = function (container) { - $(document.body).off('mousedown.select2.' + container.id); - }; - - BaseSelection.prototype.position = function ($selection, $container) { - var $selectionContainer = $container.find('.selection'); - $selectionContainer.append($selection); - }; - - BaseSelection.prototype.destroy = function () { - this._detachCloseHandler(this.container); - }; - - BaseSelection.prototype.update = function (data) { - throw new Error('The `update` method must be defined in child classes.'); - }; - - /** - * Helper method to abstract the "enabled" (not "disabled") state of this - * object. - * - * @return {true} if the instance is not disabled. - * @return {false} if the instance is disabled. - */ - BaseSelection.prototype.isEnabled = function () { - return !this.isDisabled(); - }; - - /** - * Helper method to abstract the "disabled" state of this object. - * - * @return {true} if the disabled option is true. - * @return {false} if the disabled option is false. - */ - BaseSelection.prototype.isDisabled = function () { - return this.options.get('disabled'); - }; - - return BaseSelection; - }); - - S2.define('select2/selection/single',[ - 'jquery', - './base', - '../utils', - '../keys' - ], function ($, BaseSelection, Utils, KEYS) { - function SingleSelection () { - SingleSelection.__super__.constructor.apply(this, arguments); - } - - Utils.Extend(SingleSelection, BaseSelection); - - SingleSelection.prototype.render = function () { - var $selection = SingleSelection.__super__.render.call(this); - - $selection.addClass('select2-selection--single'); - - $selection.html( - '' + - '' + - '' + - '' - ); - - return $selection; - }; - - SingleSelection.prototype.bind = function (container, $container) { - var self = this; - - SingleSelection.__super__.bind.apply(this, arguments); - - var id = container.id + '-container'; - var first_click_datetime = new Date(); - - this.$selection.find('.select2-selection__rendered') - .attr('id', id) - .attr('role', 'textbox') - .attr('aria-readonly', 'true'); - this.$selection.attr('aria-labelledby', id); - - this.$selection.on('mousedown', function (evt) { - var second_click_datetime = new Date(); - //We add a time guard to make sure this button can only be clicked once in 200ms - if (second_click_datetime > first_click_datetime){ - // Only respond to left clicks - if (evt.which !== 1) { - return; - } - self.trigger('toggle', { - originalEvent: evt - }); - //set the first click time to 200 ms later. - first_click_datetime=second_click_datetime.setMilliseconds(second_click_datetime.getMilliseconds() + 200); - } - }); - - this.$selection.on('focus', function (evt) { - // User focuses on the container - }); - - this.$selection.on('blur', function (evt) { - // User exits the container - }); - - container.on('focus', function (evt) { - if (!container.isOpen()) { - self.$selection.trigger('focus'); - } - }); - }; - - SingleSelection.prototype.clear = function () { - var $rendered = this.$selection.find('.select2-selection__rendered'); - $rendered.empty(); - $rendered.removeAttr('title'); // clear tooltip on empty - }; - - SingleSelection.prototype.display = function (data, container) { - var template = this.options.get('templateSelection'); - var escapeMarkup = this.options.get('escapeMarkup'); - - return escapeMarkup(template(data, container)); - }; - - SingleSelection.prototype.selectionContainer = function () { - return $(''); - }; - - SingleSelection.prototype.update = function (data) { - if (data.length === 0) { - this.clear(); - return; - } - - var selection = data[0]; - - var $rendered = this.$selection.find('.select2-selection__rendered'); - var formatted = this.display(selection, $rendered); - - $rendered.empty().append(formatted); - - var title = selection.title || selection.text; - - if (title) { - $rendered.attr('title', title); - } else { - $rendered.removeAttr('title'); - } - }; - - return SingleSelection; - }); - - S2.define('select2/selection/multiple',[ - 'jquery', - './base', - '../utils' - ], function ($, BaseSelection, Utils) { - function MultipleSelection ($element, options) { - MultipleSelection.__super__.constructor.apply(this, arguments); - } - - Utils.Extend(MultipleSelection, BaseSelection); - - MultipleSelection.prototype.render = function () { - var $selection = MultipleSelection.__super__.render.call(this); - - $selection[0].classList.add('select2-selection--multiple'); - - $selection.html( - '
        ' - ); - - return $selection; - }; - - MultipleSelection.prototype.bind = function (container, $container) { - var self = this; - - MultipleSelection.__super__.bind.apply(this, arguments); - - var id = container.id + '-container'; - this.$selection.find('.select2-selection__rendered').attr('id', id); - - this.$selection.on('click', function (evt) { - self.trigger('toggle', { - originalEvent: evt - }); - }); - - this.$selection.on( - 'click', - '.select2-selection__choice__remove', - function (evt) { - // Ignore the event if it is disabled - if (self.isDisabled()) { - return; - } - - var $remove = $(this); - var $selection = $remove.parent(); - - var data = Utils.GetData($selection[0], 'data'); - - self.trigger('unselect', { - originalEvent: evt, - data: data - }); - } - ); - - this.$selection.on( - 'keydown', - '.select2-selection__choice__remove', - function (evt) { - // Ignore the event if it is disabled - if (self.isDisabled()) { - return; - } - - evt.stopPropagation(); - } - ); - }; - - MultipleSelection.prototype.clear = function () { - var $rendered = this.$selection.find('.select2-selection__rendered'); - $rendered.empty(); - $rendered.removeAttr('title'); - }; - - MultipleSelection.prototype.display = function (data, container) { - var template = this.options.get('templateSelection'); - var escapeMarkup = this.options.get('escapeMarkup'); - - return escapeMarkup(template(data, container)); - }; - - MultipleSelection.prototype.selectionContainer = function () { - var $container = $( - '
      • ' + - '' + - '' + - '
      • ' - ); - - return $container; - }; - - MultipleSelection.prototype.update = function (data) { - this.clear(); - - if (data.length === 0) { - return; - } - - var $selections = []; - - var selectionIdPrefix = this.$selection.find('.select2-selection__rendered') - .attr('id') + '-choice-'; - - for (var d = 0; d < data.length; d++) { - var selection = data[d]; - - var $selection = this.selectionContainer(); - var formatted = this.display(selection, $selection); - - var selectionId = selectionIdPrefix + Utils.generateChars(4) + '-'; - - if (selection.id) { - selectionId += selection.id; - } else { - selectionId += Utils.generateChars(4); - } - - $selection.find('.select2-selection__choice__display') - .append(formatted) - .attr('id', selectionId); - - var title = selection.title || selection.text; - - if (title) { - $selection.attr('title', title); - } - - var removeItem = this.options.get('translations').get('removeItem'); - - var $remove = $selection.find('.select2-selection__choice__remove'); - - $remove.attr('title', removeItem()); - $remove.attr('aria-label', removeItem()); - $remove.attr('aria-describedby', selectionId); - - Utils.StoreData($selection[0], 'data', selection); - - $selections.push($selection); - } - - var $rendered = this.$selection.find('.select2-selection__rendered'); - - $rendered.append($selections); - }; - - return MultipleSelection; - }); - - S2.define('select2/selection/placeholder',[ - - ], function () { - function Placeholder (decorated, $element, options) { - this.placeholder = this.normalizePlaceholder(options.get('placeholder')); - - decorated.call(this, $element, options); - } - - Placeholder.prototype.normalizePlaceholder = function (_, placeholder) { - if (typeof placeholder === 'string') { - placeholder = { - id: '', - text: placeholder - }; - } - - return placeholder; - }; - - Placeholder.prototype.createPlaceholder = function (decorated, placeholder) { - var $placeholder = this.selectionContainer(); - - $placeholder.html(this.display(placeholder)); - $placeholder[0].classList.add('select2-selection__placeholder'); - $placeholder[0].classList.remove('select2-selection__choice'); - - return $placeholder; - }; - - Placeholder.prototype.update = function (decorated, data) { - var singlePlaceholder = ( - data.length == 1 && data[0].id != this.placeholder.id - ); - var multipleSelections = data.length > 1; - - if (multipleSelections || singlePlaceholder) { - return decorated.call(this, data); - } - - this.clear(); - - var $placeholder = this.createPlaceholder(this.placeholder); - - this.$selection.find('.select2-selection__rendered').append($placeholder); - }; - - return Placeholder; - }); - - S2.define('select2/selection/allowClear',[ - 'jquery', - '../keys', - '../utils' - ], function ($, KEYS, Utils) { - function AllowClear () { } - - AllowClear.prototype.bind = function (decorated, container, $container) { - var self = this; - - decorated.call(this, container, $container); - - if (this.placeholder == null) { - if (this.options.get('debug') && window.console && console.error) { - console.error( - 'Select2: The `allowClear` option should be used in combination ' + - 'with the `placeholder` option.' - ); - } - } - - this.$selection.on('mousedown', '.select2-selection__clear', - function (evt) { - self._handleClear(evt); - }); - - container.on('keypress', function (evt) { - self._handleKeyboardClear(evt, container); - }); - }; - - AllowClear.prototype._handleClear = function (_, evt) { - // Ignore the event if it is disabled - if (this.isDisabled()) { - return; - } - - var $clear = this.$selection.find('.select2-selection__clear'); - - // Ignore the event if nothing has been selected - if ($clear.length === 0) { - return; - } - - evt.stopPropagation(); - - var data = Utils.GetData($clear[0], 'data'); - - var previousVal = this.$element.val(); - this.$element.val(this.placeholder.id); - - var unselectData = { - data: data - }; - this.trigger('clear', unselectData); - if (unselectData.prevented) { - this.$element.val(previousVal); - return; - } - - for (var d = 0; d < data.length; d++) { - unselectData = { - data: data[d] - }; - - // Trigger the `unselect` event, so people can prevent it from being - // cleared. - this.trigger('unselect', unselectData); - - // If the event was prevented, don't clear it out. - if (unselectData.prevented) { - this.$element.val(previousVal); - return; - } - } - - this.$element.trigger('input').trigger('change'); - - this.trigger('toggle', {}); - }; - - AllowClear.prototype._handleKeyboardClear = function (_, evt, container) { - if (container.isOpen()) { - return; - } - - if (evt.which == KEYS.DELETE || evt.which == KEYS.BACKSPACE) { - this._handleClear(evt); - } - }; - - AllowClear.prototype.update = function (decorated, data) { - decorated.call(this, data); - - this.$selection.find('.select2-selection__clear').remove(); - - if (this.$selection.find('.select2-selection__placeholder').length > 0 || - data.length === 0) { - return; - } - - var selectionId = this.$selection.find('.select2-selection__rendered') - .attr('id'); - - var removeAll = this.options.get('translations').get('removeAllItems'); - - var $remove = $( - '' - ); - $remove.attr('title', removeAll()); - $remove.attr('aria-label', removeAll()); - $remove.attr('aria-describedby', selectionId); - Utils.StoreData($remove[0], 'data', data); - - this.$selection.prepend($remove); - }; - - return AllowClear; - }); - - S2.define('select2/selection/search',[ - 'jquery', - '../utils', - '../keys' - ], function ($, Utils, KEYS) { - function Search (decorated, $element, options) { - decorated.call(this, $element, options); - } - - Search.prototype.render = function (decorated) { - var $search = $( - '' + - '' + - '' - ); - - this.$searchContainer = $search; - this.$search = $search.find('input'); - - this.$search.prop('autocomplete', this.options.get('autocomplete')); - - var $rendered = decorated.call(this); - - this._transferTabIndex(); - $rendered.append(this.$searchContainer); - - return $rendered; - }; - - Search.prototype.bind = function (decorated, container, $container) { - var self = this; - - var resultsId = container.id + '-results'; - var selectionId = container.id + '-container'; - - decorated.call(this, container, $container); - - self.$search.attr('aria-describedby', selectionId); - - container.on('open', function () { - self.$search.attr('aria-controls', resultsId); - self.$search.trigger('focus'); - }); - - container.on('close', function () { - self.$search.val(''); - self.resizeSearch(); - self.$search.removeAttr('aria-controls'); - self.$search.removeAttr('aria-activedescendant'); - self.$search.trigger('focus'); - }); - - container.on('enable', function () { - self.$search.prop('disabled', false); - - self._transferTabIndex(); - }); - - container.on('disable', function () { - self.$search.prop('disabled', true); - }); - - container.on('focus', function (evt) { - self.$search.trigger('focus'); - }); - - container.on('results:focus', function (params) { - if (params.data._resultId) { - self.$search.attr('aria-activedescendant', params.data._resultId); - } else { - self.$search.removeAttr('aria-activedescendant'); - } - }); - - this.$selection.on('focusin', '.select2-search--inline', function (evt) { - self.trigger('focus', evt); - }); - - this.$selection.on('focusout', '.select2-search--inline', function (evt) { - self._handleBlur(evt); - }); - - this.$selection.on('keydown', '.select2-search--inline', function (evt) { - evt.stopPropagation(); - - self.trigger('keypress', evt); - - self._keyUpPrevented = evt.isDefaultPrevented(); - - var key = evt.which; - - if (key === KEYS.BACKSPACE && self.$search.val() === '') { - var $previousChoice = self.$selection - .find('.select2-selection__choice').last(); - - if ($previousChoice.length > 0) { - var item = Utils.GetData($previousChoice[0], 'data'); - - self.searchRemoveChoice(item); - - evt.preventDefault(); - } - } - }); - - this.$selection.on('click', '.select2-search--inline', function (evt) { - if (self.$search.val()) { - evt.stopPropagation(); - } - }); - - // Try to detect the IE version should the `documentMode` property that - // is stored on the document. This is only implemented in IE and is - // slightly cleaner than doing a user agent check. - // This property is not available in Edge, but Edge also doesn't have - // this bug. - var msie = document.documentMode; - var disableInputEvents = msie && msie <= 11; - - // Workaround for browsers which do not support the `input` event - // This will prevent double-triggering of events for browsers which support - // both the `keyup` and `input` events. - this.$selection.on( - 'input.searchcheck', - '.select2-search--inline', - function (evt) { - // IE will trigger the `input` event when a placeholder is used on a - // search box. To get around this issue, we are forced to ignore all - // `input` events in IE and keep using `keyup`. - if (disableInputEvents) { - self.$selection.off('input.search input.searchcheck'); - return; - } - - // Unbind the duplicated `keyup` event - self.$selection.off('keyup.search'); - } - ); - - this.$selection.on( - 'keyup.search input.search', - '.select2-search--inline', - function (evt) { - // IE will trigger the `input` event when a placeholder is used on a - // search box. To get around this issue, we are forced to ignore all - // `input` events in IE and keep using `keyup`. - if (disableInputEvents && evt.type === 'input') { - self.$selection.off('input.search input.searchcheck'); - return; - } - - var key = evt.which; - - // We can freely ignore events from modifier keys - if (key == KEYS.SHIFT || key == KEYS.CTRL || key == KEYS.ALT) { - return; - } - - // Tabbing will be handled during the `keydown` phase - if (key == KEYS.TAB) { - return; - } - - self.handleSearch(evt); - } - ); - }; - - /** - * This method will transfer the tabindex attribute from the rendered - * selection to the search box. This allows for the search box to be used as - * the primary focus instead of the selection container. - * - * @private - */ - Search.prototype._transferTabIndex = function (decorated) { - this.$search.attr('tabindex', this.$selection.attr('tabindex')); - this.$selection.attr('tabindex', '-1'); - }; - - Search.prototype.createPlaceholder = function (decorated, placeholder) { - this.$search.attr('placeholder', placeholder.text); - }; - - Search.prototype.update = function (decorated, data) { - var searchHadFocus = this.$search[0] == document.activeElement; - - this.$search.attr('placeholder', ''); - - decorated.call(this, data); - - this.resizeSearch(); - if (searchHadFocus) { - this.$search.trigger('focus'); - } - }; - - Search.prototype.handleSearch = function () { - this.resizeSearch(); - - if (!this._keyUpPrevented) { - var input = this.$search.val(); - - this.trigger('query', { - term: input - }); - } - - this._keyUpPrevented = false; - }; - - Search.prototype.searchRemoveChoice = function (decorated, item) { - this.trigger('unselect', { - data: item - }); - - this.$search.val(item.text); - this.handleSearch(); - }; - - Search.prototype.resizeSearch = function () { - this.$search.css('width', '25px'); - - var width = '100%'; - - if (this.$search.attr('placeholder') === '') { - var minimumWidth = this.$search.val().length + 1; - - width = (minimumWidth * 0.75) + 'em'; - } - - this.$search.css('width', width); - }; - - return Search; - }); - - S2.define('select2/selection/selectionCss',[ - '../utils' - ], function (Utils) { - function SelectionCSS () { } - - SelectionCSS.prototype.render = function (decorated) { - var $selection = decorated.call(this); - - var selectionCssClass = this.options.get('selectionCssClass') || ''; - - if (selectionCssClass.indexOf(':all:') !== -1) { - selectionCssClass = selectionCssClass.replace(':all:', ''); - - Utils.copyNonInternalCssClasses($selection[0], this.$element[0]); - } - - $selection.addClass(selectionCssClass); - - return $selection; - }; - - return SelectionCSS; - }); - - S2.define('select2/selection/eventRelay',[ - 'jquery' - ], function ($) { - function EventRelay () { } - - EventRelay.prototype.bind = function (decorated, container, $container) { - var self = this; - var relayEvents = [ - 'open', 'opening', - 'close', 'closing', - 'select', 'selecting', - 'unselect', 'unselecting', - 'clear', 'clearing' - ]; - - var preventableEvents = [ - 'opening', 'closing', 'selecting', 'unselecting', 'clearing' - ]; - - decorated.call(this, container, $container); - - container.on('*', function (name, params) { - // Ignore events that should not be relayed - if (relayEvents.indexOf(name) === -1) { - return; - } - - // The parameters should always be an object - params = params || {}; - - // Generate the jQuery event for the Select2 event - var evt = $.Event('select2:' + name, { - params: params - }); - - self.$element.trigger(evt); - - // Only handle preventable events if it was one - if (preventableEvents.indexOf(name) === -1) { - return; - } - - params.prevented = evt.isDefaultPrevented(); - }); - }; - - return EventRelay; - }); - - S2.define('select2/translation',[ - 'jquery', - 'require' - ], function ($, require) { - function Translation (dict) { - this.dict = dict || {}; - } - - Translation.prototype.all = function () { - return this.dict; - }; - - Translation.prototype.get = function (key) { - return this.dict[key]; - }; - - Translation.prototype.extend = function (translation) { - this.dict = $.extend({}, translation.all(), this.dict); - }; - - // Static functions - - Translation._cache = {}; - - Translation.loadPath = function (path) { - if (!(path in Translation._cache)) { - var translations = require(path); - - Translation._cache[path] = translations; - } - - return new Translation(Translation._cache[path]); - }; - - return Translation; - }); - - S2.define('select2/diacritics',[ - - ], function () { - var diacritics = { - '\u24B6': 'A', - '\uFF21': 'A', - '\u00C0': 'A', - '\u00C1': 'A', - '\u00C2': 'A', - '\u1EA6': 'A', - '\u1EA4': 'A', - '\u1EAA': 'A', - '\u1EA8': 'A', - '\u00C3': 'A', - '\u0100': 'A', - '\u0102': 'A', - '\u1EB0': 'A', - '\u1EAE': 'A', - '\u1EB4': 'A', - '\u1EB2': 'A', - '\u0226': 'A', - '\u01E0': 'A', - '\u00C4': 'A', - '\u01DE': 'A', - '\u1EA2': 'A', - '\u00C5': 'A', - '\u01FA': 'A', - '\u01CD': 'A', - '\u0200': 'A', - '\u0202': 'A', - '\u1EA0': 'A', - '\u1EAC': 'A', - '\u1EB6': 'A', - '\u1E00': 'A', - '\u0104': 'A', - '\u023A': 'A', - '\u2C6F': 'A', - '\uA732': 'AA', - '\u00C6': 'AE', - '\u01FC': 'AE', - '\u01E2': 'AE', - '\uA734': 'AO', - '\uA736': 'AU', - '\uA738': 'AV', - '\uA73A': 'AV', - '\uA73C': 'AY', - '\u24B7': 'B', - '\uFF22': 'B', - '\u1E02': 'B', - '\u1E04': 'B', - '\u1E06': 'B', - '\u0243': 'B', - '\u0182': 'B', - '\u0181': 'B', - '\u24B8': 'C', - '\uFF23': 'C', - '\u0106': 'C', - '\u0108': 'C', - '\u010A': 'C', - '\u010C': 'C', - '\u00C7': 'C', - '\u1E08': 'C', - '\u0187': 'C', - '\u023B': 'C', - '\uA73E': 'C', - '\u24B9': 'D', - '\uFF24': 'D', - '\u1E0A': 'D', - '\u010E': 'D', - '\u1E0C': 'D', - '\u1E10': 'D', - '\u1E12': 'D', - '\u1E0E': 'D', - '\u0110': 'D', - '\u018B': 'D', - '\u018A': 'D', - '\u0189': 'D', - '\uA779': 'D', - '\u01F1': 'DZ', - '\u01C4': 'DZ', - '\u01F2': 'Dz', - '\u01C5': 'Dz', - '\u24BA': 'E', - '\uFF25': 'E', - '\u00C8': 'E', - '\u00C9': 'E', - '\u00CA': 'E', - '\u1EC0': 'E', - '\u1EBE': 'E', - '\u1EC4': 'E', - '\u1EC2': 'E', - '\u1EBC': 'E', - '\u0112': 'E', - '\u1E14': 'E', - '\u1E16': 'E', - '\u0114': 'E', - '\u0116': 'E', - '\u00CB': 'E', - '\u1EBA': 'E', - '\u011A': 'E', - '\u0204': 'E', - '\u0206': 'E', - '\u1EB8': 'E', - '\u1EC6': 'E', - '\u0228': 'E', - '\u1E1C': 'E', - '\u0118': 'E', - '\u1E18': 'E', - '\u1E1A': 'E', - '\u0190': 'E', - '\u018E': 'E', - '\u24BB': 'F', - '\uFF26': 'F', - '\u1E1E': 'F', - '\u0191': 'F', - '\uA77B': 'F', - '\u24BC': 'G', - '\uFF27': 'G', - '\u01F4': 'G', - '\u011C': 'G', - '\u1E20': 'G', - '\u011E': 'G', - '\u0120': 'G', - '\u01E6': 'G', - '\u0122': 'G', - '\u01E4': 'G', - '\u0193': 'G', - '\uA7A0': 'G', - '\uA77D': 'G', - '\uA77E': 'G', - '\u24BD': 'H', - '\uFF28': 'H', - '\u0124': 'H', - '\u1E22': 'H', - '\u1E26': 'H', - '\u021E': 'H', - '\u1E24': 'H', - '\u1E28': 'H', - '\u1E2A': 'H', - '\u0126': 'H', - '\u2C67': 'H', - '\u2C75': 'H', - '\uA78D': 'H', - '\u24BE': 'I', - '\uFF29': 'I', - '\u00CC': 'I', - '\u00CD': 'I', - '\u00CE': 'I', - '\u0128': 'I', - '\u012A': 'I', - '\u012C': 'I', - '\u0130': 'I', - '\u00CF': 'I', - '\u1E2E': 'I', - '\u1EC8': 'I', - '\u01CF': 'I', - '\u0208': 'I', - '\u020A': 'I', - '\u1ECA': 'I', - '\u012E': 'I', - '\u1E2C': 'I', - '\u0197': 'I', - '\u24BF': 'J', - '\uFF2A': 'J', - '\u0134': 'J', - '\u0248': 'J', - '\u24C0': 'K', - '\uFF2B': 'K', - '\u1E30': 'K', - '\u01E8': 'K', - '\u1E32': 'K', - '\u0136': 'K', - '\u1E34': 'K', - '\u0198': 'K', - '\u2C69': 'K', - '\uA740': 'K', - '\uA742': 'K', - '\uA744': 'K', - '\uA7A2': 'K', - '\u24C1': 'L', - '\uFF2C': 'L', - '\u013F': 'L', - '\u0139': 'L', - '\u013D': 'L', - '\u1E36': 'L', - '\u1E38': 'L', - '\u013B': 'L', - '\u1E3C': 'L', - '\u1E3A': 'L', - '\u0141': 'L', - '\u023D': 'L', - '\u2C62': 'L', - '\u2C60': 'L', - '\uA748': 'L', - '\uA746': 'L', - '\uA780': 'L', - '\u01C7': 'LJ', - '\u01C8': 'Lj', - '\u24C2': 'M', - '\uFF2D': 'M', - '\u1E3E': 'M', - '\u1E40': 'M', - '\u1E42': 'M', - '\u2C6E': 'M', - '\u019C': 'M', - '\u24C3': 'N', - '\uFF2E': 'N', - '\u01F8': 'N', - '\u0143': 'N', - '\u00D1': 'N', - '\u1E44': 'N', - '\u0147': 'N', - '\u1E46': 'N', - '\u0145': 'N', - '\u1E4A': 'N', - '\u1E48': 'N', - '\u0220': 'N', - '\u019D': 'N', - '\uA790': 'N', - '\uA7A4': 'N', - '\u01CA': 'NJ', - '\u01CB': 'Nj', - '\u24C4': 'O', - '\uFF2F': 'O', - '\u00D2': 'O', - '\u00D3': 'O', - '\u00D4': 'O', - '\u1ED2': 'O', - '\u1ED0': 'O', - '\u1ED6': 'O', - '\u1ED4': 'O', - '\u00D5': 'O', - '\u1E4C': 'O', - '\u022C': 'O', - '\u1E4E': 'O', - '\u014C': 'O', - '\u1E50': 'O', - '\u1E52': 'O', - '\u014E': 'O', - '\u022E': 'O', - '\u0230': 'O', - '\u00D6': 'O', - '\u022A': 'O', - '\u1ECE': 'O', - '\u0150': 'O', - '\u01D1': 'O', - '\u020C': 'O', - '\u020E': 'O', - '\u01A0': 'O', - '\u1EDC': 'O', - '\u1EDA': 'O', - '\u1EE0': 'O', - '\u1EDE': 'O', - '\u1EE2': 'O', - '\u1ECC': 'O', - '\u1ED8': 'O', - '\u01EA': 'O', - '\u01EC': 'O', - '\u00D8': 'O', - '\u01FE': 'O', - '\u0186': 'O', - '\u019F': 'O', - '\uA74A': 'O', - '\uA74C': 'O', - '\u0152': 'OE', - '\u01A2': 'OI', - '\uA74E': 'OO', - '\u0222': 'OU', - '\u24C5': 'P', - '\uFF30': 'P', - '\u1E54': 'P', - '\u1E56': 'P', - '\u01A4': 'P', - '\u2C63': 'P', - '\uA750': 'P', - '\uA752': 'P', - '\uA754': 'P', - '\u24C6': 'Q', - '\uFF31': 'Q', - '\uA756': 'Q', - '\uA758': 'Q', - '\u024A': 'Q', - '\u24C7': 'R', - '\uFF32': 'R', - '\u0154': 'R', - '\u1E58': 'R', - '\u0158': 'R', - '\u0210': 'R', - '\u0212': 'R', - '\u1E5A': 'R', - '\u1E5C': 'R', - '\u0156': 'R', - '\u1E5E': 'R', - '\u024C': 'R', - '\u2C64': 'R', - '\uA75A': 'R', - '\uA7A6': 'R', - '\uA782': 'R', - '\u24C8': 'S', - '\uFF33': 'S', - '\u1E9E': 'S', - '\u015A': 'S', - '\u1E64': 'S', - '\u015C': 'S', - '\u1E60': 'S', - '\u0160': 'S', - '\u1E66': 'S', - '\u1E62': 'S', - '\u1E68': 'S', - '\u0218': 'S', - '\u015E': 'S', - '\u2C7E': 'S', - '\uA7A8': 'S', - '\uA784': 'S', - '\u24C9': 'T', - '\uFF34': 'T', - '\u1E6A': 'T', - '\u0164': 'T', - '\u1E6C': 'T', - '\u021A': 'T', - '\u0162': 'T', - '\u1E70': 'T', - '\u1E6E': 'T', - '\u0166': 'T', - '\u01AC': 'T', - '\u01AE': 'T', - '\u023E': 'T', - '\uA786': 'T', - '\uA728': 'TZ', - '\u24CA': 'U', - '\uFF35': 'U', - '\u00D9': 'U', - '\u00DA': 'U', - '\u00DB': 'U', - '\u0168': 'U', - '\u1E78': 'U', - '\u016A': 'U', - '\u1E7A': 'U', - '\u016C': 'U', - '\u00DC': 'U', - '\u01DB': 'U', - '\u01D7': 'U', - '\u01D5': 'U', - '\u01D9': 'U', - '\u1EE6': 'U', - '\u016E': 'U', - '\u0170': 'U', - '\u01D3': 'U', - '\u0214': 'U', - '\u0216': 'U', - '\u01AF': 'U', - '\u1EEA': 'U', - '\u1EE8': 'U', - '\u1EEE': 'U', - '\u1EEC': 'U', - '\u1EF0': 'U', - '\u1EE4': 'U', - '\u1E72': 'U', - '\u0172': 'U', - '\u1E76': 'U', - '\u1E74': 'U', - '\u0244': 'U', - '\u24CB': 'V', - '\uFF36': 'V', - '\u1E7C': 'V', - '\u1E7E': 'V', - '\u01B2': 'V', - '\uA75E': 'V', - '\u0245': 'V', - '\uA760': 'VY', - '\u24CC': 'W', - '\uFF37': 'W', - '\u1E80': 'W', - '\u1E82': 'W', - '\u0174': 'W', - '\u1E86': 'W', - '\u1E84': 'W', - '\u1E88': 'W', - '\u2C72': 'W', - '\u24CD': 'X', - '\uFF38': 'X', - '\u1E8A': 'X', - '\u1E8C': 'X', - '\u24CE': 'Y', - '\uFF39': 'Y', - '\u1EF2': 'Y', - '\u00DD': 'Y', - '\u0176': 'Y', - '\u1EF8': 'Y', - '\u0232': 'Y', - '\u1E8E': 'Y', - '\u0178': 'Y', - '\u1EF6': 'Y', - '\u1EF4': 'Y', - '\u01B3': 'Y', - '\u024E': 'Y', - '\u1EFE': 'Y', - '\u24CF': 'Z', - '\uFF3A': 'Z', - '\u0179': 'Z', - '\u1E90': 'Z', - '\u017B': 'Z', - '\u017D': 'Z', - '\u1E92': 'Z', - '\u1E94': 'Z', - '\u01B5': 'Z', - '\u0224': 'Z', - '\u2C7F': 'Z', - '\u2C6B': 'Z', - '\uA762': 'Z', - '\u24D0': 'a', - '\uFF41': 'a', - '\u1E9A': 'a', - '\u00E0': 'a', - '\u00E1': 'a', - '\u00E2': 'a', - '\u1EA7': 'a', - '\u1EA5': 'a', - '\u1EAB': 'a', - '\u1EA9': 'a', - '\u00E3': 'a', - '\u0101': 'a', - '\u0103': 'a', - '\u1EB1': 'a', - '\u1EAF': 'a', - '\u1EB5': 'a', - '\u1EB3': 'a', - '\u0227': 'a', - '\u01E1': 'a', - '\u00E4': 'a', - '\u01DF': 'a', - '\u1EA3': 'a', - '\u00E5': 'a', - '\u01FB': 'a', - '\u01CE': 'a', - '\u0201': 'a', - '\u0203': 'a', - '\u1EA1': 'a', - '\u1EAD': 'a', - '\u1EB7': 'a', - '\u1E01': 'a', - '\u0105': 'a', - '\u2C65': 'a', - '\u0250': 'a', - '\uA733': 'aa', - '\u00E6': 'ae', - '\u01FD': 'ae', - '\u01E3': 'ae', - '\uA735': 'ao', - '\uA737': 'au', - '\uA739': 'av', - '\uA73B': 'av', - '\uA73D': 'ay', - '\u24D1': 'b', - '\uFF42': 'b', - '\u1E03': 'b', - '\u1E05': 'b', - '\u1E07': 'b', - '\u0180': 'b', - '\u0183': 'b', - '\u0253': 'b', - '\u24D2': 'c', - '\uFF43': 'c', - '\u0107': 'c', - '\u0109': 'c', - '\u010B': 'c', - '\u010D': 'c', - '\u00E7': 'c', - '\u1E09': 'c', - '\u0188': 'c', - '\u023C': 'c', - '\uA73F': 'c', - '\u2184': 'c', - '\u24D3': 'd', - '\uFF44': 'd', - '\u1E0B': 'd', - '\u010F': 'd', - '\u1E0D': 'd', - '\u1E11': 'd', - '\u1E13': 'd', - '\u1E0F': 'd', - '\u0111': 'd', - '\u018C': 'd', - '\u0256': 'd', - '\u0257': 'd', - '\uA77A': 'd', - '\u01F3': 'dz', - '\u01C6': 'dz', - '\u24D4': 'e', - '\uFF45': 'e', - '\u00E8': 'e', - '\u00E9': 'e', - '\u00EA': 'e', - '\u1EC1': 'e', - '\u1EBF': 'e', - '\u1EC5': 'e', - '\u1EC3': 'e', - '\u1EBD': 'e', - '\u0113': 'e', - '\u1E15': 'e', - '\u1E17': 'e', - '\u0115': 'e', - '\u0117': 'e', - '\u00EB': 'e', - '\u1EBB': 'e', - '\u011B': 'e', - '\u0205': 'e', - '\u0207': 'e', - '\u1EB9': 'e', - '\u1EC7': 'e', - '\u0229': 'e', - '\u1E1D': 'e', - '\u0119': 'e', - '\u1E19': 'e', - '\u1E1B': 'e', - '\u0247': 'e', - '\u025B': 'e', - '\u01DD': 'e', - '\u24D5': 'f', - '\uFF46': 'f', - '\u1E1F': 'f', - '\u0192': 'f', - '\uA77C': 'f', - '\u24D6': 'g', - '\uFF47': 'g', - '\u01F5': 'g', - '\u011D': 'g', - '\u1E21': 'g', - '\u011F': 'g', - '\u0121': 'g', - '\u01E7': 'g', - '\u0123': 'g', - '\u01E5': 'g', - '\u0260': 'g', - '\uA7A1': 'g', - '\u1D79': 'g', - '\uA77F': 'g', - '\u24D7': 'h', - '\uFF48': 'h', - '\u0125': 'h', - '\u1E23': 'h', - '\u1E27': 'h', - '\u021F': 'h', - '\u1E25': 'h', - '\u1E29': 'h', - '\u1E2B': 'h', - '\u1E96': 'h', - '\u0127': 'h', - '\u2C68': 'h', - '\u2C76': 'h', - '\u0265': 'h', - '\u0195': 'hv', - '\u24D8': 'i', - '\uFF49': 'i', - '\u00EC': 'i', - '\u00ED': 'i', - '\u00EE': 'i', - '\u0129': 'i', - '\u012B': 'i', - '\u012D': 'i', - '\u00EF': 'i', - '\u1E2F': 'i', - '\u1EC9': 'i', - '\u01D0': 'i', - '\u0209': 'i', - '\u020B': 'i', - '\u1ECB': 'i', - '\u012F': 'i', - '\u1E2D': 'i', - '\u0268': 'i', - '\u0131': 'i', - '\u24D9': 'j', - '\uFF4A': 'j', - '\u0135': 'j', - '\u01F0': 'j', - '\u0249': 'j', - '\u24DA': 'k', - '\uFF4B': 'k', - '\u1E31': 'k', - '\u01E9': 'k', - '\u1E33': 'k', - '\u0137': 'k', - '\u1E35': 'k', - '\u0199': 'k', - '\u2C6A': 'k', - '\uA741': 'k', - '\uA743': 'k', - '\uA745': 'k', - '\uA7A3': 'k', - '\u24DB': 'l', - '\uFF4C': 'l', - '\u0140': 'l', - '\u013A': 'l', - '\u013E': 'l', - '\u1E37': 'l', - '\u1E39': 'l', - '\u013C': 'l', - '\u1E3D': 'l', - '\u1E3B': 'l', - '\u017F': 'l', - '\u0142': 'l', - '\u019A': 'l', - '\u026B': 'l', - '\u2C61': 'l', - '\uA749': 'l', - '\uA781': 'l', - '\uA747': 'l', - '\u01C9': 'lj', - '\u24DC': 'm', - '\uFF4D': 'm', - '\u1E3F': 'm', - '\u1E41': 'm', - '\u1E43': 'm', - '\u0271': 'm', - '\u026F': 'm', - '\u24DD': 'n', - '\uFF4E': 'n', - '\u01F9': 'n', - '\u0144': 'n', - '\u00F1': 'n', - '\u1E45': 'n', - '\u0148': 'n', - '\u1E47': 'n', - '\u0146': 'n', - '\u1E4B': 'n', - '\u1E49': 'n', - '\u019E': 'n', - '\u0272': 'n', - '\u0149': 'n', - '\uA791': 'n', - '\uA7A5': 'n', - '\u01CC': 'nj', - '\u24DE': 'o', - '\uFF4F': 'o', - '\u00F2': 'o', - '\u00F3': 'o', - '\u00F4': 'o', - '\u1ED3': 'o', - '\u1ED1': 'o', - '\u1ED7': 'o', - '\u1ED5': 'o', - '\u00F5': 'o', - '\u1E4D': 'o', - '\u022D': 'o', - '\u1E4F': 'o', - '\u014D': 'o', - '\u1E51': 'o', - '\u1E53': 'o', - '\u014F': 'o', - '\u022F': 'o', - '\u0231': 'o', - '\u00F6': 'o', - '\u022B': 'o', - '\u1ECF': 'o', - '\u0151': 'o', - '\u01D2': 'o', - '\u020D': 'o', - '\u020F': 'o', - '\u01A1': 'o', - '\u1EDD': 'o', - '\u1EDB': 'o', - '\u1EE1': 'o', - '\u1EDF': 'o', - '\u1EE3': 'o', - '\u1ECD': 'o', - '\u1ED9': 'o', - '\u01EB': 'o', - '\u01ED': 'o', - '\u00F8': 'o', - '\u01FF': 'o', - '\u0254': 'o', - '\uA74B': 'o', - '\uA74D': 'o', - '\u0275': 'o', - '\u0153': 'oe', - '\u01A3': 'oi', - '\u0223': 'ou', - '\uA74F': 'oo', - '\u24DF': 'p', - '\uFF50': 'p', - '\u1E55': 'p', - '\u1E57': 'p', - '\u01A5': 'p', - '\u1D7D': 'p', - '\uA751': 'p', - '\uA753': 'p', - '\uA755': 'p', - '\u24E0': 'q', - '\uFF51': 'q', - '\u024B': 'q', - '\uA757': 'q', - '\uA759': 'q', - '\u24E1': 'r', - '\uFF52': 'r', - '\u0155': 'r', - '\u1E59': 'r', - '\u0159': 'r', - '\u0211': 'r', - '\u0213': 'r', - '\u1E5B': 'r', - '\u1E5D': 'r', - '\u0157': 'r', - '\u1E5F': 'r', - '\u024D': 'r', - '\u027D': 'r', - '\uA75B': 'r', - '\uA7A7': 'r', - '\uA783': 'r', - '\u24E2': 's', - '\uFF53': 's', - '\u00DF': 's', - '\u015B': 's', - '\u1E65': 's', - '\u015D': 's', - '\u1E61': 's', - '\u0161': 's', - '\u1E67': 's', - '\u1E63': 's', - '\u1E69': 's', - '\u0219': 's', - '\u015F': 's', - '\u023F': 's', - '\uA7A9': 's', - '\uA785': 's', - '\u1E9B': 's', - '\u24E3': 't', - '\uFF54': 't', - '\u1E6B': 't', - '\u1E97': 't', - '\u0165': 't', - '\u1E6D': 't', - '\u021B': 't', - '\u0163': 't', - '\u1E71': 't', - '\u1E6F': 't', - '\u0167': 't', - '\u01AD': 't', - '\u0288': 't', - '\u2C66': 't', - '\uA787': 't', - '\uA729': 'tz', - '\u24E4': 'u', - '\uFF55': 'u', - '\u00F9': 'u', - '\u00FA': 'u', - '\u00FB': 'u', - '\u0169': 'u', - '\u1E79': 'u', - '\u016B': 'u', - '\u1E7B': 'u', - '\u016D': 'u', - '\u00FC': 'u', - '\u01DC': 'u', - '\u01D8': 'u', - '\u01D6': 'u', - '\u01DA': 'u', - '\u1EE7': 'u', - '\u016F': 'u', - '\u0171': 'u', - '\u01D4': 'u', - '\u0215': 'u', - '\u0217': 'u', - '\u01B0': 'u', - '\u1EEB': 'u', - '\u1EE9': 'u', - '\u1EEF': 'u', - '\u1EED': 'u', - '\u1EF1': 'u', - '\u1EE5': 'u', - '\u1E73': 'u', - '\u0173': 'u', - '\u1E77': 'u', - '\u1E75': 'u', - '\u0289': 'u', - '\u24E5': 'v', - '\uFF56': 'v', - '\u1E7D': 'v', - '\u1E7F': 'v', - '\u028B': 'v', - '\uA75F': 'v', - '\u028C': 'v', - '\uA761': 'vy', - '\u24E6': 'w', - '\uFF57': 'w', - '\u1E81': 'w', - '\u1E83': 'w', - '\u0175': 'w', - '\u1E87': 'w', - '\u1E85': 'w', - '\u1E98': 'w', - '\u1E89': 'w', - '\u2C73': 'w', - '\u24E7': 'x', - '\uFF58': 'x', - '\u1E8B': 'x', - '\u1E8D': 'x', - '\u24E8': 'y', - '\uFF59': 'y', - '\u1EF3': 'y', - '\u00FD': 'y', - '\u0177': 'y', - '\u1EF9': 'y', - '\u0233': 'y', - '\u1E8F': 'y', - '\u00FF': 'y', - '\u1EF7': 'y', - '\u1E99': 'y', - '\u1EF5': 'y', - '\u01B4': 'y', - '\u024F': 'y', - '\u1EFF': 'y', - '\u24E9': 'z', - '\uFF5A': 'z', - '\u017A': 'z', - '\u1E91': 'z', - '\u017C': 'z', - '\u017E': 'z', - '\u1E93': 'z', - '\u1E95': 'z', - '\u01B6': 'z', - '\u0225': 'z', - '\u0240': 'z', - '\u2C6C': 'z', - '\uA763': 'z', - '\u0386': '\u0391', - '\u0388': '\u0395', - '\u0389': '\u0397', - '\u038A': '\u0399', - '\u03AA': '\u0399', - '\u038C': '\u039F', - '\u038E': '\u03A5', - '\u03AB': '\u03A5', - '\u038F': '\u03A9', - '\u03AC': '\u03B1', - '\u03AD': '\u03B5', - '\u03AE': '\u03B7', - '\u03AF': '\u03B9', - '\u03CA': '\u03B9', - '\u0390': '\u03B9', - '\u03CC': '\u03BF', - '\u03CD': '\u03C5', - '\u03CB': '\u03C5', - '\u03B0': '\u03C5', - '\u03CE': '\u03C9', - '\u03C2': '\u03C3', - '\u2019': '\'' - }; - - return diacritics; - }); - - S2.define('select2/data/base',[ - '../utils' - ], function (Utils) { - function BaseAdapter ($element, options) { - BaseAdapter.__super__.constructor.call(this); - } - - Utils.Extend(BaseAdapter, Utils.Observable); - - BaseAdapter.prototype.current = function (callback) { - throw new Error('The `current` method must be defined in child classes.'); - }; - - BaseAdapter.prototype.query = function (params, callback) { - throw new Error('The `query` method must be defined in child classes.'); - }; - - BaseAdapter.prototype.bind = function (container, $container) { - // Can be implemented in subclasses - }; - - BaseAdapter.prototype.destroy = function () { - // Can be implemented in subclasses - }; - - BaseAdapter.prototype.generateResultId = function (container, data) { - var id = container.id + '-result-'; - - id += Utils.generateChars(4); - - if (data.id != null) { - id += '-' + data.id.toString(); - } else { - id += '-' + Utils.generateChars(4); - } - return id; - }; - - return BaseAdapter; - }); - - S2.define('select2/data/select',[ - './base', - '../utils', - 'jquery' - ], function (BaseAdapter, Utils, $) { - function SelectAdapter ($element, options) { - this.$element = $element; - this.options = options; - - SelectAdapter.__super__.constructor.call(this); - } - - Utils.Extend(SelectAdapter, BaseAdapter); - - SelectAdapter.prototype.current = function (callback) { - var self = this; - - var data = Array.prototype.map.call( - this.$element[0].querySelectorAll(':checked'), - function (selectedElement) { - return self.item($(selectedElement)); - } - ); - - callback(data); - }; - - SelectAdapter.prototype.select = function (data) { - var self = this; - - data.selected = true; - - // If data.element is a DOM node, use it instead - if ( - data.element != null && data.element.tagName.toLowerCase() === 'option' - ) { - data.element.selected = true; - - this.$element.trigger('input').trigger('change'); - - return; - } - - if (this.$element.prop('multiple')) { - this.current(function (currentData) { - var val = []; - - data = [data]; - data.push.apply(data, currentData); - - for (var d = 0; d < data.length; d++) { - var id = data[d].id; - - if (val.indexOf(id) === -1) { - val.push(id); - } - } - - self.$element.val(val); - self.$element.trigger('input').trigger('change'); - }); - } else { - var val = data.id; - - this.$element.val(val); - this.$element.trigger('input').trigger('change'); - } - }; - - SelectAdapter.prototype.unselect = function (data) { - var self = this; - - if (!this.$element.prop('multiple')) { - return; - } - - data.selected = false; - - if ( - data.element != null && - data.element.tagName.toLowerCase() === 'option' - ) { - data.element.selected = false; - - this.$element.trigger('input').trigger('change'); - - return; - } - - this.current(function (currentData) { - var val = []; - - for (var d = 0; d < currentData.length; d++) { - var id = currentData[d].id; - - if (id !== data.id && val.indexOf(id) === -1) { - val.push(id); - } - } - - self.$element.val(val); - - self.$element.trigger('input').trigger('change'); - }); - }; - - SelectAdapter.prototype.bind = function (container, $container) { - var self = this; - - this.container = container; - - container.on('select', function (params) { - self.select(params.data); - }); - - container.on('unselect', function (params) { - self.unselect(params.data); - }); - }; - - SelectAdapter.prototype.destroy = function () { - // Remove anything added to child elements - this.$element.find('*').each(function () { - // Remove any custom data set by Select2 - Utils.RemoveData(this); - }); - }; - - SelectAdapter.prototype.query = function (params, callback) { - var data = []; - var self = this; - - var $options = this.$element.children(); - - $options.each(function () { - if ( - this.tagName.toLowerCase() !== 'option' && - this.tagName.toLowerCase() !== 'optgroup' - ) { - return; - } - - var $option = $(this); - - var option = self.item($option); - - var matches = self.matches(params, option); - - if (matches !== null) { - data.push(matches); - } - }); - - callback({ - results: data - }); - }; - - SelectAdapter.prototype.addOptions = function ($options) { - this.$element.append($options); - }; - - SelectAdapter.prototype.option = function (data) { - var option; - - if (data.children) { - option = document.createElement('optgroup'); - option.label = data.text; - } else { - option = document.createElement('option'); - - if (option.textContent !== undefined) { - option.textContent = data.text; - } else { - option.innerText = data.text; - } - } - - if (data.id !== undefined) { - option.value = data.id; - } - - if (data.disabled) { - option.disabled = true; - } - - if (data.selected) { - option.selected = true; - } - - if (data.title) { - option.title = data.title; - } - - var normalizedData = this._normalizeItem(data); - normalizedData.element = option; - - // Override the option's data with the combined data - Utils.StoreData(option, 'data', normalizedData); - - return $(option); - }; - - SelectAdapter.prototype.item = function ($option) { - var data = {}; - - data = Utils.GetData($option[0], 'data'); - - if (data != null) { - return data; - } - - var option = $option[0]; - - if (option.tagName.toLowerCase() === 'option') { - data = { - id: $option.val(), - text: $option.text(), - disabled: $option.prop('disabled'), - selected: $option.prop('selected'), - title: $option.prop('title') - }; - } else if (option.tagName.toLowerCase() === 'optgroup') { - data = { - text: $option.prop('label'), - children: [], - title: $option.prop('title') - }; - - var $children = $option.children('option'); - var children = []; - - for (var c = 0; c < $children.length; c++) { - var $child = $($children[c]); - - var child = this.item($child); - - children.push(child); - } - - data.children = children; - } - - data = this._normalizeItem(data); - data.element = $option[0]; - - Utils.StoreData($option[0], 'data', data); - - return data; - }; - - SelectAdapter.prototype._normalizeItem = function (item) { - if (item !== Object(item)) { - item = { - id: item, - text: item - }; - } - - item = $.extend({}, { - text: '' - }, item); - - var defaults = { - selected: false, - disabled: false - }; - - if (item.id != null) { - item.id = item.id.toString(); - } - - if (item.text != null) { - item.text = item.text.toString(); - } - - if (item._resultId == null && item.id && this.container != null) { - item._resultId = this.generateResultId(this.container, item); - } - - return $.extend({}, defaults, item); - }; - - SelectAdapter.prototype.matches = function (params, data) { - var matcher = this.options.get('matcher'); - - return matcher(params, data); - }; - - return SelectAdapter; - }); - - S2.define('select2/data/array',[ - './select', - '../utils', - 'jquery' - ], function (SelectAdapter, Utils, $) { - function ArrayAdapter ($element, options) { - this._dataToConvert = options.get('data') || []; - - ArrayAdapter.__super__.constructor.call(this, $element, options); - } - - Utils.Extend(ArrayAdapter, SelectAdapter); - - ArrayAdapter.prototype.bind = function (container, $container) { - ArrayAdapter.__super__.bind.call(this, container, $container); - - this.addOptions(this.convertToOptions(this._dataToConvert)); - }; - - ArrayAdapter.prototype.select = function (data) { - var $option = this.$element.find('option').filter(function (i, elm) { - return elm.value == data.id.toString(); - }); - - if ($option.length === 0) { - $option = this.option(data); - - this.addOptions($option); - } - - ArrayAdapter.__super__.select.call(this, data); - }; - - ArrayAdapter.prototype.convertToOptions = function (data) { - var self = this; - - var $existing = this.$element.find('option'); - var existingIds = $existing.map(function () { - return self.item($(this)).id; - }).get(); - - var $options = []; - - // Filter out all items except for the one passed in the argument - function onlyItem (item) { - return function () { - return $(this).val() == item.id; - }; - } - - for (var d = 0; d < data.length; d++) { - var item = this._normalizeItem(data[d]); - - // Skip items which were pre-loaded, only merge the data - if (existingIds.indexOf(item.id) >= 0) { - var $existingOption = $existing.filter(onlyItem(item)); - - var existingData = this.item($existingOption); - var newData = $.extend(true, {}, item, existingData); - - var $newOption = this.option(newData); - - $existingOption.replaceWith($newOption); - - continue; - } - - var $option = this.option(item); - - if (item.children) { - var $children = this.convertToOptions(item.children); - - $option.append($children); - } - - $options.push($option); - } - - return $options; - }; - - return ArrayAdapter; - }); - - S2.define('select2/data/ajax',[ - './array', - '../utils', - 'jquery' - ], function (ArrayAdapter, Utils, $) { - function AjaxAdapter ($element, options) { - this.ajaxOptions = this._applyDefaults(options.get('ajax')); - - if (this.ajaxOptions.processResults != null) { - this.processResults = this.ajaxOptions.processResults; - } - - AjaxAdapter.__super__.constructor.call(this, $element, options); - } - - Utils.Extend(AjaxAdapter, ArrayAdapter); - - AjaxAdapter.prototype._applyDefaults = function (options) { - var defaults = { - data: function (params) { - return $.extend({}, params, { - q: params.term - }); - }, - transport: function (params, success, failure) { - var $request = $.ajax(params); - - $request.then(success); - $request.fail(failure); - - return $request; - } - }; - - return $.extend({}, defaults, options, true); - }; - - AjaxAdapter.prototype.processResults = function (results) { - return results; - }; - - AjaxAdapter.prototype.query = function (params, callback) { - var matches = []; - var self = this; - - if (this._request != null) { - // JSONP requests cannot always be aborted - if ($.isFunction(this._request.abort)) { - this._request.abort(); - } - - this._request = null; - } - - var options = $.extend({ - type: 'GET' - }, this.ajaxOptions); - - if (typeof options.url === 'function') { - options.url = options.url.call(this.$element, params); - } - - if (typeof options.data === 'function') { - options.data = options.data.call(this.$element, params); - } - - function request () { - var $request = options.transport(options, function (data) { - var results = self.processResults(data, params); - - if (self.options.get('debug') && window.console && console.error) { - // Check to make sure that the response included a `results` key. - if (!results || !results.results || !Array.isArray(results.results)) { - console.error( - 'Select2: The AJAX results did not return an array in the ' + - '`results` key of the response.' - ); - } - } - - callback(results); - }, function () { - // Attempt to detect if a request was aborted - // Only works if the transport exposes a status property - if ('status' in $request && - ($request.status === 0 || $request.status === '0')) { - return; - } - - self.trigger('results:message', { - message: 'errorLoading' - }); - }); - - self._request = $request; - } - - if (this.ajaxOptions.delay && params.term != null) { - if (this._queryTimeout) { - window.clearTimeout(this._queryTimeout); - } - - this._queryTimeout = window.setTimeout(request, this.ajaxOptions.delay); - } else { - request(); - } - }; - - return AjaxAdapter; - }); - - S2.define('select2/data/tags',[ - 'jquery' - ], function ($) { - function Tags (decorated, $element, options) { - var tags = options.get('tags'); - - var createTag = options.get('createTag'); - - if (createTag !== undefined) { - this.createTag = createTag; - } - - var insertTag = options.get('insertTag'); - - if (insertTag !== undefined) { - this.insertTag = insertTag; - } - - decorated.call(this, $element, options); - - if (Array.isArray(tags)) { - for (var t = 0; t < tags.length; t++) { - var tag = tags[t]; - var item = this._normalizeItem(tag); - - var $option = this.option(item); - - this.$element.append($option); - } - } - } - - Tags.prototype.query = function (decorated, params, callback) { - var self = this; - - this._removeOldTags(); - - if (params.term == null || params.page != null) { - decorated.call(this, params, callback); - return; - } - - function wrapper (obj, child) { - var data = obj.results; - - for (var i = 0; i < data.length; i++) { - var option = data[i]; - - var checkChildren = ( - option.children != null && - !wrapper({ - results: option.children - }, true) - ); - - var optionText = (option.text || '').toUpperCase(); - var paramsTerm = (params.term || '').toUpperCase(); - - var checkText = optionText === paramsTerm; - - if (checkText || checkChildren) { - if (child) { - return false; - } - - obj.data = data; - callback(obj); - - return; - } - } - - if (child) { - return true; - } - - var tag = self.createTag(params); - - if (tag != null) { - var $option = self.option(tag); - $option.attr('data-select2-tag', true); - - self.addOptions([$option]); - - self.insertTag(data, tag); - } - - obj.results = data; - - callback(obj); - } - - decorated.call(this, params, wrapper); - }; - - Tags.prototype.createTag = function (decorated, params) { - if (params.term == null) { - return null; - } - - var term = params.term.trim(); - - if (term === '') { - return null; - } - - return { - id: term, - text: term - }; - }; - - Tags.prototype.insertTag = function (_, data, tag) { - data.unshift(tag); - }; - - Tags.prototype._removeOldTags = function (_) { - var $options = this.$element.find('option[data-select2-tag]'); - - $options.each(function () { - if (this.selected) { - return; - } - - $(this).remove(); - }); - }; - - return Tags; - }); - - S2.define('select2/data/tokenizer',[ - 'jquery' - ], function ($) { - function Tokenizer (decorated, $element, options) { - var tokenizer = options.get('tokenizer'); - - if (tokenizer !== undefined) { - this.tokenizer = tokenizer; - } - - decorated.call(this, $element, options); - } - - Tokenizer.prototype.bind = function (decorated, container, $container) { - decorated.call(this, container, $container); - - this.$search = container.dropdown.$search || container.selection.$search || - $container.find('.select2-search__field'); - }; - - Tokenizer.prototype.query = function (decorated, params, callback) { - var self = this; - - function createAndSelect (data) { - // Normalize the data object so we can use it for checks - var item = self._normalizeItem(data); - - // Check if the data object already exists as a tag - // Select it if it doesn't - var $existingOptions = self.$element.find('option').filter(function () { - return $(this).val() === item.id; - }); - - // If an existing option wasn't found for it, create the option - if (!$existingOptions.length) { - var $option = self.option(item); - $option.attr('data-select2-tag', true); - - self._removeOldTags(); - self.addOptions([$option]); - } - - // Select the item, now that we know there is an option for it - select(item); - } - - function select (data) { - self.trigger('select', { - data: data - }); - } - - params.term = params.term || ''; - - var tokenData = this.tokenizer(params, this.options, createAndSelect); - - if (tokenData.term !== params.term) { - // Replace the search term if we have the search box - if (this.$search.length) { - this.$search.val(tokenData.term); - this.$search.trigger('focus'); - } - - params.term = tokenData.term; - } - - decorated.call(this, params, callback); - }; - - Tokenizer.prototype.tokenizer = function (_, params, options, callback) { - var separators = options.get('tokenSeparators') || []; - var term = params.term; - var i = 0; - - var createTag = this.createTag || function (params) { - return { - id: params.term, - text: params.term - }; - }; - - while (i < term.length) { - var termChar = term[i]; - - if (separators.indexOf(termChar) === -1) { - i++; - - continue; - } - - var part = term.substr(0, i); - var partParams = $.extend({}, params, { - term: part - }); - - var data = createTag(partParams); - - if (data == null) { - i++; - continue; - } - - callback(data); - - // Reset the term to not include the tokenized portion - term = term.substr(i + 1) || ''; - i = 0; - } - - return { - term: term - }; - }; - - return Tokenizer; - }); - - S2.define('select2/data/minimumInputLength',[ - - ], function () { - function MinimumInputLength (decorated, $e, options) { - this.minimumInputLength = options.get('minimumInputLength'); - - decorated.call(this, $e, options); - } - - MinimumInputLength.prototype.query = function (decorated, params, callback) { - params.term = params.term || ''; - - if (params.term.length < this.minimumInputLength) { - this.trigger('results:message', { - message: 'inputTooShort', - args: { - minimum: this.minimumInputLength, - input: params.term, - params: params - } - }); - - return; - } - - decorated.call(this, params, callback); - }; - - return MinimumInputLength; - }); - - S2.define('select2/data/maximumInputLength',[ - - ], function () { - function MaximumInputLength (decorated, $e, options) { - this.maximumInputLength = options.get('maximumInputLength'); - - decorated.call(this, $e, options); - } - - MaximumInputLength.prototype.query = function (decorated, params, callback) { - params.term = params.term || ''; - - if (this.maximumInputLength > 0 && - params.term.length > this.maximumInputLength) { - this.trigger('results:message', { - message: 'inputTooLong', - args: { - maximum: this.maximumInputLength, - input: params.term, - params: params - } - }); - - return; - } - - decorated.call(this, params, callback); - }; - - return MaximumInputLength; - }); - - S2.define('select2/data/maximumSelectionLength',[ - - ], function (){ - function MaximumSelectionLength (decorated, $e, options) { - this.maximumSelectionLength = options.get('maximumSelectionLength'); - - decorated.call(this, $e, options); - } - - MaximumSelectionLength.prototype.bind = - function (decorated, container, $container) { - var self = this; - - decorated.call(this, container, $container); - - container.on('select', function () { - self._checkIfMaximumSelected(); - }); - }; - - MaximumSelectionLength.prototype.query = - function (decorated, params, callback) { - var self = this; - - this._checkIfMaximumSelected(function () { - decorated.call(self, params, callback); - }); - }; - - MaximumSelectionLength.prototype._checkIfMaximumSelected = - function (_, successCallback) { - var self = this; - - this.current(function (currentData) { - var count = currentData != null ? currentData.length : 0; - if (self.maximumSelectionLength > 0 && - count >= self.maximumSelectionLength) { - self.trigger('results:message', { - message: 'maximumSelected', - args: { - maximum: self.maximumSelectionLength - } - }); - return; - } - - if (successCallback) { - successCallback(); - } - }); - }; - - return MaximumSelectionLength; - }); - - S2.define('select2/dropdown',[ - 'jquery', - './utils' - ], function ($, Utils) { - function Dropdown ($element, options) { - this.$element = $element; - this.options = options; - - Dropdown.__super__.constructor.call(this); - } - - Utils.Extend(Dropdown, Utils.Observable); - - Dropdown.prototype.render = function () { - var $dropdown = $( - '' + - '' + - '' - ); - - $dropdown.attr('dir', this.options.get('dir')); - - this.$dropdown = $dropdown; - - return $dropdown; - }; - - Dropdown.prototype.bind = function () { - // Should be implemented in subclasses - }; - - Dropdown.prototype.position = function ($dropdown, $container) { - // Should be implemented in subclasses - }; - - Dropdown.prototype.destroy = function () { - // Remove the dropdown from the DOM - this.$dropdown.remove(); - }; - - return Dropdown; - }); - - S2.define('select2/dropdown/search',[ - 'jquery' - ], function ($) { - function Search () { } - - Search.prototype.render = function (decorated) { - var $rendered = decorated.call(this); - - var $search = $( - '' + - '' + - '' - ); - - this.$searchContainer = $search; - this.$search = $search.find('input'); - - this.$search.prop('autocomplete', this.options.get('autocomplete')); - - $rendered.prepend($search); - - return $rendered; - }; - - Search.prototype.bind = function (decorated, container, $container) { - var self = this; - - var resultsId = container.id + '-results'; - - decorated.call(this, container, $container); - - this.$search.on('keydown', function (evt) { - self.trigger('keypress', evt); - - self._keyUpPrevented = evt.isDefaultPrevented(); - }); - - // Workaround for browsers which do not support the `input` event - // This will prevent double-triggering of events for browsers which support - // both the `keyup` and `input` events. - this.$search.on('input', function (evt) { - // Unbind the duplicated `keyup` event - $(this).off('keyup'); - }); - - this.$search.on('keyup input', function (evt) { - self.handleSearch(evt); - }); - - container.on('open', function () { - self.$search.attr('tabindex', 0); - self.$search.attr('aria-controls', resultsId); - - self.$search.trigger('focus'); - - window.setTimeout(function () { - self.$search.trigger('focus'); - }, 0); - }); - - container.on('close', function () { - self.$search.attr('tabindex', -1); - self.$search.removeAttr('aria-controls'); - self.$search.removeAttr('aria-activedescendant'); - - self.$search.val(''); - self.$search.trigger('blur'); - }); - - container.on('focus', function () { - if (!container.isOpen()) { - self.$search.trigger('focus'); - } - }); - - container.on('results:all', function (params) { - if (params.query.term == null || params.query.term === '') { - var showSearch = self.showSearch(params); - - if (showSearch) { - self.$searchContainer[0].classList.remove('select2-search--hide'); - } else { - self.$searchContainer[0].classList.add('select2-search--hide'); - } - } - }); - - container.on('results:focus', function (params) { - if (params.data._resultId) { - self.$search.attr('aria-activedescendant', params.data._resultId); - } else { - self.$search.removeAttr('aria-activedescendant'); - } - }); - }; - - Search.prototype.handleSearch = function (evt) { - if (!this._keyUpPrevented) { - var input = this.$search.val(); - - this.trigger('query', { - term: input - }); - } - - this._keyUpPrevented = false; - }; - - Search.prototype.showSearch = function (_, params) { - return true; - }; - - return Search; - }); - - S2.define('select2/dropdown/hidePlaceholder',[ - - ], function () { - function HidePlaceholder (decorated, $element, options, dataAdapter) { - this.placeholder = this.normalizePlaceholder(options.get('placeholder')); - - decorated.call(this, $element, options, dataAdapter); - } - - HidePlaceholder.prototype.append = function (decorated, data) { - data.results = this.removePlaceholder(data.results); - - decorated.call(this, data); - }; - - HidePlaceholder.prototype.normalizePlaceholder = function (_, placeholder) { - if (typeof placeholder === 'string') { - placeholder = { - id: '', - text: placeholder - }; - } - - return placeholder; - }; - - HidePlaceholder.prototype.removePlaceholder = function (_, data) { - var modifiedData = data.slice(0); - - for (var d = data.length - 1; d >= 0; d--) { - var item = data[d]; - - if (this.placeholder.id === item.id) { - modifiedData.splice(d, 1); - } - } - - return modifiedData; - }; - - return HidePlaceholder; - }); - - S2.define('select2/dropdown/infiniteScroll',[ - 'jquery' - ], function ($) { - function InfiniteScroll (decorated, $element, options, dataAdapter) { - this.lastParams = {}; - - decorated.call(this, $element, options, dataAdapter); - - this.$loadingMore = this.createLoadingMore(); - this.loading = false; - } - - InfiniteScroll.prototype.append = function (decorated, data) { - this.$loadingMore.remove(); - this.loading = false; - - decorated.call(this, data); - - if (this.showLoadingMore(data)) { - this.$results.append(this.$loadingMore); - this.loadMoreIfNeeded(); - } - }; - - InfiniteScroll.prototype.bind = function (decorated, container, $container) { - var self = this; - - decorated.call(this, container, $container); - - container.on('query', function (params) { - self.lastParams = params; - self.loading = true; - }); - - container.on('query:append', function (params) { - self.lastParams = params; - self.loading = true; - }); - - this.$results.on('scroll', this.loadMoreIfNeeded.bind(this)); - }; - - InfiniteScroll.prototype.loadMoreIfNeeded = function () { - var isLoadMoreVisible = $.contains( - document.documentElement, - this.$loadingMore[0] - ); - - if (this.loading || !isLoadMoreVisible) { - return; - } - - var currentOffset = this.$results.offset().top + - this.$results.outerHeight(false); - var loadingMoreOffset = this.$loadingMore.offset().top + - this.$loadingMore.outerHeight(false); - - if (currentOffset + 50 >= loadingMoreOffset) { - this.loadMore(); - } - }; - - InfiniteScroll.prototype.loadMore = function () { - this.loading = true; - - var params = $.extend({}, {page: 1}, this.lastParams); - - params.page++; - - this.trigger('query:append', params); - }; - - InfiniteScroll.prototype.showLoadingMore = function (_, data) { - return data.pagination && data.pagination.more; - }; - - InfiniteScroll.prototype.createLoadingMore = function () { - var $option = $( - '
      • ' - ); - - var message = this.options.get('translations').get('loadingMore'); - - $option.html(message(this.lastParams)); - - return $option; - }; - - return InfiniteScroll; - }); - - S2.define('select2/dropdown/attachBody',[ - 'jquery', - '../utils' - ], function ($, Utils) { - function AttachBody (decorated, $element, options) { - this.$dropdownParent = $(options.get('dropdownParent') || document.body); - - decorated.call(this, $element, options); - } - - AttachBody.prototype.bind = function (decorated, container, $container) { - var self = this; - - decorated.call(this, container, $container); - - container.on('open', function () { - self._showDropdown(); - self._attachPositioningHandler(container); - - // Must bind after the results handlers to ensure correct sizing - self._bindContainerResultHandlers(container); - }); - - container.on('close', function () { - self._hideDropdown(); - self._detachPositioningHandler(container); - }); - - this.$dropdownContainer.on('mousedown', function (evt) { - evt.stopPropagation(); - }); - }; - - AttachBody.prototype.destroy = function (decorated) { - decorated.call(this); - - this.$dropdownContainer.remove(); - }; - - AttachBody.prototype.position = function (decorated, $dropdown, $container) { - // Clone all of the container classes - $dropdown.attr('class', $container.attr('class')); - - $dropdown[0].classList.remove('select2'); - $dropdown[0].classList.add('select2-container--open'); - - $dropdown.css({ - position: 'absolute', - top: -999999 - }); - - this.$container = $container; - }; - - AttachBody.prototype.render = function (decorated) { - var $container = $(''); - - var $dropdown = decorated.call(this); - $container.append($dropdown); - - this.$dropdownContainer = $container; - - return $container; - }; - - AttachBody.prototype._hideDropdown = function (decorated) { - this.$dropdownContainer.detach(); - }; - - AttachBody.prototype._bindContainerResultHandlers = - function (decorated, container) { - - // These should only be bound once - if (this._containerResultsHandlersBound) { - return; - } - - var self = this; - - container.on('results:all', function () { - self._positionDropdown(); - self._resizeDropdown(); - }); - - container.on('results:append', function () { - self._positionDropdown(); - self._resizeDropdown(); - }); - - container.on('results:message', function () { - self._positionDropdown(); - self._resizeDropdown(); - }); - - container.on('select', function () { - self._positionDropdown(); - self._resizeDropdown(); - }); - - container.on('unselect', function () { - self._positionDropdown(); - self._resizeDropdown(); - }); - - this._containerResultsHandlersBound = true; - }; - - AttachBody.prototype._attachPositioningHandler = - function (decorated, container) { - var self = this; - - var scrollEvent = 'scroll.select2.' + container.id; - var resizeEvent = 'resize.select2.' + container.id; - var orientationEvent = 'orientationchange.select2.' + container.id; - - var $watchers = this.$container.parents().filter(Utils.hasScroll); - $watchers.each(function () { - Utils.StoreData(this, 'select2-scroll-position', { - x: $(this).scrollLeft(), - y: $(this).scrollTop() - }); - }); - - $watchers.on(scrollEvent, function (ev) { - var position = Utils.GetData(this, 'select2-scroll-position'); - $(this).scrollTop(position.y); - }); - - $(window).on(scrollEvent + ' ' + resizeEvent + ' ' + orientationEvent, - function (e) { - self._positionDropdown(); - self._resizeDropdown(); - }); - }; - - AttachBody.prototype._detachPositioningHandler = - function (decorated, container) { - var scrollEvent = 'scroll.select2.' + container.id; - var resizeEvent = 'resize.select2.' + container.id; - var orientationEvent = 'orientationchange.select2.' + container.id; - - var $watchers = this.$container.parents().filter(Utils.hasScroll); - $watchers.off(scrollEvent); - - $(window).off(scrollEvent + ' ' + resizeEvent + ' ' + orientationEvent); - }; - - AttachBody.prototype._positionDropdown = function () { - var $window = $(window); - - var isCurrentlyAbove = this.$dropdown[0].classList - .contains('select2-dropdown--above'); - var isCurrentlyBelow = this.$dropdown[0].classList - .contains('select2-dropdown--below'); - - var newDirection = null; - - var offset = this.$container.offset(); - - offset.bottom = offset.top + this.$container.outerHeight(false); - - var container = { - height: this.$container.outerHeight(false) - }; - - container.top = offset.top; - container.bottom = offset.top + container.height; - - var dropdown = { - height: this.$dropdown.outerHeight(false) - }; - - var viewport = { - top: $window.scrollTop(), - bottom: $window.scrollTop() + $window.height() - }; - - var enoughRoomAbove = viewport.top < (offset.top - dropdown.height); - var enoughRoomBelow = viewport.bottom > (offset.bottom + dropdown.height); - - var css = { - left: offset.left, - top: container.bottom - }; - - // Determine what the parent element is to use for calculating the offset - var $offsetParent = this.$dropdownParent; - - // For statically positioned elements, we need to get the element - // that is determining the offset - if ($offsetParent.css('position') === 'static') { - $offsetParent = $offsetParent.offsetParent(); - } - - var parentOffset = { - top: 0, - left: 0 - }; - - if ( - $.contains(document.body, $offsetParent[0]) || - $offsetParent[0].isConnected - ) { - parentOffset = $offsetParent.offset(); - } - - css.top -= parentOffset.top; - css.left -= parentOffset.left; - - if (!isCurrentlyAbove && !isCurrentlyBelow) { - newDirection = 'below'; - } - - if (!enoughRoomBelow && enoughRoomAbove && !isCurrentlyAbove) { - newDirection = 'above'; - } else if (!enoughRoomAbove && enoughRoomBelow && isCurrentlyAbove) { - newDirection = 'below'; - } - - if (newDirection == 'above' || - (isCurrentlyAbove && newDirection !== 'below')) { - css.top = container.top - parentOffset.top - dropdown.height; - } - - if (newDirection != null) { - this.$dropdown[0].classList.remove('select2-dropdown--below'); - this.$dropdown[0].classList.remove('select2-dropdown--above'); - this.$dropdown[0].classList.add('select2-dropdown--' + newDirection); - - this.$container[0].classList.remove('select2-container--below'); - this.$container[0].classList.remove('select2-container--above'); - this.$container[0].classList.add('select2-container--' + newDirection); - } - - this.$dropdownContainer.css(css); - }; - - AttachBody.prototype._resizeDropdown = function () { - var css = { - width: this.$container.outerWidth(false) + 'px' - }; - - if (this.options.get('dropdownAutoWidth')) { - css.minWidth = css.width; - css.position = 'relative'; - css.width = 'auto'; - } - - this.$dropdown.css(css); - }; - - AttachBody.prototype._showDropdown = function (decorated) { - this.$dropdownContainer.appendTo(this.$dropdownParent); - - this._positionDropdown(); - this._resizeDropdown(); - }; - - return AttachBody; - }); - - S2.define('select2/dropdown/minimumResultsForSearch',[ - - ], function () { - function countResults (data) { - var count = 0; - - for (var d = 0; d < data.length; d++) { - var item = data[d]; - - if (item.children) { - count += countResults(item.children); - } else { - count++; - } - } - - return count; - } - - function MinimumResultsForSearch (decorated, $element, options, dataAdapter) { - this.minimumResultsForSearch = options.get('minimumResultsForSearch'); - - if (this.minimumResultsForSearch < 0) { - this.minimumResultsForSearch = Infinity; - } - - decorated.call(this, $element, options, dataAdapter); - } - - MinimumResultsForSearch.prototype.showSearch = function (decorated, params) { - if (countResults(params.data.results) < this.minimumResultsForSearch) { - return false; - } - - return decorated.call(this, params); - }; - - return MinimumResultsForSearch; - }); - - S2.define('select2/dropdown/selectOnClose',[ - '../utils' - ], function (Utils) { - function SelectOnClose () { } - - SelectOnClose.prototype.bind = function (decorated, container, $container) { - var self = this; - - decorated.call(this, container, $container); - - container.on('close', function (params) { - self._handleSelectOnClose(params); - }); - }; - - SelectOnClose.prototype._handleSelectOnClose = function (_, params) { - if (params && params.originalSelect2Event != null) { - var event = params.originalSelect2Event; - - // Don't select an item if the close event was triggered from a select or - // unselect event - if (event._type === 'select' || event._type === 'unselect') { - return; - } - } - - var $highlightedResults = this.getHighlightedResults(); - - // Only select highlighted results - if ($highlightedResults.length < 1) { - return; - } - - var data = Utils.GetData($highlightedResults[0], 'data'); - - // Don't re-select already selected resulte - if ( - (data.element != null && data.element.selected) || - (data.element == null && data.selected) - ) { - return; - } - - this.trigger('select', { - data: data - }); - }; - - return SelectOnClose; - }); - - S2.define('select2/dropdown/closeOnSelect',[ - - ], function () { - function CloseOnSelect () { } - - CloseOnSelect.prototype.bind = function (decorated, container, $container) { - var self = this; - - decorated.call(this, container, $container); - - container.on('select', function (evt) { - self._selectTriggered(evt); - }); - - container.on('unselect', function (evt) { - self._selectTriggered(evt); - }); - }; - - CloseOnSelect.prototype._selectTriggered = function (_, evt) { - var originalEvent = evt.originalEvent; - - // Don't close if the control key is being held - if (originalEvent && (originalEvent.ctrlKey || originalEvent.metaKey)) { - return; - } - - this.trigger('close', { - originalEvent: originalEvent, - originalSelect2Event: evt - }); - }; - - return CloseOnSelect; - }); - - S2.define('select2/dropdown/dropdownCss',[ - '../utils' - ], function (Utils) { - function DropdownCSS () { } - - DropdownCSS.prototype.render = function (decorated) { - var $dropdown = decorated.call(this); - - var dropdownCssClass = this.options.get('dropdownCssClass') || ''; - - if (dropdownCssClass.indexOf(':all:') !== -1) { - dropdownCssClass = dropdownCssClass.replace(':all:', ''); - - Utils.copyNonInternalCssClasses($dropdown[0], this.$element[0]); - } - - $dropdown.addClass(dropdownCssClass); - - return $dropdown; - }; - - return DropdownCSS; - }); - - S2.define('select2/i18n/en',[],function () { - // English - return { - errorLoading: function () { - return 'The results could not be loaded.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Please delete ' + overChars + ' character'; - - if (overChars != 1) { - message += 's'; - } - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Please enter ' + remainingChars + ' or more characters'; - - return message; - }, - loadingMore: function () { - return 'Loading more results…'; - }, - maximumSelected: function (args) { - var message = 'You can only select ' + args.maximum + ' item'; - - if (args.maximum != 1) { - message += 's'; - } - - return message; - }, - noResults: function () { - return 'No results found'; - }, - searching: function () { - return 'Searching…'; - }, - removeAllItems: function () { - return 'Remove all items'; - }, - removeItem: function () { - return 'Remove item'; - } - }; - }); - - S2.define('select2/defaults',[ - 'jquery', - - './results', - - './selection/single', - './selection/multiple', - './selection/placeholder', - './selection/allowClear', - './selection/search', - './selection/selectionCss', - './selection/eventRelay', - - './utils', - './translation', - './diacritics', - - './data/select', - './data/array', - './data/ajax', - './data/tags', - './data/tokenizer', - './data/minimumInputLength', - './data/maximumInputLength', - './data/maximumSelectionLength', - - './dropdown', - './dropdown/search', - './dropdown/hidePlaceholder', - './dropdown/infiniteScroll', - './dropdown/attachBody', - './dropdown/minimumResultsForSearch', - './dropdown/selectOnClose', - './dropdown/closeOnSelect', - './dropdown/dropdownCss', - - './i18n/en' - ], function ($, - - ResultsList, - - SingleSelection, MultipleSelection, Placeholder, AllowClear, - SelectionSearch, SelectionCSS, EventRelay, - - Utils, Translation, DIACRITICS, - - SelectData, ArrayData, AjaxData, Tags, Tokenizer, - MinimumInputLength, MaximumInputLength, MaximumSelectionLength, - - Dropdown, DropdownSearch, HidePlaceholder, InfiniteScroll, - AttachBody, MinimumResultsForSearch, SelectOnClose, CloseOnSelect, - DropdownCSS, - - EnglishTranslation) { - function Defaults () { - this.reset(); - } - - Defaults.prototype.apply = function (options) { - options = $.extend(true, {}, this.defaults, options); - - if (options.dataAdapter == null) { - if (options.ajax != null) { - options.dataAdapter = AjaxData; - } else if (options.data != null) { - options.dataAdapter = ArrayData; - } else { - options.dataAdapter = SelectData; - } - - if (options.minimumInputLength > 0) { - options.dataAdapter = Utils.Decorate( - options.dataAdapter, - MinimumInputLength - ); - } - - if (options.maximumInputLength > 0) { - options.dataAdapter = Utils.Decorate( - options.dataAdapter, - MaximumInputLength - ); - } - - if (options.maximumSelectionLength > 0) { - options.dataAdapter = Utils.Decorate( - options.dataAdapter, - MaximumSelectionLength - ); - } - - if (options.tags) { - options.dataAdapter = Utils.Decorate(options.dataAdapter, Tags); - } - - if (options.tokenSeparators != null || options.tokenizer != null) { - options.dataAdapter = Utils.Decorate( - options.dataAdapter, - Tokenizer - ); - } - } - - if (options.resultsAdapter == null) { - options.resultsAdapter = ResultsList; - - if (options.ajax != null) { - options.resultsAdapter = Utils.Decorate( - options.resultsAdapter, - InfiniteScroll - ); - } - - if (options.placeholder != null) { - options.resultsAdapter = Utils.Decorate( - options.resultsAdapter, - HidePlaceholder - ); - } - - if (options.selectOnClose) { - options.resultsAdapter = Utils.Decorate( - options.resultsAdapter, - SelectOnClose - ); - } - } - - if (options.dropdownAdapter == null) { - if (options.multiple) { - options.dropdownAdapter = Dropdown; - } else { - var SearchableDropdown = Utils.Decorate(Dropdown, DropdownSearch); - - options.dropdownAdapter = SearchableDropdown; - } - - if (options.minimumResultsForSearch !== 0) { - options.dropdownAdapter = Utils.Decorate( - options.dropdownAdapter, - MinimumResultsForSearch - ); - } - - if (options.closeOnSelect) { - options.dropdownAdapter = Utils.Decorate( - options.dropdownAdapter, - CloseOnSelect - ); - } - - if (options.dropdownCssClass != null) { - options.dropdownAdapter = Utils.Decorate( - options.dropdownAdapter, - DropdownCSS - ); - } - - options.dropdownAdapter = Utils.Decorate( - options.dropdownAdapter, - AttachBody - ); - } - - if (options.selectionAdapter == null) { - if (options.multiple) { - options.selectionAdapter = MultipleSelection; - } else { - options.selectionAdapter = SingleSelection; - } - - // Add the placeholder mixin if a placeholder was specified - if (options.placeholder != null) { - options.selectionAdapter = Utils.Decorate( - options.selectionAdapter, - Placeholder - ); - } - - if (options.allowClear) { - options.selectionAdapter = Utils.Decorate( - options.selectionAdapter, - AllowClear - ); - } - - if (options.multiple) { - options.selectionAdapter = Utils.Decorate( - options.selectionAdapter, - SelectionSearch - ); - } - - if (options.selectionCssClass != null) { - options.selectionAdapter = Utils.Decorate( - options.selectionAdapter, - SelectionCSS - ); - } - - options.selectionAdapter = Utils.Decorate( - options.selectionAdapter, - EventRelay - ); - } - - // If the defaults were not previously applied from an element, it is - // possible for the language option to have not been resolved - options.language = this._resolveLanguage(options.language); - - // Always fall back to English since it will always be complete - options.language.push('en'); - - var uniqueLanguages = []; - - for (var l = 0; l < options.language.length; l++) { - var language = options.language[l]; - - if (uniqueLanguages.indexOf(language) === -1) { - uniqueLanguages.push(language); - } - } - - options.language = uniqueLanguages; - - options.translations = this._processTranslations( - options.language, - options.debug - ); - - return options; - }; - - Defaults.prototype.reset = function () { - function stripDiacritics (text) { - // Used 'uni range + named function' from http://jsperf.com/diacritics/18 - function match(a) { - return DIACRITICS[a] || a; - } - - return text.replace(/[^\u0000-\u007E]/g, match); - } - - function matcher (params, data) { - // Always return the object if there is nothing to compare - if (params.term == null || params.term.trim() === '') { - return data; - } - - // Do a recursive check for options with children - if (data.children && data.children.length > 0) { - // Clone the data object if there are children - // This is required as we modify the object to remove any non-matches - var match = $.extend(true, {}, data); - - // Check each child of the option - for (var c = data.children.length - 1; c >= 0; c--) { - var child = data.children[c]; - - var matches = matcher(params, child); - - // If there wasn't a match, remove the object in the array - if (matches == null) { - match.children.splice(c, 1); - } - } - - // If any children matched, return the new object - if (match.children.length > 0) { - return match; - } - - // If there were no matching children, check just the plain object - return matcher(params, match); - } - - var original = stripDiacritics(data.text).toUpperCase(); - var term = stripDiacritics(params.term).toUpperCase(); - - // Check if the text contains the term - if (original.indexOf(term) > -1) { - return data; - } - - // If it doesn't contain the term, don't return anything - return null; - } - - this.defaults = { - amdLanguageBase: './i18n/', - autocomplete: 'off', - closeOnSelect: true, - debug: false, - dropdownAutoWidth: false, - escapeMarkup: Utils.escapeMarkup, - language: {}, - matcher: matcher, - minimumInputLength: 0, - maximumInputLength: 0, - maximumSelectionLength: 0, - minimumResultsForSearch: 0, - selectOnClose: false, - scrollAfterSelect: false, - sorter: function (data) { - return data; - }, - templateResult: function (result) { - return result.text; - }, - templateSelection: function (selection) { - return selection.text; - }, - theme: 'default', - width: 'resolve' - }; - }; - - Defaults.prototype.applyFromElement = function (options, $element) { - var optionLanguage = options.language; - var defaultLanguage = this.defaults.language; - var elementLanguage = $element.prop('lang'); - var parentLanguage = $element.closest('[lang]').prop('lang'); - - var languages = Array.prototype.concat.call( - this._resolveLanguage(elementLanguage), - this._resolveLanguage(optionLanguage), - this._resolveLanguage(defaultLanguage), - this._resolveLanguage(parentLanguage) - ); - - options.language = languages; - - return options; - }; - - Defaults.prototype._resolveLanguage = function (language) { - if (!language) { - return []; - } - - if ($.isEmptyObject(language)) { - return []; - } - - if ($.isPlainObject(language)) { - return [language]; - } - - var languages; - - if (!Array.isArray(language)) { - languages = [language]; - } else { - languages = language; - } - - var resolvedLanguages = []; - - for (var l = 0; l < languages.length; l++) { - resolvedLanguages.push(languages[l]); - - if (typeof languages[l] === 'string' && languages[l].indexOf('-') > 0) { - // Extract the region information if it is included - var languageParts = languages[l].split('-'); - var baseLanguage = languageParts[0]; - - resolvedLanguages.push(baseLanguage); - } - } - - return resolvedLanguages; - }; - - Defaults.prototype._processTranslations = function (languages, debug) { - var translations = new Translation(); - - for (var l = 0; l < languages.length; l++) { - var languageData = new Translation(); - - var language = languages[l]; - - if (typeof language === 'string') { - try { - // Try to load it with the original name - languageData = Translation.loadPath(language); - } catch (e) { - try { - // If we couldn't load it, check if it wasn't the full path - language = this.defaults.amdLanguageBase + language; - languageData = Translation.loadPath(language); - } catch (ex) { - // The translation could not be loaded at all. Sometimes this is - // because of a configuration problem, other times this can be - // because of how Select2 helps load all possible translation files - if (debug && window.console && console.warn) { - console.warn( - 'Select2: The language file for "' + language + '" could ' + - 'not be automatically loaded. A fallback will be used instead.' - ); - } - } - } - } else if ($.isPlainObject(language)) { - languageData = new Translation(language); - } else { - languageData = language; - } - - translations.extend(languageData); - } - - return translations; - }; - - Defaults.prototype.set = function (key, value) { - var camelKey = $.camelCase(key); - - var data = {}; - data[camelKey] = value; - - var convertedData = Utils._convertData(data); - - $.extend(true, this.defaults, convertedData); - }; - - var defaults = new Defaults(); - - return defaults; - }); - - S2.define('select2/options',[ - 'jquery', - './defaults', - './utils' - ], function ($, Defaults, Utils) { - function Options (options, $element) { - this.options = options; - - if ($element != null) { - this.fromElement($element); - } - - if ($element != null) { - this.options = Defaults.applyFromElement(this.options, $element); - } - - this.options = Defaults.apply(this.options); - } - - Options.prototype.fromElement = function ($e) { - var excludedData = ['select2']; - - if (this.options.multiple == null) { - this.options.multiple = $e.prop('multiple'); - } - - if (this.options.disabled == null) { - this.options.disabled = $e.prop('disabled'); - } - - if (this.options.autocomplete == null && $e.prop('autocomplete')) { - this.options.autocomplete = $e.prop('autocomplete'); - } - - if (this.options.dir == null) { - if ($e.prop('dir')) { - this.options.dir = $e.prop('dir'); - } else if ($e.closest('[dir]').prop('dir')) { - this.options.dir = $e.closest('[dir]').prop('dir'); - } else { - this.options.dir = 'ltr'; - } - } - - $e.prop('disabled', this.options.disabled); - $e.prop('multiple', this.options.multiple); - - if (Utils.GetData($e[0], 'select2Tags')) { - if (this.options.debug && window.console && console.warn) { - console.warn( - 'Select2: The `data-select2-tags` attribute has been changed to ' + - 'use the `data-data` and `data-tags="true"` attributes and will be ' + - 'removed in future versions of Select2.' - ); - } - - Utils.StoreData($e[0], 'data', Utils.GetData($e[0], 'select2Tags')); - Utils.StoreData($e[0], 'tags', true); - } - - if (Utils.GetData($e[0], 'ajaxUrl')) { - if (this.options.debug && window.console && console.warn) { - console.warn( - 'Select2: The `data-ajax-url` attribute has been changed to ' + - '`data-ajax--url` and support for the old attribute will be removed' + - ' in future versions of Select2.' - ); - } - - $e.attr('ajax--url', Utils.GetData($e[0], 'ajaxUrl')); - Utils.StoreData($e[0], 'ajax-Url', Utils.GetData($e[0], 'ajaxUrl')); - } - - var dataset = {}; - - function upperCaseLetter(_, letter) { - return letter.toUpperCase(); - } - - // Pre-load all of the attributes which are prefixed with `data-` - for (var attr = 0; attr < $e[0].attributes.length; attr++) { - var attributeName = $e[0].attributes[attr].name; - var prefix = 'data-'; - - if (attributeName.substr(0, prefix.length) == prefix) { - // Get the contents of the attribute after `data-` - var dataName = attributeName.substring(prefix.length); - - // Get the data contents from the consistent source - // This is more than likely the jQuery data helper - var dataValue = Utils.GetData($e[0], dataName); - - // camelCase the attribute name to match the spec - var camelDataName = dataName.replace(/-([a-z])/g, upperCaseLetter); - - // Store the data attribute contents into the dataset since - dataset[camelDataName] = dataValue; - } - } - - // Prefer the element's `dataset` attribute if it exists - // jQuery 1.x does not correctly handle data attributes with multiple dashes - if ($.fn.jquery && $.fn.jquery.substr(0, 2) == '1.' && $e[0].dataset) { - dataset = $.extend(true, {}, $e[0].dataset, dataset); - } - - // Prefer our internal data cache if it exists - var data = $.extend(true, {}, Utils.GetData($e[0]), dataset); - - data = Utils._convertData(data); - - for (var key in data) { - if (excludedData.indexOf(key) > -1) { - continue; - } - - if ($.isPlainObject(this.options[key])) { - $.extend(this.options[key], data[key]); - } else { - this.options[key] = data[key]; - } - } - - return this; - }; - - Options.prototype.get = function (key) { - return this.options[key]; - }; - - Options.prototype.set = function (key, val) { - this.options[key] = val; - }; - - return Options; - }); - - S2.define('select2/core',[ - 'jquery', - './options', - './utils', - './keys' - ], function ($, Options, Utils, KEYS) { - var Select2 = function ($element, options) { - if (Utils.GetData($element[0], 'select2') != null) { - Utils.GetData($element[0], 'select2').destroy(); - } - - this.$element = $element; - - this.id = this._generateId($element); - - options = options || {}; - - this.options = new Options(options, $element); - - Select2.__super__.constructor.call(this); - - // Set up the tabindex - - var tabindex = $element.attr('tabindex') || 0; - Utils.StoreData($element[0], 'old-tabindex', tabindex); - $element.attr('tabindex', '-1'); - - // Set up containers and adapters - - var DataAdapter = this.options.get('dataAdapter'); - this.dataAdapter = new DataAdapter($element, this.options); - - var $container = this.render(); - - this._placeContainer($container); - - var SelectionAdapter = this.options.get('selectionAdapter'); - this.selection = new SelectionAdapter($element, this.options); - this.$selection = this.selection.render(); - - this.selection.position(this.$selection, $container); - - var DropdownAdapter = this.options.get('dropdownAdapter'); - this.dropdown = new DropdownAdapter($element, this.options); - this.$dropdown = this.dropdown.render(); - - this.dropdown.position(this.$dropdown, $container); - - var ResultsAdapter = this.options.get('resultsAdapter'); - this.results = new ResultsAdapter($element, this.options, this.dataAdapter); - this.$results = this.results.render(); - - this.results.position(this.$results, this.$dropdown); - - // Bind events - - var self = this; - - // Bind the container to all of the adapters - this._bindAdapters(); - - // Register any DOM event handlers - this._registerDomEvents(); - - // Register any internal event handlers - this._registerDataEvents(); - this._registerSelectionEvents(); - this._registerDropdownEvents(); - this._registerResultsEvents(); - this._registerEvents(); - - // Set the initial state - this.dataAdapter.current(function (initialData) { - self.trigger('selection:update', { - data: initialData - }); - }); - - // Hide the original select - $element[0].classList.add('select2-hidden-accessible'); - $element.attr('aria-hidden', 'true'); - - // Synchronize any monitored attributes - this._syncAttributes(); - - Utils.StoreData($element[0], 'select2', this); - - // Ensure backwards compatibility with $element.data('select2'). - $element.data('select2', this); - }; - - Utils.Extend(Select2, Utils.Observable); - - Select2.prototype._generateId = function ($element) { - var id = ''; - - if ($element.attr('id') != null) { - id = $element.attr('id'); - } else if ($element.attr('name') != null) { - id = $element.attr('name') + '-' + Utils.generateChars(2); - } else { - id = Utils.generateChars(4); - } - - id = id.replace(/(:|\.|\[|\]|,)/g, ''); - id = 'select2-' + id; - - return id; - }; - - Select2.prototype._placeContainer = function ($container) { - $container.insertAfter(this.$element); - - var width = this._resolveWidth(this.$element, this.options.get('width')); - - if (width != null) { - $container.css('width', width); - } - }; - - Select2.prototype._resolveWidth = function ($element, method) { - var WIDTH = /^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i; - - if (method == 'resolve') { - var styleWidth = this._resolveWidth($element, 'style'); - - if (styleWidth != null) { - return styleWidth; - } - - return this._resolveWidth($element, 'element'); - } - - if (method == 'element') { - var elementWidth = $element.outerWidth(false); - - if (elementWidth <= 0) { - return 'auto'; - } - - return elementWidth + 'px'; - } - - if (method == 'style') { - var style = $element.attr('style'); - - if (typeof(style) !== 'string') { - return null; - } - - var attrs = style.split(';'); - - for (var i = 0, l = attrs.length; i < l; i = i + 1) { - var attr = attrs[i].replace(/\s/g, ''); - var matches = attr.match(WIDTH); - - if (matches !== null && matches.length >= 1) { - return matches[1]; - } - } - - return null; - } - - if (method == 'computedstyle') { - var computedStyle = window.getComputedStyle($element[0]); - - return computedStyle.width; - } - - return method; - }; - - Select2.prototype._bindAdapters = function () { - this.dataAdapter.bind(this, this.$container); - this.selection.bind(this, this.$container); - - this.dropdown.bind(this, this.$container); - this.results.bind(this, this.$container); - }; - - Select2.prototype._registerDomEvents = function () { - var self = this; - - this.$element.on('change.select2', function () { - self.dataAdapter.current(function (data) { - self.trigger('selection:update', { - data: data - }); - }); - }); - - this.$element.on('focus.select2', function (evt) { - self.trigger('focus', evt); - }); - - this._syncA = Utils.bind(this._syncAttributes, this); - this._syncS = Utils.bind(this._syncSubtree, this); - - this._observer = new window.MutationObserver(function (mutations) { - self._syncA(); - self._syncS(mutations); - }); - this._observer.observe(this.$element[0], { - attributes: true, - childList: true, - subtree: false - }); - }; - - Select2.prototype._registerDataEvents = function () { - var self = this; - - this.dataAdapter.on('*', function (name, params) { - self.trigger(name, params); - }); - }; - - Select2.prototype._registerSelectionEvents = function () { - var self = this; - var nonRelayEvents = ['toggle', 'focus']; - - this.selection.on('toggle', function () { - self.toggleDropdown(); - }); - - this.selection.on('focus', function (params) { - self.focus(params); - }); - - this.selection.on('*', function (name, params) { - if (nonRelayEvents.indexOf(name) !== -1) { - return; - } - - self.trigger(name, params); - }); - }; - - Select2.prototype._registerDropdownEvents = function () { - var self = this; - - this.dropdown.on('*', function (name, params) { - self.trigger(name, params); - }); - }; - - Select2.prototype._registerResultsEvents = function () { - var self = this; - - this.results.on('*', function (name, params) { - self.trigger(name, params); - }); - }; - - Select2.prototype._registerEvents = function () { - var self = this; - - this.on('open', function () { - self.$container[0].classList.add('select2-container--open'); - }); - - this.on('close', function () { - self.$container[0].classList.remove('select2-container--open'); - }); - - this.on('enable', function () { - self.$container[0].classList.remove('select2-container--disabled'); - }); - - this.on('disable', function () { - self.$container[0].classList.add('select2-container--disabled'); - }); - - this.on('blur', function () { - self.$container[0].classList.remove('select2-container--focus'); - }); - - this.on('query', function (params) { - if (!self.isOpen()) { - self.trigger('open', {}); - } - - this.dataAdapter.query(params, function (data) { - self.trigger('results:all', { - data: data, - query: params - }); - }); - }); - - this.on('query:append', function (params) { - this.dataAdapter.query(params, function (data) { - self.trigger('results:append', { - data: data, - query: params - }); - }); - }); - - this.on('keypress', function (evt) { - var key = evt.which; - - if (self.isOpen()) { - if (key === KEYS.ESC || key === KEYS.TAB || - (key === KEYS.UP && evt.altKey)) { - self.close(evt); - - evt.preventDefault(); - } else if (key === KEYS.ENTER) { - self.trigger('results:select', {}); - - evt.preventDefault(); - } else if ((key === KEYS.SPACE && evt.ctrlKey)) { - self.trigger('results:toggle', {}); - - evt.preventDefault(); - } else if (key === KEYS.UP) { - self.trigger('results:previous', {}); - - evt.preventDefault(); - } else if (key === KEYS.DOWN) { - self.trigger('results:next', {}); - - evt.preventDefault(); - } - } else { - if (key === KEYS.ENTER || key === KEYS.SPACE || - (key === KEYS.DOWN && evt.altKey)) { - self.open(); - - evt.preventDefault(); - } - } - }); - }; - - Select2.prototype._syncAttributes = function () { - this.options.set('disabled', this.$element.prop('disabled')); - - if (this.isDisabled()) { - if (this.isOpen()) { - this.close(); - } - - this.trigger('disable', {}); - } else { - this.trigger('enable', {}); - } - }; - - Select2.prototype._isChangeMutation = function (mutations) { - var self = this; - - if (mutations.addedNodes && mutations.addedNodes.length > 0) { - for (var n = 0; n < mutations.addedNodes.length; n++) { - var node = mutations.addedNodes[n]; - - if (node.selected) { - return true; - } - } - } else if (mutations.removedNodes && mutations.removedNodes.length > 0) { - return true; - } else if (Array.isArray(mutations)) { - return mutations.some(function (mutation) { - return self._isChangeMutation(mutation); - }); - } - - return false; - }; - - Select2.prototype._syncSubtree = function (mutations) { - var changed = this._isChangeMutation(mutations); - var self = this; - - // Only re-pull the data if we think there is a change - if (changed) { - this.dataAdapter.current(function (currentData) { - self.trigger('selection:update', { - data: currentData - }); - }); - } - }; - - /** - * Override the trigger method to automatically trigger pre-events when - * there are events that can be prevented. - */ - Select2.prototype.trigger = function (name, args) { - var actualTrigger = Select2.__super__.trigger; - var preTriggerMap = { - 'open': 'opening', - 'close': 'closing', - 'select': 'selecting', - 'unselect': 'unselecting', - 'clear': 'clearing' - }; - - if (args === undefined) { - args = {}; - } - - if (name in preTriggerMap) { - var preTriggerName = preTriggerMap[name]; - var preTriggerArgs = { - prevented: false, - name: name, - args: args - }; - - actualTrigger.call(this, preTriggerName, preTriggerArgs); - - if (preTriggerArgs.prevented) { - args.prevented = true; - - return; - } - } - - actualTrigger.call(this, name, args); - }; - - Select2.prototype.toggleDropdown = function () { - if (this.isDisabled()) { - return; - } - - if (this.isOpen()) { - this.close(); - } else { - this.open(); - } - }; - - Select2.prototype.open = function () { - if (this.isOpen()) { - return; - } - - if (this.isDisabled()) { - return; - } - - this.trigger('query', {}); - }; - - Select2.prototype.close = function (evt) { - if (!this.isOpen()) { - return; - } - - this.trigger('close', { originalEvent : evt }); - }; - - /** - * Helper method to abstract the "enabled" (not "disabled") state of this - * object. - * - * @return {true} if the instance is not disabled. - * @return {false} if the instance is disabled. - */ - Select2.prototype.isEnabled = function () { - return !this.isDisabled(); - }; - - /** - * Helper method to abstract the "disabled" state of this object. - * - * @return {true} if the disabled option is true. - * @return {false} if the disabled option is false. - */ - Select2.prototype.isDisabled = function () { - return this.options.get('disabled'); - }; - - Select2.prototype.isOpen = function () { - return this.$container[0].classList.contains('select2-container--open'); - }; - - Select2.prototype.hasFocus = function () { - return this.$container[0].classList.contains('select2-container--focus'); - }; - - Select2.prototype.focus = function (data) { - // No need to re-trigger focus events if we are already focused - if (this.hasFocus()) { - return; - } - - this.$container[0].classList.add('select2-container--focus'); - this.trigger('focus', {}); - }; - - Select2.prototype.enable = function (args) { - if (this.options.get('debug') && window.console && console.warn) { - console.warn( - 'Select2: The `select2("enable")` method has been deprecated and will' + - ' be removed in later Select2 versions. Use $element.prop("disabled")' + - ' instead.' - ); - } - - if (args == null || args.length === 0) { - args = [true]; - } - - var disabled = !args[0]; - - this.$element.prop('disabled', disabled); - }; - - Select2.prototype.data = function () { - if (this.options.get('debug') && - arguments.length > 0 && window.console && console.warn) { - console.warn( - 'Select2: Data can no longer be set using `select2("data")`. You ' + - 'should consider setting the value instead using `$element.val()`.' - ); - } - - var data = []; - - this.dataAdapter.current(function (currentData) { - data = currentData; - }); - - return data; - }; - - Select2.prototype.val = function (args) { - if (this.options.get('debug') && window.console && console.warn) { - console.warn( - 'Select2: The `select2("val")` method has been deprecated and will be' + - ' removed in later Select2 versions. Use $element.val() instead.' - ); - } - - if (args == null || args.length === 0) { - return this.$element.val(); - } - - var newVal = args[0]; - - if (Array.isArray(newVal)) { - newVal = newVal.map(function (obj) { - return obj.toString(); - }); - } - - this.$element.val(newVal).trigger('input').trigger('change'); - }; - - Select2.prototype.destroy = function () { - this.$container.remove(); - - this._observer.disconnect(); - this._observer = null; - - this._syncA = null; - this._syncS = null; - - this.$element.off('.select2'); - this.$element.attr('tabindex', - Utils.GetData(this.$element[0], 'old-tabindex')); - - this.$element[0].classList.remove('select2-hidden-accessible'); - this.$element.attr('aria-hidden', 'false'); - Utils.RemoveData(this.$element[0]); - this.$element.removeData('select2'); - - this.dataAdapter.destroy(); - this.selection.destroy(); - this.dropdown.destroy(); - this.results.destroy(); - - this.dataAdapter = null; - this.selection = null; - this.dropdown = null; - this.results = null; - }; - - Select2.prototype.render = function () { - var $container = $( - '' + - '' + - '' + - '' - ); - - $container.attr('dir', this.options.get('dir')); - - this.$container = $container; - - this.$container[0].classList - .add('select2-container--' + this.options.get('theme')); - - Utils.StoreData($container[0], 'element', this.$element); - - return $container; - }; - - return Select2; - }); - - S2.define('jquery-mousewheel',[ - 'jquery' - ], function ($) { - // Used to shim jQuery.mousewheel for non-full builds. - return $; - }); - - S2.define('jquery.select2',[ - 'jquery', - 'jquery-mousewheel', - - './select2/core', - './select2/defaults', - './select2/utils' - ], function ($, _, Select2, Defaults, Utils) { - if ($.fn.select2 == null) { - // All methods that should return the element - var thisMethods = ['open', 'close', 'destroy']; - - $.fn.select2 = function (options) { - options = options || {}; - - if (typeof options === 'object') { - this.each(function () { - var instanceOptions = $.extend(true, {}, options); - - var instance = new Select2($(this), instanceOptions); - }); - - return this; - } else if (typeof options === 'string') { - var ret; - var args = Array.prototype.slice.call(arguments, 1); - - this.each(function () { - var instance = Utils.GetData(this, 'select2'); - - if (instance == null && window.console && console.error) { - console.error( - 'The select2(\'' + options + '\') method was called on an ' + - 'element that is not using Select2.' - ); - } - - ret = instance[options].apply(instance, args); - }); - - // Check if we should be returning `this` - if (thisMethods.indexOf(options) > -1) { - return this; - } - - return ret; - } else { - throw new Error('Invalid arguments for Select2: ' + options); - } - }; - } - - if ($.fn.select2.defaults == null) { - $.fn.select2.defaults = Defaults; - } - - return Select2; - }); - - // Return the AMD loader configuration so it can be used outside of this file - return { - define: S2.define, - require: S2.require - }; - }()); - - // Autoload the jQuery bindings - // We know that all of the modules exist above this, so we're safe - var select2 = S2.require('jquery.select2'); - - // Hold the AMD module references on the jQuery function that was just loaded - // This allows Select2 to use the internal loader outside of this file, such - // as in the language files. - jQuery.fn.select2.amd = S2; - - // Return the Select2 instance for anyone who is importing it. - return select2; -})); diff --git a/obp-api/src/main/webapp/media/js/select2.min.js b/obp-api/src/main/webapp/media/js/select2.min.js new file mode 100644 index 000000000..fd88a10fe --- /dev/null +++ b/obp-api/src/main/webapp/media/js/select2.min.js @@ -0,0 +1 @@ +;(function(factory){if(typeof define==='function'&&define.amd){define(['jquery'],factory)}else if(typeof module==='object'&&module.exports){module.exports=function(root,jQuery){if(jQuery===undefined){if(typeof window!=='undefined'){jQuery=require('jquery')}else{jQuery=require('jquery')(root)}}factory(jQuery);return jQuery}}else{factory(jQuery)}}(function(jQuery){var S2=(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd){var S2=jQuery.fn.select2.amd}var S2;(function(){if(!S2||!S2.requirejs){if(!S2){S2={}}else{require=S2}var requirejs,require,define;(function(undef){var main,req,makeMap,handlers,defined={},waiting={},config={},defining={},hasOwn=Object.prototype.hasOwnProperty,aps=[].slice,jsSuffixRegExp=/\.js$/;function hasProp(obj,prop){return hasOwn.call(obj,prop)}function normalize(name,baseName){var nameParts,nameSegment,mapValue,foundMap,lastIndex,foundI,foundStarMap,starI,i,j,part,normalizedBaseParts,baseParts=baseName&&baseName.split("/"),map=config.map,starMap=(map&&map['*'])||{};if(name){name=name.split('/');lastIndex=name.length-1;if(config.nodeIdCompat&&jsSuffixRegExp.test(name[lastIndex])){name[lastIndex]=name[lastIndex].replace(jsSuffixRegExp,'')}if(name[0].charAt(0)==='.'&&baseParts){normalizedBaseParts=baseParts.slice(0,baseParts.length-1);name=normalizedBaseParts.concat(name)}for(i=0;i0){name.splice(i-1,2);i-=2}}}name=name.join('/')}if((baseParts||starMap)&&map){nameParts=name.split('/');for(i=nameParts.length;i>0;i-=1){nameSegment=nameParts.slice(0,i).join("/");if(baseParts){for(j=baseParts.length;j>0;j-=1){mapValue=map[baseParts.slice(0,j).join('/')];if(mapValue){mapValue=mapValue[nameSegment];if(mapValue){foundMap=mapValue;foundI=i;break}}}}if(foundMap){break}if(!foundStarMap&&starMap&&starMap[nameSegment]){foundStarMap=starMap[nameSegment];starI=i}}if(!foundMap&&foundStarMap){foundMap=foundStarMap;foundI=starI}if(foundMap){nameParts.splice(0,foundI,foundMap);name=nameParts.join('/')}}return name}function makeRequire(relName,forceSync){return function(){var args=aps.call(arguments,0);if(typeof args[0]!=='string'&&args.length===1){args.push(null)}return req.apply(undef,args.concat([relName,forceSync]))}}function makeNormalize(relName){return function(name){return normalize(name,relName)}}function makeLoad(depName){return function(value){defined[depName]=value}}function callDep(name){if(hasProp(waiting,name)){var args=waiting[name];delete waiting[name];defining[name]=true;main.apply(undef,args)}if(!hasProp(defined,name)&&!hasProp(defining,name)){throw new Error('No '+name);}return defined[name]}function splitPrefix(name){var prefix,index=name?name.indexOf('!'):-1;if(index>-1){prefix=name.substring(0,index);name=name.substring(index+1,name.length)}return[prefix,name]}function makeRelParts(relName){return relName?splitPrefix(relName):[]}makeMap=function(name,relParts){var plugin,parts=splitPrefix(name),prefix=parts[0],relResourceName=relParts[1];name=parts[1];if(prefix){prefix=normalize(prefix,relResourceName);plugin=callDep(prefix)}if(prefix){if(plugin&&plugin.normalize){name=plugin.normalize(name,makeNormalize(relResourceName))}else{name=normalize(name,relResourceName)}}else{name=normalize(name,relResourceName);parts=splitPrefix(name);prefix=parts[0];name=parts[1];if(prefix){plugin=callDep(prefix)}}return{f:prefix?prefix+'!'+name:name,n:name,pr:prefix,p:plugin}};function makeConfig(name){return function(){return(config&&config.config&&config.config[name])||{}}}handlers={require:function(name){return makeRequire(name)},exports:function(name){var e=defined[name];if(typeof e!=='undefined'){return e}else{return(defined[name]={})}},module:function(name){return{id:name,uri:'',exports:defined[name],config:makeConfig(name)}}};main=function(name,deps,callback,relName){var cjsModule,depName,ret,map,i,relParts,args=[],callbackType=typeof callback,usingExports;relName=relName||name;relParts=makeRelParts(relName);if(callbackType==='undefined'||callbackType==='function'){deps=!deps.length&&callback.length?['require','exports','module']:deps;for(i=0;i0){unshift.call(arguments,SuperClass.prototype.constructor);calledConstructor=DecoratorClass.prototype.constructor}calledConstructor.apply(this,arguments)}DecoratorClass.displayName=SuperClass.displayName;function ctr(){this.constructor=DecoratedClass}DecoratedClass.prototype=new ctr();for(var m=0;m':'>','"':'"','\'':''','/':'/'};if(typeof markup!=='string'){return markup}return String(markup).replace(/[&<>"'\/\\]/g,function(match){return replaceMap[match]})};Utils.appendMany=function($element,$nodes){if($.fn.jquery.substr(0,3)==='1.7'){var $jqNodes=$();$.map($nodes,function(node){$jqNodes=$jqNodes.add(node)});$nodes=$jqNodes}$element.append($nodes)};Utils.__cache={};var id=0;Utils.GetUniqueElementId=function(element){var select2Id=element.getAttribute('data-select2-id');if(select2Id==null){if(element.id){select2Id=element.id;element.setAttribute('data-select2-id',select2Id)}else{element.setAttribute('data-select2-id',++id);select2Id=id.toString()}}return select2Id};Utils.StoreData=function(element,name,value){var id=Utils.GetUniqueElementId(element);if(!Utils.__cache[id]){Utils.__cache[id]={}}Utils.__cache[id][name]=value};Utils.GetData=function(element,name){var id=Utils.GetUniqueElementId(element);if(name){if(Utils.__cache[id]){if(Utils.__cache[id][name]!=null){return Utils.__cache[id][name]}return $(element).data(name)}return $(element).data(name)}else{return Utils.__cache[id]}};Utils.RemoveData=function(element){var id=Utils.GetUniqueElementId(element);if(Utils.__cache[id]!=null){delete Utils.__cache[id]}element.removeAttribute('data-select2-id')};return Utils});S2.define('select2/results',['jquery','./utils'],function($,Utils){function Results($element,options,dataAdapter){this.$element=$element;this.data=dataAdapter;this.options=options;Results.__super__.constructor.call(this)}Utils.Extend(Results,Utils.Observable);Results.prototype.render=function(){var $results=$('
          ');if(this.options.get('multiple')){$results.attr('aria-multiselectable','true')}this.$results=$results;return $results};Results.prototype.clear=function(){this.$results.empty()};Results.prototype.displayMessage=function(params){var escapeMarkup=this.options.get('escapeMarkup');this.clear();this.hideLoading();var $message=$('');var message=this.options.get('translations').get(params.message);$message.append(escapeMarkup(message(params.args)));$message[0].className+=' select2-results__message';this.$results.append($message)};Results.prototype.hideMessages=function(){this.$results.find('.select2-results__message').remove()};Results.prototype.append=function(data){this.hideLoading();var $options=[];if(data.results==null||data.results.length===0){if(this.$results.children().length===0){this.trigger('results:message',{message:'noResults'})}return}data.results=this.sort(data.results);for(var d=0;d0){$selected.first().trigger('mouseenter')}else{$options.first().trigger('mouseenter')}this.ensureHighlightVisible()};Results.prototype.setClasses=function(){var self=this;this.data.current(function(selected){var selectedIds=$.map(selected,function(s){return s.id.toString()});var $options=self.$results.find('.select2-results__option[aria-selected]');$options.each(function(){var $option=$(this);var item=Utils.GetData(this,'data');var id=''+item.id;if((item.element!=null&&item.element.selected)||(item.element==null&&$.inArray(id,selectedIds)>-1)){$option.attr('aria-selected','true')}else{$option.attr('aria-selected','false')}})})};Results.prototype.showLoading=function(params){this.hideLoading();var loadingMore=this.options.get('translations').get('searching');var loading={disabled:true,loading:true,text:loadingMore(params)};var $loading=this.option(loading);$loading.className+=' loading-results';this.$results.prepend($loading)};Results.prototype.hideLoading=function(){this.$results.find('.loading-results').remove()};Results.prototype.option=function(data){var option=document.createElement('li');option.className='select2-results__option';var attrs={'role':'option','aria-selected':'false'};var matches=window.Element.prototype.matches||window.Element.prototype.msMatchesSelector||window.Element.prototype.webkitMatchesSelector;if((data.element!=null&&matches.call(data.element,':disabled'))||(data.element==null&&data.disabled)){delete attrs['aria-selected'];attrs['aria-disabled']='true'}if(data.id==null){delete attrs['aria-selected']}if(data._resultId!=null){option.id=data._resultId}if(data.title){option.title=data.title}if(data.children){attrs.role='group';attrs['aria-label']=data.text;delete attrs['aria-selected']}for(var attr in attrs){var val=attrs[attr];option.setAttribute(attr,val)}if(data.children){var $option=$(option);var label=document.createElement('strong');label.className='select2-results__group';var $label=$(label);this.template(data,label);var $children=[];for(var c=0;c',{'class':'select2-results__options select2-results__options--nested'});$childrenContainer.append($children);$option.append(label);$option.append($childrenContainer)}else{this.template(data,option)}Utils.StoreData(option,'data',data);return option};Results.prototype.bind=function(container,$container){var self=this;var id=container.id+'-results';this.$results.attr('id',id);container.on('results:all',function(params){self.clear();self.append(params.data);if(container.isOpen()){self.setClasses();self.highlightFirstItem()}});container.on('results:append',function(params){self.append(params.data);if(container.isOpen()){self.setClasses()}});container.on('query',function(params){self.hideMessages();self.showLoading(params)});container.on('select',function(){if(!container.isOpen()){return}self.setClasses();if(self.options.get('scrollAfterSelect')){self.highlightFirstItem()}});container.on('unselect',function(){if(!container.isOpen()){return}self.setClasses();if(self.options.get('scrollAfterSelect')){self.highlightFirstItem()}});container.on('open',function(){self.$results.attr('aria-expanded','true');self.$results.attr('aria-hidden','false');self.setClasses();self.ensureHighlightVisible()});container.on('close',function(){self.$results.attr('aria-expanded','false');self.$results.attr('aria-hidden','true');self.$results.removeAttr('aria-activedescendant')});container.on('results:toggle',function(){var $highlighted=self.getHighlightedResults();if($highlighted.length===0){return}$highlighted.trigger('mouseup')});container.on('results:select',function(){var $highlighted=self.getHighlightedResults();if($highlighted.length===0){return}var data=Utils.GetData($highlighted[0],'data');if($highlighted.attr('aria-selected')=='true'){self.trigger('close',{})}else{self.trigger('select',{data:data})}});container.on('results:previous',function(){var $highlighted=self.getHighlightedResults();var $options=self.$results.find('[aria-selected]');var currentIndex=$options.index($highlighted);if(currentIndex<=0){return}var nextIndex=currentIndex-1;if($highlighted.length===0){nextIndex=0}var $next=$options.eq(nextIndex);$next.trigger('mouseenter');var currentOffset=self.$results.offset().top;var nextTop=$next.offset().top;var nextOffset=self.$results.scrollTop()+(nextTop-currentOffset);if(nextIndex===0){self.$results.scrollTop(0)}else if(nextTop-currentOffset<0){self.$results.scrollTop(nextOffset)}});container.on('results:next',function(){var $highlighted=self.getHighlightedResults();var $options=self.$results.find('[aria-selected]');var currentIndex=$options.index($highlighted);var nextIndex=currentIndex+1;if(nextIndex>=$options.length){return}var $next=$options.eq(nextIndex);$next.trigger('mouseenter');var currentOffset=self.$results.offset().top+self.$results.outerHeight(false);var nextBottom=$next.offset().top+$next.outerHeight(false);var nextOffset=self.$results.scrollTop()+nextBottom-currentOffset;if(nextIndex===0){self.$results.scrollTop(0)}else if(nextBottom>currentOffset){self.$results.scrollTop(nextOffset)}});container.on('results:focus',function(params){params.element.addClass('select2-results__option--highlighted')});container.on('results:message',function(params){self.displayMessage(params)});if($.fn.mousewheel){this.$results.on('mousewheel',function(e){var top=self.$results.scrollTop();var bottom=self.$results.get(0).scrollHeight-top+e.deltaY;var isAtTop=e.deltaY>0&&top-e.deltaY<=0;var isAtBottom=e.deltaY<0&&bottom<=self.$results.height();if(isAtTop){self.$results.scrollTop(0);e.preventDefault();e.stopPropagation()}else if(isAtBottom){self.$results.scrollTop(self.$results.get(0).scrollHeight-self.$results.height());e.preventDefault();e.stopPropagation()}})}this.$results.on('mouseup','.select2-results__option[aria-selected]',function(evt){var $this=$(this);var data=Utils.GetData(this,'data');if($this.attr('aria-selected')==='true'){if(self.options.get('multiple')){self.trigger('unselect',{originalEvent:evt,data:data})}else{self.trigger('close',{})}return}self.trigger('select',{originalEvent:evt,data:data})});this.$results.on('mouseenter','.select2-results__option[aria-selected]',function(evt){var data=Utils.GetData(this,'data');self.getHighlightedResults().removeClass('select2-results__option--highlighted');self.trigger('results:focus',{data:data,element:$(this)})})};Results.prototype.getHighlightedResults=function(){var $highlighted=this.$results.find('.select2-results__option--highlighted');return $highlighted};Results.prototype.destroy=function(){this.$results.remove()};Results.prototype.ensureHighlightVisible=function(){var $highlighted=this.getHighlightedResults();if($highlighted.length===0){return}var $options=this.$results.find('[aria-selected]');var currentIndex=$options.index($highlighted);var currentOffset=this.$results.offset().top;var nextTop=$highlighted.offset().top;var nextOffset=this.$results.scrollTop()+(nextTop-currentOffset);var offsetDelta=nextTop-currentOffset;nextOffset-=$highlighted.outerHeight(false)*2;if(currentIndex<=2){this.$results.scrollTop(0)}else if(offsetDelta>this.$results.outerHeight()||offsetDelta<0){this.$results.scrollTop(nextOffset)}};Results.prototype.template=function(result,container){var template=this.options.get('templateResult');var escapeMarkup=this.options.get('escapeMarkup');var content=template(result,container);if(content==null){container.style.display='none'}else if(typeof content==='string'){container.innerHTML=escapeMarkup(content)}else{$(container).append(content)}};return Results});S2.define('select2/keys',[],function(){var KEYS={BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46};return KEYS});S2.define('select2/selection/base',['jquery','../utils','../keys'],function($,Utils,KEYS){function BaseSelection($element,options){this.$element=$element;this.options=options;BaseSelection.__super__.constructor.call(this)}Utils.Extend(BaseSelection,Utils.Observable);BaseSelection.prototype.render=function(){var $selection=$('');this._tabindex=0;if(Utils.GetData(this.$element[0],'old-tabindex')!=null){this._tabindex=Utils.GetData(this.$element[0],'old-tabindex')}else if(this.$element.attr('tabindex')!=null){this._tabindex=this.$element.attr('tabindex')}$selection.attr('title',this.$element.attr('title'));$selection.attr('tabindex',this._tabindex);$selection.attr('aria-disabled','false');this.$selection=$selection;return $selection};BaseSelection.prototype.bind=function(container,$container){var self=this;var resultsId=container.id+'-results';this.container=container;this.$selection.on('focus',function(evt){self.trigger('focus',evt)});this.$selection.on('blur',function(evt){self._handleBlur(evt)});this.$selection.on('keydown',function(evt){self.trigger('keypress',evt);if(evt.which===KEYS.SPACE){evt.preventDefault()}});container.on('results:focus',function(params){self.$selection.attr('aria-activedescendant',params.data._resultId)});container.on('selection:update',function(params){self.update(params.data)});container.on('open',function(){self.$selection.attr('aria-expanded','true');self.$selection.attr('aria-owns',resultsId);self._attachCloseHandler(container)});container.on('close',function(){self.$selection.attr('aria-expanded','false');self.$selection.removeAttr('aria-activedescendant');self.$selection.removeAttr('aria-owns');self.$selection.trigger('focus');self._detachCloseHandler(container)});container.on('enable',function(){self.$selection.attr('tabindex',self._tabindex);self.$selection.attr('aria-disabled','false')});container.on('disable',function(){self.$selection.attr('tabindex','-1');self.$selection.attr('aria-disabled','true')})};BaseSelection.prototype._handleBlur=function(evt){var self=this;window.setTimeout(function(){if((document.activeElement==self.$selection[0])||($.contains(self.$selection[0],document.activeElement))){return}self.trigger('blur',evt)},1)};BaseSelection.prototype._attachCloseHandler=function(container){$(document.body).on('mousedown.select2.'+container.id,function(e){var $target=$(e.target);var $select=$target.closest('.select2');var $all=$('.select2.select2-container--open');$all.each(function(){if(this==$select[0]){return}var $element=Utils.GetData(this,'element');$element.select2('close')})})};BaseSelection.prototype._detachCloseHandler=function(container){$(document.body).off('mousedown.select2.'+container.id)};BaseSelection.prototype.position=function($selection,$container){var $selectionContainer=$container.find('.selection');$selectionContainer.append($selection)};BaseSelection.prototype.destroy=function(){this._detachCloseHandler(this.container)};BaseSelection.prototype.update=function(data){throw new Error('The `update` method must be defined in child classes.');};BaseSelection.prototype.isEnabled=function(){return!this.isDisabled()};BaseSelection.prototype.isDisabled=function(){return this.options.get('disabled')};return BaseSelection});S2.define('select2/selection/single',['jquery','./base','../utils','../keys'],function($,BaseSelection,Utils,KEYS){function SingleSelection(){SingleSelection.__super__.constructor.apply(this,arguments)}Utils.Extend(SingleSelection,BaseSelection);SingleSelection.prototype.render=function(){var $selection=SingleSelection.__super__.render.call(this);$selection.addClass('select2-selection--single');$selection.html(''+''+''+'');return $selection};SingleSelection.prototype.bind=function(container,$container){var self=this;SingleSelection.__super__.bind.apply(this,arguments);var id=container.id+'-container';this.$selection.find('.select2-selection__rendered').attr('id',id).attr('role','textbox').attr('aria-readonly','true');this.$selection.attr('aria-labelledby',id);this.$selection.on('mousedown',function(evt){if(evt.which!==1){return}self.trigger('toggle',{originalEvent:evt})});this.$selection.on('focus',function(evt){});this.$selection.on('blur',function(evt){});container.on('focus',function(evt){if(!container.isOpen()){self.$selection.trigger('focus')}})};SingleSelection.prototype.clear=function(){var $rendered=this.$selection.find('.select2-selection__rendered');$rendered.empty();$rendered.removeAttr('title')};SingleSelection.prototype.display=function(data,container){var template=this.options.get('templateSelection');var escapeMarkup=this.options.get('escapeMarkup');return escapeMarkup(template(data,container))};SingleSelection.prototype.selectionContainer=function(){return $('')};SingleSelection.prototype.update=function(data){if(data.length===0){this.clear();return}var selection=data[0];var $rendered=this.$selection.find('.select2-selection__rendered');var formatted=this.display(selection,$rendered);$rendered.empty().append(formatted);var title=selection.title||selection.text;if(title){$rendered.attr('title',title)}else{$rendered.removeAttr('title')}};return SingleSelection});S2.define('select2/selection/multiple',['jquery','./base','../utils'],function($,BaseSelection,Utils){function MultipleSelection($element,options){MultipleSelection.__super__.constructor.apply(this,arguments)}Utils.Extend(MultipleSelection,BaseSelection);MultipleSelection.prototype.render=function(){var $selection=MultipleSelection.__super__.render.call(this);$selection.addClass('select2-selection--multiple');$selection.html('
            ');return $selection};MultipleSelection.prototype.bind=function(container,$container){var self=this;MultipleSelection.__super__.bind.apply(this,arguments);this.$selection.on('click',function(evt){self.trigger('toggle',{originalEvent:evt})});this.$selection.on('click','.select2-selection__choice__remove',function(evt){if(self.isDisabled()){return}var $remove=$(this);var $selection=$remove.parent();var data=Utils.GetData($selection[0],'data');self.trigger('unselect',{originalEvent:evt,data:data})})};MultipleSelection.prototype.clear=function(){var $rendered=this.$selection.find('.select2-selection__rendered');$rendered.empty();$rendered.removeAttr('title')};MultipleSelection.prototype.display=function(data,container){var template=this.options.get('templateSelection');var escapeMarkup=this.options.get('escapeMarkup');return escapeMarkup(template(data,container))};MultipleSelection.prototype.selectionContainer=function(){var $container=$('
          • '+''+'×'+''+'
          • ');return $container};MultipleSelection.prototype.update=function(data){this.clear();if(data.length===0){return}var $selections=[];for(var d=0;d1;if(multipleSelections||singlePlaceholder){return decorated.call(this,data)}this.clear();var $placeholder=this.createPlaceholder(this.placeholder);this.$selection.find('.select2-selection__rendered').append($placeholder)};return Placeholder});S2.define('select2/selection/allowClear',['jquery','../keys','../utils'],function($,KEYS,Utils){function AllowClear(){}AllowClear.prototype.bind=function(decorated,container,$container){var self=this;decorated.call(this,container,$container);if(this.placeholder==null){if(this.options.get('debug')&&window.console&&console.error){console.error('Select2: The `allowClear` option should be used in combination '+'with the `placeholder` option.')}}this.$selection.on('mousedown','.select2-selection__clear',function(evt){self._handleClear(evt)});container.on('keypress',function(evt){self._handleKeyboardClear(evt,container)})};AllowClear.prototype._handleClear=function(_,evt){if(this.isDisabled()){return}var $clear=this.$selection.find('.select2-selection__clear');if($clear.length===0){return}evt.stopPropagation();var data=Utils.GetData($clear[0],'data');var previousVal=this.$element.val();this.$element.val(this.placeholder.id);var unselectData={data:data};this.trigger('clear',unselectData);if(unselectData.prevented){this.$element.val(previousVal);return}for(var d=0;d0||data.length===0){return}var removeAll=this.options.get('translations').get('removeAllItems');var $remove=$(''+'×'+'');Utils.StoreData($remove[0],'data',data);this.$selection.find('.select2-selection__rendered').prepend($remove)};return AllowClear});S2.define('select2/selection/search',['jquery','../utils','../keys'],function($,Utils,KEYS){function Search(decorated,$element,options){decorated.call(this,$element,options)}Search.prototype.render=function(decorated){var $search=$('');this.$searchContainer=$search;this.$search=$search.find('input');var $rendered=decorated.call(this);this._transferTabIndex();return $rendered};Search.prototype.bind=function(decorated,container,$container){var self=this;var resultsId=container.id+'-results';decorated.call(this,container,$container);container.on('open',function(){self.$search.attr('aria-controls',resultsId);self.$search.trigger('focus')});container.on('close',function(){self.$search.val('');self.$search.removeAttr('aria-controls');self.$search.removeAttr('aria-activedescendant');self.$search.trigger('focus')});container.on('enable',function(){self.$search.prop('disabled',false);self._transferTabIndex()});container.on('disable',function(){self.$search.prop('disabled',true)});container.on('focus',function(evt){self.$search.trigger('focus')});container.on('results:focus',function(params){if(params.data._resultId){self.$search.attr('aria-activedescendant',params.data._resultId)}else{self.$search.removeAttr('aria-activedescendant')}});this.$selection.on('focusin','.select2-search--inline',function(evt){self.trigger('focus',evt)});this.$selection.on('focusout','.select2-search--inline',function(evt){self._handleBlur(evt)});this.$selection.on('keydown','.select2-search--inline',function(evt){evt.stopPropagation();self.trigger('keypress',evt);self._keyUpPrevented=evt.isDefaultPrevented();var key=evt.which;if(key===KEYS.BACKSPACE&&self.$search.val()===''){var $previousChoice=self.$searchContainer.prev('.select2-selection__choice');if($previousChoice.length>0){var item=Utils.GetData($previousChoice[0],'data');self.searchRemoveChoice(item);evt.preventDefault()}}});this.$selection.on('click','.select2-search--inline',function(evt){if(self.$search.val()){evt.stopPropagation()}});var msie=document.documentMode;var disableInputEvents=msie&&msie<=11;this.$selection.on('input.searchcheck','.select2-search--inline',function(evt){if(disableInputEvents){self.$selection.off('input.search input.searchcheck');return}self.$selection.off('keyup.search')});this.$selection.on('keyup.search input.search','.select2-search--inline',function(evt){if(disableInputEvents&&evt.type==='input'){self.$selection.off('input.search input.searchcheck');return}var key=evt.which;if(key==KEYS.SHIFT||key==KEYS.CTRL||key==KEYS.ALT){return}if(key==KEYS.TAB){return}self.handleSearch(evt)})};Search.prototype._transferTabIndex=function(decorated){this.$search.attr('tabindex',this.$selection.attr('tabindex'));this.$selection.attr('tabindex','-1')};Search.prototype.createPlaceholder=function(decorated,placeholder){this.$search.attr('placeholder',placeholder.text)};Search.prototype.update=function(decorated,data){var searchHadFocus=this.$search[0]==document.activeElement;this.$search.attr('placeholder','');decorated.call(this,data);this.$selection.find('.select2-selection__rendered').append(this.$searchContainer);this.resizeSearch();if(searchHadFocus){this.$search.trigger('focus')}};Search.prototype.handleSearch=function(){this.resizeSearch();if(!this._keyUpPrevented){var input=this.$search.val();this.trigger('query',{term:input})}this._keyUpPrevented=false};Search.prototype.searchRemoveChoice=function(decorated,item){this.trigger('unselect',{data:item});this.$search.val(item.text);this.handleSearch()};Search.prototype.resizeSearch=function(){this.$search.css('width','25px');var width='';if(this.$search.attr('placeholder')!==''){width=this.$selection.find('.select2-selection__rendered').width()}else{var minimumWidth=this.$search.val().length+1;width=(minimumWidth*0.75)+'em'}this.$search.css('width',width)};return Search});S2.define('select2/selection/eventRelay',['jquery'],function($){function EventRelay(){}EventRelay.prototype.bind=function(decorated,container,$container){var self=this;var relayEvents=['open','opening','close','closing','select','selecting','unselect','unselecting','clear','clearing'];var preventableEvents=['opening','closing','selecting','unselecting','clearing'];decorated.call(this,container,$container);container.on('*',function(name,params){if($.inArray(name,relayEvents)===-1){return}params=params||{};var evt=$.Event('select2:'+name,{params:params});self.$element.trigger(evt);if($.inArray(name,preventableEvents)===-1){return}params.prevented=evt.isDefaultPrevented()})};return EventRelay});S2.define('select2/translation',['jquery','require'],function($,require){function Translation(dict){this.dict=dict||{}}Translation.prototype.all=function(){return this.dict};Translation.prototype.get=function(key){return this.dict[key]};Translation.prototype.extend=function(translation){this.dict=$.extend({},translation.all(),this.dict)};Translation._cache={};Translation.loadPath=function(path){if(!(path in Translation._cache)){var translations=require(path);Translation._cache[path]=translations}return new Translation(Translation._cache[path])};return Translation});S2.define('select2/diacritics',[],function(){var diacritics={'\u24B6':'A','\uFF21':'A','\u00C0':'A','\u00C1':'A','\u00C2':'A','\u1EA6':'A','\u1EA4':'A','\u1EAA':'A','\u1EA8':'A','\u00C3':'A','\u0100':'A','\u0102':'A','\u1EB0':'A','\u1EAE':'A','\u1EB4':'A','\u1EB2':'A','\u0226':'A','\u01E0':'A','\u00C4':'A','\u01DE':'A','\u1EA2':'A','\u00C5':'A','\u01FA':'A','\u01CD':'A','\u0200':'A','\u0202':'A','\u1EA0':'A','\u1EAC':'A','\u1EB6':'A','\u1E00':'A','\u0104':'A','\u023A':'A','\u2C6F':'A','\uA732':'AA','\u00C6':'AE','\u01FC':'AE','\u01E2':'AE','\uA734':'AO','\uA736':'AU','\uA738':'AV','\uA73A':'AV','\uA73C':'AY','\u24B7':'B','\uFF22':'B','\u1E02':'B','\u1E04':'B','\u1E06':'B','\u0243':'B','\u0182':'B','\u0181':'B','\u24B8':'C','\uFF23':'C','\u0106':'C','\u0108':'C','\u010A':'C','\u010C':'C','\u00C7':'C','\u1E08':'C','\u0187':'C','\u023B':'C','\uA73E':'C','\u24B9':'D','\uFF24':'D','\u1E0A':'D','\u010E':'D','\u1E0C':'D','\u1E10':'D','\u1E12':'D','\u1E0E':'D','\u0110':'D','\u018B':'D','\u018A':'D','\u0189':'D','\uA779':'D','\u01F1':'DZ','\u01C4':'DZ','\u01F2':'Dz','\u01C5':'Dz','\u24BA':'E','\uFF25':'E','\u00C8':'E','\u00C9':'E','\u00CA':'E','\u1EC0':'E','\u1EBE':'E','\u1EC4':'E','\u1EC2':'E','\u1EBC':'E','\u0112':'E','\u1E14':'E','\u1E16':'E','\u0114':'E','\u0116':'E','\u00CB':'E','\u1EBA':'E','\u011A':'E','\u0204':'E','\u0206':'E','\u1EB8':'E','\u1EC6':'E','\u0228':'E','\u1E1C':'E','\u0118':'E','\u1E18':'E','\u1E1A':'E','\u0190':'E','\u018E':'E','\u24BB':'F','\uFF26':'F','\u1E1E':'F','\u0191':'F','\uA77B':'F','\u24BC':'G','\uFF27':'G','\u01F4':'G','\u011C':'G','\u1E20':'G','\u011E':'G','\u0120':'G','\u01E6':'G','\u0122':'G','\u01E4':'G','\u0193':'G','\uA7A0':'G','\uA77D':'G','\uA77E':'G','\u24BD':'H','\uFF28':'H','\u0124':'H','\u1E22':'H','\u1E26':'H','\u021E':'H','\u1E24':'H','\u1E28':'H','\u1E2A':'H','\u0126':'H','\u2C67':'H','\u2C75':'H','\uA78D':'H','\u24BE':'I','\uFF29':'I','\u00CC':'I','\u00CD':'I','\u00CE':'I','\u0128':'I','\u012A':'I','\u012C':'I','\u0130':'I','\u00CF':'I','\u1E2E':'I','\u1EC8':'I','\u01CF':'I','\u0208':'I','\u020A':'I','\u1ECA':'I','\u012E':'I','\u1E2C':'I','\u0197':'I','\u24BF':'J','\uFF2A':'J','\u0134':'J','\u0248':'J','\u24C0':'K','\uFF2B':'K','\u1E30':'K','\u01E8':'K','\u1E32':'K','\u0136':'K','\u1E34':'K','\u0198':'K','\u2C69':'K','\uA740':'K','\uA742':'K','\uA744':'K','\uA7A2':'K','\u24C1':'L','\uFF2C':'L','\u013F':'L','\u0139':'L','\u013D':'L','\u1E36':'L','\u1E38':'L','\u013B':'L','\u1E3C':'L','\u1E3A':'L','\u0141':'L','\u023D':'L','\u2C62':'L','\u2C60':'L','\uA748':'L','\uA746':'L','\uA780':'L','\u01C7':'LJ','\u01C8':'Lj','\u24C2':'M','\uFF2D':'M','\u1E3E':'M','\u1E40':'M','\u1E42':'M','\u2C6E':'M','\u019C':'M','\u24C3':'N','\uFF2E':'N','\u01F8':'N','\u0143':'N','\u00D1':'N','\u1E44':'N','\u0147':'N','\u1E46':'N','\u0145':'N','\u1E4A':'N','\u1E48':'N','\u0220':'N','\u019D':'N','\uA790':'N','\uA7A4':'N','\u01CA':'NJ','\u01CB':'Nj','\u24C4':'O','\uFF2F':'O','\u00D2':'O','\u00D3':'O','\u00D4':'O','\u1ED2':'O','\u1ED0':'O','\u1ED6':'O','\u1ED4':'O','\u00D5':'O','\u1E4C':'O','\u022C':'O','\u1E4E':'O','\u014C':'O','\u1E50':'O','\u1E52':'O','\u014E':'O','\u022E':'O','\u0230':'O','\u00D6':'O','\u022A':'O','\u1ECE':'O','\u0150':'O','\u01D1':'O','\u020C':'O','\u020E':'O','\u01A0':'O','\u1EDC':'O','\u1EDA':'O','\u1EE0':'O','\u1EDE':'O','\u1EE2':'O','\u1ECC':'O','\u1ED8':'O','\u01EA':'O','\u01EC':'O','\u00D8':'O','\u01FE':'O','\u0186':'O','\u019F':'O','\uA74A':'O','\uA74C':'O','\u0152':'OE','\u01A2':'OI','\uA74E':'OO','\u0222':'OU','\u24C5':'P','\uFF30':'P','\u1E54':'P','\u1E56':'P','\u01A4':'P','\u2C63':'P','\uA750':'P','\uA752':'P','\uA754':'P','\u24C6':'Q','\uFF31':'Q','\uA756':'Q','\uA758':'Q','\u024A':'Q','\u24C7':'R','\uFF32':'R','\u0154':'R','\u1E58':'R','\u0158':'R','\u0210':'R','\u0212':'R','\u1E5A':'R','\u1E5C':'R','\u0156':'R','\u1E5E':'R','\u024C':'R','\u2C64':'R','\uA75A':'R','\uA7A6':'R','\uA782':'R','\u24C8':'S','\uFF33':'S','\u1E9E':'S','\u015A':'S','\u1E64':'S','\u015C':'S','\u1E60':'S','\u0160':'S','\u1E66':'S','\u1E62':'S','\u1E68':'S','\u0218':'S','\u015E':'S','\u2C7E':'S','\uA7A8':'S','\uA784':'S','\u24C9':'T','\uFF34':'T','\u1E6A':'T','\u0164':'T','\u1E6C':'T','\u021A':'T','\u0162':'T','\u1E70':'T','\u1E6E':'T','\u0166':'T','\u01AC':'T','\u01AE':'T','\u023E':'T','\uA786':'T','\uA728':'TZ','\u24CA':'U','\uFF35':'U','\u00D9':'U','\u00DA':'U','\u00DB':'U','\u0168':'U','\u1E78':'U','\u016A':'U','\u1E7A':'U','\u016C':'U','\u00DC':'U','\u01DB':'U','\u01D7':'U','\u01D5':'U','\u01D9':'U','\u1EE6':'U','\u016E':'U','\u0170':'U','\u01D3':'U','\u0214':'U','\u0216':'U','\u01AF':'U','\u1EEA':'U','\u1EE8':'U','\u1EEE':'U','\u1EEC':'U','\u1EF0':'U','\u1EE4':'U','\u1E72':'U','\u0172':'U','\u1E76':'U','\u1E74':'U','\u0244':'U','\u24CB':'V','\uFF36':'V','\u1E7C':'V','\u1E7E':'V','\u01B2':'V','\uA75E':'V','\u0245':'V','\uA760':'VY','\u24CC':'W','\uFF37':'W','\u1E80':'W','\u1E82':'W','\u0174':'W','\u1E86':'W','\u1E84':'W','\u1E88':'W','\u2C72':'W','\u24CD':'X','\uFF38':'X','\u1E8A':'X','\u1E8C':'X','\u24CE':'Y','\uFF39':'Y','\u1EF2':'Y','\u00DD':'Y','\u0176':'Y','\u1EF8':'Y','\u0232':'Y','\u1E8E':'Y','\u0178':'Y','\u1EF6':'Y','\u1EF4':'Y','\u01B3':'Y','\u024E':'Y','\u1EFE':'Y','\u24CF':'Z','\uFF3A':'Z','\u0179':'Z','\u1E90':'Z','\u017B':'Z','\u017D':'Z','\u1E92':'Z','\u1E94':'Z','\u01B5':'Z','\u0224':'Z','\u2C7F':'Z','\u2C6B':'Z','\uA762':'Z','\u24D0':'a','\uFF41':'a','\u1E9A':'a','\u00E0':'a','\u00E1':'a','\u00E2':'a','\u1EA7':'a','\u1EA5':'a','\u1EAB':'a','\u1EA9':'a','\u00E3':'a','\u0101':'a','\u0103':'a','\u1EB1':'a','\u1EAF':'a','\u1EB5':'a','\u1EB3':'a','\u0227':'a','\u01E1':'a','\u00E4':'a','\u01DF':'a','\u1EA3':'a','\u00E5':'a','\u01FB':'a','\u01CE':'a','\u0201':'a','\u0203':'a','\u1EA1':'a','\u1EAD':'a','\u1EB7':'a','\u1E01':'a','\u0105':'a','\u2C65':'a','\u0250':'a','\uA733':'aa','\u00E6':'ae','\u01FD':'ae','\u01E3':'ae','\uA735':'ao','\uA737':'au','\uA739':'av','\uA73B':'av','\uA73D':'ay','\u24D1':'b','\uFF42':'b','\u1E03':'b','\u1E05':'b','\u1E07':'b','\u0180':'b','\u0183':'b','\u0253':'b','\u24D2':'c','\uFF43':'c','\u0107':'c','\u0109':'c','\u010B':'c','\u010D':'c','\u00E7':'c','\u1E09':'c','\u0188':'c','\u023C':'c','\uA73F':'c','\u2184':'c','\u24D3':'d','\uFF44':'d','\u1E0B':'d','\u010F':'d','\u1E0D':'d','\u1E11':'d','\u1E13':'d','\u1E0F':'d','\u0111':'d','\u018C':'d','\u0256':'d','\u0257':'d','\uA77A':'d','\u01F3':'dz','\u01C6':'dz','\u24D4':'e','\uFF45':'e','\u00E8':'e','\u00E9':'e','\u00EA':'e','\u1EC1':'e','\u1EBF':'e','\u1EC5':'e','\u1EC3':'e','\u1EBD':'e','\u0113':'e','\u1E15':'e','\u1E17':'e','\u0115':'e','\u0117':'e','\u00EB':'e','\u1EBB':'e','\u011B':'e','\u0205':'e','\u0207':'e','\u1EB9':'e','\u1EC7':'e','\u0229':'e','\u1E1D':'e','\u0119':'e','\u1E19':'e','\u1E1B':'e','\u0247':'e','\u025B':'e','\u01DD':'e','\u24D5':'f','\uFF46':'f','\u1E1F':'f','\u0192':'f','\uA77C':'f','\u24D6':'g','\uFF47':'g','\u01F5':'g','\u011D':'g','\u1E21':'g','\u011F':'g','\u0121':'g','\u01E7':'g','\u0123':'g','\u01E5':'g','\u0260':'g','\uA7A1':'g','\u1D79':'g','\uA77F':'g','\u24D7':'h','\uFF48':'h','\u0125':'h','\u1E23':'h','\u1E27':'h','\u021F':'h','\u1E25':'h','\u1E29':'h','\u1E2B':'h','\u1E96':'h','\u0127':'h','\u2C68':'h','\u2C76':'h','\u0265':'h','\u0195':'hv','\u24D8':'i','\uFF49':'i','\u00EC':'i','\u00ED':'i','\u00EE':'i','\u0129':'i','\u012B':'i','\u012D':'i','\u00EF':'i','\u1E2F':'i','\u1EC9':'i','\u01D0':'i','\u0209':'i','\u020B':'i','\u1ECB':'i','\u012F':'i','\u1E2D':'i','\u0268':'i','\u0131':'i','\u24D9':'j','\uFF4A':'j','\u0135':'j','\u01F0':'j','\u0249':'j','\u24DA':'k','\uFF4B':'k','\u1E31':'k','\u01E9':'k','\u1E33':'k','\u0137':'k','\u1E35':'k','\u0199':'k','\u2C6A':'k','\uA741':'k','\uA743':'k','\uA745':'k','\uA7A3':'k','\u24DB':'l','\uFF4C':'l','\u0140':'l','\u013A':'l','\u013E':'l','\u1E37':'l','\u1E39':'l','\u013C':'l','\u1E3D':'l','\u1E3B':'l','\u017F':'l','\u0142':'l','\u019A':'l','\u026B':'l','\u2C61':'l','\uA749':'l','\uA781':'l','\uA747':'l','\u01C9':'lj','\u24DC':'m','\uFF4D':'m','\u1E3F':'m','\u1E41':'m','\u1E43':'m','\u0271':'m','\u026F':'m','\u24DD':'n','\uFF4E':'n','\u01F9':'n','\u0144':'n','\u00F1':'n','\u1E45':'n','\u0148':'n','\u1E47':'n','\u0146':'n','\u1E4B':'n','\u1E49':'n','\u019E':'n','\u0272':'n','\u0149':'n','\uA791':'n','\uA7A5':'n','\u01CC':'nj','\u24DE':'o','\uFF4F':'o','\u00F2':'o','\u00F3':'o','\u00F4':'o','\u1ED3':'o','\u1ED1':'o','\u1ED7':'o','\u1ED5':'o','\u00F5':'o','\u1E4D':'o','\u022D':'o','\u1E4F':'o','\u014D':'o','\u1E51':'o','\u1E53':'o','\u014F':'o','\u022F':'o','\u0231':'o','\u00F6':'o','\u022B':'o','\u1ECF':'o','\u0151':'o','\u01D2':'o','\u020D':'o','\u020F':'o','\u01A1':'o','\u1EDD':'o','\u1EDB':'o','\u1EE1':'o','\u1EDF':'o','\u1EE3':'o','\u1ECD':'o','\u1ED9':'o','\u01EB':'o','\u01ED':'o','\u00F8':'o','\u01FF':'o','\u0254':'o','\uA74B':'o','\uA74D':'o','\u0275':'o','\u0153':'oe','\u01A3':'oi','\u0223':'ou','\uA74F':'oo','\u24DF':'p','\uFF50':'p','\u1E55':'p','\u1E57':'p','\u01A5':'p','\u1D7D':'p','\uA751':'p','\uA753':'p','\uA755':'p','\u24E0':'q','\uFF51':'q','\u024B':'q','\uA757':'q','\uA759':'q','\u24E1':'r','\uFF52':'r','\u0155':'r','\u1E59':'r','\u0159':'r','\u0211':'r','\u0213':'r','\u1E5B':'r','\u1E5D':'r','\u0157':'r','\u1E5F':'r','\u024D':'r','\u027D':'r','\uA75B':'r','\uA7A7':'r','\uA783':'r','\u24E2':'s','\uFF53':'s','\u00DF':'s','\u015B':'s','\u1E65':'s','\u015D':'s','\u1E61':'s','\u0161':'s','\u1E67':'s','\u1E63':'s','\u1E69':'s','\u0219':'s','\u015F':'s','\u023F':'s','\uA7A9':'s','\uA785':'s','\u1E9B':'s','\u24E3':'t','\uFF54':'t','\u1E6B':'t','\u1E97':'t','\u0165':'t','\u1E6D':'t','\u021B':'t','\u0163':'t','\u1E71':'t','\u1E6F':'t','\u0167':'t','\u01AD':'t','\u0288':'t','\u2C66':'t','\uA787':'t','\uA729':'tz','\u24E4':'u','\uFF55':'u','\u00F9':'u','\u00FA':'u','\u00FB':'u','\u0169':'u','\u1E79':'u','\u016B':'u','\u1E7B':'u','\u016D':'u','\u00FC':'u','\u01DC':'u','\u01D8':'u','\u01D6':'u','\u01DA':'u','\u1EE7':'u','\u016F':'u','\u0171':'u','\u01D4':'u','\u0215':'u','\u0217':'u','\u01B0':'u','\u1EEB':'u','\u1EE9':'u','\u1EEF':'u','\u1EED':'u','\u1EF1':'u','\u1EE5':'u','\u1E73':'u','\u0173':'u','\u1E77':'u','\u1E75':'u','\u0289':'u','\u24E5':'v','\uFF56':'v','\u1E7D':'v','\u1E7F':'v','\u028B':'v','\uA75F':'v','\u028C':'v','\uA761':'vy','\u24E6':'w','\uFF57':'w','\u1E81':'w','\u1E83':'w','\u0175':'w','\u1E87':'w','\u1E85':'w','\u1E98':'w','\u1E89':'w','\u2C73':'w','\u24E7':'x','\uFF58':'x','\u1E8B':'x','\u1E8D':'x','\u24E8':'y','\uFF59':'y','\u1EF3':'y','\u00FD':'y','\u0177':'y','\u1EF9':'y','\u0233':'y','\u1E8F':'y','\u00FF':'y','\u1EF7':'y','\u1E99':'y','\u1EF5':'y','\u01B4':'y','\u024F':'y','\u1EFF':'y','\u24E9':'z','\uFF5A':'z','\u017A':'z','\u1E91':'z','\u017C':'z','\u017E':'z','\u1E93':'z','\u1E95':'z','\u01B6':'z','\u0225':'z','\u0240':'z','\u2C6C':'z','\uA763':'z','\u0386':'\u0391','\u0388':'\u0395','\u0389':'\u0397','\u038A':'\u0399','\u03AA':'\u0399','\u038C':'\u039F','\u038E':'\u03A5','\u03AB':'\u03A5','\u038F':'\u03A9','\u03AC':'\u03B1','\u03AD':'\u03B5','\u03AE':'\u03B7','\u03AF':'\u03B9','\u03CA':'\u03B9','\u0390':'\u03B9','\u03CC':'\u03BF','\u03CD':'\u03C5','\u03CB':'\u03C5','\u03B0':'\u03C5','\u03CE':'\u03C9','\u03C2':'\u03C3','\u2019':'\''};return diacritics});S2.define('select2/data/base',['../utils'],function(Utils){function BaseAdapter($element,options){BaseAdapter.__super__.constructor.call(this)}Utils.Extend(BaseAdapter,Utils.Observable);BaseAdapter.prototype.current=function(callback){throw new Error('The `current` method must be defined in child classes.');};BaseAdapter.prototype.query=function(params,callback){throw new Error('The `query` method must be defined in child classes.');};BaseAdapter.prototype.bind=function(container,$container){};BaseAdapter.prototype.destroy=function(){};BaseAdapter.prototype.generateResultId=function(container,data){var id=container.id+'-result-';id+=Utils.generateChars(4);if(data.id!=null){id+='-'+data.id.toString()}else{id+='-'+Utils.generateChars(4)}return id};return BaseAdapter});S2.define('select2/data/select',['./base','../utils','jquery'],function(BaseAdapter,Utils,$){function SelectAdapter($element,options){this.$element=$element;this.options=options;SelectAdapter.__super__.constructor.call(this)}Utils.Extend(SelectAdapter,BaseAdapter);SelectAdapter.prototype.current=function(callback){var data=[];var self=this;this.$element.find(':selected').each(function(){var $option=$(this);var option=self.item($option);data.push(option)});callback(data)};SelectAdapter.prototype.select=function(data){var self=this;data.selected=true;if($(data.element).is('option')){data.element.selected=true;this.$element.trigger('input').trigger('change');return}if(this.$element.prop('multiple')){this.current(function(currentData){var val=[];data=[data];data.push.apply(data,currentData);for(var d=0;d=0){var $existingOption=$existing.filter(onlyItem(item));var existingData=this.item($existingOption);var newData=$.extend(true,{},item,existingData);var $newOption=this.option(newData);$existingOption.replaceWith($newOption);continue}var $option=this.option(item);if(item.children){var $children=this.convertToOptions(item.children);Utils.appendMany($option,$children)}$options.push($option)}return $options};return ArrayAdapter});S2.define('select2/data/ajax',['./array','../utils','jquery'],function(ArrayAdapter,Utils,$){function AjaxAdapter($element,options){this.ajaxOptions=this._applyDefaults(options.get('ajax'));if(this.ajaxOptions.processResults!=null){this.processResults=this.ajaxOptions.processResults}AjaxAdapter.__super__.constructor.call(this,$element,options)}Utils.Extend(AjaxAdapter,ArrayAdapter);AjaxAdapter.prototype._applyDefaults=function(options){var defaults={data:function(params){return $.extend({},params,{q:params.term})},transport:function(params,success,failure){var $request=$.ajax(params);$request.then(success);$request.fail(failure);return $request}};return $.extend({},defaults,options,true)};AjaxAdapter.prototype.processResults=function(results){return results};AjaxAdapter.prototype.query=function(params,callback){var matches=[];var self=this;if(this._request!=null){if($.isFunction(this._request.abort)){this._request.abort()}this._request=null}var options=$.extend({type:'GET'},this.ajaxOptions);if(typeof options.url==='function'){options.url=options.url.call(this.$element,params)}if(typeof options.data==='function'){options.data=options.data.call(this.$element,params)}function request(){var $request=options.transport(options,function(data){var results=self.processResults(data,params);if(self.options.get('debug')&&window.console&&console.error){if(!results||!results.results||!$.isArray(results.results)){console.error('Select2: The AJAX results did not return an array in the '+'`results` key of the response.')}}callback(results)},function(){if('status'in $request&&($request.status===0||$request.status==='0')){return}self.trigger('results:message',{message:'errorLoading'})});self._request=$request}if(this.ajaxOptions.delay&¶ms.term!=null){if(this._queryTimeout){window.clearTimeout(this._queryTimeout)}this._queryTimeout=window.setTimeout(request,this.ajaxOptions.delay)}else{request()}};return AjaxAdapter});S2.define('select2/data/tags',['jquery'],function($){function Tags(decorated,$element,options){var tags=options.get('tags');var createTag=options.get('createTag');if(createTag!==undefined){this.createTag=createTag}var insertTag=options.get('insertTag');if(insertTag!==undefined){this.insertTag=insertTag}decorated.call(this,$element,options);if($.isArray(tags)){for(var t=0;t0&¶ms.term.length>this.maximumInputLength){this.trigger('results:message',{message:'inputTooLong',args:{maximum:this.maximumInputLength,input:params.term,params:params}});return}decorated.call(this,params,callback)};return MaximumInputLength});S2.define('select2/data/maximumSelectionLength',[],function(){function MaximumSelectionLength(decorated,$e,options){this.maximumSelectionLength=options.get('maximumSelectionLength');decorated.call(this,$e,options)}MaximumSelectionLength.prototype.bind=function(decorated,container,$container){var self=this;decorated.call(this,container,$container);container.on('select',function(){self._checkIfMaximumSelected()})};MaximumSelectionLength.prototype.query=function(decorated,params,callback){var self=this;this._checkIfMaximumSelected(function(){decorated.call(self,params,callback)})};MaximumSelectionLength.prototype._checkIfMaximumSelected=function(_,successCallback){var self=this;this.current(function(currentData){var count=currentData!=null?currentData.length:0;if(self.maximumSelectionLength>0&&count>=self.maximumSelectionLength){self.trigger('results:message',{message:'maximumSelected',args:{maximum:self.maximumSelectionLength}});return}if(successCallback){successCallback()}})};return MaximumSelectionLength});S2.define('select2/dropdown',['jquery','./utils'],function($,Utils){function Dropdown($element,options){this.$element=$element;this.options=options;Dropdown.__super__.constructor.call(this)}Utils.Extend(Dropdown,Utils.Observable);Dropdown.prototype.render=function(){var $dropdown=$(''+''+'');$dropdown.attr('dir',this.options.get('dir'));this.$dropdown=$dropdown;return $dropdown};Dropdown.prototype.bind=function(){};Dropdown.prototype.position=function($dropdown,$container){};Dropdown.prototype.destroy=function(){this.$dropdown.remove()};return Dropdown});S2.define('select2/dropdown/search',['jquery','../utils'],function($,Utils){function Search(){}Search.prototype.render=function(decorated){var $rendered=decorated.call(this);var $search=$(''+''+'');this.$searchContainer=$search;this.$search=$search.find('input');$rendered.prepend($search);return $rendered};Search.prototype.bind=function(decorated,container,$container){var self=this;var resultsId=container.id+'-results';decorated.call(this,container,$container);this.$search.on('keydown',function(evt){self.trigger('keypress',evt);self._keyUpPrevented=evt.isDefaultPrevented()});this.$search.on('input',function(evt){$(this).off('keyup')});this.$search.on('keyup input',function(evt){self.handleSearch(evt)});container.on('open',function(){self.$search.attr('tabindex',0);self.$search.attr('aria-controls',resultsId);self.$search.trigger('focus');window.setTimeout(function(){self.$search.trigger('focus')},0)});container.on('close',function(){self.$search.attr('tabindex',-1);self.$search.removeAttr('aria-controls');self.$search.removeAttr('aria-activedescendant');self.$search.val('');self.$search.trigger('blur')});container.on('focus',function(){if(!container.isOpen()){self.$search.trigger('focus')}});container.on('results:all',function(params){if(params.query.term==null||params.query.term===''){var showSearch=self.showSearch(params);if(showSearch){self.$searchContainer.removeClass('select2-search--hide')}else{self.$searchContainer.addClass('select2-search--hide')}}});container.on('results:focus',function(params){if(params.data._resultId){self.$search.attr('aria-activedescendant',params.data._resultId)}else{self.$search.removeAttr('aria-activedescendant')}})};Search.prototype.handleSearch=function(evt){if(!this._keyUpPrevented){var input=this.$search.val();this.trigger('query',{term:input})}this._keyUpPrevented=false};Search.prototype.showSearch=function(_,params){return true};return Search});S2.define('select2/dropdown/hidePlaceholder',[],function(){function HidePlaceholder(decorated,$element,options,dataAdapter){this.placeholder=this.normalizePlaceholder(options.get('placeholder'));decorated.call(this,$element,options,dataAdapter)}HidePlaceholder.prototype.append=function(decorated,data){data.results=this.removePlaceholder(data.results);decorated.call(this,data)};HidePlaceholder.prototype.normalizePlaceholder=function(_,placeholder){if(typeof placeholder==='string'){placeholder={id:'',text:placeholder}}return placeholder};HidePlaceholder.prototype.removePlaceholder=function(_,data){var modifiedData=data.slice(0);for(var d=data.length-1;d>=0;d--){var item=data[d];if(this.placeholder.id===item.id){modifiedData.splice(d,1)}}return modifiedData};return HidePlaceholder});S2.define('select2/dropdown/infiniteScroll',['jquery'],function($){function InfiniteScroll(decorated,$element,options,dataAdapter){this.lastParams={};decorated.call(this,$element,options,dataAdapter);this.$loadingMore=this.createLoadingMore();this.loading=false}InfiniteScroll.prototype.append=function(decorated,data){this.$loadingMore.remove();this.loading=false;decorated.call(this,data);if(this.showLoadingMore(data)){this.$results.append(this.$loadingMore);this.loadMoreIfNeeded()}};InfiniteScroll.prototype.bind=function(decorated,container,$container){var self=this;decorated.call(this,container,$container);container.on('query',function(params){self.lastParams=params;self.loading=true});container.on('query:append',function(params){self.lastParams=params;self.loading=true});this.$results.on('scroll',this.loadMoreIfNeeded.bind(this))};InfiniteScroll.prototype.loadMoreIfNeeded=function(){var isLoadMoreVisible=$.contains(document.documentElement,this.$loadingMore[0]);if(this.loading||!isLoadMoreVisible){return}var currentOffset=this.$results.offset().top+this.$results.outerHeight(false);var loadingMoreOffset=this.$loadingMore.offset().top+this.$loadingMore.outerHeight(false);if(currentOffset+50>=loadingMoreOffset){this.loadMore()}};InfiniteScroll.prototype.loadMore=function(){this.loading=true;var params=$.extend({},{page:1},this.lastParams);params.page++;this.trigger('query:append',params)};InfiniteScroll.prototype.showLoadingMore=function(_,data){return data.pagination&&data.pagination.more};InfiniteScroll.prototype.createLoadingMore=function(){var $option=$('
          • ');var message=this.options.get('translations').get('loadingMore');$option.html(message(this.lastParams));return $option};return InfiniteScroll});S2.define('select2/dropdown/attachBody',['jquery','../utils'],function($,Utils){function AttachBody(decorated,$element,options){this.$dropdownParent=$(options.get('dropdownParent')||document.body);decorated.call(this,$element,options)}AttachBody.prototype.bind=function(decorated,container,$container){var self=this;decorated.call(this,container,$container);container.on('open',function(){self._showDropdown();self._attachPositioningHandler(container);self._bindContainerResultHandlers(container)});container.on('close',function(){self._hideDropdown();self._detachPositioningHandler(container)});this.$dropdownContainer.on('mousedown',function(evt){evt.stopPropagation()})};AttachBody.prototype.destroy=function(decorated){decorated.call(this);this.$dropdownContainer.remove()};AttachBody.prototype.position=function(decorated,$dropdown,$container){$dropdown.attr('class',$container.attr('class'));$dropdown.removeClass('select2');$dropdown.addClass('select2-container--open');$dropdown.css({position:'absolute',top:-999999});this.$container=$container};AttachBody.prototype.render=function(decorated){var $container=$('');var $dropdown=decorated.call(this);$container.append($dropdown);this.$dropdownContainer=$container;return $container};AttachBody.prototype._hideDropdown=function(decorated){this.$dropdownContainer.detach()};AttachBody.prototype._bindContainerResultHandlers=function(decorated,container){if(this._containerResultsHandlersBound){return}var self=this;container.on('results:all',function(){self._positionDropdown();self._resizeDropdown()});container.on('results:append',function(){self._positionDropdown();self._resizeDropdown()});container.on('results:message',function(){self._positionDropdown();self._resizeDropdown()});container.on('select',function(){self._positionDropdown();self._resizeDropdown()});container.on('unselect',function(){self._positionDropdown();self._resizeDropdown()});this._containerResultsHandlersBound=true};AttachBody.prototype._attachPositioningHandler=function(decorated,container){var self=this;var scrollEvent='scroll.select2.'+container.id;var resizeEvent='resize.select2.'+container.id;var orientationEvent='orientationchange.select2.'+container.id;var $watchers=this.$container.parents().filter(Utils.hasScroll);$watchers.each(function(){Utils.StoreData(this,'select2-scroll-position',{x:$(this).scrollLeft(),y:$(this).scrollTop()})});$watchers.on(scrollEvent,function(ev){var position=Utils.GetData(this,'select2-scroll-position');$(this).scrollTop(position.y)});$(window).on(scrollEvent+' '+resizeEvent+' '+orientationEvent,function(e){self._positionDropdown();self._resizeDropdown()})};AttachBody.prototype._detachPositioningHandler=function(decorated,container){var scrollEvent='scroll.select2.'+container.id;var resizeEvent='resize.select2.'+container.id;var orientationEvent='orientationchange.select2.'+container.id;var $watchers=this.$container.parents().filter(Utils.hasScroll);$watchers.off(scrollEvent);$(window).off(scrollEvent+' '+resizeEvent+' '+orientationEvent)};AttachBody.prototype._positionDropdown=function(){var $window=$(window);var isCurrentlyAbove=this.$dropdown.hasClass('select2-dropdown--above');var isCurrentlyBelow=this.$dropdown.hasClass('select2-dropdown--below');var newDirection=null;var offset=this.$container.offset();offset.bottom=offset.top+this.$container.outerHeight(false);var container={height:this.$container.outerHeight(false)};container.top=offset.top;container.bottom=offset.top+container.height;var dropdown={height:this.$dropdown.outerHeight(false)};var viewport={top:$window.scrollTop(),bottom:$window.scrollTop()+$window.height()};var enoughRoomAbove=viewport.top<(offset.top-dropdown.height);var enoughRoomBelow=viewport.bottom>(offset.bottom+dropdown.height);var css={left:offset.left,top:container.bottom};var $offsetParent=this.$dropdownParent;if($offsetParent.css('position')==='static'){$offsetParent=$offsetParent.offsetParent()}var parentOffset={top:0,left:0};if($.contains(document.body,$offsetParent[0])||$offsetParent[0].isConnected){parentOffset=$offsetParent.offset()}css.top-=parentOffset.top;css.left-=parentOffset.left;if(!isCurrentlyAbove&&!isCurrentlyBelow){newDirection='below'}if(!enoughRoomBelow&&enoughRoomAbove&&!isCurrentlyAbove){newDirection='above'}else if(!enoughRoomAbove&&enoughRoomBelow&&isCurrentlyAbove){newDirection='below'}if(newDirection=='above'||(isCurrentlyAbove&&newDirection!=='below')){css.top=container.top-parentOffset.top-dropdown.height}if(newDirection!=null){this.$dropdown.removeClass('select2-dropdown--below select2-dropdown--above').addClass('select2-dropdown--'+newDirection);this.$container.removeClass('select2-container--below select2-container--above').addClass('select2-container--'+newDirection)}this.$dropdownContainer.css(css)};AttachBody.prototype._resizeDropdown=function(){var css={width:this.$container.outerWidth(false)+'px'};if(this.options.get('dropdownAutoWidth')){css.minWidth=css.width;css.position='relative';css.width='auto'}this.$dropdown.css(css)};AttachBody.prototype._showDropdown=function(decorated){this.$dropdownContainer.appendTo(this.$dropdownParent);this._positionDropdown();this._resizeDropdown()};return AttachBody});S2.define('select2/dropdown/minimumResultsForSearch',[],function(){function countResults(data){var count=0;for(var d=0;d0){options.dataAdapter=Utils.Decorate(options.dataAdapter,MinimumInputLength)}if(options.maximumInputLength>0){options.dataAdapter=Utils.Decorate(options.dataAdapter,MaximumInputLength)}if(options.maximumSelectionLength>0){options.dataAdapter=Utils.Decorate(options.dataAdapter,MaximumSelectionLength)}if(options.tags){options.dataAdapter=Utils.Decorate(options.dataAdapter,Tags)}if(options.tokenSeparators!=null||options.tokenizer!=null){options.dataAdapter=Utils.Decorate(options.dataAdapter,Tokenizer)}if(options.query!=null){var Query=require(options.amdBase+'compat/query');options.dataAdapter=Utils.Decorate(options.dataAdapter,Query)}if(options.initSelection!=null){var InitSelection=require(options.amdBase+'compat/initSelection');options.dataAdapter=Utils.Decorate(options.dataAdapter,InitSelection)}}if(options.resultsAdapter==null){options.resultsAdapter=ResultsList;if(options.ajax!=null){options.resultsAdapter=Utils.Decorate(options.resultsAdapter,InfiniteScroll)}if(options.placeholder!=null){options.resultsAdapter=Utils.Decorate(options.resultsAdapter,HidePlaceholder)}if(options.selectOnClose){options.resultsAdapter=Utils.Decorate(options.resultsAdapter,SelectOnClose)}}if(options.dropdownAdapter==null){if(options.multiple){options.dropdownAdapter=Dropdown}else{var SearchableDropdown=Utils.Decorate(Dropdown,DropdownSearch);options.dropdownAdapter=SearchableDropdown}if(options.minimumResultsForSearch!==0){options.dropdownAdapter=Utils.Decorate(options.dropdownAdapter,MinimumResultsForSearch)}if(options.closeOnSelect){options.dropdownAdapter=Utils.Decorate(options.dropdownAdapter,CloseOnSelect)}if(options.dropdownCssClass!=null||options.dropdownCss!=null||options.adaptDropdownCssClass!=null){var DropdownCSS=require(options.amdBase+'compat/dropdownCss');options.dropdownAdapter=Utils.Decorate(options.dropdownAdapter,DropdownCSS)}options.dropdownAdapter=Utils.Decorate(options.dropdownAdapter,AttachBody)}if(options.selectionAdapter==null){if(options.multiple){options.selectionAdapter=MultipleSelection}else{options.selectionAdapter=SingleSelection}if(options.placeholder!=null){options.selectionAdapter=Utils.Decorate(options.selectionAdapter,Placeholder)}if(options.allowClear){options.selectionAdapter=Utils.Decorate(options.selectionAdapter,AllowClear)}if(options.multiple){options.selectionAdapter=Utils.Decorate(options.selectionAdapter,SelectionSearch)}if(options.containerCssClass!=null||options.containerCss!=null||options.adaptContainerCssClass!=null){var ContainerCSS=require(options.amdBase+'compat/containerCss');options.selectionAdapter=Utils.Decorate(options.selectionAdapter,ContainerCSS)}options.selectionAdapter=Utils.Decorate(options.selectionAdapter,EventRelay)}options.language=this._resolveLanguage(options.language);options.language.push('en');var uniqueLanguages=[];for(var l=0;l0){var match=$.extend(true,{},data);for(var c=data.children.length-1;c>=0;c--){var child=data.children[c];var matches=matcher(params,child);if(matches==null){match.children.splice(c,1)}}if(match.children.length>0){return match}return matcher(params,match)}var original=stripDiacritics(data.text).toUpperCase();var term=stripDiacritics(params.term).toUpperCase();if(original.indexOf(term)>-1){return data}return null}this.defaults={amdBase:'./',amdLanguageBase:'./i18n/',closeOnSelect:true,debug:false,dropdownAutoWidth:false,escapeMarkup:Utils.escapeMarkup,language:{},matcher:matcher,minimumInputLength:0,maximumInputLength:0,maximumSelectionLength:0,minimumResultsForSearch:0,selectOnClose:false,scrollAfterSelect:false,sorter:function(data){return data},templateResult:function(result){return result.text},templateSelection:function(selection){return selection.text},theme:'default',width:'resolve'}};Defaults.prototype.applyFromElement=function(options,$element){var optionLanguage=options.language;var defaultLanguage=this.defaults.language;var elementLanguage=$element.prop('lang');var parentLanguage=$element.closest('[lang]').prop('lang');var languages=Array.prototype.concat.call(this._resolveLanguage(elementLanguage),this._resolveLanguage(optionLanguage),this._resolveLanguage(defaultLanguage),this._resolveLanguage(parentLanguage));options.language=languages;return options};Defaults.prototype._resolveLanguage=function(language){if(!language){return[]}if($.isEmptyObject(language)){return[]}if($.isPlainObject(language)){return[language]}var languages;if(!$.isArray(language)){languages=[language]}else{languages=language}var resolvedLanguages=[];for(var l=0;l0){var languageParts=languages[l].split('-');var baseLanguage=languageParts[0];resolvedLanguages.push(baseLanguage)}}return resolvedLanguages};Defaults.prototype._processTranslations=function(languages,debug){var translations=new Translation();for(var l=0;l-1){continue}if($.isPlainObject(this.options[key])){$.extend(this.options[key],data[key])}else{this.options[key]=data[key]}}return this};Options.prototype.get=function(key){return this.options[key]};Options.prototype.set=function(key,val){this.options[key]=val};return Options});S2.define('select2/core',['jquery','./options','./utils','./keys'],function($,Options,Utils,KEYS){var Select2=function($element,options){if(Utils.GetData($element[0],'select2')!=null){Utils.GetData($element[0],'select2').destroy()}this.$element=$element;this.id=this._generateId($element);options=options||{};this.options=new Options(options,$element);Select2.__super__.constructor.call(this);var tabindex=$element.attr('tabindex')||0;Utils.StoreData($element[0],'old-tabindex',tabindex);$element.attr('tabindex','-1');var DataAdapter=this.options.get('dataAdapter');this.dataAdapter=new DataAdapter($element,this.options);var $container=this.render();this._placeContainer($container);var SelectionAdapter=this.options.get('selectionAdapter');this.selection=new SelectionAdapter($element,this.options);this.$selection=this.selection.render();this.selection.position(this.$selection,$container);var DropdownAdapter=this.options.get('dropdownAdapter');this.dropdown=new DropdownAdapter($element,this.options);this.$dropdown=this.dropdown.render();this.dropdown.position(this.$dropdown,$container);var ResultsAdapter=this.options.get('resultsAdapter');this.results=new ResultsAdapter($element,this.options,this.dataAdapter);this.$results=this.results.render();this.results.position(this.$results,this.$dropdown);var self=this;this._bindAdapters();this._registerDomEvents();this._registerDataEvents();this._registerSelectionEvents();this._registerDropdownEvents();this._registerResultsEvents();this._registerEvents();this.dataAdapter.current(function(initialData){self.trigger('selection:update',{data:initialData})});$element.addClass('select2-hidden-accessible');$element.attr('aria-hidden','true');this._syncAttributes();Utils.StoreData($element[0],'select2',this);$element.data('select2',this)};Utils.Extend(Select2,Utils.Observable);Select2.prototype._generateId=function($element){var id='';if($element.attr('id')!=null){id=$element.attr('id')}else if($element.attr('name')!=null){id=$element.attr('name')+'-'+Utils.generateChars(2)}else{id=Utils.generateChars(4)}id=id.replace(/(:|\.|\[|\]|,)/g,'');id='select2-'+id;return id};Select2.prototype._placeContainer=function($container){$container.insertAfter(this.$element);var width=this._resolveWidth(this.$element,this.options.get('width'));if(width!=null){$container.css('width',width)}};Select2.prototype._resolveWidth=function($element,method){var WIDTH=/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;if(method=='resolve'){var styleWidth=this._resolveWidth($element,'style');if(styleWidth!=null){return styleWidth}return this._resolveWidth($element,'element')}if(method=='element'){var elementWidth=$element.outerWidth(false);if(elementWidth<=0){return'auto'}return elementWidth+'px'}if(method=='style'){var style=$element.attr('style');if(typeof(style)!=='string'){return null}var attrs=style.split(';');for(var i=0,l=attrs.length;i=1){return matches[1]}}return null}if(method=='computedstyle'){var computedStyle=window.getComputedStyle($element[0]);return computedStyle.width}return method};Select2.prototype._bindAdapters=function(){this.dataAdapter.bind(this,this.$container);this.selection.bind(this,this.$container);this.dropdown.bind(this,this.$container);this.results.bind(this,this.$container)};Select2.prototype._registerDomEvents=function(){var self=this;this.$element.on('change.select2',function(){self.dataAdapter.current(function(data){self.trigger('selection:update',{data:data})})});this.$element.on('focus.select2',function(evt){self.trigger('focus',evt)});this._syncA=Utils.bind(this._syncAttributes,this);this._syncS=Utils.bind(this._syncSubtree,this);if(this.$element[0].attachEvent){this.$element[0].attachEvent('onpropertychange',this._syncA)}var observer=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver;if(observer!=null){this._observer=new observer(function(mutations){self._syncA();self._syncS(null,mutations)});this._observer.observe(this.$element[0],{attributes:true,childList:true,subtree:false})}else if(this.$element[0].addEventListener){this.$element[0].addEventListener('DOMAttrModified',self._syncA,false);this.$element[0].addEventListener('DOMNodeInserted',self._syncS,false);this.$element[0].addEventListener('DOMNodeRemoved',self._syncS,false)}};Select2.prototype._registerDataEvents=function(){var self=this;this.dataAdapter.on('*',function(name,params){self.trigger(name,params)})};Select2.prototype._registerSelectionEvents=function(){var self=this;var nonRelayEvents=['toggle','focus'];this.selection.on('toggle',function(){self.toggleDropdown()});this.selection.on('focus',function(params){self.focus(params)});this.selection.on('*',function(name,params){if($.inArray(name,nonRelayEvents)!==-1){return}self.trigger(name,params)})};Select2.prototype._registerDropdownEvents=function(){var self=this;this.dropdown.on('*',function(name,params){self.trigger(name,params)})};Select2.prototype._registerResultsEvents=function(){var self=this;this.results.on('*',function(name,params){self.trigger(name,params)})};Select2.prototype._registerEvents=function(){var self=this;this.on('open',function(){self.$container.addClass('select2-container--open')});this.on('close',function(){self.$container.removeClass('select2-container--open')});this.on('enable',function(){self.$container.removeClass('select2-container--disabled')});this.on('disable',function(){self.$container.addClass('select2-container--disabled')});this.on('blur',function(){self.$container.removeClass('select2-container--focus')});this.on('query',function(params){if(!self.isOpen()){self.trigger('open',{})}this.dataAdapter.query(params,function(data){self.trigger('results:all',{data:data,query:params})})});this.on('query:append',function(params){this.dataAdapter.query(params,function(data){self.trigger('results:append',{data:data,query:params})})});this.on('keypress',function(evt){var key=evt.which;if(self.isOpen()){if(key===KEYS.ESC||key===KEYS.TAB||(key===KEYS.UP&&evt.altKey)){self.close(evt);evt.preventDefault()}else if(key===KEYS.ENTER){self.trigger('results:select',{});evt.preventDefault()}else if((key===KEYS.SPACE&&evt.ctrlKey)){self.trigger('results:toggle',{});evt.preventDefault()}else if(key===KEYS.UP){self.trigger('results:previous',{});evt.preventDefault()}else if(key===KEYS.DOWN){self.trigger('results:next',{});evt.preventDefault()}}else{if(key===KEYS.ENTER||key===KEYS.SPACE||(key===KEYS.DOWN&&evt.altKey)){self.open();evt.preventDefault()}}})};Select2.prototype._syncAttributes=function(){this.options.set('disabled',this.$element.prop('disabled'));if(this.isDisabled()){if(this.isOpen()){this.close()}this.trigger('disable',{})}else{this.trigger('enable',{})}};Select2.prototype._isChangeMutation=function(evt,mutations){var changed=false;var self=this;if(evt&&evt.target&&(evt.target.nodeName!=='OPTION'&&evt.target.nodeName!=='OPTGROUP')){return}if(!mutations){changed=true}else if(mutations.addedNodes&&mutations.addedNodes.length>0){for(var n=0;n0){changed=true}else if($.isArray(mutations)){$.each(mutations,function(evt,mutation){if(self._isChangeMutation(evt,mutation)){changed=true;return false}})}return changed};Select2.prototype._syncSubtree=function(evt,mutations){var changed=this._isChangeMutation(evt,mutations);var self=this;if(changed){this.dataAdapter.current(function(currentData){self.trigger('selection:update',{data:currentData})})}};Select2.prototype.trigger=function(name,args){var actualTrigger=Select2.__super__.trigger;var preTriggerMap={'open':'opening','close':'closing','select':'selecting','unselect':'unselecting','clear':'clearing'};if(args===undefined){args={}}if(name in preTriggerMap){var preTriggerName=preTriggerMap[name];var preTriggerArgs={prevented:false,name:name,args:args};actualTrigger.call(this,preTriggerName,preTriggerArgs);if(preTriggerArgs.prevented){args.prevented=true;return}}actualTrigger.call(this,name,args)};Select2.prototype.toggleDropdown=function(){if(this.isDisabled()){return}if(this.isOpen()){this.close()}else{this.open()}};Select2.prototype.open=function(){if(this.isOpen()){return}if(this.isDisabled()){return}this.trigger('query',{})};Select2.prototype.close=function(evt){if(!this.isOpen()){return}this.trigger('close',{originalEvent:evt})};Select2.prototype.isEnabled=function(){return!this.isDisabled()};Select2.prototype.isDisabled=function(){return this.options.get('disabled')};Select2.prototype.isOpen=function(){return this.$container.hasClass('select2-container--open')};Select2.prototype.hasFocus=function(){return this.$container.hasClass('select2-container--focus')};Select2.prototype.focus=function(data){if(this.hasFocus()){return}this.$container.addClass('select2-container--focus');this.trigger('focus',{})};Select2.prototype.enable=function(args){if(this.options.get('debug')&&window.console&&console.warn){console.warn('Select2: The `select2("enable")` method has been deprecated and will'+' be removed in later Select2 versions. Use $element.prop("disabled")'+' instead.')}if(args==null||args.length===0){args=[true]}var disabled=!args[0];this.$element.prop('disabled',disabled)};Select2.prototype.data=function(){if(this.options.get('debug')&&arguments.length>0&&window.console&&console.warn){console.warn('Select2: Data can no longer be set using `select2("data")`. You '+'should consider setting the value instead using `$element.val()`.')}var data=[];this.dataAdapter.current(function(currentData){data=currentData});return data};Select2.prototype.val=function(args){if(this.options.get('debug')&&window.console&&console.warn){console.warn('Select2: The `select2("val")` method has been deprecated and will be'+' removed in later Select2 versions. Use $element.val() instead.')}if(args==null||args.length===0){return this.$element.val()}var newVal=args[0];if($.isArray(newVal)){newVal=$.map(newVal,function(obj){return obj.toString()})}this.$element.val(newVal).trigger('input').trigger('change')};Select2.prototype.destroy=function(){this.$container.remove();if(this.$element[0].detachEvent){this.$element[0].detachEvent('onpropertychange',this._syncA)}if(this._observer!=null){this._observer.disconnect();this._observer=null}else if(this.$element[0].removeEventListener){this.$element[0].removeEventListener('DOMAttrModified',this._syncA,false);this.$element[0].removeEventListener('DOMNodeInserted',this._syncS,false);this.$element[0].removeEventListener('DOMNodeRemoved',this._syncS,false)}this._syncA=null;this._syncS=null;this.$element.off('.select2');this.$element.attr('tabindex',Utils.GetData(this.$element[0],'old-tabindex'));this.$element.removeClass('select2-hidden-accessible');this.$element.attr('aria-hidden','false');Utils.RemoveData(this.$element[0]);this.$element.removeData('select2');this.dataAdapter.destroy();this.selection.destroy();this.dropdown.destroy();this.results.destroy();this.dataAdapter=null;this.selection=null;this.dropdown=null;this.results=null};Select2.prototype.render=function(){var $container=$(''+''+''+'');$container.attr('dir',this.options.get('dir'));this.$container=$container;this.$container.addClass('select2-container--'+this.options.get('theme'));Utils.StoreData($container[0],'element',this.$element);return $container};return Select2});S2.define('jquery-mousewheel',['jquery'],function($){return $});S2.define('jquery.select2',['jquery','jquery-mousewheel','./select2/core','./select2/defaults','./select2/utils'],function($,_,Select2,Defaults,Utils){if($.fn.select2==null){var thisMethods=['open','close','destroy'];$.fn.select2=function(options){options=options||{};if(typeof options==='object'){this.each(function(){var instanceOptions=$.extend(true,{},options);var instance=new Select2($(this),instanceOptions)});return this}else if(typeof options==='string'){var ret;var args=Array.prototype.slice.call(arguments,1);this.each(function(){var instance=Utils.GetData(this,'select2');if(instance==null&&window.console&&console.error){console.error('The select2(\''+options+'\') method was called on an '+'element that is not using Select2.')}ret=instance[options].apply(instance,args)});if($.inArray(options,thisMethods)>-1){return this}return ret}else{throw new Error('Invalid arguments for Select2: '+options);}}}if($.fn.select2.defaults==null){$.fn.select2.defaults=Defaults}return Select2});return{define:S2.define,require:S2.require}}());var select2=S2.require('jquery.select2');jQuery.fn.select2.amd=S2;return select2})); \ No newline at end of file diff --git a/obp-api/src/main/webapp/media/js/website.js b/obp-api/src/main/webapp/media/js/website.js index 563995aca..ac02198c5 100644 --- a/obp-api/src/main/webapp/media/js/website.js +++ b/obp-api/src/main/webapp/media/js/website.js @@ -120,9 +120,6 @@ $(document).ready(function() { $("#small-screen-navbar #small-nav-log-on-button").css("width","63px"); } - $(".main-support-item .support-platform-link").text("chat.openbankproject.com"); - - var htmlTitle = $(document).find("title").text(); if (htmlTitle.indexOf("Get API") > -1){ @@ -343,6 +340,13 @@ $(document).ready(function() { } else{ consumerRegistrationAppRequestUriError.parent().addClass('hide'); } + + var dataAreaErrors = $('#data-area-input #data-area-errors'); + if (dataAreaErrors.length > 0 && dataAreaErrors.html().length > 0) { + dataAreaErrors.parent().removeClass('hide'); + } else{ + dataAreaErrors.parent().addClass('hide'); + } { var consumerRegistrationJwksError = $('#register-consumer-input #consumer-registration-app-signing_jwks-error'); diff --git a/obp-api/src/main/webapp/plain.html b/obp-api/src/main/webapp/plain.html index 657cec28b..8f1567748 100644 --- a/obp-api/src/main/webapp/plain.html +++ b/obp-api/src/main/webapp/plain.html @@ -1,4 +1,5 @@ - + + Example HTML

            I am some Example HTML

            diff --git a/obp-api/src/main/webapp/templates-hidden/default.html b/obp-api/src/main/webapp/templates-hidden/default.html index 57103f807..1384cb02c 100644 --- a/obp-api/src/main/webapp/templates-hidden/default.html +++ b/obp-api/src/main/webapp/templates-hidden/default.html @@ -47,7 +47,7 @@ Berlin 13359, Germany - + @@ -70,9 +70,11 @@ Berlin 13359, Germany
            - +
            - + +
            +
            left logo image @@ -133,7 +135,7 @@ Berlin 13359, Germany
            diff --git a/obp-api/src/main/webapp/user-information.html b/obp-api/src/main/webapp/user-information.html new file mode 100644 index 000000000..d60122c6d --- /dev/null +++ b/obp-api/src/main/webapp/user-information.html @@ -0,0 +1,64 @@ + +
            +
            +
            +

            User Information

            +
            + +
            +
            +
            + + +
            +
            + + +
            +
            + + +
            +
            + + +
            + +
            +
            +
            +
            +
            +
            + + diff --git a/obp-api/src/main/webapp/user-invitation.html b/obp-api/src/main/webapp/user-invitation.html index 29bd87f1d..55764e737 100644 --- a/obp-api/src/main/webapp/user-invitation.html +++ b/obp-api/src/main/webapp/user-invitation.html @@ -23,11 +23,11 @@ Berlin 13359, Germany This product includes software developed at TESOBE (http://www.tesobe.com/) --> -
            +
            -
            -
            +
            +

            Complete your user invitation

            Please complete the information about the user invitation application below.

            All fields are required unless marked as 'optional'

            @@ -91,24 +91,34 @@ Berlin 13359, Germany
            +
            - -
            - + +
            +
            diff --git a/obp-api/src/test/resources/cert/public_dauth.pem b/obp-api/src/test/resources/cert/public_dauth.pem new file mode 100644 index 000000000..d6562bc8a --- /dev/null +++ b/obp-api/src/test/resources/cert/public_dauth.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICsjCCAZqgAwIBAgIGAX0EbA7DMA0GCSqGSIb3DQEBCwUAMBoxGDAWBgNVBAMM +D2FwcC5leGFtcGxlLmNvbTAeFw0yMTExMDkxMTE4NTBaFw0yMzExMDkxMTE4NTBa +MBoxGDAWBgNVBAMMD2FwcC5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBALBf/GQFAiT3c6eeiNbF+G3qHgy5xPOHrgsCr13Vkh2CL5x1 +AoIG7eHSDTZFcIrW7cescf6KEqYSQil5Z64GiwxbaXT3CPZI6wHpc7S0FD89WQVQ +yeJSUr8me0LLOQ5YCmnJ6fn41X4K2PCekJx86vgF4Wbgw5+35Ud9vTxWYVkM247i +O4MdiVR9fd53o9eAOsVJxSJ8ITw41tomqp0h6Wuam9MMuEOIwo48KSMAR+Lx2XmF +9nUIAsxyVFhYibSVT0lnt2OPtm9x/PYEswC0HSM2YNKb31gOyDrGBwrLCDXRArnk +dBSWeK63n85LtK2Th/rf6Qy01aG4e+LFx3f6YI0CAwEAATANBgkqhkiG9w0BAQsF +AAOCAQEAonMKkbeqcUOdzqRQ4yE3Did3yM3oxaN2vuB0g4QQ85u++xdGNl5wnl9j +xDzkkvjgO8x1YHsfF0+YoOxF04ObQkZqG8NJUNazPIkRwkEUj5QFQ9YA9uoShaS6 +78w+eXunb6BkPuPsj/nzcB8loUrBgUwfB3NvUx9oCL6snB4iuS7PULtSZ894kXi+ +lyk8C3uuY4o050CQ23LmNtV49yTrzYpE3sLtx3s8u3MxcEEDazvKW9x0o4/Y2sSx +iN7YfI2sF3Jldp2zrlWIoboQMnrJoT5KmKpolRmlR5iEl23fYmzi7Hn+piXrvmS/ +IiACsMLX56d+ggduyvrWnqiWPjvKDA== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/obp-api/src/test/scala/RunMTLSWebApp.scala b/obp-api/src/test/scala/RunMTLSWebApp.scala index 3b3c9fbe3..edc181489 100644 --- a/obp-api/src/test/scala/RunMTLSWebApp.scala +++ b/obp-api/src/test/scala/RunMTLSWebApp.scala @@ -30,6 +30,8 @@ import java.security.cert.X509Certificate import bootstrap.liftweb.Boot import code.api.RequestHeader +import code.api.util.APIUtil +import code.setup.PropsProgrammatically import net.liftweb.http.LiftRules import net.liftweb.http.provider.HTTPContext import org.apache.commons.codec.binary.Base64 @@ -38,10 +40,16 @@ import org.eclipse.jetty.util.ssl.SslContextFactory import org.eclipse.jetty.webapp.WebAppContext import sun.security.provider.X509Factory -object RunMTLSWebApp extends App { +object RunMTLSWebApp extends App with PropsProgrammatically { val servletContextPath = "/" //set run mode value to "development", So the value is true of Props.devMode System.setProperty("run.mode", "development") + // Props hostname MUST be set to https protocol. + // Otherwise OAuth1.0a computed signature at OBP-API side cannot match API-Explorer generates + // This automatic adjustment should enable out-of-box feature + APIUtil.getPropsValue("hostname").map{ x => + setPropsValues("hostname"-> x.replaceFirst("http", "https")) + } { val tempHTTPContext = JProxy.newProxyInstance(this.getClass.getClassLoader, Array(classOf[HTTPContext]), @@ -50,7 +58,7 @@ object RunMTLSWebApp extends App { servletContextPath } else { throw new IllegalAccessException(s"Should not call this object method except 'path' method, current call method name is: ${method.getName}") - ??? // should not call other method. +// ??? // should not call other method. } }).asInstanceOf[HTTPContext] LiftRules.setContext(tempHTTPContext) @@ -110,7 +118,8 @@ object RunMTLSWebApp extends App { context.setWar(s"${basePath}src/main/webapp") // rename JSESSIONID, avoid conflict with other project when start two project at local - context.getSessionHandler.getSessionCookieConfig.setName("JSESSIONID_OBP_API") + val propsApiInstanceId = APIUtil.getPropsValue("api_instance_id").openOrThrowException("connector props filed `api_instance_id` not set") + context.getSessionHandler.getSessionCookieConfig.setName("JSESSIONID_OBP_API_" + propsApiInstanceId) server.setHandler(context) diff --git a/obp-api/src/test/scala/RunWebApp.scala b/obp-api/src/test/scala/RunWebApp.scala index 791c1ceb3..7f2484a77 100644 --- a/obp-api/src/test/scala/RunWebApp.scala +++ b/obp-api/src/test/scala/RunWebApp.scala @@ -51,7 +51,7 @@ object RunWebApp extends App { servletContextPath } else { throw new IllegalAccessException(s"Should not call this object method except 'path' method, current call method name is: ${method.getName}") - ??? // should not call other method. +// ??? // should not call other method. } }).asInstanceOf[HTTPContext] LiftRules.setContext(tempHTTPContext) diff --git a/obp-api/src/test/scala/code/api/directloginTest.scala b/obp-api/src/test/scala/code/api/DirectLoginTest.scala similarity index 82% rename from obp-api/src/test/scala/code/api/directloginTest.scala rename to obp-api/src/test/scala/code/api/DirectLoginTest.scala index a375e4b58..2824a6f80 100644 --- a/obp-api/src/test/scala/code/api/directloginTest.scala +++ b/obp-api/src/test/scala/code/api/DirectLoginTest.scala @@ -1,6 +1,5 @@ package code.api -import com.openbankproject.commons.util.ApiVersion import code.api.util.ErrorMessages import code.api.util.ErrorMessages._ import code.api.v2_0_0.OBPAPI2_0_0.Implementations2_0_0 @@ -10,8 +9,10 @@ import code.consumer.Consumers import code.loginattempts.LoginAttempt import code.model.dataAccess.AuthUser import code.setup.{APIResponse, ServerSetup} +import code.userlocks.UserLocksProvider import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.model.ErrorMessage +import com.openbankproject.commons.util.ApiVersion import net.liftweb.json.JsonAST.{JArray, JField, JObject, JString} import net.liftweb.mapper.By import net.liftweb.util.Helpers._ @@ -19,7 +20,7 @@ import org.scalatest.{BeforeAndAfter, Tag} -class directloginTest extends ServerSetup with BeforeAndAfter { +class DirectLoginTest extends ServerSetup with BeforeAndAfter { /** * Test tags @@ -37,7 +38,9 @@ class directloginTest extends ServerSetup with BeforeAndAfter { val SECRET = randomString(40).toLowerCase val EMAIL = randomString(10).toLowerCase + "@example.com" val USERNAME = "username with spaces" - val PASSWORD = """G!y"k9GHD$D""" + //these are password, but sonarcloud: "password" detected here, make sure this is not a hard-coded credential. + val NO_EXISTING_PW = "notExistingPassword" + val VALID_PW = """G!y"k9GHD$D""" val KEY_DISABLED = randomString(40).toLowerCase val SECRET_DISABLED = randomString(40).toLowerCase @@ -50,7 +53,7 @@ class directloginTest extends ServerSetup with BeforeAndAfter { AuthUser.create. email(EMAIL). username(USERNAME). - password(PASSWORD). + password(VALID_PW). validated(true). firstName(randomString(10)). lastName(randomString(10)). @@ -77,22 +80,22 @@ class directloginTest extends ServerSetup with BeforeAndAfter { val accessControlOriginHeader = ("Access-Control-Allow-Origin", "*") val invalidUsernamePasswordHeader = ("Authorization", ("DirectLogin username=\"notExistingUser\", " + - "password=\"notExistingPassword\", consumer_key=%s").format(KEY)) + "password=%s, consumer_key=%s").format(NO_EXISTING_PW, KEY)) val invalidUsernamePasswordCharaterHeader = ("Authorization", ("DirectLogin username=\" a#s \", " + - "password=\"no-good-password\", consumer_key=%s").format(KEY)) + "password=%s, consumer_key=%s").format(NO_EXISTING_PW, KEY)) val validUsernameInvalidPasswordHeader = ("Authorization", ("DirectLogin username=%s," + - "password=\"notExistingPassword\", consumer_key=%s").format(USERNAME, KEY)) + "password=%s, consumer_key=%s").format(USERNAME, NO_EXISTING_PW, KEY)) val invalidConsumerKeyHeader = ("Authorization", ("DirectLogin username=%s, " + - "password=%s, consumer_key=%s").format(USERNAME, PASSWORD, "invalid")) + "password=%s, consumer_key=%s").format(USERNAME, VALID_PW, "invalid")) val validDeprecatedHeader = ("Authorization", "DirectLogin username=%s, password=%s, consumer_key=%s". - format(USERNAME, PASSWORD, KEY)) + format(USERNAME, VALID_PW, KEY)) val validHeader = ("DirectLogin", "username=%s, password=%s, consumer_key=%s". - format(USERNAME, PASSWORD, KEY)) + format(USERNAME, VALID_PW, KEY)) val disabledConsumerValidHeader = ("Authorization", "DirectLogin username=%s, password=%s, consumer_key=%s". format(USERNAME_DISABLED, PASSWORD_DISABLED, KEY_DISABLED)) @@ -338,6 +341,58 @@ class directloginTest extends ServerSetup with BeforeAndAfter { currentUserNewStyle.username shouldBe currentUserOldStyle.username } + scenario("Login with correct everything but the user is locked", ApiEndpoint1, ApiEndpoint2) { + lazy val username = "firstname.lastname" + lazy val header = ("DirectLogin", "username=%s, password=%s, consumer_key=%s". + format(username, VALID_PW, KEY)) + + // Delete the user + AuthUser.findUserByUsernameLocally(username).map(_.delete_!()) + // Create the user + AuthUser.create. + email(EMAIL). + username(username). + password(VALID_PW). + validated(true). + firstName(randomString(10)). + lastName(randomString(10)). + saveMe + + When("the header and credentials are good") + lazy val response = makePostRequestAdditionalHeader(directLoginRequest, "", List(accessControlOriginHeader, header)) + var token = "" + Then("We should get a 201 - OK and a token") + response.code should equal(201) + response.body match { + case JObject(List(JField(name, JString(value)))) => + name should equal("token") + value.length should be > 0 + token = value + case _ => fail("Expected a token") + } + + When("when we use the token it should work") + lazy val headerWithToken = ("DirectLogin", "token=%s".format(token)) + lazy val validHeadersWithToken = List(accessControlOriginHeader, headerWithToken) + + // Lock the user in order to test functionality + UserLocksProvider.lockUser(username) + + When("when we use the token to get current user and it should NOT work due to locked user - New Style") + lazy val requestCurrentUserNewStyle = baseRequest / "obp" / "v3.0.0" / "users" / "current" + lazy val responseCurrentUserNewStyle = makeGetRequest(requestCurrentUserNewStyle, validHeadersWithToken) + And("We should get a 401") + responseCurrentUserNewStyle.code should equal(401) + responseCurrentUserNewStyle.body.extract[ErrorMessage].message should include(ErrorMessages.UsernameHasBeenLocked) + + When("when we use the token to get current user and it should NOT work due to locked user - Old Style") + lazy val requestCurrentUserOldStyle = baseRequest / "obp" / "v2.0.0" / "users" / "current" + lazy val responseCurrentUserOldStyle = makeGetRequest(requestCurrentUserOldStyle, validHeadersWithToken) + And("We should get a 400") + responseCurrentUserOldStyle.code should equal(400) + responseCurrentUserOldStyle.body.extract[ErrorMessage].message should include(ErrorMessages.UsernameHasBeenLocked) + } + } private def assertResponse(response: APIResponse, expectedErrorMessage: String): Unit = { diff --git a/obp-api/src/test/scala/code/api/dauthTest.scala b/obp-api/src/test/scala/code/api/dauthTest.scala new file mode 100644 index 000000000..d00c821aa --- /dev/null +++ b/obp-api/src/test/scala/code/api/dauthTest.scala @@ -0,0 +1,82 @@ +package code.api + +import code.api.util.ErrorMessages +import code.setup.{DefaultUsers, PropsReset, ServerSetup} +import org.scalatest._ + +class dauthTest extends ServerSetup with BeforeAndAfter with DefaultUsers with PropsReset{ + + val accessControlOriginHeader = ("Access-Control-Allow-Origin", "*") + /* Payload data. verified by wrong secret "123" -- show : DAuthJwtTokenIsNotValid + { + "smart_contract_address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "network_name": "ETHEREUM", + "msg_sender": "0xe90980927f1725E7734CE288F8367e1Bb143E90fhku767", + "consumer_key": "0x19255a4ec31e89cea54d1f125db7536e874ab4a96b4d4f6438668b6bb10a6adb", + "timestamp": "2018-08-20T14:13:40Z", + "request_id": "0Xe876987694328763492876348928736497869273649" +} + */ + val wrongPublicKeyJwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyIiwibmV0d29ya19uYW1lIjoiRVRIRVJFVU0iLCJtc2dfc2VuZGVyIjoiMHhlOTA5ODA5MjdmMTcyNUU3NzM0Q0UyODhGODM2N2UxQmIxNDNFOTBmaGt1NzY3IiwiY29uc3VtZXJfa2V5IjoiMHgxOTI1NWE0ZWMzMWU4OWNlYTU0ZDFmMTI1ZGI3NTM2ZTg3NGFiNGE5NmI0ZDRmNjQzODY2OGI2YmIxMGE2YWRiIiwidGltZXN0YW1wIjoiMjAxOC0wOC0yMFQxNDoxMzo0MFoiLCJyZXF1ZXN0X2lkIjoiMFhlODc2OTg3Njk0MzI4NzYzNDkyODc2MzQ4OTI4NzM2NDk3ODY5MjczNjQ5In0.Wa1aaoHKSWAKcX_tnueVf3PQze-BcPeZ_EfhovxRvv9WIGn86WSShT2x2W_VGfySYJhfYYhpg2N-l-trA2T9jru5u3Mp_yQcSJZ9D1kCg3kmy2AqYp_qbPIakVQthWo1Ys7hkGB6bZHau87BOXv9v4v97LrpRfva5lw62qzdhpN67lTK1hdUc677nsneFdtnA78Ddm6u_ta_KIf_mC0t-lxSfUcuLb7LQgp2biYyYMgVB7dyexPQ7ZSBa2B8ARGXBXo0iOCjvi-Su4IYUomklRwKWYI-waaigaCDd9FZZQyDfjEySQToAG7UO0mPBRQiIVrSumecz1VESlO_c0Bm0w" + + /* Payload data. verified by correct secret "your-at-least-256-bit-secret-token" + { + "smart_contract_address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F05124", + "network_name": "ETHEREUM", + "msg_sender": "0xe90980927f1725E7734CE288F8367e1Bb143E90fhku767", + "consumer_key": "0x19255a4ec31e89cea54d1f125db7536e874ab4a96b4d4f6438668b6bb10a6adb", + "timestamp": "2018-08-20T14:13:40Z", + "request_id": "0Xe876987694328763492876348928736497869273649" +} + */ + val jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyIiwibmV0d29ya19uYW1lIjoiRVRIRVJFVU0iLCJtc2dfc2VuZGVyIjoiMHhlOTA5ODA5MjdmMTcyNUU3NzM0Q0UyODhGODM2N2UxQmIxNDNFOTBmaGt1NzY3IiwiY29uc3VtZXJfa2V5IjoiMHgxOTI1NWE0ZWMzMWU4OWNlYTU0ZDFmMTI1ZGI3NTM2ZTg3NGFiNGE5NmI0ZDRmNjQzODY2OGI2YmIxMGE2YWRiIiwidGltZXN0YW1wIjoiMjAxOC0wOC0yMFQxNDoxMzo0MFoiLCJyZXF1ZXN0X2lkIjoiMFhlODc2OTg3Njk0MzI4NzYzNDkyODc2MzQ4OTI4NzM2NDk3ODY5MjczNjQ5In0.dkAy32AjskvOaQ-gzXEiwU7RslJIawrOPsFsrqAlGHeKr6NyLJPJLYQ6e8_ABK2N-Pw43PiIzefV5QdiGxtWXCuVMRldrdNVC2VdBLVicDVWOmHCLyQ-mFbUvBR3wx8ZsU9nauEchVBsI9UY-_YYYI4yF9DsUazdMoesIjDl-zr68Dzm_ljnxv1wL4fbFpT7wq7MRFQBSy5UTN9o0JxGN_sm9dYeGf-kINQP8-zmJKQM0CRlMegdcBJdonSjlJDib_cKdbyeiSYwWTnqu9pAsOKarY7sX7uIa4A2hVkGY9hkSaGoeQcTxUHFTrJFdEeDm2num2MNLjFul3roAEG0Uw" + + val invalidJwt = ("DAuth", ("%s").format(wrongPublicKeyJwt)) + val validJwt = ("DAuth", ("%s").format(jwt)) + + def dauthRequest = baseRequest / "obp" / "v2.0.0" / "users" /"current" + def dauthNonBlockingRequest = baseRequest / "obp" / "v3.0.0" / "users" / "current" + + feature("DAuth Testing") { + + scenario("Missing parameter token in a blocking way") { + When("We try to login without parameter token in a Header") + + When("We try to login with an invalid JWT") + val responseInvalid = makeGetRequest(dauthRequest, List(invalidJwt)) + Then("We should get a 400 - Bad Request") + logger.debug("-----------------------------------------") + logger.debug("responseInvalid response: "+ responseInvalid) + logger.debug("-----------------------------------------") + responseInvalid.code should equal(400) + responseInvalid.toString contains (ErrorMessages.DAuthJwtTokenIsNotValid) should be (true) + + When("We try to login with an valid JWT") + val responseValidJwt = makeGetRequest(dauthRequest, List(validJwt)) + logger.debug("-----------------------------------------") + logger.debug("responseValidJwt response: " + responseValidJwt) + logger.debug("-----------------------------------------") + responseValidJwt.code should equal(200) + + When("We try to login with an invalid JWT") + val responseNonBlockingInvalid = makeGetRequest(dauthNonBlockingRequest, List(invalidJwt)) + Then("We should get a 400 - Bad Request") + logger.debug("-----------------------------------------") + logger.debug("responseNonBlockingInvalid responseNonBlocking: " + responseNonBlockingInvalid) + logger.debug("-----------------------------------------") + responseNonBlockingInvalid.code should equal(401) + responseNonBlockingInvalid.toString contains (ErrorMessages.DAuthJwtTokenIsNotValid) should be (true) + + When("We try to login with an valid JWT") + val responseNonBlockingValidJwt = makeGetRequest(dauthNonBlockingRequest, List(validJwt)) + logger.debug("-----------------------------------------") + logger.debug("responseNonBlockingValidJwt responseNonBlocking: " + responseNonBlockingValidJwt) + logger.debug("-----------------------------------------") + responseValidJwt.code should equal(200) + } + + } + + + +} \ No newline at end of file diff --git a/obp-api/src/test/scala/code/api/v4_0_0/AccountAccessTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/AccountAccessTest.scala index 285838a8f..1dc0f6b1e 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/AccountAccessTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/AccountAccessTest.scala @@ -28,6 +28,7 @@ class AccountAccessTest extends V400ServerSetup { object VersionOfApi extends Tag(ApiVersion.v4_0_0.toString) object ApiEndpoint1 extends Tag(nameOf(Implementations4_0_0.grantUserAccessToView)) object ApiEndpoint2 extends Tag(nameOf(Implementations4_0_0.revokeUserAccessToView)) + object ApiEndpoint3 extends Tag(nameOf(Implementations4_0_0.createUserWithAccountAccess)) lazy val bankId = randomBankId @@ -70,7 +71,7 @@ class AccountAccessTest extends V400ServerSetup { } } - feature(s"test $ApiEndpoint1 and $ApiEndpoint2 version $VersionOfApi - Authorized access") { + feature(s"test $ApiEndpoint1 and $ApiEndpoint2 and $ApiEndpoint3 version $VersionOfApi - Authorized access") { scenario("We will call the endpoint with user credentials", VersionOfApi, ApiEndpoint1, ApiEndpoint2) { val addedEntitlement: Box[Entitlement] = Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, ApiRole.CanCreateAccount.toString) @@ -95,6 +96,17 @@ class AccountAccessTest extends V400ServerSetup { Then("We should get a 201 and check the response body") responseRevoke.code should equal(201) responseRevoke.body.extract[RevokedJsonV400] + + { + val postCreateUserJson = PostCreateUserAccountAccessJsonV400(resourceUser2.userId, "dauth."+resourceUser2.provider, List(PostViewJsonV400(view.id, view.is_system))) + When("We send the request") + val request = (v4_0_0_Request / "banks" / bankId / "accounts" / account.account_id / "user-account-access").POST <@ (user1) + val response = makePostRequest(request, write(postCreateUserJson)) + Then("We should get a 201 and check the response body") + response.code should equal(201) + val views = response.body.extract[List[ViewJsonV300]] + views.length + } } } diff --git a/obp-api/src/test/scala/code/api/v4_0_0/ApiCollectionEndpointTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/ApiCollectionEndpointTest.scala index 0e6f56757..40fe52283 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/ApiCollectionEndpointTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/ApiCollectionEndpointTest.scala @@ -48,6 +48,8 @@ class ApiCollectionEndpointTest extends V400ServerSetup { object ApiEndpoint3 extends Tag(nameOf(Implementations4_0_0.getMyApiCollectionEndpoint)) object ApiEndpoint4 extends Tag(nameOf(Implementations4_0_0.deleteMyApiCollectionEndpoint)) object ApiEndpoint5 extends Tag(nameOf(Implementations4_0_0.getApiCollectionEndpoints)) + object ApiEndpoint6 extends Tag(nameOf(Implementations4_0_0.createMyApiCollectionEndpointById)) + object ApiEndpoint7 extends Tag(nameOf(Implementations4_0_0.getMyApiCollectionEndpointsById)) feature("Test the apiCollection endpoints") { scenario("We create the apiCollection Endpoint", ApiEndpoint1,ApiEndpoint2, ApiEndpoint3, ApiEndpoint4, VersionOfApi) { @@ -133,6 +135,35 @@ class ApiCollectionEndpointTest extends V400ServerSetup { apiCollectionEndpointsJsonGetAfterDelete.api_collection_endpoints.length should be (0) + { + Then(s"we test the $ApiEndpoint6") + val requestApiCollectionEndpoint = (v4_0_0_Request / "my" / "api-collection-ids" / apiCollectionId / "api-collection-endpoints").POST <@ (user1) + + lazy val postApiCollectionEndpointJson = SwaggerDefinitionsJSON.postApiCollectionEndpointJson400.copy(operation_id="OBPv4.0.0-getBanks") + + val responseApiCollectionEndpointJson = makePostRequest(requestApiCollectionEndpoint, write(postApiCollectionEndpointJson)) + Then("We should get a 201") + responseApiCollectionEndpointJson.code should equal(201) + val apiCollectionEndpoint = responseApiCollectionEndpointJson.body.extract[ApiCollectionEndpointJson400] + + apiCollectionEndpoint.operation_id should be (postApiCollectionEndpointJson.operation_id) + apiCollectionEndpoint.api_collection_endpoint_id shouldNot be (null) + + val operationId= apiCollectionEndpoint.operation_id + } + + { + Then(s"we test the $ApiEndpoint7") + val requestGet = (v4_0_0_Request / "my" / "api-collection-ids" / apiCollectionId / "api-collection-endpoints").GET <@ (user1) + + val responseGet = makeGetRequest(requestGet) + Then("We should get a 200") + responseGet.code should equal(200) + + val apiCollectionsJsonGet400 = responseGet.body.extract[ApiCollectionEndpointsJson400] + + apiCollectionsJsonGet400.api_collection_endpoints.length should be (1) + } } } diff --git a/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala b/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala index 79bf85e28..b007fa04f 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala @@ -1,15 +1,19 @@ package code.api.v4_0_0 +import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON import com.openbankproject.commons.model.ErrorMessage -import code.api.util.ApiRole.{CanGetEntitlementsForAnyBank, CanGetEntitlementsForAnyUserAtAnyBank, CanGetEntitlementsForOneBank} +import code.api.util.ApiRole.{CanCreateBranch, CanGetEntitlementsForAnyBank, CanGetEntitlementsForAnyUserAtAnyBank, CanGetEntitlementsForOneBank, CanUpdateBranch} import code.api.util.ErrorMessages.{UserHasMissingRoles, _} import code.api.util.{ApiRole, ErrorMessages} import code.entitlement.Entitlement import code.setup.DefaultUsers import code.api.util.APIUtil.OAuth._ +import code.api.util.ExampleValue.bankIdExample +import code.api.v2_0_0.CreateEntitlementJSON import code.api.v4_0_0.APIMethods400.Implementations4_0_0 import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.util.ApiVersion +import net.liftweb.json.Serialization.write import org.scalatest.Tag class EntitlementTests extends V400ServerSetupAsync with DefaultUsers { @@ -32,6 +36,7 @@ class EntitlementTests extends V400ServerSetupAsync with DefaultUsers { object VersionOfApi extends Tag(ApiVersion.v4_0_0.toString) object ApiEndpoint1 extends Tag(nameOf(Implementations4_0_0.getEntitlements)) object ApiEndpoint2 extends Tag(nameOf(Implementations4_0_0.getEntitlementsForBank)) + object ApiEndpoint3 extends Tag(nameOf(Implementations4_0_0.createUserWithRoles)) feature("Assuring that endpoint getEntitlements works as expected - v4.0.0") { @@ -109,6 +114,66 @@ class EntitlementTests extends V400ServerSetupAsync with DefaultUsers { r.code should equal(200) } } + + scenario("We try to - createUserWithRoles - not roles, only grant the roles the login user has ", ApiEndpoint3, VersionOfApi) { + And("We make the request") + val createEntitlements = List(CreateEntitlementJSON( + bank_id = testBankId1.value, + role_name = CanCreateBranch.toString() + ), CreateEntitlementJSON( + bank_id = testBankId1.value, + role_name = CanUpdateBranch.toString() + )) + val postJson = SwaggerDefinitionsJSON.postCreateUserWithRolesJsonV400.copy(roles= createEntitlements) + val requestGet = (v4_0_0_Request / "user-entitlements").GET <@ (user1) + val responseGet = makePostRequestAsync(requestGet, write(postJson)) + Then("We should get a 200") + responseGet map { r => + r.code should equal(400) + r.body.toString contains (EntitlementCannotBeGranted) shouldBe(true) + } + } + + scenario("We try to - createUserWithRoles - wrong user provider ", ApiEndpoint3, VersionOfApi) { + And("We make the request") + val createEntitlements = List(CreateEntitlementJSON( + bank_id = testBankId1.value, + role_name = CanCreateBranch.toString() + ), CreateEntitlementJSON( + bank_id = testBankId1.value, + role_name = CanUpdateBranch.toString() + )) + val postJson = SwaggerDefinitionsJSON.postCreateUserWithRolesJsonV400.copy(provider ="xx", roles= createEntitlements) + val requestGet = (v4_0_0_Request / "user-entitlements").GET <@ (user1) + val responseGet = makePostRequestAsync(requestGet, write(postJson)) + Then("We should get a 200") + responseGet map { r => + r.code should equal(400) + r.body.toString contains (InvalidUserProvider) shouldBe(true) + } + } + + scenario("We try to - createUserWithRoles", ApiEndpoint3, VersionOfApi) { + When("We add required entitlement") + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanCreateEntitlementAtAnyBank.toString) + And("We make the request") + val createEntitlements = List(CreateEntitlementJSON( + bank_id = testBankId1.value, + role_name = CanCreateBranch.toString() + ), CreateEntitlementJSON( + bank_id = testBankId1.value, + role_name = CanUpdateBranch.toString() + )) + val postJson = SwaggerDefinitionsJSON.postCreateUserWithRolesJsonV400.copy(roles= createEntitlements) + val requestGet = (v4_0_0_Request / "user-entitlements").GET <@ (user1) + val responseGet = makePostRequestAsync(requestGet, write(postJson)) + Then("We should get a 200") + responseGet map { r => + val entitlements = r.body.extract[EntitlementsJsonV400] + r.code should equal(201) + entitlements.list.length should be (2) + } + } } diff --git a/obp-api/src/test/scala/code/api/v4_0_0/FirehoseTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/FirehoseTest.scala index 02260b02f..e397c9ede 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/FirehoseTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/FirehoseTest.scala @@ -21,6 +21,7 @@ class FirehoseTest extends V400ServerSetup with PropsReset{ */ object VersionOfApi extends Tag(ApiVersion.v4_0_0.toString) object ApiEndpoint1 extends Tag(nameOf(Implementations4_0_0.getFirehoseAccountsAtOneBank)) + object ApiEndpoint2 extends Tag(nameOf(Implementations4_0_0.getFastFirehoseAccountsAtOneBank)) feature(s"test $ApiEndpoint1 version $VersionOfApi - Authorized access") { scenario("We will call the endpoint with user credentials", VersionOfApi, ApiEndpoint1) { @@ -53,7 +54,7 @@ class FirehoseTest extends V400ServerSetup with PropsReset{ response.code should equal(403) response.body.toString contains (CanUseAccountFirehoseAtAnyBank.toString()) should be(true) } - + scenario("We will call the endpoint missing props ", VersionOfApi, ApiEndpoint1) { Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanUseAccountFirehoseAtAnyBank.toString) When("We send the request") @@ -65,4 +66,70 @@ class FirehoseTest extends V400ServerSetup with PropsReset{ } } + feature(s"test $ApiEndpoint2 version $VersionOfApi - Authorized access") { + scenario("We will call the endpoint with user credentials", VersionOfApi, ApiEndpoint1) { + setPropsValues("allow_account_firehose" -> "true") + + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanUseAccountFirehoseAtAnyBank.toString) + When("We send the request") + val request = (v4_0_0_Request /"management" / "banks" / testBankId1.value /"fast-firehose" / "accounts" ).GET <@ (user1) + val response = makeGetRequest(request) + Then("We should get a 200 and check the response body") + response.code should equal(200) + val responseAccounts =response.body.extract[FastFirehoseAccountsJsonV400].accounts + + { + val params = ("limit", "2") ::("offset", "0"):: Nil + val request = (v4_0_0_Request /"management" / "banks" / testBankId1.value /"fast-firehose" / "accounts" ).GET <@ (user1) + val response = makeGetRequest(request, params) + Then("We should get a 200 and check the response body") + response.code should equal(200) + val accounts = response.body.extract[FastFirehoseAccountsJsonV400].accounts + accounts.length shouldBe (2) + } + + { + val params = ("limit", "1") ::("offset", "0"):: Nil + val request = (v4_0_0_Request /"management" / "banks" / testBankId1.value /"fast-firehose" / "accounts" ).GET <@ (user1) < "true") + When("We send the request") + val request = (v4_0_0_Request /"management" / "banks" / testBankId1.value /"fast-firehose" / "accounts" ).GET <@ (user1) + val response = makeGetRequest(request) + Then("We should get a 403 and check the response body") + response.code should equal(403) + response.body.toString contains (CanUseAccountFirehoseAtAnyBank.toString()) should be(true) + } + + scenario("We will call the endpoint missing props ", VersionOfApi, ApiEndpoint1) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanUseAccountFirehoseAtAnyBank.toString) + When("We send the request") + val request = (v4_0_0_Request /"management" / "banks" / testBankId1.value /"fast-firehose" / "accounts" ).GET <@ (user1) + val response = makeGetRequest(request) + Then("We should get a 400 and check the response body") + response.code should equal(400) + response.body.toString contains (AccountFirehoseNotAllowedOnThisInstance) should be (true) + } + } + } diff --git a/obp-api/src/test/scala/code/api/v4_0_0/UserTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/UserTest.scala index adbc5274d..614563610 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/UserTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/UserTest.scala @@ -1,10 +1,14 @@ package code.api.v4_0_0 +import java.util.UUID + import code.api.util.APIUtil.OAuth._ import code.api.util.ApiRole.CanGetAnyUser -import code.api.util.ErrorMessages.{UserHasMissingRoles, UserNotLoggedIn} +import code.api.util.ErrorMessages.{UserHasMissingRoles, UserNotLoggedIn, attemptedToOpenAnEmptyBox} import code.api.v4_0_0.OBPAPI4_0_0.Implementations4_0_0 import code.entitlement.Entitlement +import code.model.UserX +import code.users.Users import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.model.ErrorMessage import com.openbankproject.commons.util.ApiVersion @@ -21,6 +25,9 @@ class UserTest extends V400ServerSetup { object VersionOfApi extends Tag(ApiVersion.v4_0_0.toString) object ApiEndpoint1 extends Tag(nameOf(Implementations4_0_0.getCurrentUserId)) object ApiEndpoint2 extends Tag(nameOf(Implementations4_0_0.getUserByUserId)) + object ApiEndpoint3 extends Tag(nameOf(Implementations4_0_0.getUsers)) + object ApiEndpoint4 extends Tag(nameOf(Implementations4_0_0.getUserByUsername)) + object ApiEndpoint5 extends Tag(nameOf(Implementations4_0_0.getUsersByEmail)) feature(s"test $ApiEndpoint1 version $VersionOfApi - Unauthorized access") { @@ -76,6 +83,106 @@ class UserTest extends V400ServerSetup { response400.body.extract[UserJsonV400].user_id should equal(resourceUser3.userId) } } + + feature(s"test $ApiEndpoint3 version $VersionOfApi - Unauthorized access") { + scenario("We will call the endpoint without user credentials", ApiEndpoint3, VersionOfApi) { + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "users").GET + val response400 = makeGetRequest(request400) + Then("We should get a 401") + response400.code should equal(401) + response400.body.extract[ErrorMessage].message should equal(UserNotLoggedIn) + } + } + feature(s"test $ApiEndpoint3 version $VersionOfApi - Authorized access") { + scenario("We will call the endpoint with user credentials but without a proper entitlement", ApiEndpoint3, VersionOfApi) { + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "users").GET <@(user1) + val response400 = makeGetRequest(request400) + Then("error should be " + UserHasMissingRoles + CanGetAnyUser) + response400.code should equal(403) + response400.body.extract[ErrorMessage].message should be (UserHasMissingRoles + CanGetAnyUser) + } + } + feature(s"test $ApiEndpoint3 version $VersionOfApi - Authorized access") { + scenario("We will call the endpoint with user credentials and a proper entitlement", ApiEndpoint3, VersionOfApi) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAnyUser.toString) + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "users").GET <@(user1) + val response400 = makeGetRequest(request400) + Then("We get successful response") + response400.code should equal(200) + response400.body.extract[UsersJsonV400] + } + } + + feature(s"test $ApiEndpoint4 version $VersionOfApi - Unauthorized access") { + scenario("We will call the endpoint without user credentials", ApiEndpoint4, VersionOfApi) { + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "users" / "username" / "USERNAME").GET + val response400 = makeGetRequest(request400) + Then("We should get a 401") + response400.code should equal(401) + response400.body.extract[ErrorMessage].message should equal(UserNotLoggedIn) + } + } + feature(s"test $ApiEndpoint4 version $VersionOfApi - Authorized access") { + scenario("We will call the endpoint with user credentials but without a proper entitlement", ApiEndpoint4, VersionOfApi) { + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "users" / "username" / "USERNAME").GET <@(user1) + val response400 = makeGetRequest(request400) + Then("error should be " + UserHasMissingRoles + CanGetAnyUser) + response400.code should equal(403) + response400.body.extract[ErrorMessage].message should be (UserHasMissingRoles + CanGetAnyUser) + } + } + feature(s"test $ApiEndpoint4 version $VersionOfApi - Authorized access") { + scenario("We will call the endpoint with user credentials and a proper entitlement", ApiEndpoint4, VersionOfApi) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAnyUser.toString) + val user = UserX.createResourceUser(defaultProvider, Some("user.name.1"), None, Some("user.name.1"), None, Some(UUID.randomUUID.toString), None).openOrThrowException(attemptedToOpenAnEmptyBox) + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "users" / "username" / user.idGivenByProvider).GET <@(user1) + val response400 = makeGetRequest(request400) + Then("We get successful response") + response400.code should equal(200) + response400.body.extract[UserJsonV400] + Users.users.vend.deleteResourceUser(user.id.get) + } + } + + feature(s"test $ApiEndpoint5 version $VersionOfApi - Unauthorized access") { + scenario("We will call the endpoint without user credentials", ApiEndpoint5, VersionOfApi) { + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "users" / "email" / "EMAIL" / "terminator").GET + val response400 = makeGetRequest(request400) + Then("We should get a 401") + response400.code should equal(401) + response400.body.extract[ErrorMessage].message should equal(UserNotLoggedIn) + } + } + feature(s"test $ApiEndpoint5 version $VersionOfApi - Authorized access") { + scenario("We will call the endpoint with user credentials but without a proper entitlement", ApiEndpoint5, VersionOfApi) { + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "users" / "email" / "EMAIL" / "terminator").GET <@(user1) + val response400 = makeGetRequest(request400) + Then("error should be " + UserHasMissingRoles + CanGetAnyUser) + response400.code should equal(403) + response400.body.extract[ErrorMessage].message should be (UserHasMissingRoles + CanGetAnyUser) + } + } + feature(s"test $ApiEndpoint5 version $VersionOfApi - Authorized access") { + scenario("We will call the endpoint with user credentials and a proper entitlement", ApiEndpoint5, VersionOfApi) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAnyUser.toString) + val user = UserX.createResourceUser(defaultProvider, Some("user.name.1"), None, Some("user.name.1"), Some("test@tesobe.com"), Some(UUID.randomUUID.toString), None).openOrThrowException(attemptedToOpenAnEmptyBox) + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "users" / "email" / user.emailAddress / "terminator").GET <@(user1) + val response400 = makeGetRequest(request400) + Then("We get successful response") + response400.code should equal(200) + response400.body.extract[UsersJsonV400] + Users.users.vend.deleteResourceUser(user.id.get) + } + } } diff --git a/obp-api/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala b/obp-api/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala index 79aac35ef..580a02a46 100644 --- a/obp-api/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala +++ b/obp-api/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala @@ -38,7 +38,7 @@ class BankAccountCreationListenerTest extends ServerSetup with DefaultConnectorT //need to create the user for the bank accout creation process to work def getTestUser() = Users.users.vend.getUserByProviderId(userProvider, userId).getOrElse { - Users.users.vend.createResourceUser(userProvider, Some(userId), None, None, None, None, None, None).openOrThrowException(attemptedToOpenAnEmptyBox) + Users.users.vend.createResourceUser(userProvider, Some(userId), None, None, None, None, None, None, None).openOrThrowException(attemptedToOpenAnEmptyBox) } val expectedBankId = "quxbank" diff --git a/obp-api/src/test/scala/code/setup/PropsProgrammatically.scala b/obp-api/src/test/scala/code/setup/PropsProgrammatically.scala new file mode 100644 index 000000000..6eb21025a --- /dev/null +++ b/obp-api/src/test/scala/code/setup/PropsProgrammatically.scala @@ -0,0 +1,23 @@ +package code.setup + +import net.liftweb.util.Props +import org.apache.commons.lang3.reflect.FieldUtils +import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, Suite} + + +/** + * Any unit test that extends this trait, have a chance to set new Props value, + * after each test rollback original Props values + */ +trait PropsProgrammatically { + + private def getLockedProviders = { + FieldUtils.readDeclaredField(Props, "net$liftweb$util$Props$$lockedProviders", true) + .asInstanceOf[List[Map[String, String]]] + } + + def setPropsValues(keyValues: (String, String)*): Unit = { + val newLockedProviders = keyValues.toMap :: getLockedProviders + FieldUtils.writeDeclaredField(Props, "net$liftweb$util$Props$$lockedProviders", newLockedProviders, true) + } +} diff --git a/obp-api/src/test/scala/code/setup/ServerSetup.scala b/obp-api/src/test/scala/code/setup/ServerSetup.scala index bb90bb9d2..2a77bc0ac 100644 --- a/obp-api/src/test/scala/code/setup/ServerSetup.scala +++ b/obp-api/src/test/scala/code/setup/ServerSetup.scala @@ -27,6 +27,8 @@ TESOBE (http://www.tesobe.com/) package code.setup +import java.net.URI + import _root_.net.liftweb.json.JsonAST.JObject import code.TestServer import code.api.util.APIUtil._ @@ -41,7 +43,14 @@ import org.scalatest._ trait ServerSetup extends FeatureSpec with SendServerRequests with BeforeAndAfterEach with GivenWhenThen with BeforeAndAfterAll - with Matchers with MdcLoggable with CustomJsonFormats{ + with Matchers with MdcLoggable with CustomJsonFormats with PropsReset{ + + setPropsValues("migration_scripts.execute_all" -> "true") + setPropsValues("migration_scripts.execute" -> "true") + setPropsValues("allow_dauth" -> "true") + setPropsValues("dauth.host" -> "127.0.0.1") + setPropsValues("jwt.token_secret"->"your-at-least-256-bit-secret-token") + setPropsValues("jwt.public_key_rsa" -> "src/test/resources/cert/public_dauth.pem") val server = TestServer def baseRequest = host(server.host, server.port) diff --git a/obp-api/src/test/scala/code/util/MappedClassNameTest.scala b/obp-api/src/test/scala/code/util/MappedClassNameTest.scala index fa0179e06..b047b6455 100644 --- a/obp-api/src/test/scala/code/util/MappedClassNameTest.scala +++ b/obp-api/src/test/scala/code/util/MappedClassNameTest.scala @@ -26,7 +26,6 @@ class MappedClassNameTest extends FeatureSpec { val oldMappedTypeNames = Set("code.transactionrequests.MappedTransactionRequest", "code.methodrouting.MethodRouting", "code.metadata.tags.MappedTag", - "code.yearlycustomercharges.MappedYearlyCharge", "code.model.Token", "code.transaction.MappedTransaction", "code.metadata.comments.MappedComment", diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/model/BankingModel.scala b/obp-commons/src/main/scala/com/openbankproject/commons/model/BankingModel.scala index 153605a6b..39db5f68d 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/model/BankingModel.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/model/BankingModel.scala @@ -372,6 +372,28 @@ case class BankAccountBalance( balanceType: String, ) +case class FirehoseAccountUser( + id : String, + provider : String, + displayName : String +) + +case class FastFirehoseAccount( + id: String, + bankId: String, + label: String, + number: String, + owners: String, + productCode: String, + balance: AmountOfMoney, + accountRoutings: String, + accountAttributes: String +) + +case class FastFirehoseAccounts( + accounts: List[FastFirehoseAccount] +) + case class AccountsBalances( accounts: List[AccountBalance], overallBalance: AmountOfMoney, diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala b/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala index 8dceecd5e..048be111e 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala @@ -802,7 +802,16 @@ case class Transaction( val accountId = thisAccount.accountId } -case class UserCommons(userPrimaryKey : UserPrimaryKey, userId: String,idGivenByProvider: String, provider : String, emailAddress : String, name : String, createdByConsentId: Option[String] = None, createdByUserInvitationId: Option[String] = None, isDeleted: Option[Boolean] = None) extends User +case class UserCommons(userPrimaryKey : UserPrimaryKey, + userId: String, + idGivenByProvider: String, + provider : String, + emailAddress : String, + name : String, + createdByConsentId: Option[String] = None, + createdByUserInvitationId: Option[String] = None, + isDeleted: Option[Boolean] = None, + lastMarketingAgreementSignedDate: Option[Date] = None) extends User case class InternalBasicUser( userId:String, diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/model/UserModel.scala b/obp-commons/src/main/scala/com/openbankproject/commons/model/UserModel.scala index bf6a26021..4452e3b3d 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/model/UserModel.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/model/UserModel.scala @@ -26,6 +26,8 @@ TESOBE (http://www.tesobe.com/) package com.openbankproject.commons.model +import java.util.Date + /** * An O-R mapped "User" class that includes first name, last name, password * @@ -66,6 +68,7 @@ trait User { def isOriginalUser = createdByConsentId.isEmpty def isConsentUser = createdByConsentId.nonEmpty def isDeleted: Option[Boolean] + def lastMarketingAgreementSignedDate: Option[Date] } case class UserPrimaryKey(val value : Long) { diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/util/ReflectUtils.scala b/obp-commons/src/main/scala/com/openbankproject/commons/util/ReflectUtils.scala index bda7df656..dc32cad3f 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/util/ReflectUtils.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/util/ReflectUtils.scala @@ -45,7 +45,7 @@ object ReflectUtils { def isFieldOrCallByPath(term: ru.TermSymbol) = { term.name.decodedName.toString.trim == fieldName && - (term.isVal || term.isVal || term.isLazy || (term.isMethod && term.asMethod.paramLists.isEmpty)) + (term.isVal || term.isLazy || (term.isMethod && term.asMethod.paramLists.isEmpty)) } val fields: Iterable[ru.TermSymbol] = tp.members.collect({ @@ -495,7 +495,7 @@ object ReflectUtils { def findMethod(obj: Any, methodName: String)(predicate: Map[String, ru.Type] => Boolean): Option[MethodSymbol] = findMethod(getType(obj), methodName)(predicate) def findMethodByArgs(tp: ru.Type, methodName: String, args: Any*): Option[ru.MethodSymbol] = findMethod(tp, methodName) { nameToType => - args.size == args.size && nameToType.values.zip(args).forall(it => isTypeOf(it._1, it._2)) + nameToType.values.zip(args).forall(it => isTypeOf(it._1, it._2)) } def findMethodByArgs(obj: Any, methodName: String, args: Any*): Option[ru.MethodSymbol] = findMethodByArgs(getType(obj), methodName, args:_*) diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/util/RequiredFieldValidation.scala b/obp-commons/src/main/scala/com/openbankproject/commons/util/RequiredFieldValidation.scala index 5d20bae5b..f531c9aaa 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/util/RequiredFieldValidation.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/util/RequiredFieldValidation.scala @@ -300,7 +300,7 @@ object RequiredFieldValidation { def getAnnotations(tp: Type): Iterable[RequiredArgs] = { def isField(symbol: TermSymbol): Boolean = - symbol.isVal || symbol.isVal || symbol.isLazy || (symbol.isMethod && symbol.asMethod.paramLists.isEmpty) + symbol.isVal || symbol.isLazy || (symbol.isMethod && symbol.asMethod.paramLists.isEmpty) // constructor's parameters and fields val members: Iterable[Symbol] = diff --git a/pom.xml b/pom.xml index a2b5e94db..072bd1809 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ 1.8.2 3.4.1 9.4.12.v20180830 - 2.6.2 + 2.15.0 2016.11-RC6-SNAPSHOT UTF-8 diff --git a/release_notes.md b/release_notes.md index acf52bb2c..a8a9ce2dc 100644 --- a/release_notes.md +++ b/release_notes.md @@ -3,6 +3,8 @@ ### Most recent changes at top of file ``` Date Commit Action +01/11/2021 03305d2b Added props: rest_connector_sends_x-sign_header, default is false +17/09/2021 e65cd51d Added props: webui_main_faq_external_link, default is obp static file: /main-faq.html 09/09/2021 65952225 Added props: webui_support_email, default is contact@openbankproject.com 02/09/2021 a826d908 Renamed Web UI props: webui_post_user_invitation_privacy_conditions_value => webui_privacy_policy