diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..42e30564 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,24 @@ +language: java +jdk: + - openjdk11 +script: mvn -f powerauth-data-adapter/pom.xml clean package +branches: + only: + - master + - coverity_scan +env: + global: + - secure: "ZeE+S5KU4rA0zta+udAFBIEETAqTg5SExaFtoNur/2DG79hcGGO/nC1VouwbwYnR81xNc3A5LniTsqwYjzFFstCRUPR7zjeYIq4B9v5bdNqdLiRvY/Re9Rk9f4hVf3039SnKGaEC+iCMzs/BxFW5+y68Oe1RKGh9yI0I2WKglkIw3NHLx+qI4HBhWwgso2USBNGGfEPOwJAR0940DxLywOxNFPUuMjYIFXY7Yx6Hko0L7BX+TRERmJuxK+cwoTWkFdmAzNIsau2WpSHN2/V+kIP8sr9eeUc33pY9ztW+oir3kteDhNjPeg9NA+FG6bGC8D8hPzNIMEED/OPs/Zj3G0oOLLTS/fjulSAiTAK6afkEyffNjVfh7dJQxvfGhHBJs1PDD0+a7sYogmf/eisxO6qYI+hRZMpkjjJL590Ovk6do9anzzmJ8lSSH6r8NVCANt2q5fnjp5BOOPFWGolWMvH2r699TiL3azBM1lWz3spvvk11DbtWqDJCOFmtD4SluTFAKuZjageQY69nPpB/w3Q+ThpYJqTNNrHD1i/b5daAi8aIGZud5vvwTUC5ItGzSGhjizTFxps7FOa77P//YIQhdirURvgIkuCf0A5vGBABuyeVOmz3pkx36sO4va1ZqNNYY9tbv5rdCBynU0KgmrOr6J3Q6IwMSxrc85ykSlI=" + +before_install: + - echo -n | openssl s_client -connect https://scan.coverity.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | sudo tee -a /etc/ssl/certs/ca- + +addons: + coverity_scan: + project: + name: "wultra/powerauth-webflow-customization" + description: "Build submitted via Travis CI" + notification_email: roman.strobl@wultra.com + build_command_prepend: "mvn -f powerauth-data-adapter/pom.xml clean" + build_command: "mvn -DskipTests=true -f powerauth-data-adapter/pom.xml compile" + branch_pattern: coverity_scan diff --git a/README.md b/README.md index 9ddb0888..82c85842 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ and other changes required for customizing Web Flow for clients. ## Documentation -For the most recent documentation and tutorials, please visit [PowerAuth Web Flow Customization Documentation](./docs/Home.md). +For the most recent documentation and tutorials, please visit [PowerAuth Web Flow Customization Documentation](https://developers.wultra.com/docs/develop/powerauth-webflow-customization/). ## License diff --git a/docs/Customizing-Web-Flow-Appearance.md b/docs/Customizing-Web-Flow-Appearance.md index 1074452e..e70ee4f6 100644 --- a/docs/Customizing-Web-Flow-Appearance.md +++ b/docs/Customizing-Web-Flow-Appearance.md @@ -8,7 +8,7 @@ Web Flow resources which can be customized are available in the ext-resources fo The general process of updating Web Flow resources: -- Clone project [powerauth-webflow-customization](https://github.com/wultra/powerauth-webflow-customization) from GitHub. +- Clone project [powerauth-webflow-customization](https://github.com/wultra/powerauth-webflow-customization#docucheck-keep-link) from GitHub. - Update Web Flow resources by overriding existing texts, CSS, fonts and images or by adding additional resources. - When deploying Web Flow, configure the following Spring Boot property: @@ -58,3 +58,53 @@ Additional fonts for Web Flow can be stored in `ext-resources/fonts` folder, see - [ext-resources/fonts](../ext-resources/fonts) After you make a copy of the `powerauth-webflow-customization` project, you can add new fonts to the folder `/path/to/your/ext-resources/fonts` and update the `customization.css` file (see above) to use the added fonts in Web Flow. + +## Customizing the OAuth 2.0 Consent Form + +The OAuth 2.0 consent form used by Web Flow can be customized by implementing following methods from Data Adapter interface: + +### Initialize Consent Form + +The [initConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L177) method is used to +allow to decide whether consent form should be displayed for given operation context. Based on values of parameters `userId`, `organizationId` +and `operationContext` a decision can be made whether to display the consent form or not. In case the consent form is always displayed, +return true in response unconditionally. + +### Create Consent Form +The [createConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L189) method is used to specify +the text of consent form and define options which are available in the options form. The consent form accepts consent text as HTML, scripting of the HTML is not allowed. +The language of the consent form is specified using parameter `lang`. Each option is identified using an identifier `id`. Individual options in the form can be set as required and their default value can be set. +The form can use parameters `userId`, `organizationId` and `operationContext` including `name`, `formData` and `applicationContext` to create a customized and personalized consent form for given +user, operation name, operation parameters and application which initiated the operation. + +The response should contain following data: +- `consentHtml` - localized HTML text of OAuth 2.0 consent for given operation and its context +- `options` - list of consent options which should be checked by the user with following parameters: + - `id` - identifier of the consent option + - `descriptionHtml` - localized HTML text for the description of the consent option + - `required` - whether the option must be checked in order to complete the operation + - `defaultValue` - default value of the option + - `value` - value specified by the user (not used yet) + +_Note that the consent texts do not use automatic resource localization because the HTML texts are expected to be complex and dynamically generated._ + +### Validate Consent Form +The [validateConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L203) method is used to validate the OAuth 2.0 consent form options +before the response is persisted. The identifiers of consent options match identifiers created in the `createConsentForm` step. The error messages produced by this method should +take into account language specified using parameter `lang`. + +The response should contain following data: +- `consentValidationPassed` - whether the consent validation passed and the operation can be completed +- `validationErrorMessage` - localized HTML text of error message for overall consent form validation used in case the consent validation failed +- `optionValidationResults` - result of validation for individual consent options: + - `id` - identifier of the consent option + - `validationPassed` - whether validation of the consent option passed + - `errorMessage` - localized HTML text of error message for consent option, in case validation of consent option value failed + +_Note that the texts of error messages do not use automatic resource localization because the HTML texts are expected to be complex and dynamically generated._ + +### Save Consent Form +The [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L215) method is used to save the OAuth 2.0 consent form options. +This method is called only when form validation done in `validateConsentForm` method successfully passes. The sample implementation prints the consent form option values into log. +It is expected that in the real implementation the consent option values are persisted in a database or any other persistent storage of consent options. + \ No newline at end of file diff --git a/docs/Deploying-Wildfly.md b/docs/Deploying-Wildfly.md new file mode 100644 index 00000000..aaf4ec57 --- /dev/null +++ b/docs/Deploying-Wildfly.md @@ -0,0 +1,93 @@ +# Deploying Data Adapter on JBoss / Wildfly + +## JBoss Deployment Descriptor + +Data Adapter contains the following configuration in `jboss-deployment-structure.xml` file for JBoss: + +``` + + + + + + + + + + + + + + +``` + +The deployment descriptor requires configuration of the `com.wultra.powerauth.data-adapter.conf` module. + +## JBoss Module for Data Adapter Configuration + +Create a new module in `PATH_TO_JBOSS/modules/system/layers/base/com/wultra/powerauth/data-adapter/conf/main`. + +The files described below should be added into this folder. + +### Main Module Configuration + +The `module.xml` configuration is used for module registration. It also adds resources from the module folder to classpath: +``` + + + + + + +``` + +### Logging Configuration + +Use the `logback.xml` file to configure logging, for example: +``` + + + + + + + + + ${LOG_FILE_DIR}/${LOG_FILE_NAME}-${INSTANCE_ID}.log + true + + ${LOG_FILE_DIR}/${LOG_FILE_NAME}-${INSTANCE_ID}-%d{yyyy-MM-dd}-%i.log + 10MB + 5 + 100MB + + + UTF-8 + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + +``` + +### Application Configuration + +The `application-ext.properties` file is used to override default configuration properties, for example: +``` +powerauth.dataAdapter.service.applicationEnvironment=TEST +``` + +Data Adapter Spring application uses the `ext` Spring profile which activates overriding of default properties by `application-ext.properties`. + +### Bouncy Castle Installation + +The Bouncy Castle module for JBoss / Wildfly needs to be enabled as a global module for Data Adapter. + +Follow the instructions in the [Installing Bouncy Castle](https://github.com/wultra/powerauth-server/blob/develop/docs/Installing-Bouncy-Castle.md) chapter of PowerAuth Server documentation. +Note that the instructions differ based on Java version and application server type. diff --git a/docs/Implementing-the-Data-Adapter-Interface.md b/docs/Implementing-the-Data-Adapter-Interface.md index 40a61835..d0230c66 100644 --- a/docs/Implementing-the-Data-Adapter-Interface.md +++ b/docs/Implementing-the-Data-Adapter-Interface.md @@ -1,19 +1,28 @@ # Implementing the Data Adapter Interface -Data Adapter is used for connecting Web Flow to client backend systems. It allows to interact with backends for user authentication, SMS authorization, read additional data required for the operation as well as notify client backend about operation changes. +Data Adapter is used for connecting Web Flow to client backend systems. It allows to interact with backends for user authentication, SMS authorization, read additional data required for the operation as well as notify client backend about operation changes. +Furthermore, the Data Adapter can be used to customize text and options for the OAuth 2.0 consent screen. ## DataAdapter Interface The interface methods are defined in the [DataAdapter interface](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java): -- [authenticateUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L43) - perform user authentication with remote backend based on provided credentials -- [fetchUserDetail](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L52) - retrieve user details for given user ID -- [decorateFormData](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L62) - retrieve operation form data and decorate it -- [formDataChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L71) - method is called when operation form data changes to allow notification of client backends -- [operationChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L80) - method is called when operation status changes to allow notification of client backends -- [generateAuthorizationCode](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L89) - generate authorization code for authorization SMS message -- [generateSMSText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L100) - generate SMS text for authorization SMS message -- [sendAuthorizationSMS](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L110) - send authorization SMS message +- [lookupUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L39) - lookup user account based on username +- [authenticateUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L51) - perform user authentication with remote backend based on provided credentials +- [fetchUserDetail](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L62) - retrieve user details for given user ID +- [decorateFormData](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L73) - retrieve operation form data and decorate it +- [formDataChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L83) - method is called when operation form data changes to allow notification of client backends +- [operationChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L93) - method is called when operation status changes to allow notification of client backends +- [createAuthorizationSms](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L105) - create and send authorization SMS message +- [generateAuthorizationCode](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L115) - generate authorization code for authorization SMS message +- [generateSmsText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L127) - generate SMS text for authorization SMS message +- [sendAuthorizationSms](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L138) - send authorization SMS message +- [verifyAuthorizationSms](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L151) - verify authorization code from SMS message +- [verifyAuthorizationSmsAndPassword](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L166) - verify authorization code from SMS message together with verifying user password +- [initConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L177) - initialize the OAuth 2.0 consent form and decide whether consent form should be displayed +- [createConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L189) - create the OAuth 2.0 consent form text and options +- [validateConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L203) - validate the OAuth 2.0 consent form options +- [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L215) - save the OAuth 2.0 consent form options selected by the user ## Customizing Data Adapter @@ -23,20 +32,28 @@ Following steps are required for customization of Data Adapter. Consider which of the following methods need to be implemented in your project: - - [authenticateUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L43) (optional) - implementation is required in case any Web Flow operation needs to authenticate the user using a username/password login form - - [fetchUserDetail](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L52) (required) - provides information about the user (user ID and name) for the OAuth 2.0 protocol - - [decorateFormData](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L62) (optional) - implementation is required in case any Web Flow operation form data needs to be updated after authentication (e.g. add information about user bank accounts) - - [formDataChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L71) (optional) - implementation is required in case the client backends need to be notified about user input during an operation - - [operationChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L80) (optional) - implementation is required in case the client backends need to be notified about operation status changes - - [generateAuthorizationCode](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L89) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization - - [generateSMSText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L100) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization - - [sendAuthorizationSMS](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L110) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization +- [lookupUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L39) - (required) - provides mapping of username to user ID which is used by other methods +- [authenticateUser](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L51) (optional) - implementation is required in case any Web Flow operation needs to authenticate the user using a username/password +- [fetchUserDetail](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L62) (required) - provides information about the user (user ID and name) for the OAuth 2.0 protocol +- [decorateFormData](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L73) (optional) - implementation is required in case any Web Flow operation form data needs to be updated after authentication (e.g. add information about user bank accounts) +- [formDataChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L83) (optional) - implementation is required in case the client backends need to be notified about user input during an operation +- [operationChangedNotification](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L93) (optional) - implementation is required in case the client backends need to be notified about operation status changes +- [createAuthorizationSms](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L105) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization +- [generateAuthorizationCode](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L115) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization +- [generateSmsText](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L127) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization +- [sendAuthorizationSms](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L138) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization +- [verifyAuthorizationSms](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L151) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization +- [verifyAuthorizationSmsAndPassword](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L166) (optional) - implementation is required in case any Web Flow operation needs to authorize the user using SMS authorization and password +- [initConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L177) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled +- [createConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L139) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled +- [validateConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L153) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled +- [saveConsentForm](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java#L165) (optional) - implementation is required in case the OAuth 2.0 consent step is enabled ### 2. Implement the `DataAdapter` Interface Implement the actual changes in Data Adapter so that it connects to an actual data source. - - Clone project [powerauth-webflow-customization](https://github.com/wultra/powerauth-webflow-customization) from GitHub. + - Clone project [powerauth-webflow-customization](https://github.com/wultra/powerauth-webflow-customization#docucheck-keep-link) from GitHub. - Update the `pom.xml` to add any required additional dependencies. - Create a proprietary client (+ client config) for your web services. - Implement the Data Adapter interface by providing your own implementation in the [DataAdapterService class](../powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java). You can override the sample implementation. diff --git a/docs/Home.md b/docs/Readme.md similarity index 100% rename from docs/Home.md rename to docs/Readme.md diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index f1fc987e..7a2f73b5 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -1,6 +1,7 @@ **Customizing Web Flow** -- [Home](./Home.md) +- [Home](./Readme.md) - [Customizing Web Flow Appearance](./Customizing-Web-Flow-Appearance.md) - [Implementing Data Adapter Interface](./Implementing-the-Data-Adapter-Interface.md) -- [Data Adapter REST API Reference](https://github.com/wultra/powerauth-webflow/blob/develop/docs/Data-Adapter-REST-API-Reference.md) \ No newline at end of file +- [Data Adapter REST API Reference](https://github.com/wultra/powerauth-webflow/blob/develop/docs/Data-Adapter-REST-API-Reference.md) +- [Deploy Web Flow Customization on JBoss / Wildfly](./Deploying-Wildfly.md) \ No newline at end of file diff --git a/ext-resources/css/base.css b/ext-resources/css/base.css deleted file mode 100644 index 58f5de72..00000000 --- a/ext-resources/css/base.css +++ /dev/null @@ -1,312 +0,0 @@ -body { - font-family: "Source Sans Pro", "Helvetica Neue", "Helvetica", sans-serif; - color: #777777; - font-size: 14pt; -} - -.background { - /* background: url('../images/background.png') 0 0 repeat-x;*/ - position: fixed; - top: 0; - left: 0; - z-index: -1; - width: 100%; - height: 100%; -} - -a { - color: #7FC000; - text-decoration: none; -} - -a:hover, a:active, a:focus { - color: #4c7400 !important; - text-decoration: none; -} - -.message-information { - color: #777777; - word-wrap: break-word; -} - -.message-success { - color: #4BA819; - word-wrap: break-word; -} - -.network-error { - width: 200px; - height: auto; - position: absolute; - left: 50%; - margin-left: -100px; - background-color: #383838; - color: #F0F0F0; - font-size: 20px; - padding: 10px; - text-align: center; - border-radius: 5px; - -webkit-box-shadow: 0px 0 24px -1px rgba(150, 150, 150, 1); - -moz-box-shadow: 0px 0px 24px -1px rgba(150, 150, 150, 1); - box-shadow: 0px 0px 24px -1px rgba(150, 150, 150, 1); -} - -.message-error { - color: #C0007F; - word-wrap: break-word; -} - -.font-small { - font-size: 12pt; -} - -.font-tiny { - font-size: 10pt; -} - -#logo { - margin-bottom: 40px; - height: 60px; - background-image: url("../images/logo.png"); - background-position: 50% 50%; - background-size: contain; - background-repeat: no-repeat; -} - -#page-wrap { - margin: 80px auto 40px auto; - max-width: 980px; -} - -#react { - margin-left: 10px; - margin-right: 10px; -} - -#home { - min-height: 400px; -} - -.content-wrap { - margin: 0 auto; - width: 100%; -} - -.btn-success { - background-color: #7FC000; - border: none; -} - -.btn-success:hover, .btn-success:active, .btn-success:focus { - background-color: #4c7400 !important; - color: white !important; - border: none; -} - -.image { - margin: 40px auto; - background-position: 50% 50%; - background-size: contain; - background-repeat: no-repeat; -} - -.image.mtoken { - background-image: url("../images/token.png"); - width: 120px; - height: 120px; - margin: 20px auto; -} - -.image-result { - margin: 40px auto; - width: 80px; - height: 80px; - background-position: 50% 50%; - background-size: contain; - background-repeat: no-repeat; -} - -.image-result.error { - background-image: url("../images/image-error.png"); -} - -.image-result.success { - background-image: url("../images/image-information.png"); -} - -#lang { - position: absolute; - top: 10px; - right: 10px; -} - -#operation .panel-body { - padding: 30px; -} - -#operation .operation-approve { - text-align: center; -} - -#operation .operation-approve h3 { - color: #7FC000; - font-size: 18pt; - margin-top: 0; - margin-bottom: 10px; -} - -#operation .operation-approve p { - color: #777777; - font-size: 14pt; -} - -#operation .key { - color: #777777; - word-wrap: break-word; -} - -#operation .value { - color: #333333; - word-wrap: break-word; -} - -#operation .col-xs-6.key { - text-align: left; -} - -#operation .col-xs-6.value { - text-align: right; -} - -#operation .col-xs-12 .key { - text-align: left; -} - -#operation .col-xs-12 { - text-align: left; -} - -#operation .col-xs-12 .value { - text-align: left; -} - -#operation .amount { - font-size: 16pt; -} - -#operation .heading { - font-size: 16pt; - color: #7FC000; -} - -#operation .attribute { - margin-top: 10px; - margin-bottom: 10px; -} - -#operation .auth-actions { - margin-top: 20px; -} - -#operation .buttons { - margin-top: 20px; -} - -#operation .btn { - width: 100%; -} - -/* Base styling */ - -.tint { - color: #7FC000; -} - -.strong { - font-weight: bold; -} - -/* React Select Fix */ - -.Select-input { - height: 68px; -} - -.Select-placeholder { - line-height: 68px -} - -/* LOGIN */ - -#login .panel-body { - padding: 30px; -} - -#login .panel-body .title { - color: #7FC000; -} - -#login .buttons { - margin-top: 40px; -} - -#login .btn { - width: 100%; -} - -.panel { - border-radius: 20px; - box-shadow: 0 0 12px rgba(0, 0, 0, 0.20); -} - -.title { - font-size: 16pt; - margin: 10px 10px 20px 10px; -} - -.alert { - font-size: 12pt; - text-align: left; - width: 100%; -} - -.alert-form { - margin: 0 0 15px 0; - padding: 10px 15px 10px 15px; -} - -.alert-field { - margin: 0; - padding: 2px 15px 2px 15px; -} - -.party-info-wrapper { - background-color: #FAFAFA; - border-radius: 5px; - padding: 10px; -} - -.party-info-wrapper * { - margin: 0; -} - -.party-info-logo-wrapper { - padding: 10px; -} - -.party-info-logo { - width: 100%; -} - -.party-info-name { - font-size: 16pt; - font-weight: bold; -} - -.party-info-description { - font-size: 14pt; -} - -.party-info-link { - font-size: 12pt; -} \ No newline at end of file diff --git a/ext-resources/css/bootstrap.min.css b/ext-resources/css/bootstrap.min.css deleted file mode 100644 index 4cf729e4..00000000 --- a/ext-resources/css/bootstrap.min.css +++ /dev/null @@ -1,6 +0,0 @@ -/*! - * Bootstrap v3.3.6 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;filter:alpha(opacity=0);opacity:0;line-break:auto}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);line-break:auto}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} -/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/ext-resources/css/customization.css b/ext-resources/css/customization.css deleted file mode 100644 index 25c3b006..00000000 --- a/ext-resources/css/customization.css +++ /dev/null @@ -1 +0,0 @@ -/* Add customized CSS styles into this file. This CSS file is loaded after all other CSS files. */ \ No newline at end of file diff --git a/ext-resources/css/react-select.css b/ext-resources/css/react-select.css deleted file mode 100644 index c28af38f..00000000 --- a/ext-resources/css/react-select.css +++ /dev/null @@ -1,344 +0,0 @@ -.Select, .Select-control { - position: relative -} - -.Select, .Select div, .Select input, .Select span { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box -} - -.Select.is-disabled > .Select-control { - background-color: #f6f6f6 -} - -.Select.is-disabled .Select-arrow-zone { - cursor: default; - pointer-events: none -} - -.Select-control { - background-color: #fff; - border-radius: 4px; - border: 1px solid #ccc; - color: #333; - cursor: default; - display: table; - height: 36px; - outline: 0; - overflow: hidden; - width: 100% -} - -.is-searchable.is-focused:not(.is-open) > .Select-control, .is-searchable.is-open > .Select-control { - cursor: text -} - -.Select-placeholder, .Select-value { - left: 0; - position: absolute; - top: 0; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap -} - -.Select-control:hover { - box-shadow: 0 1px 0 rgba(0, 0, 0, .06) -} - -.is-open > .Select-control { - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; - background: #fff; - border-color: #b3b3b3 #ccc #d9d9d9 -} - -.is-open > .Select-control > .Select-arrow { - border-color: transparent transparent #999; - border-width: 0 5px 5px -} - -.is-focused:not(.is-open) > .Select-control { - border-color: #08c #0099e6 #0099e6; - box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1), 0 0 5px -1px rgba(0, 136, 204, .5) -} - -.Select-placeholder { - bottom: 0; - color: #aaa; - line-height: 34px; - padding-left: 10px; - padding-right: 10px; - right: 0 -} - -.has-value > .Select-control > .Select-placeholder { - color: #333 -} - -.Select-value { - color: #aaa; - padding: 8px 52px 8px 10px; - right: -15px -} - -.Select-arrow-zone, .Select-clear-zone, .Select-loading, .Select-loading-zone { - position: relative; - vertical-align: middle -} - -.has-value > .Select-control > .Select-value { - color: #333 -} - -.Select-input { - height: 34px; - padding-left: 10px; - padding-right: 10px; - vertical-align: middle; - visibility: hidden -} - -.Select-input > input { - background: none; - border: 0; - box-shadow: none; - cursor: default; - display: inline-block; - font-family: inherit; - font-size: inherit; - height: 34px; - margin: 0; - outline: 0; - padding: 0; - -webkit-appearance: none -} - -.is-focused .Select-input > input { - cursor: text -} - -.Select-control:not(.is-searchable) > .Select-input { - outline: 0 -} - -.Select-loading-zone { - cursor: pointer; - display: table-cell; - text-align: center; - width: 16px -} - -.Select-loading { - -webkit-animation: Select-animation-spin .4s infinite linear; - -o-animation: Select-animation-spin .4s infinite linear; - animation: Select-animation-spin .4s infinite linear; - width: 16px; - height: 16px; - box-sizing: border-box; - border-radius: 50%; - border: 2px solid #ccc; - border-right-color: #333; - display: inline-block -} - -.Select-clear-zone { - -webkit-animation: Select-animation-fadeIn .2s; - -o-animation: Select-animation-fadeIn .2s; - animation: Select-animation-fadeIn .2s; - color: #999; - cursor: pointer; - display: table-cell; - text-align: center; - width: 17px -} - -.Select-clear-zone:hover { - color: #D0021B -} - -.Select-clear { - display: inline-block; - font-size: 18px; - line-height: 1 -} - -.Select--multi .Select-clear-zone { - width: 17px -} - -.Select-arrow-zone { - cursor: pointer; - display: table-cell; - text-align: center; - width: 25px; - padding-right: 5px -} - -.Select-arrow { - border-color: #999 transparent transparent; - border-style: solid; - border-width: 5px 5px 2.5px; - display: inline-block; - height: 0; - width: 0 -} - -.Select-arrow-zone:hover > .Select-arrow, .is-open .Select-arrow { - border-top-color: #666 -} - -@-webkit-keyframes Select-animation-fadeIn { - from { - opacity: 0 - } - to { - opacity: 1 - } -} - -@keyframes Select-animation-fadeIn { - from { - opacity: 0 - } - to { - opacity: 1 - } -} - -.Select-menu-outer { - border-bottom-right-radius: 4px; - border-bottom-left-radius: 4px; - background-color: #fff; - border: 1px solid #ccc; - border-top-color: #e6e6e6; - box-shadow: 0 1px 0 rgba(0, 0, 0, .06); - box-sizing: border-box; - margin-top: -1px; - max-height: 200px; - position: absolute; - top: 100%; - width: 100%; - z-index: 1000; - -webkit-overflow-scrolling: touch -} - -.Select-menu { - max-height: 198px; - overflow-y: auto -} - -.Select-option { - box-sizing: border-box; - color: #666; - cursor: pointer; - display: block; - padding: 8px 10px -} - -.Select-option:last-child { - border-bottom-right-radius: 4px; - border-bottom-left-radius: 4px -} - -.Select-option.is-focused { - background-color: #f2f9fc; - color: #333 -} - -.Select-option.is-disabled { - color: #ccc; - cursor: not-allowed -} - -.Select-noresults, .Select-search-prompt, .Select-searching { - box-sizing: border-box; - color: #999; - cursor: default; - display: block; - padding: 8px 10px -} - -.Select--multi .Select-input { - vertical-align: middle; - margin-left: 0; - padding: 0 -} - -.Select--multi.has-value .Select-input, .Select-item { - margin-left: 5px -} - -.Select-item { - background-color: #f2f9fc; - border-radius: 2px; - border: 1px solid #c9e6f2; - color: #08c; - display: inline-block; - font-size: .9em; - margin-top: 5px; - vertical-align: top -} - -.Select-item-icon, .Select-item-label { - display: inline-block; - vertical-align: middle -} - -.Select-item-label { - border-bottom-right-radius: 2px; - border-top-right-radius: 2px; - cursor: default; - padding: 2px 5px -} - -.Select-item-label .Select-item-label__a { - color: #08c; - cursor: pointer -} - -.Select-item-icon { - cursor: pointer; - border-bottom-left-radius: 2px; - border-top-left-radius: 2px; - border-right: 1px solid #c9e6f2; - padding: 1px 5px 3px -} - -.Select-item-icon:focus, .Select-item-icon:hover { - background-color: #ddeff7; - color: #0077b3 -} - -.Select-item-icon:active { - background-color: #c9e6f2 -} - -.Select--multi.is-disabled .Select-item { - background-color: #f2f2f2; - border: 1px solid #d9d9d9; - color: #888 -} - -.Select--multi.is-disabled .Select-item-icon { - cursor: not-allowed; - border-right: 1px solid #d9d9d9 -} - -.Select--multi.is-disabled .Select-item-icon:active, .Select--multi.is-disabled .Select-item-icon:focus, .Select--multi.is-disabled .Select-item-icon:hover { - background-color: #f2f2f2 -} - -@keyframes Select-animation-spin { - to { - transform: rotate(1turn) - } -} - -@-webkit-keyframes Select-animation-spin { - to { - -webkit-transform: rotate(1turn) - } -} \ No newline at end of file diff --git a/ext-resources/fonts/glyphicons-halflings-regular.eot b/ext-resources/fonts/glyphicons-halflings-regular.eot deleted file mode 100644 index b93a4953..00000000 Binary files a/ext-resources/fonts/glyphicons-halflings-regular.eot and /dev/null differ diff --git a/ext-resources/fonts/glyphicons-halflings-regular.svg b/ext-resources/fonts/glyphicons-halflings-regular.svg deleted file mode 100644 index 94fb5490..00000000 --- a/ext-resources/fonts/glyphicons-halflings-regular.svg +++ /dev/null @@ -1,288 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ext-resources/fonts/glyphicons-halflings-regular.ttf b/ext-resources/fonts/glyphicons-halflings-regular.ttf deleted file mode 100644 index 1413fc60..00000000 Binary files a/ext-resources/fonts/glyphicons-halflings-regular.ttf and /dev/null differ diff --git a/ext-resources/fonts/glyphicons-halflings-regular.woff b/ext-resources/fonts/glyphicons-halflings-regular.woff deleted file mode 100644 index 9e612858..00000000 Binary files a/ext-resources/fonts/glyphicons-halflings-regular.woff and /dev/null differ diff --git a/ext-resources/fonts/glyphicons-halflings-regular.woff2 b/ext-resources/fonts/glyphicons-halflings-regular.woff2 deleted file mode 100644 index 64539b54..00000000 Binary files a/ext-resources/fonts/glyphicons-halflings-regular.woff2 and /dev/null differ diff --git a/ext-resources/images/image-error.png b/ext-resources/images/image-error.png deleted file mode 100644 index 19f3a783..00000000 Binary files a/ext-resources/images/image-error.png and /dev/null differ diff --git a/ext-resources/images/image-information.png b/ext-resources/images/image-information.png deleted file mode 100644 index ada3be22..00000000 Binary files a/ext-resources/images/image-information.png and /dev/null differ diff --git a/ext-resources/images/logo.png b/ext-resources/images/logo.png deleted file mode 100644 index 6ee5bced..00000000 Binary files a/ext-resources/images/logo.png and /dev/null differ diff --git a/ext-resources/images/token.png b/ext-resources/images/token.png deleted file mode 100644 index 98140fa0..00000000 Binary files a/ext-resources/images/token.png and /dev/null differ diff --git a/ext-resources/messages_cs.properties b/ext-resources/messages_cs.properties deleted file mode 100644 index fcf7d364..00000000 --- a/ext-resources/messages_cs.properties +++ /dev/null @@ -1,119 +0,0 @@ -error.authPage=Nelze zobrazit autentizační stránku. -error.sessionExpired=Vaše relace byla ukončena z důvodu delší nečinnosti. -error.sessionTerminated=Relace byla ukončena z důvodu delší nečinnosti. -error.invalidRequest=Došlo k chybě při vykonávání požadavku. -error.noAuthMethod=Nebyla nalezena žádná vhodná autentizační metoda. -error.remote=Nastala chyba při komunikaci se vzdáleným systémem. -error.unknown=Nastala neznámá chyba. -login.signIn=Přihlásit -login.cancel=Zrušit -login.loginNumber=Přihlašovací číslo -login.password=Heslo -login.pleaseLogIn=Přihlašte se prosím -login.authenticationFailed=Přihlášení se nezdařilo. -login.authenticationBlocked=Byl překročen maximální počet pokusů pro přihlášení. Váš účet byl proto dočasně zablokován. -login.username.empty=Vyplňte vaše přihlašovací číslo. -login.password.empty=Vyplňte heslo. -login.username.empty\ login.password.empty=Vyplňte přihlašovací číslo a heslo. -login.username.long=Přihlašovací číslo je příliš dlouhé. -login.password.long=Heslo je příliš dlouhé. -login.username.long\ login.password.long=Přihlašovací číslo i heslo jsou příliš dlouhá. -login.password.empty\ login.username.long=Přihlašovací číslo je příliš dlouhé. Vyplňte prosím heslo. -login.username.empty\ login.password.long=Vyplňte přihlašovací číslo. Heslo je příliš dlouhé. -login.type.unsupported=Nepodporovaný typ autentizace. -operationContext.missing=Není dostupný kontext operace. -operationConfig.missing=Chybí konfigurace operace. -operationData.invalid=Operace obsahuje chybná data. -operation.confirm=Potvrdit -operation.cancel=Zrušit -authentication.success=Operace byla úspěšně potvrzena. -authentication.fail=Nepodařilo se potvrdit operaci. -authentication.attemptsRemaining=Zbývá pokusů: -authentication.maxAttemptsExceeded=Byl překročen maximální počet pokusů pro přihlášení. -authentication.authenticationBlocked=Byl překročen maximální počet pokusů pro přihlášení. Váš účet byl proto dočasně zablokován. -authorization.success=Úspěšně jsme vás ověřili. -authorization.fail=Nepodařilo se ověřit uživatele. -message.redirect=Za okamžik proběhne přesměrování. -message.networkError=Není dostupné připojení k Internetu. -message.token.confirm=Potvrďte prosím operaci na vašem mobilním zařízení. -message.token.offline=Nemáte na mobilním zařízení data a nepřišla vám notifikace? Nevadí. -message.token.offline.link=Potvrďte platbu ověřením přes QR kód -message.sms.confirm=Potvrďte operaci pomocí SMS klíče. -method.usernamePassword=Přihlášení -method.showOperationDetail=Detail operace -method.powerauthToken=Mobilní aplikace -method.smsKey=Autorizační SMS kód -operation.timeout=Vypršel časový limit pro potvrzení operace. -operation.canceled=Operace byla zrušena uživatelem. -operation.notAvailable=Operace již není dostupná. -operation.alreadyFailed=Operace již selhala. -operation.noMethod=Operaci nelze ověřit, protože není k dispozici žádná vhodná autentizační metoda. -operation.confirmationTextChoice=Vyberte způsob potvrzení -operation.confirmationText=Potvrďte operaci: -operation.methodSelectionOr=nebo -operation.invalidChosenMethod=Vybraná autentizační metoda není platná. -operation.missingHistory=Není dostupná historie operace. -operation.methodNotAvailable=Autentizační metoda není dostupná. -operation.interrupted=Tato operace byla přerušena, protože došlo k vytvoření novější operace. -canceled.unknown=Operace byla zrušena z neznámého důvodu. -canceled.incorrectData=Operace byla zrušena, protože obsahovala nesprávná data. -canceled.unexpectedOperation=Operace byla zrušena, protože nebyla očekávána. -smsAuthorization.invalidMessage=Nalezena chybná zpráva při ověřování operace pomocí SMS. -smsAuthorization.invalidCode=Nalezen chybný autentizační kód při ověřování operace pomocí SMS. -smsAuthorization.expired=Zpráva pro ověření pomocí SMS již není aktuální. -smsAuthorization.alreadyVerified=Zpráva pro ověření pomocí SMS již byla použita. -smsAuthorization.maxAttemptsExceeded=Byl překročen maximální počet pokusů o ověření pomocí SMS zprávy. -smsAuthorization.failed=Ověření pomocí SMS klíče se nezdařilo. -smsAuthorization.userId.empty=Ověření pomocí SMS klíče selhalo z důvodu chybějící hodnoty userId. -smsAuthorization.operationName.empty=Ověření pomocí SMS klíče selhalo z důvodu chybějícího názvu operace. -smsAuthorization.userId.long=Ověření pomocí SMS klíče selhalo z důvodu příliš dlouhé hodnoty userId. -smsAuthorization.operationName.long=Ověření pomocí SMS klíče selhalo z důvodu příliš dlouhého názvu operace. -smsAuthorization.amount.empty=Ověření pomocí SMS klíče selhalo z důvodu chybějící částky. -smsAuthorization.amount.invalid=Ověření pomocí SMS klíče selhalo z důvodu špatného formátu částky. -smsAuthorization.currency.empty=Ověření pomocí SMS klíče selhalo z důvodu chybějící měny. -smsAuthorization.account.empty=Ověření pomocí SMS klíče selhalo z důvodu chybějícího účtu. -smsAuthorization.authCodeText=Zadejte autorizační kód,\n který jsme vám poslali v SMS zprávě. -operationReview.bankAccountsMissing=Operace selhala, protože chybí informace o bankovních účtech. -operationReview.bankAccount.balance=Zůstatek: -offlineMode.instructions=V mobilní aplikaci\nzvolte menu 'Operace k potvrzení', následně 'Ověření přes QR kód' a načtěte tento QR kód: -offlineMode.authCodeText=Opište číselný kód z mobilní aplikace -offlineMode.invalidAuthCode=Chybný ověřovací kód. -offlineMode.device=Mobilní zařízení pro autorizaci -offlineMode.invalidData=Chybná data pro podpis. -offlineMode.noActivation=Nebylo nalezené žádné odpovídající mobilní zařízení. -offlineMode.invalidActivation=Mobilního zařízení je chybně nastavené. -offlineMode.activationNotActive=Mobilní zařízení není nastaveno pro potvrzování operací. -offlineMode.disabled=Offline mód není dostupný. -pushMessage.fail=Nepodařilo se odeslat push notifikaci. -pushMessage.noActivation=Nepodařilo se odeslat push notifikaci, protože nebyla nalezena aktivace mobilního zařízení. -pushMessage.activationNotActive=Nepodařilo se odeslat push notifikaci, protože mobilní zařízení není aktivováno. -pushMessage.noApplication=Nepodařilo se odeslat push notifikaci kvůli chybnému nastavení aplikace. -push.confirmOperation=Potvrďte operaci -push.data=Data: {0} - -# Localize operations here -login.title=Přihlášení -login.greeting=Dobrý den,\npřihlašte se, prosím. -login.summary=Potvrďte prosím přihlášení uživatele. -operation.title=Potvrzení platby -operation.greeting=Dobrý den,\nprosíme o potvrzení následující platby: -operation.summary=Potvrďte platbu {operation.amount} {operation.currency} na účet {operation.account}. -operation.amount=Částka -operation.bankAccountChoice=Z vašeho účtu -operation.account=Na účet -operation.currency=Měna -operation.note=Poznámka -operation.dueDate=Datum splatnosti -operation.partyInfo=Aplikace - -# Browser support -browser.unsupported=Váš prohlížeč není podporován. Prosím použijte novější verzi prohlížeče. - -# Currencies -currency.pattern=###0.00 -currency.USD.name=USD -currency.EUR.name=EUR -currency.CZK.name=Kč - -# Party info -partyInfo.websiteLink=Více informací diff --git a/ext-resources/messages_en.properties b/ext-resources/messages_en.properties deleted file mode 100644 index d047a258..00000000 --- a/ext-resources/messages_en.properties +++ /dev/null @@ -1,119 +0,0 @@ -error.authPage=Unable to display authentication page. -error.sessionExpired=Your session has expired due to inactivity. -error.sessionTerminated=Session has been terminated due to inactivity. -error.invalidRequest=Invalid request. -error.noAuthMethod=No authentication method is available to serve the request. -error.remote=Error occurred during communication with the remote system. -error.unknown=Unknown error occurred. -login.signIn=Sign In -login.cancel=Cancel -login.loginNumber=Login number -login.password=Password -login.pleaseLogIn=Please sign in -login.authenticationFailed=User authentication failed. -login.authenticationBlocked=The maximum number of authentication attempts has been exceeded. Your account was blocked temporarily. -login.username.empty=Fill in the login number. -login.password.empty=Fill in the password. -login.username.empty\ login.password.empty=Fill in the login number and password. -login.username.long=Supplied login number is too long. -login.password.long=Supplied password is too long. -login.username.long\ login.password.long=Supplied login number and password are too long. -login.password.empty\ login.username.long=Supplied login number is too long. Fill in the password. -login.username.empty\ login.password.long=Fill in the login number. Supplied password is too long. -login.type.unsupported=Unsupported authentication type. -operationContext.missing=Operation context is not available. -operationConfig.missing=Operation is not configured. -operationData.invalid=Operation contains invalid data. -operation.confirm=Confirm -operation.cancel=Cancel -authentication.success=Operation was successfully authorized. -authentication.fail=Authorization failed. -authentication.attemptsRemaining=Remaining attempts: -authentication.maxAttemptsExceeded=The maximum number of authentication attempts has been exceeded. -authentication.authenticationBlocked=The maximum number of authentication attempts has been exceeded. Your account was blocked temporarily. -authorization.success=Authorization succeeded. -authorization.fail=Authorization failed. -message.redirect=You will be redirected back in a moment. -message.networkError=Internet connection is not available. -message.token.confirm=Please confirm the operation on your mobile device. -message.token.offline=No data on the mobile device? No problem. -message.token.offline.link=Confirm the operation via the QR code. -message.sms.confirm=Please authorize the operation using the SMS key. -method.usernamePassword=Login -method.showOperationDetail=Operation Detail -method.powerauthToken=Mobile Application -method.smsKey=SMS Key Authorization -operation.timeout=Operation has timed out. -operation.canceled=Operation was canceled by the user. -operation.notAvailable=Operation is no longer available. -operation.alreadyFailed=Operation has already failed. -operation.confirmationTextChoice=Choose the confirmation method -operation.confirmationText=Confirm the operation: -operation.methodSelectionOr=or -operation.noMethod=Operation cannot be confirmed, because there is no authorization method available. -operation.invalidChosenMethod=Chosen authentication method is not valid. -operation.missingHistory=Operation history is not available. -operation.methodNotAvailable=Authentication method is not available. -operation.interrupted=This operation has been interrupted by a newer operation. -canceled.unknown=Operation has been canceled due to an unknown reason. -canceled.incorrectData=Operation has been canceled because it contained incorrect data. -canceled.unexpectedOperation=Operation has been canceled because it was not expected. -smsAuthorization.invalidMessage=Invalid message sent while authorizing operation using SMS. -smsAuthorization.invalidCode=Invalid authentication code sent while authorizing operation using SMS. -smsAuthorization.expired=SMS authorization message is already expired. -smsAuthorization.alreadyVerified=SMS authorization message has already been used. -smsAuthorization.maxAttemptsExceeded=SMS authorization code has been rejected because maximum number of tries has been exceeded. -smsAuthorization.failed=SMS authorization failed. -smsAuthorization.userId.empty=SMS authorization failed due to missing userId value. -smsAuthorization.operationName.empty=SMS authorization failed due to missing operation name. -smsAuthorization.userId.long=SMS authorization failed due to too long userId value. -smsAuthorization.operationName.long=SMS authorization failed due to too long operation name. -smsAuthorization.amount.empty=SMS authorization failed due to missing amount. -smsAuthorization.amount.invalid=SMS authorization failed due to invalid format of amount. -smsAuthorization.currency.empty=SMS authorization failed due to missing currency. -smsAuthorization.account.empty=SMS authorization failed due to missing account. -smsAuthorization.authCodeText=Enter authorization code we sent you in SMS message. -operationReview.bankAccountsMissing=Operation failed because bank account details are not available. -operationReview.bankAccount.balance=Balance: -offlineMode.instructions=Choose 'Pending operations' menu, 'QR Code Verification' in mobile app and scan this QR code: -offlineMode.authCodeText=Retype numeric code shown in mobile application -offlineMode.invalidAuthCode=Invalid authorization code. -offlineMode.device=Mobile device for authorization -offlineMode.invalidData=Invalid signature data. -offlineMode.noActivation=Mobile device activation is missing. -offlineMode.invalidActivation=Mobile device activation is invalid. -offlineMode.activationNotActive=Mobile device is not activated. -offlineMode.disabled=Offline mode is not available. -pushMessage.fail=Push notification delivery failed. -pushMessage.noActivation=Push notification delivery failed because of missing mobile device activation. -pushMessage.activationNotActive=Push notification delivery failed because mobile device is not activated. -pushMessage.noApplication=Push notification delivery failed because of invalid application configuration. -push.confirmOperation=Confirm operation -push.data=Data: {0} - -# Localize operations here -login.title=Login -login.greeting=Hello,\nplease sign in. -login.summary=Please confirm the login attempt. -operation.title=Confirm Payment -operation.greeting=Hello,\nplease confirm following payment: -operation.summary=Hello, please confirm payment {operation.amount} {operation.currency} to account {operation.account}. -operation.amount=Amount -operation.bankAccountChoice=From Your Account -operation.account=To Account -operation.currency=Currency -operation.note=Note -operation.dueDate=Due Date -operation.partyInfo=Application - -# Browser support -browser.unsupported=Your web browser is not supported. Please switch to a newer version of a web browser. - -# Currencies -currency.pattern=#,##0.00 -currency.USD.name=USD -currency.EUR.name=EUR -currency.CZK.name=CZK - -# Party info -partyInfo.websiteLink=More information diff --git a/powerauth-data-adapter/pom.xml b/powerauth-data-adapter/pom.xml index e6851e67..825ef93a 100644 --- a/powerauth-data-adapter/pom.xml +++ b/powerauth-data-adapter/pom.xml @@ -5,7 +5,7 @@ powerauth-data-adapter io.getlime.security - 0.21.0 + 0.23.0 war powerauth-data-adapter @@ -14,8 +14,8 @@ org.springframework.boot spring-boot-starter-parent - 2.0.8.RELEASE - + 2.2.1.RELEASE + 2018 @@ -77,6 +77,10 @@ org.springframework.boot spring-boot-starter-data-jpa + + org.springframework.boot + spring-boot-starter-actuator + @@ -89,24 +93,37 @@ io.getlime.security powerauth-data-adapter-model - 0.21.0 + 0.23.0 io.getlime.security powerauth-java-crypto - 0.21.0 + 0.23.0 com.fasterxml.jackson.datatype jackson-datatype-joda - 2.9.8 + 2.10.0 org.bouncycastle bcprov-jdk15on - 1.60 + 1.64 + provided + + + + + javax.xml.bind + jaxb-api + 2.3.1 + + + org.glassfish.jaxb + jaxb-runtime + 2.3.1 @@ -120,6 +137,12 @@ springfox-swagger-ui 2.9.2 + + + com.google.guava + guava + 28.1-jre + diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java index 93df7943..02a5597e 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/api/DataAdapter.java @@ -16,12 +16,13 @@ package io.getlime.security.powerauth.app.dataadapter.api; import io.getlime.security.powerauth.app.dataadapter.exception.*; -import io.getlime.security.powerauth.lib.dataadapter.model.entity.AuthorizationCode; -import io.getlime.security.powerauth.lib.dataadapter.model.entity.FormDataChange; -import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationChange; -import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; -import io.getlime.security.powerauth.lib.dataadapter.model.response.DecorateOperationFormDataResponse; -import io.getlime.security.powerauth.lib.dataadapter.model.response.UserDetailResponse; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.*; +import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.AccountStatus; +import io.getlime.security.powerauth.lib.dataadapter.model.request.AfsRequestParameters; +import io.getlime.security.powerauth.lib.dataadapter.model.response.*; + +import java.util.List; +import java.util.Map; /** * Interface defines methods which should be implemented for integration of Web Flow with 3rd parties. @@ -30,83 +31,175 @@ */ public interface DataAdapter { + /** + * Lookup user account - map username to user ID. + * @param username Username which user uses for authentication. + * @param organizationId Organization ID for this request. + * @param operationContext Operation context. + * @return Detail about the user. + * @throws DataAdapterRemoteException Thrown when remote communication fails. + * @throws UserNotFoundException Thrown when user does not exist. + */ + UserDetailResponse lookupUser(String username, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, UserNotFoundException; + /** * Authenticate user using provided credentials. - * - * @param username Username for user authentication. + * @param userId User ID for user authentication. * @param password Password for user authentication. + * @param authenticationContext Authentication context. + * @param organizationId Organization ID. * @param operationContext Operation context. - * @return UserDetailResponse Response with user details. + * @return User authentication result. * @throws DataAdapterRemoteException Thrown when remote communication fails. - * @throws AuthenticationFailedException Thrown when authentication fails. */ - UserDetailResponse authenticateUser(String username, String password, OperationContext operationContext) throws DataAdapterRemoteException, AuthenticationFailedException; + UserAuthenticationResponse authenticateUser(String userId, String password, AuthenticationContext authenticationContext, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException; /** * Fetch user detail for given user. * @param userId User ID. + * @param organizationId Organization ID. + * @param operationContext Operation context which can be null in case request is initiated outside of operation scope. * @return Response with user details. * @throws DataAdapterRemoteException Thrown when remote communication fails. * @throws UserNotFoundException Thrown when user does not exist. */ - UserDetailResponse fetchUserDetail(String userId) throws DataAdapterRemoteException, UserNotFoundException; + UserDetailResponse fetchUserDetail(String userId, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, UserNotFoundException; /** * Decorate operation form data. * @param userId User ID. + * @param organizationId Organization ID. * @param operationContext Operation context. * @return Response with decorated operation form data * @throws DataAdapterRemoteException Thrown when remote communication fails. * @throws UserNotFoundException Thrown when user does not exist. */ - DecorateOperationFormDataResponse decorateFormData(String userId, OperationContext operationContext) throws DataAdapterRemoteException, UserNotFoundException; + DecorateOperationFormDataResponse decorateFormData(String userId, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, UserNotFoundException; /** * Receive notification about form data change. * @param userId User ID. + * @param organizationId Organization ID. * @param formDataChange Form data change. * @param operationContext Operation context. * @throws DataAdapterRemoteException Thrown when remote communication fails. */ - void formDataChangedNotification(String userId, FormDataChange formDataChange, OperationContext operationContext) throws DataAdapterRemoteException; + void formDataChangedNotification(String userId, String organizationId, FormDataChange formDataChange, OperationContext operationContext) throws DataAdapterRemoteException; /** * Receive notification about operation change. * @param userId User ID. + * @param organizationId Organization ID. * @param operationChange Operation change. * @param operationContext Operation context. * @throws DataAdapterRemoteException Thrown when remote communication fails. */ - void operationChangedNotification(String userId, OperationChange operationChange, OperationContext operationContext) throws DataAdapterRemoteException; + void operationChangedNotification(String userId, String organizationId, OperationChange operationChange, OperationContext operationContext) throws DataAdapterRemoteException; /** - * Generate authorization code for SMS authorization. + * Create authorization SMS message and send it. * @param userId User ID. + * @param organizationId Organization ID. + * @param accountStatus User account status. * @param operationContext Operation context. - * @return Authorization code. + * @param lang Language for localization. + * @return Message ID. * @throws InvalidOperationContextException Thrown when operation context is invalid. + * @throws DataAdapterRemoteException Thrown when remote communication fails or SMS message could not be delivered. */ - AuthorizationCode generateAuthorizationCode(String userId, OperationContext operationContext) throws InvalidOperationContextException; + CreateSmsAuthorizationResponse createAndSendAuthorizationSms(String userId, String organizationId, AccountStatus accountStatus, OperationContext operationContext, String lang) throws InvalidOperationContextException, DataAdapterRemoteException; /** - * Generate text for SMS authorization. + * Verify authorization code from SMS message. * @param userId User ID. + * @param organizationId Organization ID. + * @param accountStatus Current user account status. + * @param messageId Message ID. + * @param authorizationCode Authorization code. * @param operationContext Operation context. + * @return SMS authorization code verification response. + * @throws DataAdapterRemoteException Thrown when remote communication fails. + * @throws InvalidOperationContextException Thrown when operation context is invalid. + */ + VerifySmsAuthorizationResponse verifyAuthorizationSms(String userId, String organizationId, AccountStatus accountStatus, String messageId, String authorizationCode, OperationContext operationContext) throws DataAdapterRemoteException, InvalidOperationContextException; + + /** + * Verify authorization code from SMS message together with user password. + * @param userId User ID. + * @param organizationId Organization ID. + * @param accountStatus Current user account status. + * @param messageId Message ID. * @param authorizationCode Authorization code. - * @param lang Language for localization. - * @return Generated SMS text with OTP authorization code. + * @param operationContext Operation context. + * @param authenticationContext Authentication context. + * @param password User password. + * @return SMS authorization code and password verification response. + * @throws DataAdapterRemoteException Thrown when remote communication fails. * @throws InvalidOperationContextException Thrown when operation context is invalid. */ - String generateSMSText(String userId, OperationContext operationContext, AuthorizationCode authorizationCode, String lang) throws InvalidOperationContextException; + VerifySmsAndPasswordResponse verifyAuthorizationSmsAndPassword(String userId, String organizationId, AccountStatus accountStatus, String messageId, String authorizationCode, OperationContext operationContext, AuthenticationContext authenticationContext, String password) throws DataAdapterRemoteException, InvalidOperationContextException; /** - * Send an authorization SMS with generated OTP. + * Decide whether OAuth 2.0 consent form should be displayed based on operation context. * @param userId User ID. - * @param messageText Text of SMS message. + * @param organizationId Organization ID. * @param operationContext Operation context. + * @return Response with information whether consent form should be displayed. * @throws DataAdapterRemoteException Thrown when remote communication fails. - * @throws SMSAuthorizationFailedException Thrown when message could not be created. + * @throws InvalidOperationContextException Thrown when operation context is invalid. + */ + InitConsentFormResponse initConsentForm(String userId, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, InvalidOperationContextException; + + /** + * Create OAuth 2.0 consent form - prepare HTML text of consent form and add form options. + * @param userId User ID. + * @param organizationId Organization ID. + * @param operationContext Operation context. + * @param lang Language to use for the text of the consent form. + * @return Consent form contents with HTML text and form options. + * @throws DataAdapterRemoteException Thrown when remote communication fails. + * @throws InvalidOperationContextException Thrown when operation context is invalid. + */ + CreateConsentFormResponse createConsentForm(String userId, String organizationId, OperationContext operationContext, String lang) throws DataAdapterRemoteException, InvalidOperationContextException; + + /** + * Validate consent form values and generate response with validation result with optional error messages in case validation fails. + * @param userId User ID. + * @param organizationId Organization ID. + * @param operationContext Operation context. + * @param lang Language to use for error messages. + * @param options Options selected by the user. + * @return Consent form validation result with optional error messages in case validation fails. + * @throws DataAdapterRemoteException Thrown when remote communication fails. + * @throws InvalidOperationContextException Thrown when operation context is invalid. + * @throws InvalidConsentDataException In case consent options are invalid. + */ + ValidateConsentFormResponse validateConsentForm(String userId, String organizationId, OperationContext operationContext, String lang, List options) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException; + + /** + * Save consent form options selected by the user for an operation. + * @param userId User ID. + * @param organizationId Organization ID. + * @param operationContext Operation context. + * @param options Options selected by the user. + * @return Response with result of saving the consent form. + * @throws DataAdapterRemoteException Thrown when remote communication fails. + * @throws InvalidOperationContextException Thrown when operation context is invalid. + * @throws InvalidConsentDataException In case consent options are invalid. + */ + SaveConsentFormResponse saveConsentForm(String userId, String organizationId, OperationContext operationContext, List options) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException; + + /** + * Execute an anti-fraud system action and return response for usage in Web Flow. + * @param userId User ID. + * @param organizationId Organization ID. + * @param operationContext Operation context. + * @param afsRequestParameters Request parameters for AFS. + * @param extras Extra parameters for AFS. + * @return Response from AFS for usage in Web Flow. + * @throws DataAdapterRemoteException Thrown when remote communication fails. + * @throws InvalidOperationContextException Thrown when operation context is invalid. */ - void sendAuthorizationSMS(String userId, String messageText, OperationContext operationContext) throws DataAdapterRemoteException, SMSAuthorizationFailedException; + AfsResponse executeAfsAction(String userId, String organizationId, OperationContext operationContext, AfsRequestParameters afsRequestParameters, Map extras) throws DataAdapterRemoteException, InvalidOperationContextException; } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/configuration/DataAdapterConfiguration.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/configuration/DataAdapterConfiguration.java index 1bc370fc..be0ca0d2 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/configuration/DataAdapterConfiguration.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/configuration/DataAdapterConfiguration.java @@ -16,6 +16,7 @@ package io.getlime.security.powerauth.app.dataadapter.configuration; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @@ -25,6 +26,7 @@ * @author Roman Strobl, roman.strobl@wultra.com */ @Configuration +@ConfigurationProperties("ext") @ComponentScan(basePackages = {"io.getlime.security.powerauth"}) public class DataAdapterConfiguration { diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java new file mode 100644 index 00000000..6e2fb56d --- /dev/null +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AfsController.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getlime.security.powerauth.app.dataadapter.controller; + +import io.getlime.core.rest.model.base.request.ObjectRequest; +import io.getlime.core.rest.model.base.response.ObjectResponse; +import io.getlime.security.powerauth.app.dataadapter.api.DataAdapter; +import io.getlime.security.powerauth.app.dataadapter.exception.DataAdapterRemoteException; +import io.getlime.security.powerauth.app.dataadapter.exception.InvalidOperationContextException; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; +import io.getlime.security.powerauth.lib.dataadapter.model.request.*; +import io.getlime.security.powerauth.lib.dataadapter.model.response.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +/** + * Controller class which handles OAuth 2.0 consent actions. + * + * @author Roman Strobl, roman.strobl@wultra.com + */ +@RestController +@RequestMapping("/api/afs/action") +public class AfsController { + + private static final Logger logger = LoggerFactory.getLogger(AfsController.class); + + private final DataAdapter dataAdapter; + + /** + * Consent controller constructor. + * @param dataAdapter Data adapter. + */ + @Autowired + public AfsController(DataAdapter dataAdapter) { + this.dataAdapter = dataAdapter; + } + + /** + * Execute an anti-fraud system action and return response for usage in Web Flow. + * @param request AFS request. + * @return AFS response. + * @throws DataAdapterRemoteException In case communication with remote system fails. + * @throws InvalidOperationContextException In case operation context is invalid. + */ + @RequestMapping(value = "/execute", method = RequestMethod.POST) + public ObjectResponse executeAfsAction(@RequestBody ObjectRequest request) throws DataAdapterRemoteException, InvalidOperationContextException { + logger.info("Received executeAfsAction request for user: {}, operation ID: {}", + request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); + AfsRequest afsRequest = request.getRequestObject(); + String userId = afsRequest.getUserId(); + String organizationId = afsRequest.getOrganizationId(); + OperationContext operationContext = afsRequest.getOperationContext(); + AfsRequestParameters requestParameters = afsRequest.getAfsRequestParameters(); + Map extras = afsRequest.getExtras(); + AfsResponse response = dataAdapter.executeAfsAction(userId, organizationId, operationContext, requestParameters, extras); + logger.debug("The executeAfsAction request succeeded"); + return new ObjectResponse<>(response); + } + +} diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java index 701e76fd..9b7b2c18 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/AuthenticationController.java @@ -18,22 +18,19 @@ import io.getlime.core.rest.model.base.request.ObjectRequest; import io.getlime.core.rest.model.base.response.ObjectResponse; import io.getlime.security.powerauth.app.dataadapter.api.DataAdapter; -import io.getlime.security.powerauth.app.dataadapter.exception.AuthenticationFailedException; import io.getlime.security.powerauth.app.dataadapter.exception.DataAdapterRemoteException; import io.getlime.security.powerauth.app.dataadapter.exception.UserNotFoundException; import io.getlime.security.powerauth.app.dataadapter.impl.validation.AuthenticationRequestValidator; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.AuthenticationContext; import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; -import io.getlime.security.powerauth.lib.dataadapter.model.request.AuthenticationRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.UserAuthenticationRequest; import io.getlime.security.powerauth.lib.dataadapter.model.request.UserDetailRequest; -import io.getlime.security.powerauth.lib.dataadapter.model.response.AuthenticationResponse; +import io.getlime.security.powerauth.lib.dataadapter.model.request.UserLookupRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.response.UserAuthenticationResponse; import io.getlime.security.powerauth.lib.dataadapter.model.response.UserDetailResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.MethodParameter; -import org.springframework.stereotype.Controller; -import org.springframework.validation.BindingResult; -import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.*; @@ -44,7 +41,7 @@ * * @author Roman Strobl, roman.strobl@wultra.com */ -@Controller +@RestController @RequestMapping("/api/auth/user") public class AuthenticationController { @@ -74,31 +71,49 @@ private void initBinder(WebDataBinder binder) { } /** - * Authenticate user with given username and password. + * Lookup user account. + * + * @param request Lookup user account request. + * @return Response with user detail. + * @throws DataAdapterRemoteException Thrown in case of remote communication errors. + * @throws UserNotFoundException Thrown in case that user does not exist. + */ + @PostMapping(value = "/lookup") + public ObjectResponse lookupUser(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, UserNotFoundException { + logger.info("Received user lookup request, username: {}, organization ID: {}, operation ID: {}", + request.getRequestObject().getUsername(), request.getRequestObject().getOrganizationId(), + request.getRequestObject().getOperationContext().getId()); + UserLookupRequest lookupRequest = request.getRequestObject(); + String username = lookupRequest.getUsername(); + String organizationId = lookupRequest.getOrganizationId(); + OperationContext operationContext = lookupRequest.getOperationContext(); + UserDetailResponse response = dataAdapter.lookupUser(username, organizationId, operationContext); + logger.info("The user lookup request succeeded, user ID: {}, organization ID: {}, operation ID: {}", + response.getId(), response.getOrganizationId(), request.getRequestObject().getOperationContext().getId()); + return new ObjectResponse<>(response); + } + + /** + * Authenticate user with given credentials. * * @param request Authenticate user request. - * @param result BindingResult for input validation. * @return Response with authenticated user ID. - * @throws MethodArgumentNotValidException Thrown in case form parameters are not valid. * @throws DataAdapterRemoteException Thrown in case of remote communication errors. - * @throws AuthenticationFailedException Thrown in case that authentication fails. */ - @RequestMapping(value = "/authenticate", method = RequestMethod.POST) - public @ResponseBody ObjectResponse authenticate(@Valid @RequestBody ObjectRequest request, BindingResult result) throws MethodArgumentNotValidException, DataAdapterRemoteException, AuthenticationFailedException { - if (result.hasErrors()) { - // Call of getEnclosingMethod() on new object returns a reference to current method - MethodParameter methodParam = new MethodParameter(new Object(){}.getClass().getEnclosingMethod(), 0); - logger.warn("The authenticate request failed due to validation errors"); - throw new MethodArgumentNotValidException(methodParam, result); - } - logger.info("Received authenticate request, username: {}, operation ID: {}", new String[]{request.getRequestObject().getUsername(), request.getRequestObject().getOperationContext().getId()}); - AuthenticationRequest authenticationRequest = request.getRequestObject(); - String username = authenticationRequest.getUsername(); + @PostMapping(value = "/authenticate") + public ObjectResponse authenticate(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException { + logger.info("Received authenticate request, user ID: {}, organization ID: {}, operation ID: {}", + request.getRequestObject().getUserId(), request.getRequestObject().getOrganizationId(), + request.getRequestObject().getOperationContext().getId()); + UserAuthenticationRequest authenticationRequest = request.getRequestObject(); + String userId = authenticationRequest.getUserId(); String password = authenticationRequest.getPassword(); + AuthenticationContext authenticationContext = authenticationRequest.getAuthenticationContext(); + String organizationId = authenticationRequest.getOrganizationId(); OperationContext operationContext = authenticationRequest.getOperationContext(); - UserDetailResponse userDetailResponse = dataAdapter.authenticateUser(username, password, operationContext); - AuthenticationResponse response = new AuthenticationResponse(userDetailResponse.getId()); - logger.info("The authenticate request succeeded, user ID: {}, operation ID: {}", new String[]{request.getRequestObject().getUsername(), request.getRequestObject().getOperationContext().getId()}); + UserAuthenticationResponse response = dataAdapter.authenticateUser(userId, password, authenticationContext, organizationId, operationContext); + logger.info("The authenticate request succeeded, user ID: {}, organization ID: {}, operation ID: {}", userId, + organizationId, request.getRequestObject().getOperationContext().getId()); return new ObjectResponse<>(response); } @@ -110,12 +125,13 @@ private void initBinder(WebDataBinder binder) { * @throws DataAdapterRemoteException Thrown in case of remote communication errors. * @throws UserNotFoundException Thrown in case user is not found. */ - @RequestMapping(value = "/info", method = RequestMethod.POST) - public @ResponseBody ObjectResponse fetchUserDetail(@RequestBody ObjectRequest request) throws DataAdapterRemoteException, UserNotFoundException { - logger.info("Received fetchUserDetail request, user ID: {}", request.getRequestObject().getId()); + @PostMapping(value = "/info") + public ObjectResponse fetchUserDetail(@RequestBody ObjectRequest request) throws DataAdapterRemoteException, UserNotFoundException { + logger.info("Received fetchUserDetail request, user ID: {}", request.getRequestObject().getUserId()); UserDetailRequest userDetailRequest = request.getRequestObject(); - String userId = userDetailRequest.getId(); - UserDetailResponse response = dataAdapter.fetchUserDetail(userId); + String userId = userDetailRequest.getUserId(); + String organizationId = userDetailRequest.getOrganizationId(); + UserDetailResponse response = dataAdapter.fetchUserDetail(userId, organizationId, null); logger.info("The fetchUserDetail request succeeded"); return new ObjectResponse<>(response); } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java new file mode 100644 index 00000000..a9bd2d63 --- /dev/null +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ConsentController.java @@ -0,0 +1,165 @@ +/* + * Copyright 2019 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getlime.security.powerauth.app.dataadapter.controller; + + +import io.getlime.core.rest.model.base.request.ObjectRequest; +import io.getlime.core.rest.model.base.response.ObjectResponse; +import io.getlime.security.powerauth.app.dataadapter.api.DataAdapter; +import io.getlime.security.powerauth.app.dataadapter.exception.DataAdapterRemoteException; +import io.getlime.security.powerauth.app.dataadapter.exception.InvalidConsentDataException; +import io.getlime.security.powerauth.app.dataadapter.exception.InvalidOperationContextException; +import io.getlime.security.powerauth.app.dataadapter.impl.validation.ConsentFormRequestValidator; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.ConsentOption; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; +import io.getlime.security.powerauth.lib.dataadapter.model.request.CreateConsentFormRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.InitConsentFormRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.SaveConsentFormRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.ValidateConsentFormRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.response.CreateConsentFormResponse; +import io.getlime.security.powerauth.lib.dataadapter.model.response.InitConsentFormResponse; +import io.getlime.security.powerauth.lib.dataadapter.model.response.SaveConsentFormResponse; +import io.getlime.security.powerauth.lib.dataadapter.model.response.ValidateConsentFormResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +/** + * Controller class which handles OAuth 2.0 consent actions. + * + * @author Roman Strobl, roman.strobl@wultra.com + */ +@RestController +@RequestMapping("/api/auth/consent") +public class ConsentController { + + private static final Logger logger = LoggerFactory.getLogger(ConsentController.class); + + private final DataAdapter dataAdapter; + private final ConsentFormRequestValidator requestValidator; + + /** + * Consent controller constructor. + * @param dataAdapter Data adapter. + * @param requestValidator Request validator. + */ + @Autowired + public ConsentController(DataAdapter dataAdapter, ConsentFormRequestValidator requestValidator) { + this.dataAdapter = dataAdapter; + this.requestValidator = requestValidator; + } + + /** + * Initializes the request validator. + * @param binder Data binder. + */ + @InitBinder + private void initBinder(WebDataBinder binder) { + binder.setValidator(requestValidator); + } + + /** + * Initialize OAuth 2.0 consent form - verify that consent form is required. + * @param request Initialize consent form request. + * @return Initialize consent form response. + * @throws DataAdapterRemoteException In case communication with remote system fails. + * @throws InvalidOperationContextException In case operation context is invalid. + */ + @PostMapping(value = "/init") + public ObjectResponse initConsentForm(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, InvalidOperationContextException { + logger.info("Received initConsentForm request for user: {}, operation ID: {}", + request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); + InitConsentFormRequest createRequest = request.getRequestObject(); + String userId = createRequest.getUserId(); + String organizationId = createRequest.getOrganizationId(); + OperationContext operationContext = createRequest.getOperationContext(); + InitConsentFormResponse response = dataAdapter.initConsentForm(userId, organizationId, operationContext); + logger.debug("The initConsentForm request succeeded"); + return new ObjectResponse<>(response); + } + + /** + * Create OAuth 2.0 consent form. + * @param request Create consent form request. + * @return Create consent form response. + * @throws DataAdapterRemoteException In case communication with remote system fails. + * @throws InvalidOperationContextException In case operation context is invalid. + */ + @PostMapping(value = "/create") + public ObjectResponse createConsentForm(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, InvalidOperationContextException { + logger.info("Received createConsentForm request for user: {}, operation ID: {}", + request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); + CreateConsentFormRequest createRequest = request.getRequestObject(); + String userId = createRequest.getUserId(); + String organizationId = createRequest.getOrganizationId(); + OperationContext operationContext = createRequest.getOperationContext(); + String lang = createRequest.getLang(); + CreateConsentFormResponse response = dataAdapter.createConsentForm(userId, organizationId, operationContext, lang); + logger.debug("The createConsent request succeeded"); + return new ObjectResponse<>(response); + } + + /** + * Validate OAuth 2.0 consent form. + * @param request Validate consent form request. + * @return Validate consent form response. + * @throws DataAdapterRemoteException In case communication with remote system fails. + * @throws InvalidOperationContextException In case operation context is invalid. + * @throws InvalidConsentDataException In case consent options are invalid. + */ + @PostMapping(value = "/validate") + public ObjectResponse validateConsentForm(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException { + logger.info("Received validateConsentForm request for user: {}, operation ID: {}", + request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); + ValidateConsentFormRequest validateRequest = request.getRequestObject(); + String userId = validateRequest.getUserId(); + String organizationId = validateRequest.getOrganizationId(); + OperationContext operationContext = validateRequest.getOperationContext(); + String lang = validateRequest.getLang(); + List options = validateRequest.getOptions(); + ValidateConsentFormResponse response = dataAdapter.validateConsentForm(userId, organizationId, operationContext, lang, options); + logger.debug("The validateConsentForm request succeeded"); + return new ObjectResponse<>(response); + } + + /** + * Save OAuth 2.0 consent form. + * @param request Save consent form request. + * @return Save consent form response. + * @throws DataAdapterRemoteException In case communication with remote system fails. + * @throws InvalidOperationContextException In case operation context is invalid. + * @throws InvalidConsentDataException In case consent options are invalid. + */ + @PostMapping(value = "/save") + public ObjectResponse saveConsentForm(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException { + logger.info("Received saveConsentForm request for user: {}, operation ID: {}", + request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); + SaveConsentFormRequest saveRequest = request.getRequestObject(); + String userId = saveRequest.getUserId(); + String organizationId = saveRequest.getOrganizationId(); + OperationContext operationContext = saveRequest.getOperationContext(); + List options = saveRequest.getOptions(); + SaveConsentFormResponse response = dataAdapter.saveConsentForm(userId, organizationId, operationContext, options); + logger.debug("The saveConsentForm request succeeded"); + return new ObjectResponse<>(response); + } + +} diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/FormDataChangeController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/FormDataChangeController.java index f6eb916a..f387bc1d 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/FormDataChangeController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/FormDataChangeController.java @@ -29,24 +29,20 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.*; /** * Controller class which handles notifications about changes of operation form data. * * @author Roman Strobl, roman.strobl@wultra.com */ -@Controller +@RestController @RequestMapping("/api/operation/formdata") public class FormDataChangeController { private static final Logger logger = LoggerFactory.getLogger(FormDataChangeController.class); - private DataAdapter dataAdapter; + private final DataAdapter dataAdapter; /** * Controller constructor. @@ -64,15 +60,16 @@ public FormDataChangeController(DataAdapter dataAdapter) { * @return Object response. * @throws DataAdapterRemoteException Thrown in case of remote communication errors. */ - @RequestMapping(value = "/change", method = RequestMethod.POST) - public @ResponseBody Response formDataChangedNotification(@RequestBody ObjectRequest request) throws DataAdapterRemoteException { + @PostMapping(value = "/change") + public Response formDataChangedNotification(@RequestBody ObjectRequest request) throws DataAdapterRemoteException { logger.info("Received formDataChangedNotification request for user: {}, operation ID: {}", - new String[]{request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()}); + request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); FormDataChangeNotificationRequest notification = request.getRequestObject(); String userId = notification.getUserId(); + String organizationId = notification.getOrganizationId(); OperationContext operationContext = notification.getOperationContext(); FormDataChange formDataChange = notification.getFormDataChange(); - dataAdapter.formDataChangedNotification(userId, formDataChange, operationContext); + dataAdapter.formDataChangedNotification(userId, organizationId, formDataChange, operationContext); logger.debug("The formDataChangedNotification request succeeded"); return new Response(); } @@ -85,14 +82,15 @@ public FormDataChangeController(DataAdapter dataAdapter) { * @throws DataAdapterRemoteException Thrown in case of remote communication errors. * @throws UserNotFoundException Thrown in case user is not found. */ - @RequestMapping(value = "/decorate", method = RequestMethod.POST) - public @ResponseBody ObjectResponse decorateOperationFormData(@RequestBody ObjectRequest request) throws DataAdapterRemoteException, UserNotFoundException { + @PostMapping(value = "/decorate") + public ObjectResponse decorateOperationFormData(@RequestBody ObjectRequest request) throws DataAdapterRemoteException, UserNotFoundException { logger.info("Received decorateOperationFormData request for user: {}, operation ID: {}", - new String[]{request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()}); + request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); DecorateOperationFormDataRequest requestObject = request.getRequestObject(); String userId = requestObject.getUserId(); + String organizationId = requestObject.getOrganizationId(); OperationContext operationContext = requestObject.getOperationContext(); - DecorateOperationFormDataResponse response = dataAdapter.decorateFormData(userId, operationContext); + DecorateOperationFormDataResponse response = dataAdapter.decorateFormData(userId, organizationId, operationContext); logger.debug("The decorateOperationFormData request succeeded"); return new ObjectResponse<>(response); } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/OperationChangeController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/OperationChangeController.java index dad27586..7f48b9f4 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/OperationChangeController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/OperationChangeController.java @@ -25,24 +25,20 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.*; /** * Controller class which handles notifications about changes of operation state. * * @author Roman Strobl, roman.strobl@wultra.com */ -@Controller +@RestController @RequestMapping("/api/operation") public class OperationChangeController { private static final Logger logger = LoggerFactory.getLogger(OperationChangeController.class); - private DataAdapter dataAdapter; + private final DataAdapter dataAdapter; /** * Controller constructor. @@ -60,15 +56,16 @@ public OperationChangeController(DataAdapter dataAdapter) { * @return Object response. * @throws DataAdapterRemoteException Thrown in case of remote communication errors. */ - @RequestMapping(value = "/change", method = RequestMethod.POST) - public @ResponseBody Response operationChangedNotification(@RequestBody ObjectRequest request) throws DataAdapterRemoteException { + @PostMapping(value = "/change") + public Response operationChangedNotification(@RequestBody ObjectRequest request) throws DataAdapterRemoteException { logger.info("Received operationChangedNotification request for user: {}, operation ID: {}", - new String[]{request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()}); + request.getRequestObject().getUserId(), request.getRequestObject().getOperationContext().getId()); OperationChangeNotificationRequest notification = request.getRequestObject(); String userId = notification.getUserId(); + String organizationId = notification.getOrganizationId(); OperationContext operationContext = notification.getOperationContext(); OperationChange operationChange = notification.getOperationChange(); - dataAdapter.operationChangedNotification(userId, operationChange, operationContext); + dataAdapter.operationChangedNotification(userId, organizationId, operationChange, operationContext); logger.debug("The operationChangedNotification request succeeded"); return new Response(); } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java deleted file mode 100644 index 3cf1b3f7..00000000 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SMSAuthorizationController.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright 2017 Wultra s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.getlime.security.powerauth.app.dataadapter.controller; - -import io.getlime.core.rest.model.base.request.ObjectRequest; -import io.getlime.core.rest.model.base.response.ObjectResponse; -import io.getlime.core.rest.model.base.response.Response; -import io.getlime.security.powerauth.app.dataadapter.api.DataAdapter; -import io.getlime.security.powerauth.app.dataadapter.exception.DataAdapterRemoteException; -import io.getlime.security.powerauth.app.dataadapter.exception.InvalidOperationContextException; -import io.getlime.security.powerauth.app.dataadapter.exception.SMSAuthorizationFailedException; -import io.getlime.security.powerauth.app.dataadapter.impl.validation.CreateSMSAuthorizationRequestValidator; -import io.getlime.security.powerauth.app.dataadapter.repository.model.entity.SMSAuthorizationEntity; -import io.getlime.security.powerauth.app.dataadapter.service.SMSPersistenceService; -import io.getlime.security.powerauth.lib.dataadapter.model.request.CreateSMSAuthorizationRequest; -import io.getlime.security.powerauth.lib.dataadapter.model.request.VerifySMSAuthorizationRequest; -import io.getlime.security.powerauth.lib.dataadapter.model.response.CreateSMSAuthorizationResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.MethodParameter; -import org.springframework.stereotype.Controller; -import org.springframework.validation.BindingResult; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.WebDataBinder; -import org.springframework.web.bind.annotation.*; - -import javax.validation.Valid; - -/** - * Controller class which handles SMS OTP authorization. - * - * @author Roman Strobl, roman.strobl@wultra.com - */ -@Controller -@RequestMapping("/api/auth/sms") -public class SMSAuthorizationController { - - private static final Logger logger = LoggerFactory.getLogger(SMSAuthorizationController.class); - - private final SMSPersistenceService smsPersistenceService; - private final CreateSMSAuthorizationRequestValidator requestValidator; - private final DataAdapter dataAdapter; - - /** - * Controller constructor. - * @param smsPersistenceService SMS persistence service. - * @param requestValidator Validator for SMS requests. - * @param dataAdapter Data adapter. - */ - @Autowired - public SMSAuthorizationController(SMSPersistenceService smsPersistenceService, CreateSMSAuthorizationRequestValidator requestValidator, DataAdapter dataAdapter) { - this.smsPersistenceService = smsPersistenceService; - this.requestValidator = requestValidator; - this.dataAdapter = dataAdapter; - } - - - /** - * Initializes the request validator. - * @param binder Data binder. - */ - @InitBinder - private void initBinder(WebDataBinder binder) { - binder.setValidator(requestValidator); - } - - /** - * Create a new SMS OTP authorization message. - * - * @param request Request data. - * @param result BindingResult for input validation. - * @return Response with message ID. - * @throws MethodArgumentNotValidException Thrown in case request is not valid. - * @throws DataAdapterRemoteException Thrown in case of remote communication errors. - * @throws SMSAuthorizationFailedException Thrown in case that SMS message could not be delivered. - */ - @RequestMapping(value = "create", method = RequestMethod.POST) - public @ResponseBody ObjectResponse createAuthorizationSMS(@Valid @RequestBody ObjectRequest request, BindingResult result) throws MethodArgumentNotValidException, DataAdapterRemoteException, SMSAuthorizationFailedException, InvalidOperationContextException { - if (result.hasErrors()) { - // Call of getEnclosingMethod() on new object returns a reference to current method - MethodParameter methodParam = new MethodParameter(new Object(){}.getClass().getEnclosingMethod(), 0); - logger.warn("The createAuthorizationSMS request failed due to validation errors"); - throw new MethodArgumentNotValidException(methodParam, result); - } - logger.info("Received createAuthorizationSMS request, operation ID: "+request.getRequestObject().getOperationContext().getId()); - CreateSMSAuthorizationRequest smsRequest = request.getRequestObject(); - - // Create authorization SMS and persist it. - SMSAuthorizationEntity smsEntity = createAuthorizationSMS(smsRequest); - - // Send SMS with generated text to target user. - String userId = smsEntity.getUserId(); - String messageId = smsEntity.getMessageId(); - String messageText = smsEntity.getMessageText(); - dataAdapter.sendAuthorizationSMS(userId, messageText, smsRequest.getOperationContext()); - - // Create response. - CreateSMSAuthorizationResponse response = new CreateSMSAuthorizationResponse(messageId); - logger.info("The createAuthorizationSMS request succeeded, operation ID: "+request.getRequestObject().getOperationContext().getId()); - return new ObjectResponse<>(response); - } - - /** - * Validates the request and sends SMS. - * @param smsRequest Create SMS request. - * @return SMS entity. - */ - private SMSAuthorizationEntity createAuthorizationSMS(@Valid CreateSMSAuthorizationRequest smsRequest) throws InvalidOperationContextException { - String userId = smsRequest.getUserId(); - String lang = smsRequest.getLang(); - return smsPersistenceService.createAuthorizationSMS(userId, smsRequest.getOperationContext(), lang); - } - - /** - * Verify a SMS OTP authorization code. - * - * @param request Request data. - * @return Authorization response. - * @throws SMSAuthorizationFailedException Thrown in case that SMS verification fails. - */ - @RequestMapping(value = "verify", method = RequestMethod.POST) - public @ResponseBody Response verifyAuthorizationSMS(@RequestBody ObjectRequest request) throws SMSAuthorizationFailedException { - logger.info("Received verifyAuthorizationSMS request, operation ID: "+request.getRequestObject().getOperationContext().getId()); - VerifySMSAuthorizationRequest verifyRequest = request.getRequestObject(); - String messageId = verifyRequest.getMessageId(); - String authorizationCode = verifyRequest.getAuthorizationCode(); - // Verify authorization code. - smsPersistenceService.verifyAuthorizationSMS(messageId, authorizationCode); - logger.info("The verifyAuthorizationSMS request succeeded, operation ID: "+request.getRequestObject().getOperationContext().getId()); - return new Response(); - } - -} diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ServiceController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ServiceController.java index cb69e724..13377134 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ServiceController.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/ServiceController.java @@ -23,10 +23,9 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.info.BuildProperties; -import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.GetMapping; import java.util.Date; @@ -35,7 +34,7 @@ * * @author Petr Dvorak, petr@wultra.com */ -@Controller +@RestController @RequestMapping(value = "/api/service") public class ServiceController { @@ -59,8 +58,8 @@ public ServiceController(DataAdapterConfiguration dataAdapterConfiguration, Buil * Controller resource with system information. * @return System status info. */ - @RequestMapping(value = "status", method = RequestMethod.GET) - public @ResponseBody ObjectResponse getServiceStatus() { + @GetMapping(value = "status") + public ObjectResponse getServiceStatus() { logger.info("Received getServiceStatus request"); ServiceStatusResponse response = new ServiceStatusResponse(); response.setApplicationName(dataAdapterConfiguration.getApplicationName()); diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java new file mode 100644 index 00000000..47f5dd41 --- /dev/null +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/controller/SmsAuthorizationController.java @@ -0,0 +1,149 @@ +/* + * Copyright 2017 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getlime.security.powerauth.app.dataadapter.controller; + +import io.getlime.core.rest.model.base.request.ObjectRequest; +import io.getlime.core.rest.model.base.response.ObjectResponse; +import io.getlime.security.powerauth.app.dataadapter.api.DataAdapter; +import io.getlime.security.powerauth.app.dataadapter.exception.DataAdapterRemoteException; +import io.getlime.security.powerauth.app.dataadapter.exception.InvalidOperationContextException; +import io.getlime.security.powerauth.app.dataadapter.impl.validation.CreateSmsAuthorizationRequestValidator; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.AuthenticationContext; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; +import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.AccountStatus; +import io.getlime.security.powerauth.lib.dataadapter.model.request.CreateSmsAuthorizationRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.VerifySmsAndPasswordRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.VerifySmsAuthorizationRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.response.CreateSmsAuthorizationResponse; +import io.getlime.security.powerauth.lib.dataadapter.model.response.VerifySmsAndPasswordResponse; +import io.getlime.security.powerauth.lib.dataadapter.model.response.VerifySmsAuthorizationResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +/** + * Controller class which handles SMS OTP authorization. + * + * @author Roman Strobl, roman.strobl@wultra.com + */ +@RestController +@RequestMapping("/api/auth/sms") +public class SmsAuthorizationController { + + private static final Logger logger = LoggerFactory.getLogger(SmsAuthorizationController.class); + + private final CreateSmsAuthorizationRequestValidator requestValidator; + private final DataAdapter dataAdapter; + + /** + * Controller constructor. + * @param requestValidator Validator for SMS requests. + * @param dataAdapter Data adapter. + */ + @Autowired + public SmsAuthorizationController(CreateSmsAuthorizationRequestValidator requestValidator, DataAdapter dataAdapter) { + this.requestValidator = requestValidator; + this.dataAdapter = dataAdapter; + } + + /** + * Initializes the request validator. + * @param binder Data binder. + */ + @InitBinder + private void initBinder(WebDataBinder binder) { + binder.setValidator(requestValidator); + } + + /** + * Create a new SMS OTP authorization message. + * + * @param request Request data. + * @return Response with message ID. + * @throws DataAdapterRemoteException Thrown in case of remote communication errors. + * @throws InvalidOperationContextException Thrown in case operation context is invalid. + */ + @PostMapping(value = "create") + public ObjectResponse createAuthorizationSms(@Valid @RequestBody ObjectRequest request) throws DataAdapterRemoteException, InvalidOperationContextException { + logger.info("Received createAuthorizationSms request, operation ID: {}", request.getRequestObject().getOperationContext().getId()); + CreateSmsAuthorizationRequest smsRequest = request.getRequestObject(); + + // Create authorization SMS and persist it. + String userId = smsRequest.getUserId(); + String organizationId = smsRequest.getOrganizationId(); + AccountStatus accountStatus = smsRequest.getAccountStatus(); + OperationContext operationContext = smsRequest.getOperationContext(); + String lang = smsRequest.getLang(); + CreateSmsAuthorizationResponse response = dataAdapter.createAndSendAuthorizationSms(userId, organizationId, accountStatus, operationContext, lang); + + logger.info("The createAuthorizationSms request succeeded, operation ID: {}", request.getRequestObject().getOperationContext().getId()); + return new ObjectResponse<>(response); + } + + /** + * Verify authorization code from SMS message. + * + * @param request Request data. + * @return Authorization response. + * @throws DataAdapterRemoteException Thrown in case communication with remote system fails. + * @throws InvalidOperationContextException Thrown in case operation context is invalid. + */ + @PostMapping(value = "verify") + public ObjectResponse verifyAuthorizationSms(@RequestBody ObjectRequest request) throws InvalidOperationContextException, DataAdapterRemoteException { + logger.info("Received verifyAuthorizationSms request, operation ID: {}", request.getRequestObject().getOperationContext().getId()); + VerifySmsAuthorizationRequest verifyRequest = request.getRequestObject(); + String userId = verifyRequest.getUserId(); + String organizationId = verifyRequest.getOrganizationId(); + AccountStatus accountStatus = verifyRequest.getAccountStatus(); + String messageId = verifyRequest.getMessageId(); + String authorizationCode = verifyRequest.getAuthorizationCode(); + OperationContext operationContext = verifyRequest.getOperationContext(); + // Verify authorization code + VerifySmsAuthorizationResponse response = dataAdapter.verifyAuthorizationSms(userId, organizationId, accountStatus, messageId, authorizationCode, operationContext); + logger.info("The verifyAuthorizationSms request succeeded, operation ID: {}", request.getRequestObject().getOperationContext().getId()); + return new ObjectResponse<>(response); + } + + /** + * Verify a SMS OTP authorization code and user password. + * + * @param request Verify SMS code and password request. + * @return Authorization response. + * @throws DataAdapterRemoteException Thrown in case communication with remote system fails. + * @throws InvalidOperationContextException Thrown in case operation context is invalid. + */ + @PostMapping(value = "/password/verify") + public ObjectResponse verifyAuthorizationSmsAndPassword(@RequestBody ObjectRequest request) throws DataAdapterRemoteException, InvalidOperationContextException { + logger.info("Received verifyAuthorizationSmsAndPassword request, operation ID: {}", request.getRequestObject().getOperationContext().getId()); + VerifySmsAndPasswordRequest verifyRequest = request.getRequestObject(); + String userId = verifyRequest.getUserId(); + String organizationId = verifyRequest.getOrganizationId(); + AccountStatus accountStatus = verifyRequest.getAccountStatus(); + String messageId = verifyRequest.getMessageId(); + String authorizationCode = verifyRequest.getAuthorizationCode(); + OperationContext operationContext = verifyRequest.getOperationContext(); + String password = verifyRequest.getPassword(); + AuthenticationContext authenticationContext = verifyRequest.getAuthenticationContext(); + VerifySmsAndPasswordResponse response = dataAdapter.verifyAuthorizationSmsAndPassword(userId, organizationId, accountStatus, messageId, authorizationCode, operationContext, authenticationContext, password); + logger.info("The verifyAuthorizationSmsAndPassword request succeeded, operation ID: {}", request.getRequestObject().getOperationContext().getId()); + return new ObjectResponse<>(response); + } + +} diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/AuthenticationFailedException.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/AuthenticationFailedException.java deleted file mode 100644 index 7e49b0e6..00000000 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/AuthenticationFailedException.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2017 Wultra s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.getlime.security.powerauth.app.dataadapter.exception; - -/** - * Exception used for cases when authentication fails. - * - * @author Petr Dvorak, petr@wultra.com - */ -public class AuthenticationFailedException extends Exception { - - private Integer remainingAttempts; - - /** - * Default constructor. - */ - public AuthenticationFailedException() { - } - - /** - * Constructor with authentication failure message. - * @param message Authentication failure message. - */ - public AuthenticationFailedException(String message) { - super(message); - } - - /** - * Constructor with authentication failure message and cause. - * @param message Authentication failure message. - * @param cause Cause, original exception. - */ - public AuthenticationFailedException(String message, Throwable cause) { - super(message, cause); - } - - /** - * Constructor with cause. - * @param cause Cause, original exception. - */ - public AuthenticationFailedException(Throwable cause) { - super(cause); - } - - /** - * Get number of remaining authentication attempts. - * @return Number of remaining attempts. - */ - public Integer getRemainingAttempts() { - return remainingAttempts; - } - - /** - * Set number of remaining authentication attempts. - * @param remainingAttempts Number of remaining attempts. - */ - public void setRemainingAttempts(Integer remainingAttempts) { - this.remainingAttempts = remainingAttempts; - } -} diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/DefaultExceptionResolver.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/DefaultExceptionResolver.java index d9e6d716..7e8245cb 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/DefaultExceptionResolver.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/DefaultExceptionResolver.java @@ -40,6 +40,10 @@ @ControllerAdvice public class DefaultExceptionResolver { + private static final String LOGIN_PASS_EMPTY = "login.password.empty"; + private static final String LOGIN_PASS_LONG = "login.password.long"; + private static final String LOGIN_USERNAME_LONG = "login.username.long"; + private static final Logger logger = LoggerFactory.getLogger(DefaultExceptionResolver.class); /** @@ -55,35 +59,6 @@ public class DefaultExceptionResolver { return new ErrorResponse(error); } - /** - * Handling of authentication failures. - * @param ex Authentication failure exception, with exception details. - * @return Response with error information. - */ - @ExceptionHandler(AuthenticationFailedException.class) - @ResponseStatus(HttpStatus.UNAUTHORIZED) - public @ResponseBody ErrorResponse handleAuthenticationError(AuthenticationFailedException ex) { - // regular authentication failed error - DataAdapterError error = new DataAdapterError(DataAdapterError.Code.AUTHENTICATION_FAILED, ex.getMessage()); - error.setRemainingAttempts(ex.getRemainingAttempts()); - return new ErrorResponse(error); - } - - /** - * Handling of SMS OTP authorization failures. - * - * @param ex Authorization failure exception, with exception details. - * @return Response with error information. - */ - @ExceptionHandler(SMSAuthorizationFailedException.class) - @ResponseStatus(HttpStatus.UNAUTHORIZED) - public @ResponseBody ErrorResponse handleAuthenticationError(SMSAuthorizationFailedException ex) { - // regular sms authorization failed error - DataAdapterError error = new DataAdapterError(DataAdapterError.Code.SMS_AUTHORIZATION_FAILED, ex.getMessage()); - error.setRemainingAttempts(ex.getRemainingAttempts()); - return new ErrorResponse(error); - } - /** * Handling of validation errors. * @param ex Exception. @@ -92,53 +67,28 @@ public class DefaultExceptionResolver { @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public @ResponseBody ErrorResponse handleDefaultException(MethodArgumentNotValidException ex) { + logger.error("Method argument validation failed", ex); List errorMessages = new ArrayList<>(); final List allErrors = ex.getBindingResult().getAllErrors(); - for (ObjectError objError: allErrors) { - errorMessages.addAll(Arrays.asList(objError.getCodes())); - } + allErrors.stream() + .filter(objError -> (objError.getCodes() != null)) + .forEachOrdered(objError -> + errorMessages.addAll(Arrays.asList(objError.getCodes())) + ); // preparation of user friendly error messages for the UI String message; if (errorMessages.contains("login.username.empty")) { - if (errorMessages.contains("login.password.empty")) { - message = "login.username.empty login.password.empty"; - } else { - if (errorMessages.contains("login.password.long")) { - message = "login.username.empty login.password.long"; - } else { - message = "login.username.empty"; - } - } + message = processErrorMessagesWhenUsernameEmpty(errorMessages); } else { - if (errorMessages.contains("login.password.empty")) { - if (errorMessages.contains("login.username.long")) { - message = "login.password.empty login.username.long"; - } else { - message = "login.password.empty"; - } - } else { - if (errorMessages.contains("login.username.long")) { - if (errorMessages.contains("login.password.long")) { - message = "login.username.long login.password.long"; - } else { - message = "login.username.long"; - } - } else { - if (errorMessages.contains("login.password.long")) { - message = "login.password.long"; - } else { - message = "login.authenticationFailed"; - } - } - } + message = processErrorMessagesWhenUsernameFilled(errorMessages); } DataAdapterError error = new DataAdapterError(DataAdapterError.Code.INPUT_INVALID, message); error.setValidationErrors(errorMessages); return new ErrorResponse(error); } - + /** * Handling of user not found exception. * @param ex Exception. @@ -147,7 +97,8 @@ public class DefaultExceptionResolver { @ExceptionHandler(UserNotFoundException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public @ResponseBody ErrorResponse handleUserNotFoundException(UserNotFoundException ex) { - DataAdapterError error = new DataAdapterError(DataAdapterError.Code.INPUT_INVALID, ex.getMessage()); + logger.debug("User not found", ex); + DataAdapterError error = new DataAdapterError(DataAdapterError.Code.USER_NOT_FOUND, ex.getMessage()); return new ErrorResponse(error); } @@ -159,10 +110,24 @@ public class DefaultExceptionResolver { @ExceptionHandler(InvalidOperationContextException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public @ResponseBody ErrorResponse handleInvalidOperationContextException(InvalidOperationContextException ex) { + logger.error("Invalid operation context", ex); DataAdapterError error = new DataAdapterError(DataAdapterError.Code.OPERATION_CONTEXT_INVALID, ex.getMessage()); return new ErrorResponse(error); } + /** + * Handling of invalid consent exception. + * @param ex Exception. + * @return Response with error information. + */ + @ExceptionHandler(InvalidConsentDataException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public @ResponseBody ErrorResponse handleInvalidConsentException(InvalidConsentDataException ex) { + logger.error("Invalid consent data", ex); + DataAdapterError error = new DataAdapterError(DataAdapterError.Code.CONSENT_DATA_INVALID, ex.getMessage()); + return new ErrorResponse(error); + } + /** * Handling of exceptions occurring during communication with remote backends. * @param ex Exception. @@ -176,4 +141,47 @@ public class DefaultExceptionResolver { return new ErrorResponse(error); } + private String processErrorMessagesWhenUsernameEmpty(List errorMessages) { + if (errorMessages.contains(LOGIN_PASS_EMPTY)) { + return "login.username.empty login.password.empty"; + } else { + if (errorMessages.contains(LOGIN_PASS_LONG)) { + return "login.username.empty login.password.long"; + } else { + return "login.username.empty"; + } + } + } + + private String processErrorMessagesWhenUsernameFilled(List errorMessages) { + if (errorMessages.contains(LOGIN_PASS_EMPTY)) { + return processErrorMessagesWhenLoginPasswordEmpty(errorMessages); + } else { + return processErrorMessagesWhenLoginPasswordFilled(errorMessages); + } + } + + private String processErrorMessagesWhenLoginPasswordEmpty(List errorMessages) { + if (errorMessages.contains(LOGIN_USERNAME_LONG)) { + return "login.password.empty login.username.long"; + } else { + return LOGIN_PASS_EMPTY; + } + } + + private String processErrorMessagesWhenLoginPasswordFilled(List errorMessages) { + if (errorMessages.contains(LOGIN_USERNAME_LONG)) { + if (errorMessages.contains(LOGIN_PASS_LONG)) { + return "login.username.long login.password.long"; + } else { + return LOGIN_USERNAME_LONG; + } + } else { + if (errorMessages.contains(LOGIN_PASS_LONG)) { + return LOGIN_PASS_LONG; + } else { + return "login.authenticationFailed"; + } + } + } } \ No newline at end of file diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/InvalidConsentDataException.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/InvalidConsentDataException.java new file mode 100644 index 00000000..bedf19eb --- /dev/null +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/InvalidConsentDataException.java @@ -0,0 +1,58 @@ +/* + * Copyright 2017 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getlime.security.powerauth.app.dataadapter.exception; + +/** + * Exception used for case when consent data is invalid. + * + * @author Roman Strobl, roman.strobl@wultra.com + */ +public class InvalidConsentDataException extends Exception { + + /** + * Default constructor. + */ + public InvalidConsentDataException() { + } + + /** + * Constructor with message. + * + * @param message Message. + */ + public InvalidConsentDataException(String message) { + super(message); + } + + /** + * Constructor with message and cause. + * + * @param message Message. + * @param cause Cause, original exception. + */ + public InvalidConsentDataException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructor with cause. + * + * @param cause Cause, original exception. + */ + public InvalidConsentDataException(Throwable cause) { + super(cause); + } +} diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/SMSAuthorizationFailedException.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/SMSAuthorizationFailedException.java deleted file mode 100644 index 56db9257..00000000 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/exception/SMSAuthorizationFailedException.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2017 Wultra s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.getlime.security.powerauth.app.dataadapter.exception; - -/** - * Exception used for cases when SMS OTP authorization fails. - * - * @author Roman Strobl, roman.strobl@wultra.com - */ -public class SMSAuthorizationFailedException extends Exception { - - private Integer remainingAttempts; - - /** - * Default constructor. - */ - public SMSAuthorizationFailedException() { - } - - /** - * Constructor with authorization failure message. - * - * @param message Authorization failure message. - */ - public SMSAuthorizationFailedException(String message) { - super(message); - } - - /** - * Constructor with authorization failure message and cause. - * - * @param message Authorization failure message. - * @param cause Cause, original exception. - */ - public SMSAuthorizationFailedException(String message, Throwable cause) { - super(message, cause); - } - - /** - * Constructor with cause. - * - * @param cause Cause, original exception. - */ - public SMSAuthorizationFailedException(Throwable cause) { - super(cause); - } - - /** - * Get number of remaining authentication attempts. - * @return Get remaining attempts. - */ - public Integer getRemainingAttempts() { - return remainingAttempts; - } - - /** - * Set number of remaining authentication attempts. - * @param remainingAttempts Number of remaining attempts. - */ - public void setRemainingAttempts(Integer remainingAttempts) { - this.remainingAttempts = remainingAttempts; - } -} diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java index c3cc0f72..0b4d7eb7 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/DataAdapterService.java @@ -3,21 +3,24 @@ import io.getlime.security.powerauth.app.dataadapter.api.DataAdapter; import io.getlime.security.powerauth.app.dataadapter.exception.*; import io.getlime.security.powerauth.app.dataadapter.service.DataAdapterI18NService; -import io.getlime.security.powerauth.crypto.server.util.DataDigest; +import io.getlime.security.powerauth.app.dataadapter.service.SmsPersistenceService; import io.getlime.security.powerauth.lib.dataadapter.model.entity.*; import io.getlime.security.powerauth.lib.dataadapter.model.entity.attribute.AmountAttribute; import io.getlime.security.powerauth.lib.dataadapter.model.entity.attribute.FormFieldConfig; -import io.getlime.security.powerauth.lib.dataadapter.model.response.DecorateOperationFormDataResponse; -import io.getlime.security.powerauth.lib.dataadapter.model.response.UserDetailResponse; +import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.*; +import io.getlime.security.powerauth.lib.dataadapter.model.request.AfsRequestParameters; +import io.getlime.security.powerauth.lib.dataadapter.model.response.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; -import java.util.Locale; +import java.util.Map; +import java.util.UUID; /** * Sample implementation of DataAdapter interface which should be updated in real implementation. @@ -30,50 +33,86 @@ public class DataAdapterService implements DataAdapter { private static final Logger logger = LoggerFactory.getLogger(DataAdapterService.class); private static final String BANK_ACCOUNT_CHOICE_ID = "operation.bankAccountChoice"; + private static final String AUTHENTICATION_FAILED = "login.authenticationFailed"; + private static final String SMS_DELIVERY_FAILED = "smsAuthorization.deliveryFailed"; + private static final String SMS_AUTHORIZATION_FAILED = "smsAuthorization.failed"; + private static final String INVALID_REQUEST = "error.invalidRequest"; private final DataAdapterI18NService dataAdapterI18NService; - private final OperationValueExtractionService operationValueExtractionService; + private final SmsPersistenceService smsPersistenceService; + private final SmsDeliveryService smsDeliveryService; - public DataAdapterService(DataAdapterI18NService dataAdapterI18NService, OperationValueExtractionService operationValueExtractionService) { + @Autowired + public DataAdapterService(DataAdapterI18NService dataAdapterI18NService, SmsPersistenceService smsPersistenceService, SmsDeliveryService smsDeliveryService) { this.dataAdapterI18NService = dataAdapterI18NService; - this.operationValueExtractionService = operationValueExtractionService; + this.smsPersistenceService = smsPersistenceService; + this.smsDeliveryService = smsDeliveryService; } @Override - public UserDetailResponse authenticateUser(String username, String password, OperationContext operationContext) throws DataAdapterRemoteException, AuthenticationFailedException { + public UserDetailResponse lookupUser(String username, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, UserNotFoundException { + // The sample Data Adapter code uses 1:1 mapping of username to userId. In real implementation the userId usually differs from the username, so translation of username to user ID is required. + // If the user does not exist, return null values for user ID and organization ID. + // If user account account is blocked, return AccountStatus.NOT_ACTIVE as account status. + // The SCA login fakes SMS message delivery even for case when user ID is null to disallow fishing of usernames. + // For case when an error should appear instead, throw a UserNotFoundException. + return fetchUserDetail(username, organizationId, operationContext); + } + + @Override + public UserAuthenticationResponse authenticateUser(String userId, String password, AuthenticationContext authenticationContext, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException { // Here will be the real authentication - call to the backend providing authentication. - // In case that authentication fails, throw an AuthenticationFailedException. - if ("test".equals(password)) { + // Return a response with UserAuthenticationResult based on the actual authentication result. + // The password is optionally encrypted, the authentication context contains information about encryption. + // In case of combined user authentication with SMS authorization the authentication context contains information + // about result of SMS authorization. + PasswordProtectionType passwordProtection = authenticationContext.getPasswordProtection(); + UserAuthenticationResponse authResponse = new UserAuthenticationResponse(); + if (passwordProtection == PasswordProtectionType.NO_PROTECTION && "test".equals(password)) { try { - return fetchUserDetail(username); + UserDetailResponse userDetail = fetchUserDetail(userId, organizationId, operationContext); + // The organization needs to be set in response (e.g. client authenticated against RETAIL organization or SME organization). + userDetail.setOrganizationId(organizationId); + authResponse.setUserDetail(userDetail); + authResponse.setAuthenticationResult(UserAuthenticationResult.SUCCEEDED); + return authResponse; } catch (UserNotFoundException e) { - throw new AuthenticationFailedException("login.authenticationFailed"); + authResponse.setAuthenticationResult(UserAuthenticationResult.FAILED); + authResponse.setErrorMessage(AUTHENTICATION_FAILED); + return authResponse; } } - AuthenticationFailedException authFailedException = new AuthenticationFailedException("login.authenticationFailed"); - // Set number of remaining attempts for this userId in case it is available. - // authFailedException.setRemainingAttempts(5); + authResponse.setAuthenticationResult(UserAuthenticationResult.FAILED); + authResponse.setErrorMessage(AUTHENTICATION_FAILED); + // Set number of remaining attempts for this user ID in case it is available. + // authResponse.setRemainingAttempts(5); + + // To enable showing of remaining attempts for operation, use: + // authResponse.setShowRemainingAttempts(true); // Use the following code to let the user know that the account has been blocked temporarily. - // final AuthenticationFailedException authFailedException = new AuthenticationFailedException("login.authenticationBlocked"); - // authFailedException.setRemainingAttempts(0); + // authResponse.setErrorMessage("login.authenticationBlocked"); + // authResponse.setRemainingAttempts(0); - throw authFailedException; + return authResponse; } @Override - public UserDetailResponse fetchUserDetail(String userId) throws DataAdapterRemoteException, UserNotFoundException { + public UserDetailResponse fetchUserDetail(String userId, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, UserNotFoundException { // Fetch user details here ... // In case that user is not found, throw a UserNotFoundException. + // The operation context may be null in case the method is called outside of an active operation (e.g. OAuth user profile request). UserDetailResponse responseObject = new UserDetailResponse(); responseObject.setId(userId); responseObject.setGivenName("John"); responseObject.setFamilyName("Doe"); + responseObject.setOrganizationId(organizationId); + responseObject.setAccountStatus(AccountStatus.ACTIVE); return responseObject; } @Override - public DecorateOperationFormDataResponse decorateFormData(String userId, OperationContext operationContext) throws DataAdapterRemoteException, UserNotFoundException { + public DecorateOperationFormDataResponse decorateFormData(String userId, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, UserNotFoundException { String operationName = operationContext.getName(); FormData formData = operationContext.getFormData(); // Fetch bank account list for given user here from the bank backend. @@ -81,8 +120,8 @@ public DecorateOperationFormDataResponse decorateFormData(String userId, Operati // Replace mock bank account data with real data loaded from the bank backend. // In case the bank account selection is disabled, return an empty list. - if (!"authorize_payment".equals(operationName)) { - // return empty list for operations other than authorize_payment + if ((!"authorize_payment".equals(operationName) && !"authorize_payment_sca".equals(operationName))) { + // return empty list for operations other than authorize_payment and authorize_payment_sca return new DecorateOperationFormDataResponse(formData); } @@ -119,7 +158,7 @@ public DecorateOperationFormDataResponse decorateFormData(String userId, Operati List configs = formData.getConfig(); for (FormFieldConfig config: configs) { - if ("operation.bankAccountChoice".equals(config.getId())) { + if (BANK_ACCOUNT_CHOICE_ID.equals(config.getId())) { choiceEnabled = config.isEnabled(); // You should check the default value against list of available accounts. defaultValue = config.getDefaultValue(); @@ -135,89 +174,330 @@ public DecorateOperationFormDataResponse decorateFormData(String userId, Operati } @Override - public void formDataChangedNotification(String userId, FormDataChange change, OperationContext operationContext) throws DataAdapterRemoteException { + public void formDataChangedNotification(String userId, String organizationId, FormDataChange change, OperationContext operationContext) throws DataAdapterRemoteException { String operationId = operationContext.getId(); if (change instanceof BankAccountChoice) { // Handle bank account choice here (e.g. send notification to bank backend). BankAccountChoice bankAccountChoice = (BankAccountChoice) change; - logger.info("Bank account chosen: {}, operation ID: {}", new String[]{bankAccountChoice.getBankAccountId(), operationId}); + logger.info("Bank account chosen: {}, operation ID: {}", bankAccountChoice.getBankAccountId(), operationId); return; } else if (change instanceof AuthMethodChoice) { // Handle authorization method choice here (e.g. send notification to bank backend). AuthMethodChoice authMethodChoice = (AuthMethodChoice) change; - logger.info("Authorization method chosen: {}, operation ID: {}", new String[]{authMethodChoice.getChosenAuthMethod().toString(), operationId}); + logger.info("Authorization method chosen: {}, operation ID: {}", authMethodChoice.getChosenAuthMethod().toString(), operationId); return; } throw new IllegalStateException("Invalid change entity type: " + change.getType()); } @Override - public void operationChangedNotification(String userId, OperationChange change, OperationContext operationContext) throws DataAdapterRemoteException { + public void operationChangedNotification(String userId, String organizationId, OperationChange change, OperationContext operationContext) throws DataAdapterRemoteException { String operationId = operationContext.getId(); // Handle operation change here (e.g. send notification to bank backend). - logger.info("Operation changed, status: {}, operation ID: {}", new String[] {change.toString(), operationId}); + logger.info("Operation changed, status: {}, operation ID: {}", change.toString(), operationId); } @Override - public AuthorizationCode generateAuthorizationCode(String userId, OperationContext operationContext) throws InvalidOperationContextException { - String operationName = operationContext.getName(); - List digestItems = new ArrayList<>(); - switch (operationName) { - case "login": { - digestItems.add(operationName); - break; + public CreateSmsAuthorizationResponse createAndSendAuthorizationSms(String userId, String organizationId, AccountStatus accountStatus, OperationContext operationContext, String lang) throws InvalidOperationContextException, DataAdapterRemoteException { + CreateSmsAuthorizationResponse response = new CreateSmsAuthorizationResponse(); + // MessageId is generated as random UUID, it can be overridden to provide a real message identification + String messageId = UUID.randomUUID().toString(); + response.setMessageId(messageId); + + // Fake SMS message delivery for null user ID in case of non-existent account or blocked user account + if (userId == null || accountStatus != AccountStatus.ACTIVE) { + // Make sure that user cannot recognize that the SMS was not sent, even the result is sent as fake success + response.setSmsDeliveryResult(SmsDeliveryResult.SUCCEEDED); + return response; + } + + // Generate authorization code + AuthorizationCode authorizationCode = smsDeliveryService.generateAuthorizationCode(userId, organizationId, operationContext); + + // Generate message text, include previously generated authorization code + String messageText = smsDeliveryService.generateSmsText(userId, organizationId, operationContext, authorizationCode, lang); + + // Persist authorization SMS message + smsPersistenceService.createAuthorizationSms(userId, organizationId, messageId, operationContext, authorizationCode, messageText); + + // Send SMS with generated text to target user. + SmsDeliveryResult deliveryResult = smsDeliveryService.sendAuthorizationSms(userId, organizationId, messageId, messageText, operationContext); + response.setSmsDeliveryResult(deliveryResult); + if (!SmsDeliveryResult.SUCCEEDED.equals(deliveryResult)) { + response.setErrorMessage(SMS_DELIVERY_FAILED); + } + + // Return generated message ID + return response; + } + + @Override + public VerifySmsAuthorizationResponse verifyAuthorizationSms(String userId, String organizationId, AccountStatus accountStatus, String messageId, String authorizationCode, OperationContext operationContext) throws DataAdapterRemoteException, InvalidOperationContextException { + // You can override this logic in case more complex handling of SMS verification is required. + VerifySmsAuthorizationResponse response; + + // Skip credentials verification for non-existent user accounts or blocked user accounts, such request would always fail. + // Furthermore, do not leak information that account does not exist or it is blocked by providing a regular authentication error. + if (userId == null || accountStatus != AccountStatus.ACTIVE) { + response = new VerifySmsAuthorizationResponse(); + response.setSmsAuthorizationResult(SmsAuthorizationResult.SKIPPED); + response.setErrorMessage("login.authenticationFailed"); + return response; + } + + response = smsPersistenceService.verifyAuthorizationSms(messageId, authorizationCode, false); + // Set number of remaining attempts for verification in case it is available. + // authResponse.setRemainingAttempts(5); + // You can enable showing of remaining attempts for the operation. + // response.setShowRemainingAttempts(true); + return response; + } + + @Override + public VerifySmsAndPasswordResponse verifyAuthorizationSmsAndPassword(String userId, String organizationId, AccountStatus accountStatus, String messageId, String authorizationCode, OperationContext operationContext, AuthenticationContext authenticationContext, String password) throws DataAdapterRemoteException, InvalidOperationContextException { + VerifySmsAndPasswordResponse response = new VerifySmsAndPasswordResponse(); + + // Skip credentials verification for non-existent user accounts or blocked user accounts, such request would always fail. + // Furthermore, do not leak information that account does not exist or it is blocked by providing a regular authentication error. + if (userId == null || accountStatus != AccountStatus.ACTIVE) { + response.setSmsAuthorizationResult(SmsAuthorizationResult.SKIPPED); + response.setUserAuthenticationResult(UserAuthenticationResult.SKIPPED); + response.setErrorMessage("login.authenticationFailed"); + return response; + } + + // Verify authorization code from SMS + VerifySmsAuthorizationResponse smsResponse = smsPersistenceService.verifyAuthorizationSms(messageId, authorizationCode, true); + authenticationContext.setSmsAuthorizationResult(smsResponse.getSmsAuthorizationResult()); + + // Authenticate user + UserAuthenticationResponse authResponse = authenticateUser(userId, password, authenticationContext, organizationId, operationContext); + + // Create aggregate response + response.setSmsAuthorizationResult(smsResponse.getSmsAuthorizationResult()); + response.setUserAuthenticationResult(authResponse.getAuthenticationResult()); + if (smsResponse.getSmsAuthorizationResult() != SmsAuthorizationResult.SUCCEEDED + || authResponse.getAuthenticationResult() != UserAuthenticationResult.SUCCEEDED) { + // Provide an error message which does not allow to find out reason of failed verification. + response.setErrorMessage(AUTHENTICATION_FAILED); + } + // Optionally set the number of remaining attempts, e.g. using lower of the two remaining attempt counts. + // response.setRemainingAttempts(Math.min(smsResponse.getRemainingAttempts(), authResponse.getRemainingAttempts())); + // You can enable showing of remaining attempts for the operation. + // response.setShowRemainingAttempts(true); + return response; + } + + @Override + public InitConsentFormResponse initConsentForm(String userId, String organizationId, OperationContext operationContext) throws DataAdapterRemoteException, InvalidOperationContextException { + // Override this logic in case consent form should be displayed conditionally for given operation context. + return new InitConsentFormResponse(true); + } + + @Override + public CreateConsentFormResponse createConsentForm(String userId, String organizationId, OperationContext operationContext, String lang) throws DataAdapterRemoteException, InvalidOperationContextException { + // Fallback to English for unsupported languages, see: https://github.com/wultra/powerauth-webflow-customization/issues/104 + if (!"cs".equals(lang) && !"en".equals(lang)) { + lang = "en"; + } + // Generate response with consent text and options based on requested language. + if ("login".equals(operationContext.getName()) || "login_sca".equals(operationContext.getName())) { + // Create default consent + CreateConsentFormResponse response = new CreateConsentFormResponse(); + if ("cs".equals(lang)) { + response.setConsentHtml("Tímto potvrzuji, že jsem inicioval tuto žádost o přihlášení a souhlasím s dokončením této operace."); + } else { + response.setConsentHtml("I consent that I have initiated this authentication request and give consent to complete the operation.

"); } - case "authorize_payment": { - AmountAttribute amountAttribute = operationValueExtractionService.getAmount(operationContext); - String account = operationValueExtractionService.getAccount(operationContext); - BigDecimal amount = amountAttribute.getAmount(); - String currency = amountAttribute.getCurrency(); - digestItems.add(amount.toPlainString()); - digestItems.add(currency); - digestItems.add(account); - break; + + ConsentOption option1 = new ConsentOption(); + option1.setId("CONSENT_LOGIN"); + option1.setRequired(true); + if ("cs".equals(lang)) { + option1.setDescriptionHtml("Souhlasím s dokončením operace pro přihlášení."); + } else { + option1.setDescriptionHtml("I give consent to complete the authentication operation."); } - // Add new operations here. - default: - throw new InvalidOperationContextException("Unsupported operation: " + operationName); + + response.getOptions().add(option1); + return response; } + if ("authorize_payment".equals(operationContext.getName()) || "authorize_payment_sca".equals(operationContext.getName())) { + CreateConsentFormResponse response = new CreateConsentFormResponse(); + if ("cs".equals(lang)) { + response.setConsentHtml("Tímto potvrzuji, že jsem inicioval tuto platební operaci a souhlasím s jejím dokončením."); + } else { + response.setConsentHtml("I consent that I have initiated this payment request and give consent to complete the operation."); + } + + ConsentOption option1 = new ConsentOption(); + option1.setId("CONSENT_INIT"); + option1.setRequired(true); + if ("cs".equals(lang)) { + option1.setDescriptionHtml("Potvrzuji, že jsem inicioval tuto platební operaci."); + } else { + option1.setDescriptionHtml("I consent that I have initiated this payment operation."); + } - final DataDigest.Result digestResult = new DataDigest().generateDigest(digestItems); - if (digestResult == null) { - throw new InvalidOperationContextException("Digest generation failed"); + ConsentOption option2 = new ConsentOption(); + option2.setId("CONSENT_PAYMENT"); + option2.setRequired(true); + if ("cs".equals(lang)) { + option2.setDescriptionHtml("Souhlasím s provedením platební operace."); + } else { + option2.setDescriptionHtml("I give consent to complete this payment operation."); + } + + response.getOptions().add(option1); + response.getOptions().add(option2); + return response; } - return new AuthorizationCode(digestResult.getDigest(), digestResult.getSalt()); + throw new InvalidOperationContextException("Invalid operation context"); } @Override - public String generateSMSText(String userId, OperationContext operationContext, AuthorizationCode authorizationCode, String lang) throws InvalidOperationContextException { - String operationName = operationContext.getName(); - String[] messageArgs; - switch (operationName) { - case "login": { - messageArgs = new String[]{authorizationCode.getCode()}; - break; + public ValidateConsentFormResponse validateConsentForm(String userId, String organizationId, OperationContext operationContext, String lang, List options) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException { + // Fallback to English for unsupported languages, see: https://github.com/wultra/powerauth-webflow-customization/issues/104 + if (!"cs".equals(lang) && !"en".equals(lang)) { + lang = "en"; + } + // Validate consent form options and return response with result of validation and optional error messages. + ValidateConsentFormResponse response = new ValidateConsentFormResponse(); + if (options == null || options.isEmpty()) { + throw new InvalidConsentDataException("Missing options for consent"); + } + if ("login".equals(operationContext.getName()) || "login_sca".equals(operationContext.getName())) { + if (options.size() != 1) { + throw new InvalidConsentDataException("Unexpected options count for consent"); } - case "authorize_payment": { - AmountAttribute amountAttribute = operationValueExtractionService.getAmount(operationContext); - String account = operationValueExtractionService.getAccount(operationContext); - BigDecimal amount = amountAttribute.getAmount(); - String currency = amountAttribute.getCurrency(); - messageArgs = new String[]{amount.toPlainString(), currency, account, authorizationCode.getCode()}; - break; + // Validate default consent + if (options.get(0).getValue() == ConsentOptionValue.CHECKED) { + response.setConsentValidationPassed(true); + return response; + } + response.setConsentValidationPassed(false); + if ("cs".equals(lang)) { + response.setValidationErrorMessage("Prosím vyplňte celý formulář se souhlasem."); + if (options.get(0).getValue() != ConsentOptionValue.CHECKED) { + ConsentOptionValidationResult result = new ConsentOptionValidationResult(); + result.setId("CONSENT_LOGIN"); + result.setValidationPassed(false); + result.setErrorMessage("Pro dokončení operace odsouhlaste tuto volbu."); + response.getOptionValidationResults().add(result); + } + } else { + response.setValidationErrorMessage("Please fill in the whole consent form."); + if (options.get(0).getValue() != ConsentOptionValue.CHECKED) { + ConsentOptionValidationResult result = new ConsentOptionValidationResult(); + result.setId("CONSENT_LOGIN"); + result.setValidationPassed(false); + result.setErrorMessage("Confirm this option to complete the operation."); + response.getOptionValidationResults().add(result); + } } - // Add new operations here. - default: - throw new InvalidOperationContextException("Unsupported operation: " + operationName); + return response; } + if ("authorize_payment".equals(operationContext.getName()) || "authorize_payment_sca".equals(operationContext.getName())) { + if (options.size() != 2) { + throw new InvalidConsentDataException("Unexpected options count for consent"); + } + if (options.get(0).getValue() == ConsentOptionValue.CHECKED && options.get(1).getValue() == ConsentOptionValue.CHECKED) { + response.setConsentValidationPassed(true); + return response; + } + response.setConsentValidationPassed(false); + if ("cs".equals(lang)) { + response.setValidationErrorMessage("Prosím vyplňte celý formulář se souhlasem."); + if (options.get(0).getValue() != ConsentOptionValue.CHECKED) { + ConsentOptionValidationResult result = new ConsentOptionValidationResult(); + result.setId("CONSENT_INIT"); + result.setValidationPassed(false); + result.setErrorMessage("Pro dokončení operace odsouhlaste tuto volbu."); + response.getOptionValidationResults().add(result); + } + if (options.get(1).getValue() != ConsentOptionValue.CHECKED) { + ConsentOptionValidationResult result = new ConsentOptionValidationResult(); + result.setId("CONSENT_PAYMENT"); + result.setValidationPassed(false); + result.setErrorMessage("Pro dokončení operace odsouhlaste tuto volbu."); + response.getOptionValidationResults().add(result); + } + } else { + response.setValidationErrorMessage("Please fill in the whole consent form."); + if (options.get(0).getValue() != ConsentOptionValue.CHECKED) { + ConsentOptionValidationResult result = new ConsentOptionValidationResult(); + result.setId("CONSENT_INIT"); + result.setValidationPassed(false); + result.setErrorMessage("Confirm this option to complete the operation."); + response.getOptionValidationResults().add(result); + } + if (options.get(1).getValue() != ConsentOptionValue.CHECKED) { + ConsentOptionValidationResult result = new ConsentOptionValidationResult(); + result.setId("CONSENT_PAYMENT"); + result.setValidationPassed(false); + result.setErrorMessage("Confirm this option to complete the operation."); + response.getOptionValidationResults().add(result); + } + } + return response; + } + throw new InvalidOperationContextException("Invalid operation context"); + } - return dataAdapterI18NService.messageSource().getMessage(operationName + ".smsText", messageArgs, new Locale(lang)); + @Override + public SaveConsentFormResponse saveConsentForm(String userId, String organizationId, OperationContext operationContext, List options) throws DataAdapterRemoteException, InvalidOperationContextException, InvalidConsentDataException { + // Save consent form options selected by the user. The sample implementation only logs the selected options. + logger.info("Saving consent form for user: {}, operation ID: {}", userId, operationContext.getId()); + for (ConsentOption option: options) { + logger.info("Option {}: {}", option.getId(), option.getValue()); + } + return new SaveConsentFormResponse(true); } @Override - public void sendAuthorizationSMS(String userId, String messageText, OperationContext operationContext) throws DataAdapterRemoteException, SMSAuthorizationFailedException { - // Add here code to send the SMS OTP message to user identified by userId with messageText. - // In case message delivery fails, throw an SMSAuthorizationFailedException. + public AfsResponse executeAfsAction(String userId, String organizationId, OperationContext operationContext, AfsRequestParameters afsRequestParameters, Map extras) throws DataAdapterRemoteException, InvalidOperationContextException { + if (userId == null || organizationId == null || operationContext == null || afsRequestParameters == null + || afsRequestParameters.getAfsAction() == null || afsRequestParameters.getAfsType() == null) { + logger.warn("Invalid AFS request received"); + throw new InvalidOperationContextException(INVALID_REQUEST); + } + + // Call anti-fraud system and return response for Web Flow. In default implementation of Data Adapter + // a mocked response is returned with static 2FA AFS label except for the case of payment with low amount. + AfsResponse response = new AfsResponse(); + switch (afsRequestParameters.getAfsAction()) { + case LOGIN_INIT: + case LOGIN_AUTH: + case APPROVAL_AUTH: + // Return AFS label, but do not apply response parameters on authentication form + response.setAfsResponseApplied(false); + response.setAfsLabel("2FA"); + break; + + case APPROVAL_INIT: + // Apply AFS response parameters on authentication form. + // This example performs step-down from 2FA to 1FA in case of payment in CZK with low amount. + AmountAttribute amountAttr = operationContext.getFormData().getAmount(); + if (amountAttr.getCurrency().equals("CZK") && amountAttr.getAmount().intValue() < 500) { + // Disable password verification for low amounts + response.setAfsResponseApplied(true); + response.setAfsLabel("1FA"); + response.getAuthStepOptions().setPasswordRequired(false); + response.getAuthStepOptions().setSmsOtpRequired(true); + } else { + // For higher amounts keep the password verification + response.setAfsResponseApplied(false); + response.setAfsLabel("2FA"); + } + break; + + case LOGOUT: + // Do not apply response parameters + response.setAfsResponseApplied(false); + break; + + } + return response; } } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/SmsDeliveryService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/SmsDeliveryService.java new file mode 100644 index 00000000..3bea4393 --- /dev/null +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/service/SmsDeliveryService.java @@ -0,0 +1,149 @@ +/* + * Copyright 2019 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getlime.security.powerauth.app.dataadapter.impl.service; + +import io.getlime.security.powerauth.app.dataadapter.exception.DataAdapterRemoteException; +import io.getlime.security.powerauth.app.dataadapter.exception.InvalidOperationContextException; +import io.getlime.security.powerauth.app.dataadapter.service.DataAdapterI18NService; +import io.getlime.security.powerauth.crypto.server.util.DataDigest; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.AuthorizationCode; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.attribute.AmountAttribute; +import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.SmsDeliveryResult; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Service for preparing and delivering SMS messages. + * + * @author Roman Strobl, roman.strobl@wultra.com + */ +@Service +public class SmsDeliveryService { + + private final DataAdapterI18NService dataAdapterI18NService; + private final OperationValueExtractionService operationValueExtractionService; + + /** + * Service constructor. + * @param dataAdapterI18NService I18N service. + * @param operationValueExtractionService Service for extracting values from operation. + */ + public SmsDeliveryService(DataAdapterI18NService dataAdapterI18NService, OperationValueExtractionService operationValueExtractionService) { + this.dataAdapterI18NService = dataAdapterI18NService; + this.operationValueExtractionService = operationValueExtractionService; + } + + /** + * Generate authorization code for SMS authorization. + * @param userId User ID. + * @param organizationId Organization ID. + * @param operationContext Operation context. + * @return Authorization code. + * @throws InvalidOperationContextException Thrown when operation context is invalid. + * @throws DataAdapterRemoteException Thrown when remote communication fails. + */ + public AuthorizationCode generateAuthorizationCode(String userId, String organizationId, OperationContext operationContext) throws InvalidOperationContextException, DataAdapterRemoteException { + String operationName = operationContext.getName(); + List digestItems = new ArrayList<>(); + switch (operationName) { + case "login": + case "login_sca": { + digestItems.add(operationName); + break; + } + case "authorize_payment": + case "authorize_payment_sca": { + AmountAttribute amountAttribute = operationValueExtractionService.getAmount(operationContext); + String account = operationValueExtractionService.getAccount(operationContext); + BigDecimal amount = amountAttribute.getAmount(); + String currency = amountAttribute.getCurrency(); + digestItems.add(amount.toPlainString()); + digestItems.add(currency); + digestItems.add(account); + break; + } + // Add new operations here. + default: + throw new InvalidOperationContextException("Unsupported operation: " + operationName); + } + + final DataDigest.Result digestResult = new DataDigest().generateDigest(digestItems); + if (digestResult == null) { + throw new InvalidOperationContextException("Digest generation failed"); + } + return new AuthorizationCode(digestResult.getDigest(), digestResult.getSalt()); + } + + /** + * Generate text for SMS authorization message. + * @param userId User ID. + * @param organizationId Organization ID. + * @param operationContext Operation context. + * @param authorizationCode Authorization code. + * @param lang Language for localization. + * @return Generated SMS text with authorization code. + * @throws InvalidOperationContextException Thrown when operation context is invalid. + * @throws DataAdapterRemoteException Thrown when remote communication fails. + */ + public String generateSmsText(String userId, String organizationId, OperationContext operationContext, AuthorizationCode authorizationCode, String lang) throws InvalidOperationContextException, DataAdapterRemoteException { + String operationName = operationContext.getName(); + String[] messageArgs; + switch (operationName) { + case "login": + case "login_sca": { + messageArgs = new String[]{authorizationCode.getCode()}; + break; + } + case "authorize_payment": + case "authorize_payment_sca": { + AmountAttribute amountAttribute = operationValueExtractionService.getAmount(operationContext); + String account = operationValueExtractionService.getAccount(operationContext); + BigDecimal amount = amountAttribute.getAmount(); + String currency = amountAttribute.getCurrency(); + messageArgs = new String[]{amount.toPlainString(), currency, account, authorizationCode.getCode()}; + break; + } + // Add new operations here. + default: + throw new InvalidOperationContextException("Unsupported operation: " + operationName); + } + + return dataAdapterI18NService.messageSource().getMessage(operationName + ".smsText", messageArgs, new Locale(lang)); + } + + /** + * Send an authorization SMS with generated authorization code. + * @param userId User ID. + * @param organizationId Organization ID. + * @param messageId Message ID. + * @param messageText Text of SMS message. + * @param operationContext Operation context. + * @throws InvalidOperationContextException Thrown when operation context is invalid. + * @throws DataAdapterRemoteException Thrown when remote communication fails or SMS message could not be delivered. + */ + public SmsDeliveryResult sendAuthorizationSms(String userId, String organizationId, String messageId, String messageText, OperationContext operationContext) throws InvalidOperationContextException, DataAdapterRemoteException { + // Add here code to send the SMS OTP message to user identified by userId with messageText. + // The message entity can be extracted using message ID from table da_sms_authorization. + // In case message delivery fails, throw a DataAdapterRemoteException. + return SmsDeliveryResult.SUCCEEDED; + } + +} diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java index 29e926bb..3cb450a8 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/AuthenticationRequestValidator.java @@ -16,9 +16,11 @@ package io.getlime.security.powerauth.app.dataadapter.impl.validation; import io.getlime.core.rest.model.base.request.ObjectRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.AuthenticationContext; import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; -import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.AuthenticationType; -import io.getlime.security.powerauth.lib.dataadapter.model.request.AuthenticationRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.PasswordProtectionType; +import io.getlime.security.powerauth.lib.dataadapter.model.request.UserAuthenticationRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.UserLookupRequest; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; @@ -27,7 +29,7 @@ import org.springframework.validation.Validator; /** - * Defines validations for input fields in authentication requests. + * Defines validations for input fields in user lookup and authentication requests. * * Additional validation logic can be added if applicable. * @@ -36,6 +38,11 @@ @Component public class AuthenticationRequestValidator implements Validator { + private static final String OPERATION_CONTEXT_FIELD = "requestObject.operationContext"; + private static final String MISSING_OPERATION_CONTEXT_ERROR_CODE = "operationContext.missing"; + private static final String PASS_FIELD = "requestObject.password"; + private static final String ORGANIZATION_ID_FIELD = "requestObject.organizationId"; + /** * Return whether validator can validate given class. * @param clazz Validated class. @@ -52,36 +59,77 @@ public boolean supports(@NonNull Class clazz) { * @param errors Errors object. */ @Override - @SuppressWarnings("unchecked") public void validate(@Nullable Object o, @NonNull Errors errors) { - ObjectRequest requestObject = (ObjectRequest) o; - if (requestObject == null) { - errors.rejectValue("requestObject.operationContext", "operationContext.missing"); + ObjectRequest objectRequest = (ObjectRequest) o; + if (objectRequest == null) { + errors.rejectValue(OPERATION_CONTEXT_FIELD, MISSING_OPERATION_CONTEXT_ERROR_CODE); return; } - - AuthenticationRequest authRequest = requestObject.getRequestObject(); + if (objectRequest.getRequestObject() instanceof UserLookupRequest) { + validateUserLookupRequest(objectRequest, errors); + } else if (objectRequest.getRequestObject() instanceof UserAuthenticationRequest) { + validateUserAuthenticationRequest(objectRequest, errors); + } + } + + private void validateUserLookupRequest(ObjectRequest objectRequest, Errors errors) { + UserLookupRequest authRequest = (UserLookupRequest) objectRequest.getRequestObject(); // update validation logic based on the real Data Adapter requirements String username = authRequest.getUsername(); - String password = authRequest.getPassword(); + String organizationId = authRequest.getOrganizationId(); OperationContext operationContext = authRequest.getOperationContext(); if (operationContext == null) { - errors.rejectValue("requestObject.operationContext", "operationContext.missing"); + errors.rejectValue(OPERATION_CONTEXT_FIELD, MISSING_OPERATION_CONTEXT_ERROR_CODE); } ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.username", "login.username.empty"); - if (username!=null && username.length() > 30) { + if (username != null && username.length() > 30) { errors.rejectValue("requestObject.username", "login.username.long"); } - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.password", "login.password.empty"); - if (password!=null && password.length() > 30) { - errors.rejectValue("requestObject.password", "login.password.long"); + ValidationUtils.rejectIfEmptyOrWhitespace(errors, ORGANIZATION_ID_FIELD, "login.organizationId.empty"); + if (organizationId != null && organizationId.length() > 256) { + errors.rejectValue(ORGANIZATION_ID_FIELD, "login.organizationId.long"); + } + } + + private void validateUserAuthenticationRequest(ObjectRequest objectRequest, Errors errors) { + UserAuthenticationRequest authRequest = (UserAuthenticationRequest) objectRequest.getRequestObject(); + + // update validation logic based on the real Data Adapter requirements + String userId = authRequest.getUserId(); + String password = authRequest.getPassword(); + String organizationId = authRequest.getOrganizationId(); + OperationContext operationContext = authRequest.getOperationContext(); + if (operationContext == null) { + errors.rejectValue(OPERATION_CONTEXT_FIELD, MISSING_OPERATION_CONTEXT_ERROR_CODE); + } + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.userId", "login.userId.empty"); + if (userId != null && userId.length() > 30) { + errors.rejectValue("requestObject.userId", "login.userId.long"); + } + + ValidationUtils.rejectIfEmptyOrWhitespace(errors, PASS_FIELD, "login.password.empty"); + AuthenticationContext authenticationContext = authRequest.getAuthenticationContext(); + PasswordProtectionType passwordProtection = authenticationContext.getPasswordProtection(); + if (passwordProtection == PasswordProtectionType.NO_PROTECTION) { + if (password != null && password.length() > 30) { + errors.rejectValue(PASS_FIELD, "login.password.long"); + } + } else { + // Allow longer values in password field when password is encrypted + if (password != null && password.length() > 256) { + errors.rejectValue(PASS_FIELD, "login.password.long"); + } + } + + ValidationUtils.rejectIfEmptyOrWhitespace(errors, ORGANIZATION_ID_FIELD, "login.organizationId.empty"); + if (userId != null && organizationId.length() > 256) { + errors.rejectValue(ORGANIZATION_ID_FIELD, "login.organizationId.long"); } - AuthenticationType authType = authRequest.getType(); - if (authType != AuthenticationType.BASIC) { - errors.rejectValue("requestObject.type", "login.type.unsupported"); + if (passwordProtection != PasswordProtectionType.NO_PROTECTION && passwordProtection != PasswordProtectionType.PASSWORD_ENCRYPTION_AES) { + errors.rejectValue("requestObject.authenticationContext", "login.type.unsupported"); } } } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java new file mode 100644 index 00000000..76002fc8 --- /dev/null +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/ConsentFormRequestValidator.java @@ -0,0 +1,141 @@ +/* + * Copyright 2017 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getlime.security.powerauth.app.dataadapter.impl.validation; + +import io.getlime.core.rest.model.base.request.ObjectRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.ConsentOption; +import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; +import io.getlime.security.powerauth.lib.dataadapter.model.request.CreateConsentFormRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.InitConsentFormRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.SaveConsentFormRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.ValidateConsentFormRequest; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import org.springframework.validation.Errors; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; + +import java.util.List; + +/** + * Validator for request to create OAuth 2.0 consent form. + * + * Additional validation logic can be added if applicable. + * + * @author Roman Strobl, roman.strobl@wultra.com + */ +@Component +public class ConsentFormRequestValidator implements Validator { + + private static final String INVALID_REQUEST_MESSAGE = "consent.invalidRequest"; + + /** + * Return whether validator can validate given class. + * @param clazz Validated class. + * @return Whether validator can validate given class. + */ + @Override + public boolean supports(@NonNull Class clazz) { + return ObjectRequest.class.isAssignableFrom(clazz); + } + + /** + * Validate object and add validation errors. + * @param o Validated object. + * @param errors Errors object. + */ + @Override + @SuppressWarnings("unchecked") + public void validate(@Nullable Object o, @NonNull Errors errors) { + ObjectRequest objectRequest = (ObjectRequest) o; + if (objectRequest == null) { + errors.rejectValue("requestObject.operationContext", "operationContext.missing"); + return; + } + + // update validation logic based on the real Data Adapter requirements + if (objectRequest.getRequestObject() instanceof InitConsentFormRequest) { + ObjectRequest requestObject = (ObjectRequest) o; + InitConsentFormRequest request = requestObject.getRequestObject(); + validateOperationContext(request.getOperationContext(), errors); + validateUserId(request.getUserId(), errors); + if (request.getOperationContext() != null) { + validateOperationName(request.getOperationContext().getName(), errors); + } + } else if (objectRequest.getRequestObject() instanceof CreateConsentFormRequest) { + ObjectRequest requestObject = (ObjectRequest) o; + CreateConsentFormRequest request = requestObject.getRequestObject(); + validateOperationContext(request.getOperationContext(), errors); + validateUserId(request.getUserId(), errors); + if (request.getOperationContext() != null) { + validateOperationName(request.getOperationContext().getName(), errors); + } + validateLanguage(errors); + } else if (objectRequest.getRequestObject() instanceof ValidateConsentFormRequest) { + ObjectRequest requestObject = (ObjectRequest) o; + ValidateConsentFormRequest request = requestObject.getRequestObject(); + validateOperationContext(request.getOperationContext(), errors); + validateUserId(request.getUserId(), errors); + if (request.getOperationContext() != null) { + validateOperationName(request.getOperationContext().getName(), errors); + } + validateLanguage(errors); + validateOptions(request.getOptions(), errors); + } else if (objectRequest.getRequestObject() instanceof SaveConsentFormRequest) { + ObjectRequest requestObject = (ObjectRequest) o; + SaveConsentFormRequest request = requestObject.getRequestObject(); + validateOperationContext(request.getOperationContext(), errors); + validateUserId(request.getUserId(), errors); + if (request.getOperationContext() != null) { + validateOperationName(request.getOperationContext().getName(), errors); + } + validateOptions(request.getOptions(), errors); + } + } + + private void validateOperationContext(OperationContext operationContext, Errors errors) { + if (operationContext == null) { + errors.rejectValue("requestObject.operationContext", "operationContext.missing"); + } + } + + private void validateUserId(String userId, Errors errors) { + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.userId", INVALID_REQUEST_MESSAGE); + if (userId != null && userId.length() > 30) { + errors.rejectValue("requestObject.userId", INVALID_REQUEST_MESSAGE); + } + } + + private void validateOperationName(String operationName, Errors errors) { + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.operationContext.name", INVALID_REQUEST_MESSAGE); + if (operationName != null && operationName.length() > 32) { + errors.rejectValue("requestObject.operationContext.name", INVALID_REQUEST_MESSAGE); + } + } + + private void validateLanguage(Errors errors) { + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.lang", INVALID_REQUEST_MESSAGE); + // Do not validate lang parameter, fallback to "en" instead, see: https://github.com/wultra/powerauth-webflow-customization/issues/104 + } + + private void validateOptions(List options, Errors errors) { + // Allow empty options, but do not allow null value + if (options == null) { + errors.rejectValue("requestObject.options", INVALID_REQUEST_MESSAGE); + } + } +} diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSMSAuthorizationRequestValidator.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSmsAuthorizationRequestValidator.java similarity index 51% rename from powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSMSAuthorizationRequestValidator.java rename to powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSmsAuthorizationRequestValidator.java index 690047a5..24e36f9a 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSMSAuthorizationRequestValidator.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/impl/validation/CreateSmsAuthorizationRequestValidator.java @@ -20,7 +20,7 @@ import io.getlime.security.powerauth.app.dataadapter.impl.service.OperationValueExtractionService; import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; import io.getlime.security.powerauth.lib.dataadapter.model.entity.attribute.AmountAttribute; -import io.getlime.security.powerauth.lib.dataadapter.model.request.CreateSMSAuthorizationRequest; +import io.getlime.security.powerauth.lib.dataadapter.model.request.CreateSmsAuthorizationRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; @@ -39,16 +39,19 @@ * @author Roman Strobl, roman.strobl@wultra.com */ @Component -public class CreateSMSAuthorizationRequestValidator implements Validator { +public class CreateSmsAuthorizationRequestValidator implements Validator { - private OperationValueExtractionService operationValueExtractionService; + private static final String OPERATION_CONTEXT_FIELD = "requestObject.operationContext"; + private static final String AMOUNT_EMPTY_ERROR_CODE = "smsAuthorization.amount.empty"; + + private final OperationValueExtractionService operationValueExtractionService; /** * Validator constructor. * @param operationValueExtractionService Operation form data service. */ @Autowired - public CreateSMSAuthorizationRequestValidator(OperationValueExtractionService operationValueExtractionService) { + public CreateSmsAuthorizationRequestValidator(OperationValueExtractionService operationValueExtractionService) { this.operationValueExtractionService = operationValueExtractionService; } @@ -70,73 +73,87 @@ public boolean supports(@NonNull Class clazz) { @Override @SuppressWarnings("unchecked") public void validate(@Nullable Object o, @NonNull Errors errors) { - ObjectRequest requestObject = (ObjectRequest) o; + ObjectRequest requestObject = (ObjectRequest) o; if (requestObject == null) { - errors.rejectValue("requestObject.operationContext", "operationContext.missing"); + errors.rejectValue(OPERATION_CONTEXT_FIELD, "operationContext.missing"); return; } - CreateSMSAuthorizationRequest authRequest = requestObject.getRequestObject(); + CreateSmsAuthorizationRequest authRequest = requestObject.getRequestObject(); // update validation logic based on the real Data Adapter requirements String userId = authRequest.getUserId(); + String organizationId = authRequest.getOrganizationId(); OperationContext operationContext = authRequest.getOperationContext(); if (operationContext == null) { - errors.rejectValue("requestObject.operationContext", "operationContext.missing"); + errors.rejectValue(OPERATION_CONTEXT_FIELD, "operationContext.missing"); } String operationName = authRequest.getOperationContext().getName(); - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.userId", "smsAuthorization.userId.empty"); + // Allow null user ID for case when fake SMS message is sent if (userId != null && userId.length() > 30) { errors.rejectValue("requestObject.userId", "smsAuthorization.userId.long"); } + // Allow null organization ID for case when fake SMS message is sent + if (organizationId != null && organizationId.length() > 256) { + errors.rejectValue("requestObject.organizationId", "smsAuthorization.organizationId.long"); + } + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "requestObject.operationContext.name", "smsAuthorization.operationName.empty"); if (operationName != null && operationName.length() > 32) { errors.rejectValue("requestObject.operationContext.name", "smsAuthorization.operationName.long"); } - if (operationName != null) { - switch (operationName) { - case "login": - // no field validation required - break; - case "authorize_payment": - AmountAttribute amountAttribute; - try { - amountAttribute = operationValueExtractionService.getAmount(authRequest.getOperationContext()); - if (amountAttribute == null) { - errors.rejectValue("requestObject.operationContext", "smsAuthorization.amount.empty"); - } else { - BigDecimal amount = amountAttribute.getAmount(); - String currency = amountAttribute.getCurrency(); + if (operationName == null) { + return; + } + + switch (operationName) { + case "login": + case "login_sca": + // no field validation required + break; + case "authorize_payment": + case "authorize_payment_sca": + validateFieldsForPayment(authRequest, errors); + break; + default: + throw new IllegalStateException("Unsupported operation in validator: " + operationName); + } + } + + private void validateFieldsForPayment(CreateSmsAuthorizationRequest authRequest, Errors errors) { + AmountAttribute amountAttribute; + try { + amountAttribute = operationValueExtractionService.getAmount(authRequest.getOperationContext()); + if (amountAttribute == null) { + errors.rejectValue(OPERATION_CONTEXT_FIELD, AMOUNT_EMPTY_ERROR_CODE); + } else { + BigDecimal amount = amountAttribute.getAmount(); + String currency = amountAttribute.getCurrency(); - if (amount == null) { - errors.rejectValue("requestObject.operationContext", "smsAuthorization.amount.empty"); - } else if (amount.doubleValue() <= 0) { - errors.rejectValue("requestObject.operationContext", "smsAuthorization.amount.invalid"); - } + if (amount == null) { + errors.rejectValue(OPERATION_CONTEXT_FIELD, AMOUNT_EMPTY_ERROR_CODE); + } else if (amount.doubleValue() <= 0) { + errors.rejectValue(OPERATION_CONTEXT_FIELD, "smsAuthorization.amount.invalid"); + } - if (currency == null || currency.isEmpty()) { - errors.rejectValue("requestObject.operationContext", "smsAuthorization.currency.empty"); - } - } - } catch (InvalidOperationContextException ex) { - errors.rejectValue("requestObject.operationContext", "smsAuthorization.amount.empty"); - } - String account; - try { - account = operationValueExtractionService.getAccount(authRequest.getOperationContext()); - if (account == null || account.isEmpty()) { - errors.rejectValue("requestObject.operationContext", "smsAuthorization.account.empty"); - } - } catch (InvalidOperationContextException ex) { - errors.rejectValue("requestObject.operationContext", "smsAuthorization.account.empty"); - } - break; - default: - throw new IllegalStateException("Unsupported operation in validator: " + operationName); + if (currency == null || currency.isEmpty()) { + errors.rejectValue(OPERATION_CONTEXT_FIELD, "smsAuthorization.currency.empty"); + } + } + } catch (InvalidOperationContextException ex) { + errors.rejectValue(OPERATION_CONTEXT_FIELD, AMOUNT_EMPTY_ERROR_CODE); + } + String account; + try { + account = operationValueExtractionService.getAccount(authRequest.getOperationContext()); + if (account == null || account.isEmpty()) { + errors.rejectValue(OPERATION_CONTEXT_FIELD, "smsAuthorization.account.empty"); } + } catch (InvalidOperationContextException ex) { + errors.rejectValue(OPERATION_CONTEXT_FIELD, "smsAuthorization.account.empty"); } } } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/SMSAuthorizationRepository.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/SmsAuthorizationRepository.java similarity index 82% rename from powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/SMSAuthorizationRepository.java rename to powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/SmsAuthorizationRepository.java index 13110e6b..1506b249 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/SMSAuthorizationRepository.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/SmsAuthorizationRepository.java @@ -15,9 +15,9 @@ */ package io.getlime.security.powerauth.app.dataadapter.repository; -import io.getlime.security.powerauth.app.dataadapter.repository.model.entity.SMSAuthorizationEntity; +import io.getlime.security.powerauth.app.dataadapter.repository.model.entity.SmsAuthorizationEntity; import org.springframework.data.repository.CrudRepository; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Repository; /** @@ -25,7 +25,7 @@ * * @author Roman Strobl, roman.strobl@wultra.com */ -@Component -public interface SMSAuthorizationRepository extends CrudRepository { +@Repository +public interface SmsAuthorizationRepository extends CrudRepository { } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/model/entity/SMSAuthorizationEntity.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/model/entity/SmsAuthorizationEntity.java similarity index 92% rename from powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/model/entity/SMSAuthorizationEntity.java rename to powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/model/entity/SmsAuthorizationEntity.java index c953ed38..c40dea6f 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/model/entity/SMSAuthorizationEntity.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/repository/model/entity/SmsAuthorizationEntity.java @@ -29,7 +29,7 @@ */ @Entity @Table(name = "da_sms_authorization") -public class SMSAuthorizationEntity implements Serializable { +public class SmsAuthorizationEntity implements Serializable { private static final long serialVersionUID = 6432269422572862762L; @@ -43,6 +43,9 @@ public class SMSAuthorizationEntity implements Serializable { @Column(name = "user_id") private String userId; + @Column(name = "organization_id") + private String organizationId; + @Column(name = "operation_name") private String operationName; @@ -118,6 +121,22 @@ public void setUserId(String userId) { this.userId = userId; } + /** + * Get organization ID. + * @return Organization ID. + */ + public String getOrganizationId() { + return organizationId; + } + + /** + * Set organization ID. + * @param organizationId Organization ID. + */ + public void setOrganizationId(String organizationId) { + this.organizationId = organizationId; + } + /** * Get operation name. * @return Operation name. @@ -276,7 +295,7 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - SMSAuthorizationEntity that = (SMSAuthorizationEntity) o; + SmsAuthorizationEntity that = (SmsAuthorizationEntity) o; return messageId.equals(that.messageId); } diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/DataAdapterI18NService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/DataAdapterI18NService.java index 3699643e..8992d8d9 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/DataAdapterI18NService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/DataAdapterI18NService.java @@ -28,13 +28,6 @@ @Service public class DataAdapterI18NService { - - /** - * Data adapter I18N service constructor. - */ - public DataAdapterI18NService() { - } - /** * Get message source with i18n data. * diff --git a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SMSPersistenceService.java b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SmsPersistenceService.java similarity index 56% rename from powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SMSPersistenceService.java rename to powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SmsPersistenceService.java index 20eb9759..d28549d5 100644 --- a/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SMSPersistenceService.java +++ b/powerauth-data-adapter/src/main/java/io/getlime/security/powerauth/app/dataadapter/service/SmsPersistenceService.java @@ -16,20 +16,18 @@ package io.getlime.security.powerauth.app.dataadapter.service; import io.getlime.security.powerauth.app.dataadapter.configuration.DataAdapterConfiguration; -import io.getlime.security.powerauth.app.dataadapter.exception.InvalidOperationContextException; -import io.getlime.security.powerauth.app.dataadapter.exception.SMSAuthorizationFailedException; -import io.getlime.security.powerauth.app.dataadapter.impl.service.DataAdapterService; -import io.getlime.security.powerauth.app.dataadapter.repository.SMSAuthorizationRepository; -import io.getlime.security.powerauth.app.dataadapter.repository.model.entity.SMSAuthorizationEntity; +import io.getlime.security.powerauth.app.dataadapter.repository.SmsAuthorizationRepository; +import io.getlime.security.powerauth.app.dataadapter.repository.model.entity.SmsAuthorizationEntity; import io.getlime.security.powerauth.lib.dataadapter.model.entity.AuthorizationCode; import io.getlime.security.powerauth.lib.dataadapter.model.entity.OperationContext; +import io.getlime.security.powerauth.lib.dataadapter.model.enumeration.SmsAuthorizationResult; +import io.getlime.security.powerauth.lib.dataadapter.model.response.VerifySmsAuthorizationResponse; import org.joda.time.DateTime; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Date; import java.util.Optional; -import java.util.UUID; /** * Service class for generating SMS with OTP authorization code and verification of authorization code. @@ -37,21 +35,18 @@ * @author Roman Strobl, roman.strobl@wultra.com */ @Service -public class SMSPersistenceService { +public class SmsPersistenceService { - private final DataAdapterService dataAdapterService; - private final SMSAuthorizationRepository smsAuthorizationRepository; + private final SmsAuthorizationRepository smsAuthorizationRepository; private final DataAdapterConfiguration dataAdapterConfiguration; /** * SMS persistence service constructor. - * @param dataAdapterService Data adapter service. * @param smsAuthorizationRepository SMS authorization repository. * @param dataAdapterConfiguration Data adapter configuration. */ @Autowired - public SMSPersistenceService(DataAdapterService dataAdapterService, SMSAuthorizationRepository smsAuthorizationRepository, DataAdapterConfiguration dataAdapterConfiguration) { - this.dataAdapterService = dataAdapterService; + public SmsPersistenceService(SmsAuthorizationRepository smsAuthorizationRepository, DataAdapterConfiguration dataAdapterConfiguration) { this.smsAuthorizationRepository = smsAuthorizationRepository; this.dataAdapterConfiguration = dataAdapterConfiguration; } @@ -59,28 +54,22 @@ public SMSPersistenceService(DataAdapterService dataAdapterService, SMSAuthoriza /** * Create an authorization SMS message with OTP authorization code. * @param userId User ID. + * @param organizationId Organization ID. + * @param messageId Message ID * @param operationContext Operation context. - * @param lang Language for message text. + * @param authorizationCode Authorization code for SMS message. + * @param messageText Localized SMS message text. * @return Created entity with SMS message details. */ - public SMSAuthorizationEntity createAuthorizationSMS(String userId, OperationContext operationContext, String lang) throws InvalidOperationContextException { - String operationId = operationContext.getId(); - String operationName = operationContext.getName(); + public SmsAuthorizationEntity createAuthorizationSms(String userId, String organizationId, String messageId, OperationContext operationContext, + AuthorizationCode authorizationCode, String messageText) { - // messageId is generated as random UUID, it can be overridden to provide a real message identification - String messageId = UUID.randomUUID().toString(); - - // generate authorization code - AuthorizationCode authorizationCode = dataAdapterService.generateAuthorizationCode(userId, operationContext); - - // generate message text, include previously generated authorization code - String messageText = dataAdapterService.generateSMSText(userId, operationContext, authorizationCode, lang); - - SMSAuthorizationEntity smsEntity = new SMSAuthorizationEntity(); + SmsAuthorizationEntity smsEntity = new SmsAuthorizationEntity(); smsEntity.setMessageId(messageId); - smsEntity.setOperationId(operationId); + smsEntity.setOperationId(operationContext.getId()); smsEntity.setUserId(userId); - smsEntity.setOperationName(operationName); + smsEntity.setOrganizationId(organizationId); + smsEntity.setOperationName(operationContext.getName()); smsEntity.setAuthorizationCode(authorizationCode.getCode()); smsEntity.setSalt(authorizationCode.getSalt()); smsEntity.setMessageText(messageText); @@ -97,17 +86,21 @@ public SMSAuthorizationEntity createAuthorizationSMS(String userId, OperationCon } /** - * Verify an OTP authorization code. + * Verify an authorization code from SMS message. * @param messageId Message ID. * @param authorizationCode Authorization code. - * @throws SMSAuthorizationFailedException Thrown when SMS authorization fails. + * @param allowMultipleVerifications Whether authorization code can be verified multiple times. + * @return Result of SMS verification. */ - public void verifyAuthorizationSMS(String messageId, String authorizationCode) throws SMSAuthorizationFailedException { - Optional smsEntityOptional = smsAuthorizationRepository.findById(messageId); + public VerifySmsAuthorizationResponse verifyAuthorizationSms(String messageId, String authorizationCode, boolean allowMultipleVerifications) { + Optional smsEntityOptional = smsAuthorizationRepository.findById(messageId); + VerifySmsAuthorizationResponse response = new VerifySmsAuthorizationResponse(); if (!smsEntityOptional.isPresent()) { - throw new SMSAuthorizationFailedException("smsAuthorization.invalidMessage"); + response.setSmsAuthorizationResult(SmsAuthorizationResult.FAILED); + response.setErrorMessage("smsAuthorization.invalidMessage"); + return response; } - SMSAuthorizationEntity smsEntity = smsEntityOptional.get(); + SmsAuthorizationEntity smsEntity = smsEntityOptional.get(); // increase number of verification tries and save entity smsEntity.setVerifyRequestCount(smsEntity.getVerifyRequestCount() + 1); smsAuthorizationRepository.save(smsEntity); @@ -115,30 +108,41 @@ public void verifyAuthorizationSMS(String messageId, String authorizationCode) t final Integer remainingAttempts = dataAdapterConfiguration.getSmsOtpMaxVerifyTriesPerMessage() - smsEntity.getVerifyRequestCount(); if (smsEntity.getAuthorizationCode() == null || smsEntity.getAuthorizationCode().isEmpty()) { - SMSAuthorizationFailedException ex = new SMSAuthorizationFailedException("smsAuthorization.invalidCode"); - ex.setRemainingAttempts(remainingAttempts); - throw ex; + response.setSmsAuthorizationResult(SmsAuthorizationResult.FAILED); + response.setRemainingAttempts(remainingAttempts); + response.setErrorMessage("smsAuthorization.invalidCode"); + return response; } if (smsEntity.isExpired()) { - throw new SMSAuthorizationFailedException("smsAuthorization.expired"); + response.setSmsAuthorizationResult(SmsAuthorizationResult.FAILED); + response.setErrorMessage("smsAuthorization.expired"); + return response; } - if (smsEntity.isVerified()) { - throw new SMSAuthorizationFailedException("smsAuthorization.alreadyVerified"); + if (!allowMultipleVerifications && smsEntity.isVerified()) { + response.setSmsAuthorizationResult(SmsAuthorizationResult.FAILED); + response.setErrorMessage("smsAuthorization.alreadyVerified"); + return response; } if (smsEntity.getVerifyRequestCount() > dataAdapterConfiguration.getSmsOtpMaxVerifyTriesPerMessage()) { - throw new SMSAuthorizationFailedException("smsAuthorization.maxAttemptsExceeded"); + response.setSmsAuthorizationResult(SmsAuthorizationResult.FAILED); + response.setErrorMessage("smsAuthorization.maxAttemptsExceeded"); + return response; } String authorizationCodeExpected = smsEntity.getAuthorizationCode(); if (!authorizationCode.equals(authorizationCodeExpected)) { - SMSAuthorizationFailedException ex = new SMSAuthorizationFailedException("smsAuthorization.failed"); - ex.setRemainingAttempts(remainingAttempts); - throw ex; + response.setSmsAuthorizationResult(SmsAuthorizationResult.FAILED); + response.setRemainingAttempts(remainingAttempts); + response.setErrorMessage("smsAuthorization.failed"); + return response; } // SMS OTP authorization succeeded when this line is reached, update entity verification status smsEntity.setVerified(true); smsEntity.setTimestampVerified(new Date()); smsAuthorizationRepository.save(smsEntity); + + response.setSmsAuthorizationResult(SmsAuthorizationResult.SUCCEEDED); + return response; } } diff --git a/powerauth-data-adapter/src/main/resources/application.properties b/powerauth-data-adapter/src/main/resources/application.properties index 91801876..70dbdc9f 100644 --- a/powerauth-data-adapter/src/main/resources/application.properties +++ b/powerauth-data-adapter/src/main/resources/application.properties @@ -1,16 +1,24 @@ -# Database Keep-Alive -spring.datasource.test-while-idle=true -spring.datasource.test-on-borrow=true -spring.datasource.validation-query=SELECT 1 +# Allow externalization of properties using application-ext.properties +spring.profiles.active=ext # Database Configuration - MySQL spring.datasource.url=jdbc:mysql://localhost:3306/powerauth spring.datasource.username=powerauth spring.datasource.password= spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.jpa.properties.hibernate.connection.CharSet=utf8mb4 spring.jpa.properties.hibernate.connection.characterEncoding=utf8 spring.jpa.properties.hibernate.connection.useUnicode=true +# Database Configuration - PostgreSQL +#spring.datasource.url=jdbc:postgresql://localhost:5432/postgres +#spring.datasource.username=powerauth +#spring.datasource.password= +#spring.datasource.driver-class-name=org.postgresql.Driver +#spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false +#spring.jpa.properties.hibernate.connection.characterEncoding=utf8 +#spring.jpa.properties.hibernate.connection.useUnicode=true + # Database Configuration - Oracle #spring.datasource.url=jdbc:oracle:thin:@//localhost:1521/powerauth #spring.datasource.username=powerauth @@ -33,7 +41,7 @@ spring.jmx.default-domain=powerauth-data-adapter # Application Service Configuration powerauth.dataAdapter.service.applicationName=powerauth-data-adapter -powerauth.dataAdapter.service.applicationDisplayName=PowerAuth 2.0 Data Adapter +powerauth.dataAdapter.service.applicationDisplayName=PowerAuth Data Adapter powerauth.dataAdapter.service.applicationEnvironment= # Disable open session in view to avoid startup warning of Spring boot diff --git a/powerauth-data-adapter/src/main/resources/static/resources/messages_cs.properties b/powerauth-data-adapter/src/main/resources/static/resources/messages_cs.properties index ed0e5624..cde9b6bc 100644 --- a/powerauth-data-adapter/src/main/resources/static/resources/messages_cs.properties +++ b/powerauth-data-adapter/src/main/resources/static/resources/messages_cs.properties @@ -1,3 +1,5 @@ login.smsText=Autorizační kód pro přihlášení je {0}. +login_sca.smsText=Autorizační kód pro přihlášení je {0}. authorize_payment.smsText=Autorizační kód pro platbu {0} {1} na účet {2} je {3}. +authorize_payment_sca.smsText=Autorizační kód pro platbu {0} {1} na účet {2} je {3}. operationReview.balanceTooLow=Nízký zůstatek na účtu diff --git a/powerauth-data-adapter/src/main/resources/static/resources/messages_en.properties b/powerauth-data-adapter/src/main/resources/static/resources/messages_en.properties index 9f218e85..06fcc83d 100644 --- a/powerauth-data-adapter/src/main/resources/static/resources/messages_en.properties +++ b/powerauth-data-adapter/src/main/resources/static/resources/messages_en.properties @@ -1,3 +1,5 @@ login.smsText=Authorization code for login is {0}. +login_sca.smsText=Authorization code for login is {0}. authorize_payment.smsText=Authorization code for payment of {0} {1} to account {2} is {3}. +authorize_payment_sca.smsText=Authorization code for payment of {0} {1} to account {2} is {3}. operationReview.balanceTooLow=Low account balance diff --git a/powerauth-data-adapter/src/main/webapp/WEB-INF/jboss-deployment-structure.xml b/powerauth-data-adapter/src/main/webapp/WEB-INF/jboss-deployment-structure.xml new file mode 100644 index 00000000..142171d6 --- /dev/null +++ b/powerauth-data-adapter/src/main/webapp/WEB-INF/jboss-deployment-structure.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + +